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;
93 var revisionNumber = parseInt(/\d+$/.exec(link))
95 function tracNSResolver(prefix)
98 return "http://purl.org/dc/elements/1.1/";
102 var author = doc.evaluate("./author|dc:creator", commitElement, tracNSResolver, XPathResult.STRING_TYPE).stringValue;
103 var date = doc.evaluate("./pubDate", commitElement, null, XPathResult.STRING_TYPE).stringValue;
104 date = new Date(Date.parse(date));
105 var description = doc.evaluate("./description", commitElement, null, XPathResult.STRING_TYPE).stringValue;
107 var parsedDescription = document.createElement("div");
108 parsedDescription.innerHTML = description;
111 if (parsedDescription.firstChild && parsedDescription.firstChild.className === "changes") {
112 // We can extract branch information when trac.ini contains "changeset_show_files=location".
113 location = doc.evaluate("//strong", parsedDescription.firstChild, null, XPathResult.STRING_TYPE).stringValue
114 parsedDescription.removeChild(parsedDescription.firstChild);
117 // The feed contains a <title>, but it's not parsed as well as what we are getting from description.
118 var title = document.createElement("div");
119 var node = parsedDescription.firstChild ? parsedDescription.firstChild.firstChild : null;
120 while (node && node.tagName != "BR") {
121 title.appendChild(node.cloneNode(true));
122 node = node.nextSibling;
125 // For some reason, trac titles start with a newline. Delete it.
126 if (title.firstChild && title.firstChild.nodeType == Node.TEXT_NODE && title.firstChild.textContent.length > 0 && title.firstChild.textContent[0] == "\n")
127 title.firstChild.textContent = title.firstChild.textContent.substring(1);
130 revisionNumber: revisionNumber,
135 description: parsedDescription.innerHTML,
136 containsBranchLocation: location !== ""
139 if (result.containsBranchLocation) {
140 console.assert(location[location.length - 1] !== "/");
141 location = location += "/";
142 if (location.startsWith("tags/"))
143 result.tag = location.substr(5, location.indexOf("/", 5) - 5);
144 else if (location.startsWith("branches/"))
145 result.branch = location.substr(9, location.indexOf("/", 9) - 9);
146 else if (location.startsWith("releases/"))
147 result.release = location.substr(9, location.indexOf("/", 9) - 9);
148 else if (location.startsWith("trunk/"))
149 result.branch = "trunk";
150 else if (location.startsWith("submissions/"))
151 ; // These changes are never relevant to the dashboard.
153 // result.containsBranchLocation remains true, because this commit does
154 // not match any explicitly specified branches.
155 console.assert(false);
162 _loaded: function(dataDocument)
167 var recordedRevisionNumbers = this.recordedCommits.reduce(function(previousResult, commit) {
168 previousResult[commit.revisionNumber] = commit;
169 return previousResult;
172 var knownCommitsWereUpdated = false;
175 var commitInfoElements = dataDocument.evaluate("/rss/channel/item", dataDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
176 var commitInfoElement;
177 while (commitInfoElement = commitInfoElements.iterateNext()) {
178 var commit = this._convertCommitInfoElementToObject(dataDocument, commitInfoElement);
179 if (commit.revisionNumber in recordedRevisionNumbers) {
180 // Author could have changed, as commit queue replaces it after the fact.
181 console.assert(recordedRevisionNumbers[commit.revisionNumber].revisionNumber === commit.revisionNumber);
182 if (recordedRevisionNumbers[commit.revisionNumber].author != commit.author) {
183 recordedRevisionNumbers[commit.revisionNumber].author = commit.author;
184 knownCommitWasUpdated = true;
187 newCommits.push(commit);
190 if (newCommits.length)
191 this.recordedCommits = newCommits.concat(this.recordedCommits).sort(function(a, b) { return a.revisionNumber - b.revisionNumber; });
193 if (newCommits.length || knownCommitsWereUpdated)
194 this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
197 load: function(fromDate, toDate)
199 loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
200 this._loaded(dataDocument);
201 this.dispatchEventToListeners(Trac.Event.Loaded, [fromDate, toDate]);
202 }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
207 var fromDate = new Date(this._latestLoadedDate);
208 var toDate = new Date();
210 this._latestLoadedDate = toDate;
212 loadXML(this._xmlTimelineURL(fromDate, toDate), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
215 startPeriodicUpdates: function()
217 console.assert(!this._oldestHistoricalDate);
219 var today = new Date();
221 this._oldestHistoricalDate = today;
222 this._latestLoadedDate = today;
224 this._loadingHistoricalData = true;
225 loadXML(this._xmlTimelineURL(today, today), function(dataDocument) {
226 this._loadingHistoricalData = false;
227 this._loaded(dataDocument);
228 }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
230 this.updateTimer = setInterval(this._update.bind(this), Trac.UpdateInterval);
233 loadMoreHistoricalData: function()
235 console.assert(this._oldestHistoricalDate);
237 if (this._loadingHistoricalData)
240 // Load one more day of historical data.
241 var fromDate = new Date(this._oldestHistoricalDate);
242 fromDate.setDate(fromDate.getDate() - 1);
243 var toDate = new Date(fromDate);
245 this._oldestHistoricalDate = fromDate;
247 this._loadingHistoricalData = true;
248 loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
249 this._loadingHistoricalData = false;
250 this._loaded(dataDocument);
251 }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});