build.webkit.org/dashboard asserts on some commits
[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 latestRecordedRevisionNumber()
53     {
54         if (!this.recordedCommits.length)
55             return undefined;
56         return this.recordedCommits[this.recordedCommits.length - 1].revisionNumber;
57     },
58
59     revisionURL: function(revision)
60     {
61         return this.baseURL + "changeset/" + encodeURIComponent(revision);
62     },
63
64     _xmlTimelineURL: function(fromDate, toDate)
65     {
66         if (typeof fromDate === "undefined") {
67             fromDate = new Date();
68             toDate = new Date(fromDate);
69             // By default, get at least one full day of changesets, as the current day may have only begun.
70             fromDate.setDate(fromDate.getDate() - 1);
71         } else if (typeof toDate === "undefined")
72             toDate = fromDate;
73
74         console.assert(fromDate <= toDate);
75
76         var fromDay = new Date(fromDate.getFullYear(), fromDate.getMonth(), fromDate.getDate());
77         var toDay = new Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate());
78
79         return this.baseURL + "timeline?changeset=on&format=rss&max=0" +
80             "&from=" +  (toDay.getMonth() + 1) + "%2F" + toDay.getDate() + "%2F" + (toDay.getFullYear() % 100) +
81             "&daysback=" + ((toDay - fromDay) / 1000 / 60 / 60 / 24);
82     },
83
84     _convertCommitInfoElementToObject: function(doc, commitElement)
85     {
86         var link = doc.evaluate("./link", commitElement, null, XPathResult.STRING_TYPE).stringValue;
87         var revisionNumber = parseInt(/\d+$/.exec(link))
88
89         function tracNSResolver(prefix)
90         {
91             if (prefix == "dc")
92                 return "http://purl.org/dc/elements/1.1/";
93             return null;
94         }
95
96         var author = doc.evaluate("./author|dc:creator", commitElement, tracNSResolver, XPathResult.STRING_TYPE).stringValue;
97         var date = doc.evaluate("./pubDate", commitElement, null, XPathResult.STRING_TYPE).stringValue;
98         date = new Date(Date.parse(date));
99         var description = doc.evaluate("./description", commitElement, null, XPathResult.STRING_TYPE).stringValue;
100
101         var parsedDescription = document.createElement("div");
102         parsedDescription.innerHTML = description;
103
104         var location = "";
105         if (parsedDescription.firstChild && parsedDescription.firstChild.className === "changes") {
106             // We can extract branch information when trac.ini contains "changeset_show_files=location".
107             location = doc.evaluate("//strong", parsedDescription.firstChild, null, XPathResult.STRING_TYPE).stringValue
108             parsedDescription.removeChild(parsedDescription.firstChild);
109         }
110
111         // The feed contains a <title>, but it's not parsed as well as what we are getting from description.
112         var title = document.createElement("div");
113         var node = parsedDescription.firstChild ? parsedDescription.firstChild.firstChild : null;
114         while (node && node.tagName != "BR") {
115             title.appendChild(node.cloneNode(true));
116             node = node.nextSibling;
117         }
118
119         // For some reason, trac titles start with a newline. Delete it.
120         if (title.firstChild && title.firstChild.nodeType == Node.TEXT_NODE && title.firstChild.textContent.length > 0 && title.firstChild.textContent[0] == "\n")
121             title.firstChild.textContent = title.firstChild.textContent.substring(1);
122
123         var result = {
124             revisionNumber: revisionNumber,
125             link: link,
126             title: title,
127             author: author,
128             date: date,
129             description: parsedDescription.innerHTML,
130             containsBranchLocation: location !== ""
131         };
132
133         if (result.containsBranchLocation) {
134             console.assert(location[location.length - 1] !== "/");
135             location = location += "/";
136             if (location.startsWith("tags/"))
137                 result.tag = location.substr(5, location.indexOf("/", 5) - 5);
138             else if (location.startsWith("branches/"))
139                 result.branch = location.substr(9, location.indexOf("/", 9) - 9);
140             else if (location.startsWith("releases/"))
141                 result.release = location.substr(9, location.indexOf("/", 9) - 9);
142             else if (location.startsWith("trunk/"))
143                 result.branch = "trunk";
144             else if (location.startsWith("submissions/"))
145                 ; // These changes are never relevant to the dashboard.
146             else {
147                 console.assert(false);
148                 result.containsBranchLocation = false;
149             }
150         }
151
152         return result;
153     },
154
155     _loaded: function(dataDocument)
156     {
157         if (!dataDocument)
158             return;
159
160         var recordedRevisionNumbers = this.recordedCommits.reduce(function(previousResult, commit) {
161             previousResult[commit.revisionNumber] = commit;
162             return previousResult;
163         }, {});
164
165         var knownCommitsWereUpdated = false;
166         var newCommits = [];
167
168         var commitInfoElements = dataDocument.evaluate("/rss/channel/item", dataDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
169         var commitInfoElement;
170         while (commitInfoElement = commitInfoElements.iterateNext()) {
171             var commit = this._convertCommitInfoElementToObject(dataDocument, commitInfoElement);
172             if (commit.revisionNumber in recordedRevisionNumbers) {
173                 // Author could have changed, as commit queue replaces it after the fact.
174                 console.assert(recordedRevisionNumbers[commit.revisionNumber].revisionNumber === commit.revisionNumber);
175                 if (recordedRevisionNumbers[commit.revisionNumber].author != commit.author) {
176                     recordedRevisionNumbers[commit.revisionNumber].author = commit.author;
177                     knownCommitWasUpdated = true;
178                 }
179             } else
180                 newCommits.push(commit);
181         }
182
183         if (newCommits.length)
184             this.recordedCommits = newCommits.concat(this.recordedCommits).sort(function(a, b) { return a.revisionNumber - b.revisionNumber; });
185
186         if (newCommits.length || knownCommitsWereUpdated)
187             this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
188     },
189
190     load: function(fromDate, toDate)
191     {
192         loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
193             this._loaded(dataDocument);
194             this.dispatchEventToListeners(Trac.Event.Loaded, [fromDate, toDate]);
195         }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
196     },
197
198     update: function()
199     {
200         loadXML(this._xmlTimelineURL(), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
201     },
202
203     startPeriodicUpdates: function()
204     {
205         this.update();
206         this.updateTimer = setInterval(this.update.bind(this), Trac.UpdateInterval);
207     }
208 };