build.webkit.org/dashboard should not request 50 revisions from trac each time
[WebKit-https.git] / Tools / BuildSlaveSupport / build.webkit.org-config / public_html / dashboard / Scripts / Trac.js
1 /*
2  * Copyright (C) 2013 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     this.update();
38     this.updateTimer = setInterval(this.update.bind(this), Trac.UpdateInterval);
39 };
40
41 BaseObject.addConstructorFunctions(Trac);
42
43 Trac.NeedsAuthentication = "needsAuthentication";
44 Trac.UpdateInterval = 45000; // 45 seconds
45
46 Trac.Event = {
47     CommitsUpdated: "commits-updated"
48 };
49
50 Trac.prototype = {
51     constructor: Trac,
52     __proto__: BaseObject.prototype,
53
54     get latestRecordedRevisionNumber()
55     {
56         if (!this.recordedCommits.length)
57             return undefined;
58         return this.recordedCommits[this.recordedCommits.length - 1].revisionNumber;
59     },
60
61     revisionURL: function(revision)
62     {
63         return this.baseURL + "changeset/" + encodeURIComponent(revision);
64     },
65
66     _xmlTimelineURL: function(fromDate, toDate)
67     {
68         if (typeof fromDate === "undefined") {
69             fromDate = new Date();
70             toDate = new Date(fromDate);
71             // By default, get at least one full day of changesets, as the current day may have only begun.
72             fromDate.setDate(fromDate.getDate() - 1);
73         } else if (typeof toDate === "undefined")
74             toDate = fromDate;
75
76         console.assert(fromDate <= toDate);
77
78         var fromDay = new Date(fromDate.getFullYear(), fromDate.getMonth(), fromDate.getDate());
79         var toDay = new Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate());
80
81         return this.baseURL + "timeline?changeset=on&format=rss&max=-1" +
82             "&from=" +  (toDay.getMonth() + 1) + "%2F" + toDay.getDate() + "%2F" + (toDay.getFullYear() % 100) +
83             "&daysback=" + ((toDay - fromDay) / 1000 / 60 / 60 / 24);
84     },
85
86     _convertCommitInfoElementToObject: function(doc, commitElement)
87     {
88         var link = doc.evaluate("./link", commitElement, null, XPathResult.STRING_TYPE).stringValue;
89         var revisionNumber = parseInt(/\d+$/.exec(link))
90
91         function tracNSResolver(prefix)
92         {
93             if (prefix == "dc")
94                 return "http://purl.org/dc/elements/1.1/";
95             return null;
96         }
97
98         var author = doc.evaluate("./author|dc:creator", commitElement, tracNSResolver, XPathResult.STRING_TYPE).stringValue;
99         var date = doc.evaluate("./pubDate", commitElement, null, XPathResult.STRING_TYPE).stringValue;
100         date = new Date(Date.parse(date));
101         var description = doc.evaluate("./description", commitElement, null, XPathResult.STRING_TYPE).stringValue;
102
103         // The feed contains a <title>, but it's not parsed as well as what we are getting from description.
104         var parsedDescription = document.createElement("div");
105         parsedDescription.innerHTML = description;
106         var title = document.createElement("div");
107         var node = parsedDescription.firstChild.firstChild;
108         while (node && node.tagName != "BR") {
109             title.appendChild(node.cloneNode(true));
110             node = node.nextSibling;
111         }
112
113         // For some reason, trac titles start with a newline. Delete it.
114         if (title.firstChild && title.firstChild.nodeType == Node.TEXT_NODE && title.firstChild.textContent.length > 0 && title.firstChild.textContent[0] == "\n")
115             title.firstChild.textContent = title.firstChild.textContent.substring(1);
116
117         return {
118             revisionNumber: revisionNumber,
119             link: link,
120             title: title,
121             author: author,
122             date: date,
123             description: description
124         };
125     },
126
127     _loaded: function(dataDocument)
128     {
129         var earliestKnownRevision = 0;
130         var latestKnownRevision = 0;
131         if (this.recordedCommits.length) {
132             earliestKnownRevision = this.recordedCommits[0].revisionNumber;
133             latestKnownRevision = this.recordedCommits[this.recordedCommits.length - 1].revisionNumber;
134         }
135
136         var knownCommitsWereUpdated = false;
137         var newCommits = [];
138         var newCommitsBeforeEarliestKnownRevision = [];
139
140         var commitInfoElements = dataDocument.evaluate("/rss/channel/item", dataDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
141         var commitInfoElement = undefined;
142         var indexInRecordedCommits = undefined;
143         while (commitInfoElement = commitInfoElements.iterateNext()) {
144             var commit = this._convertCommitInfoElementToObject(dataDocument, commitInfoElement);
145             if (commit.revisionNumber > latestKnownRevision) {
146                 console.assert(typeof indexInRecordedCommits === "undefined");
147                 newCommits.push(commit);
148             } else if (commit.revisionNumber < earliestKnownRevision) {
149                 console.assert(typeof indexInRecordedCommits === "undefined" || indexInRecordedCommits === -1);
150                 newCommitsBeforeEarliestKnownRevision.push(commit);
151             } else {
152                 if (typeof indexInRecordedCommits === "undefined") {
153                     // We could have started anywhere in the recorded commits array, let's find where.
154                     // With periodic updates, this will be the latest recorded commit, so starting from the end.
155                     for (var i = this.recordedCommits.length - 1; i >= 0; --i) {
156                         if (this.recordedCommits[i].revisionNumber === commit.revisionNumber) {
157                             indexInRecordedCommits = i;
158                             break;
159                         }
160                     }
161                 }
162
163                 console.assert(indexInRecordedCommits >= 0);
164                 console.assert(this.recordedCommits[indexInRecordedCommits].revisionNumber === commit.revisionNumber);
165
166                 // Author could have changed, as commit queue replaces it after the fact.
167                 if (this.recordedCommits[indexInRecordedCommits].author !== commit.author) {
168                     this.recordedCommits[indexInRecordedCommits].author = commit.author;
169                     knownCommitWasUpdated = true;
170                 }
171                 --indexInRecordedCommits;
172             }
173         }
174
175         if (newCommits.length || newCommitsBeforeEarliestKnownRevision.length)
176             this.recordedCommits = newCommitsBeforeEarliestKnownRevision.reverse().concat(this.recordedCommits, newCommits.reverse());
177
178         if (newCommits.length || newCommitsBeforeEarliestKnownRevision.length || knownCommitsWereUpdated)
179             this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
180     },
181
182     update: function()
183     {
184         loadXML(this._xmlTimelineURL(), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
185     }
186 };