build.webkit.org/dashboard should filter out commits to other branches
[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(branch, filter)
67     {
68         return this.recordedCommits.filter(function(commit) {
69             return (!commit.containsBranchLocation || commit.branch === branch) && 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.getMonth() + 1) + "%2F" + toDay.getDate() + "%2F" + (toDay.getFullYear() % 100) +
87             "&daysback=" + ((toDay - fromDay) / 1000 / 60 / 60 / 24);
88     },
89
90     _convertCommitInfoElementToObject: function(doc, commitElement)
91     {
92         var link = doc.evaluate("./link", commitElement, null, XPathResult.STRING_TYPE).stringValue;
93         var revisionNumber = parseInt(/\d+$/.exec(link))
94
95         function tracNSResolver(prefix)
96         {
97             if (prefix == "dc")
98                 return "http://purl.org/dc/elements/1.1/";
99             return null;
100         }
101
102         var author = doc.evaluate("./author|dc:creator", commitElement, tracNSResolver, XPathResult.STRING_TYPE).stringValue;
103         var date = doc.evaluate("./pubDate", commitElement, null, XPathResult.STRING_TYPE).stringValue;
104         date = new Date(Date.parse(date));
105         var description = doc.evaluate("./description", commitElement, null, XPathResult.STRING_TYPE).stringValue;
106
107         var parsedDescription = document.createElement("div");
108         parsedDescription.innerHTML = description;
109
110         var location = "";
111         if (parsedDescription.firstChild && parsedDescription.firstChild.className === "changes") {
112             // We can extract branch information when trac.ini contains "changeset_show_files=location".
113             location = doc.evaluate("//strong", parsedDescription.firstChild, null, XPathResult.STRING_TYPE).stringValue
114             parsedDescription.removeChild(parsedDescription.firstChild);
115         }
116
117         // The feed contains a <title>, but it's not parsed as well as what we are getting from description.
118         var title = document.createElement("div");
119         var node = parsedDescription.firstChild ? parsedDescription.firstChild.firstChild : null;
120         while (node && node.tagName != "BR") {
121             title.appendChild(node.cloneNode(true));
122             node = node.nextSibling;
123         }
124
125         // For some reason, trac titles start with a newline. Delete it.
126         if (title.firstChild && title.firstChild.nodeType == Node.TEXT_NODE && title.firstChild.textContent.length > 0 && title.firstChild.textContent[0] == "\n")
127             title.firstChild.textContent = title.firstChild.textContent.substring(1);
128
129         var result = {
130             revisionNumber: revisionNumber,
131             link: link,
132             title: title,
133             author: author,
134             date: date,
135             description: parsedDescription.innerHTML,
136             containsBranchLocation: location !== ""
137         };
138
139         if (result.containsBranchLocation) {
140             console.assert(location[location.length - 1] !== "/");
141             location = location += "/";
142             if (location.startsWith("tags/"))
143                 result.tag = location.substr(5, location.indexOf("/", 5) - 5);
144             else if (location.startsWith("branches/"))
145                 result.branch = location.substr(9, location.indexOf("/", 9) - 9);
146             else if (location.startsWith("releases/"))
147                 result.release = location.substr(9, location.indexOf("/", 9) - 9);
148             else if (location.startsWith("trunk/"))
149                 result.branch = "trunk";
150             else if (location.startsWith("submissions/"))
151                 ; // These changes are never relevant to the dashboard.
152             else {
153                 // result.containsBranchLocation remains true, because this commit does
154                 // not match any explicitly specified branches.
155                 console.assert(false);
156             }
157         }
158
159         return result;
160     },
161
162     _loaded: function(dataDocument)
163     {
164         if (!dataDocument)
165             return;
166
167         var recordedRevisionNumbers = this.recordedCommits.reduce(function(previousResult, commit) {
168             previousResult[commit.revisionNumber] = commit;
169             return previousResult;
170         }, {});
171
172         var knownCommitsWereUpdated = false;
173         var newCommits = [];
174
175         var commitInfoElements = dataDocument.evaluate("/rss/channel/item", dataDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
176         var commitInfoElement;
177         while (commitInfoElement = commitInfoElements.iterateNext()) {
178             var commit = this._convertCommitInfoElementToObject(dataDocument, commitInfoElement);
179             if (commit.revisionNumber in recordedRevisionNumbers) {
180                 // Author could have changed, as commit queue replaces it after the fact.
181                 console.assert(recordedRevisionNumbers[commit.revisionNumber].revisionNumber === commit.revisionNumber);
182                 if (recordedRevisionNumbers[commit.revisionNumber].author != commit.author) {
183                     recordedRevisionNumbers[commit.revisionNumber].author = commit.author;
184                     knownCommitWasUpdated = true;
185                 }
186             } else
187                 newCommits.push(commit);
188         }
189
190         if (newCommits.length)
191             this.recordedCommits = newCommits.concat(this.recordedCommits).sort(function(a, b) { return a.revisionNumber - b.revisionNumber; });
192
193         if (newCommits.length || knownCommitsWereUpdated)
194             this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
195     },
196
197     load: function(fromDate, toDate)
198     {
199         loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
200             this._loaded(dataDocument);
201             this.dispatchEventToListeners(Trac.Event.Loaded, [fromDate, toDate]);
202         }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
203     },
204
205     _update: function()
206     {
207         var fromDate = new Date(this._latestLoadedDate);
208         var toDate = new Date();
209
210         this._latestLoadedDate = toDate;
211
212         loadXML(this._xmlTimelineURL(fromDate, toDate), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
213     },
214
215     startPeriodicUpdates: function()
216     {
217         console.assert(!this._oldestHistoricalDate);
218
219         var today = new Date();
220
221         this._oldestHistoricalDate = today;
222         this._latestLoadedDate = today;
223
224         this._loadingHistoricalData = true;
225         loadXML(this._xmlTimelineURL(today, today), function(dataDocument) {
226             this._loadingHistoricalData = false;
227             this._loaded(dataDocument);
228         }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
229
230         this.updateTimer = setInterval(this._update.bind(this), Trac.UpdateInterval);
231     },
232
233     loadMoreHistoricalData: function()
234     {
235         console.assert(this._oldestHistoricalDate);
236
237         if (this._loadingHistoricalData)
238             return;
239
240         // Load one more day of historical data.
241         var fromDate = new Date(this._oldestHistoricalDate);
242         fromDate.setDate(fromDate.getDate() - 1);
243         var toDate = new Date(fromDate);
244
245         this._oldestHistoricalDate = fromDate;
246
247         this._loadingHistoricalData = true;
248         loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
249             this._loadingHistoricalData = false;
250             this._loaded(dataDocument);
251         }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
252     },
253 };