2 * Copyright (C) 2013, 2014 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
26 Trac = function(baseURL, options)
28 BaseObject.call(this);
30 console.assert(baseURL);
32 this.baseURL = baseURL;
33 this._needsAuthentication = (typeof options === "object") && options[Trac.NeedsAuthentication] === true;
35 this.recordedCommits = []; // Will be sorted in ascending order.
38 BaseObject.addConstructorFunctions(Trac);
40 Trac.NeedsAuthentication = "needsAuthentication";
41 Trac.UpdateInterval = 45000; // 45 seconds
44 CommitsUpdated: "commits-updated",
50 __proto__: BaseObject.prototype,
52 get oldestRecordedRevisionNumber()
54 if (!this.recordedCommits.length)
56 return this.recordedCommits[0].revisionNumber;
59 get latestRecordedRevisionNumber()
61 if (!this.recordedCommits.length)
63 return this.recordedCommits[this.recordedCommits.length - 1].revisionNumber;
66 commitsOnBranch: function(branch, filter)
68 return this.recordedCommits.filter(function(commit) {
69 return (!commit.containsBranchLocation || commit.branch === branch) && filter(commit);
73 revisionURL: function(revision)
75 return this.baseURL + "changeset/" + encodeURIComponent(revision);
78 _xmlTimelineURL: function(fromDate, toDate)
80 console.assert(fromDate <= toDate);
82 var fromDay = new Date(fromDate.getFullYear(), fromDate.getMonth(), fromDate.getDate());
83 var toDay = new Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate());
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);
90 _convertCommitInfoElementToObject: function(doc, commitElement)
92 var link = doc.evaluate("./link", commitElement, null, XPathResult.STRING_TYPE).stringValue;
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());
100 var revisionNumber = parseInt(linkComponents.pop());
102 function tracNSResolver(prefix)
105 return "http://purl.org/dc/elements/1.1/";
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;
114 var parsedDescription = document.createElement("div");
115 parsedDescription.innerHTML = description;
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);
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;
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);
137 revisionNumber: revisionNumber,
142 description: parsedDescription.innerHTML,
143 containsBranchLocation: location !== ""
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.branch = 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.branch = "trunk";
157 else if (location.startsWith("submissions/"))
158 ; // These changes are never relevant to the dashboard.
160 // result.containsBranchLocation remains true, because this commit does
161 // not match any explicitly specified branches.
162 console.assert(false);
169 _loaded: function(dataDocument)
174 var recordedRevisionNumbers = this.recordedCommits.reduce(function(previousResult, commit) {
175 previousResult[commit.revisionNumber] = commit;
176 return previousResult;
179 var knownCommitsWereUpdated = false;
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;
194 newCommits.push(commit);
197 if (newCommits.length)
198 this.recordedCommits = newCommits.concat(this.recordedCommits).sort(function(a, b) { return a.revisionNumber - b.revisionNumber; });
200 if (newCommits.length || knownCommitsWereUpdated)
201 this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
204 load: function(fromDate, toDate)
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 } : {});
214 var fromDate = new Date(this._latestLoadedDate);
215 var toDate = new Date();
217 this._latestLoadedDate = toDate;
219 loadXML(this._xmlTimelineURL(fromDate, toDate), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
222 startPeriodicUpdates: function()
224 console.assert(!this._oldestHistoricalDate);
226 var today = new Date();
228 this._oldestHistoricalDate = today;
229 this._latestLoadedDate = today;
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 } : {});
237 this.updateTimer = setInterval(this._update.bind(this), Trac.UpdateInterval);
240 loadMoreHistoricalData: function()
242 console.assert(this._oldestHistoricalDate);
244 if (this._loadingHistoricalData)
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);
252 this._oldestHistoricalDate = fromDate;
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 } : {});