build.webkit.org/dashboard: Further improve Trac loading
[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     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     Loaded: "loaded"
49 };
50
51 Trac.prototype = {
52     constructor: Trac,
53     __proto__: BaseObject.prototype,
54
55     get latestRecordedRevisionNumber()
56     {
57         if (!this.recordedCommits.length)
58             return undefined;
59         return this.recordedCommits[this.recordedCommits.length - 1].revisionNumber;
60     },
61
62     revisionURL: function(revision)
63     {
64         return this.baseURL + "changeset/" + encodeURIComponent(revision);
65     },
66
67     _xmlTimelineURL: function(fromDate, toDate)
68     {
69         if (typeof fromDate === "undefined") {
70             fromDate = new Date();
71             toDate = new Date(fromDate);
72             // By default, get at least one full day of changesets, as the current day may have only begun.
73             fromDate.setDate(fromDate.getDate() - 1);
74         } else if (typeof toDate === "undefined")
75             toDate = fromDate;
76
77         console.assert(fromDate <= toDate);
78
79         var fromDay = new Date(fromDate.getFullYear(), fromDate.getMonth(), fromDate.getDate());
80         var toDay = new Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate());
81
82         return this.baseURL + "timeline?changeset=on&format=rss&max=0" +
83             "&from=" +  (toDay.getMonth() + 1) + "%2F" + toDay.getDate() + "%2F" + (toDay.getFullYear() % 100) +
84             "&daysback=" + ((toDay - fromDay) / 1000 / 60 / 60 / 24);
85     },
86
87     _convertCommitInfoElementToObject: function(doc, commitElement)
88     {
89         var link = doc.evaluate("./link", commitElement, null, XPathResult.STRING_TYPE).stringValue;
90         var revisionNumber = parseInt(/\d+$/.exec(link))
91
92         function tracNSResolver(prefix)
93         {
94             if (prefix == "dc")
95                 return "http://purl.org/dc/elements/1.1/";
96             return null;
97         }
98
99         var author = doc.evaluate("./author|dc:creator", commitElement, tracNSResolver, XPathResult.STRING_TYPE).stringValue;
100         var date = doc.evaluate("./pubDate", commitElement, null, XPathResult.STRING_TYPE).stringValue;
101         date = new Date(Date.parse(date));
102         var description = doc.evaluate("./description", commitElement, null, XPathResult.STRING_TYPE).stringValue;
103
104         // The feed contains a <title>, but it's not parsed as well as what we are getting from description.
105         var parsedDescription = document.createElement("div");
106         parsedDescription.innerHTML = description;
107         var title = document.createElement("div");
108         var node = parsedDescription.firstChild.firstChild;
109         while (node && node.tagName != "BR") {
110             title.appendChild(node.cloneNode(true));
111             node = node.nextSibling;
112         }
113
114         // For some reason, trac titles start with a newline. Delete it.
115         if (title.firstChild && title.firstChild.nodeType == Node.TEXT_NODE && title.firstChild.textContent.length > 0 && title.firstChild.textContent[0] == "\n")
116             title.firstChild.textContent = title.firstChild.textContent.substring(1);
117
118         return {
119             revisionNumber: revisionNumber,
120             link: link,
121             title: title,
122             author: author,
123             date: date,
124             description: description
125         };
126     },
127
128     _loaded: function(dataDocument)
129     {
130         var recordedRevisionNumbers = this.recordedCommits.reduce(function(previousResult, commit) {
131             previousResult[commit.revisionNumber] = commit;
132             return previousResult;
133         }, {});
134
135         var knownCommitsWereUpdated = false;
136         var newCommits = [];
137
138         var commitInfoElements = dataDocument.evaluate("/rss/channel/item", dataDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
139         var commitInfoElement;
140         while (commitInfoElement = commitInfoElements.iterateNext()) {
141             var commit = this._convertCommitInfoElementToObject(dataDocument, commitInfoElement);
142             if (commit.revisionNumber in recordedRevisionNumbers) {
143                 // Author could have changed, as commit queue replaces it after the fact.
144                 console.assert(recordedRevisionNumbers[commit.revisionNumber].revisionNumber === commit.revisionNumber);
145                 if (recordedRevisionNumbers[commit.revisionNumber].author != commit.author) {
146                     recordedRevisionNumbers[commit.revisionNumber].author = commit.author;
147                     knownCommitWasUpdated = true;
148                 }
149             } else
150                 newCommits.push(commit);
151         }
152
153         if (newCommits.length)
154             this.recordedCommits = newCommits.concat(this.recordedCommits).sort(function(a, b) { return a.revisionNumber - b.revisionNumber; });
155
156         if (newCommits.length || knownCommitsWereUpdated)
157             this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
158     },
159
160     load: function(fromDate, toDate)
161     {
162         loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
163             this._loaded(dataDocument);
164             this.dispatchEventToListeners(Trac.Event.Loaded, [fromDate, toDate]);
165         }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
166     },
167
168     update: function()
169     {
170         loadXML(this._xmlTimelineURL(), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
171     }
172 };