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