Refactor logic for parsing Trac revisions into its own function and add logic for...
[WebKit-https.git] / Tools / BuildSlaveSupport / build.webkit.org-config / public_html / dashboard / Scripts / Trac.js
1 /*
2  * Copyright (C) 2013, 2014 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 Trac = function(baseURL, options)
27 {
28     BaseObject.call(this);
29
30     console.assert(baseURL);
31
32     this.baseURL = baseURL;
33     this._needsAuthentication = (typeof options === "object") && options[Trac.NeedsAuthentication] === true;
34
35     this.recordedCommits = []; // Will be sorted in ascending order.
36 };
37
38 BaseObject.addConstructorFunctions(Trac);
39
40 Trac.NeedsAuthentication = "needsAuthentication";
41 Trac.UpdateInterval = 45000; // 45 seconds
42
43 Trac.Event = {
44     CommitsUpdated: "commits-updated",
45     Loaded: "loaded"
46 };
47
48 Trac.prototype = {
49     constructor: Trac,
50     __proto__: BaseObject.prototype,
51
52     get oldestRecordedRevisionNumber()
53     {
54         if (!this.recordedCommits.length)
55             return undefined;
56         return this.recordedCommits[0].revisionNumber;
57     },
58
59     get latestRecordedRevisionNumber()
60     {
61         if (!this.recordedCommits.length)
62             return undefined;
63         return this.recordedCommits[this.recordedCommits.length - 1].revisionNumber;
64     },
65
66     commitsOnBranch: function(branchName, filter)
67     {
68         return this.recordedCommits.filter(function(commit) {
69             return (!commit.containsBranchLocation || commit.branchName === branchName) && filter(commit);
70         });
71     },
72
73     revisionURL: function(revision)
74     {
75         return this.baseURL + "changeset/" + encodeURIComponent(revision);
76     },
77
78     _xmlTimelineURL: function(fromDate, toDate)
79     {
80         console.assert(fromDate <= toDate);
81
82         var fromDay = new Date(fromDate.getFullYear(), fromDate.getMonth(), fromDate.getDate());
83         var toDay = new Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate());
84
85         return this.baseURL + "timeline?changeset=on&format=rss&max=0" +
86             "&from=" +  toDay.toISOString().slice(0, 10) +
87             "&daysback=" + ((toDay - fromDay) / 1000 / 60 / 60 / 24);
88     },
89
90     _parseRevisionFromURL: function(url)
91     {
92         // There are multiple link formats for Trac that we support:
93         // https://trac.webkit.org/changeset/190497
94         // http://trac.foobar.com/repository/changeset/75388/project
95         // https://git.foobar.com/trac/Whatever.git/changeset/0e498db5d8e5b5a342631
96         return /changeset\/([a-f0-9]+).*$/.exec(url)[1];
97     },
98
99     _convertCommitInfoElementToObject: function(doc, commitElement)
100     {
101         var link = doc.evaluate("./link", commitElement, null, XPathResult.STRING_TYPE).stringValue;
102         var revisionNumber = this._parseRevisionFromURL(link);
103
104         function tracNSResolver(prefix)
105         {
106             if (prefix == "dc")
107                 return "http://purl.org/dc/elements/1.1/";
108             return null;
109         }
110
111         var author = doc.evaluate("./author|dc:creator", commitElement, tracNSResolver, XPathResult.STRING_TYPE).stringValue;
112         var date = doc.evaluate("./pubDate", commitElement, null, XPathResult.STRING_TYPE).stringValue;
113         date = new Date(Date.parse(date));
114         var description = doc.evaluate("./description", commitElement, null, XPathResult.STRING_TYPE).stringValue;
115
116         var parsedDescription = document.createElement("div");
117         parsedDescription.innerHTML = description;
118
119         var location = "";
120         if (parsedDescription.firstChild && parsedDescription.firstChild.className === "changes") {
121             // We can extract branch information when trac.ini contains "changeset_show_files=location".
122             location = doc.evaluate("//strong", parsedDescription.firstChild, null, XPathResult.STRING_TYPE).stringValue
123             parsedDescription.removeChild(parsedDescription.firstChild);
124         }
125
126         // The feed contains a <title>, but it's not parsed as well as what we are getting from description.
127         var title = document.createElement("div");
128         var node = parsedDescription.firstChild ? parsedDescription.firstChild.firstChild : null;
129         while (node && node.tagName != "BR") {
130             title.appendChild(node.cloneNode(true));
131             node = node.nextSibling;
132         }
133
134         // For some reason, trac titles start with a newline. Delete it.
135         if (title.firstChild && title.firstChild.nodeType == Node.TEXT_NODE && title.firstChild.textContent.length > 0 && title.firstChild.textContent[0] == "\n")
136             title.firstChild.textContent = title.firstChild.textContent.substring(1);
137
138         var result = {
139             revisionNumber: revisionNumber,
140             link: link,
141             title: title,
142             author: author,
143             date: date,
144             description: parsedDescription.innerHTML,
145             containsBranchLocation: location !== ""
146         };
147
148         if (result.containsBranchLocation) {
149             console.assert(location[location.length - 1] !== "/");
150             location = location += "/";
151             if (location.startsWith("tags/"))
152                 result.tag = location.substr(5, location.indexOf("/", 5) - 5);
153             else if (location.startsWith("branches/"))
154                 result.branchName = location.substr(9, location.indexOf("/", 9) - 9);
155             else if (location.startsWith("releases/"))
156                 result.release = location.substr(9, location.indexOf("/", 9) - 9);
157             else if (location.startsWith("trunk/"))
158                 result.branchName = "trunk";
159             else if (location.startsWith("submissions/"))
160                 ; // These changes are never relevant to the dashboard.
161             else {
162                 // result.containsBranchLocation remains true, because this commit does
163                 // not match any explicitly specified branches.
164                 console.assert(false);
165             }
166         }
167
168         return result;
169     },
170
171     _loaded: function(dataDocument)
172     {
173         if (!dataDocument)
174             return;
175
176         var recordedRevisionNumbers = this.recordedCommits.reduce(function(previousResult, commit) {
177             previousResult[commit.revisionNumber] = commit;
178             return previousResult;
179         }, {});
180
181         var knownCommitsWereUpdated = false;
182         var newCommits = [];
183
184         var commitInfoElements = dataDocument.evaluate("/rss/channel/item", dataDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
185         var commitInfoElement;
186         while (commitInfoElement = commitInfoElements.iterateNext()) {
187             var commit = this._convertCommitInfoElementToObject(dataDocument, commitInfoElement);
188             if (commit.revisionNumber in recordedRevisionNumbers) {
189                 // Author could have changed, as commit queue replaces it after the fact.
190                 console.assert(recordedRevisionNumbers[commit.revisionNumber].revisionNumber === commit.revisionNumber);
191                 if (recordedRevisionNumbers[commit.revisionNumber].author != commit.author) {
192                     recordedRevisionNumbers[commit.revisionNumber].author = commit.author;
193                     knownCommitWasUpdated = true;
194                 }
195             } else
196                 newCommits.push(commit);
197         }
198
199         if (newCommits.length)
200             this.recordedCommits = newCommits.concat(this.recordedCommits).sort(function(a, b) { return a.date - b.date; });
201
202         if (newCommits.length || knownCommitsWereUpdated)
203             this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
204     },
205
206     load: function(fromDate, toDate)
207     {
208         loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
209             this._loaded(dataDocument);
210             this.dispatchEventToListeners(Trac.Event.Loaded, [fromDate, toDate]);
211         }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
212     },
213
214     _update: function()
215     {
216         var fromDate = new Date(this._latestLoadedDate);
217         var toDate = new Date();
218
219         this._latestLoadedDate = toDate;
220
221         loadXML(this._xmlTimelineURL(fromDate, toDate), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
222     },
223
224     startPeriodicUpdates: function()
225     {
226         console.assert(!this._oldestHistoricalDate);
227
228         var today = new Date();
229
230         this._oldestHistoricalDate = today;
231         this._latestLoadedDate = today;
232
233         this._loadingHistoricalData = true;
234         loadXML(this._xmlTimelineURL(today, today), function(dataDocument) {
235             this._loadingHistoricalData = false;
236             this._loaded(dataDocument);
237         }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
238
239         this.updateTimer = setInterval(this._update.bind(this), Trac.UpdateInterval);
240     },
241
242     loadMoreHistoricalData: function()
243     {
244         console.assert(this._oldestHistoricalDate);
245
246         if (this._loadingHistoricalData)
247             return;
248
249         // Load one more day of historical data.
250         var fromDate = new Date(this._oldestHistoricalDate);
251         fromDate.setDate(fromDate.getDate() - 1);
252         var toDate = new Date(fromDate);
253
254         this._oldestHistoricalDate = fromDate;
255
256         this._loadingHistoricalData = true;
257         loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
258             this._loadingHistoricalData = false;
259             this._loaded(dataDocument);
260         }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
261     },
262 };