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 latestRecordedRevisionNumber()
54 if (!this.recordedCommits.length)
56 return this.recordedCommits[this.recordedCommits.length - 1].revisionNumber;
59 revisionURL: function(revision)
61 return this.baseURL + "changeset/" + encodeURIComponent(revision);
64 _xmlTimelineURL: function(fromDate, toDate)
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")
74 console.assert(fromDate <= toDate);
76 var fromDay = new Date(fromDate.getFullYear(), fromDate.getMonth(), fromDate.getDate());
77 var toDay = new Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate());
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);
84 _convertCommitInfoElementToObject: function(doc, commitElement)
86 var link = doc.evaluate("./link", commitElement, null, XPathResult.STRING_TYPE).stringValue;
87 var revisionNumber = parseInt(/\d+$/.exec(link))
89 function tracNSResolver(prefix)
92 return "http://purl.org/dc/elements/1.1/";
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;
101 // The feed contains a <title>, but it's not parsed as well as what we are getting from description.
102 var parsedDescription = document.createElement("div");
103 parsedDescription.innerHTML = description;
104 var title = document.createElement("div");
105 var node = parsedDescription.firstChild.firstChild;
106 while (node && node.tagName != "BR") {
107 title.appendChild(node.cloneNode(true));
108 node = node.nextSibling;
111 // For some reason, trac titles start with a newline. Delete it.
112 if (title.firstChild && title.firstChild.nodeType == Node.TEXT_NODE && title.firstChild.textContent.length > 0 && title.firstChild.textContent[0] == "\n")
113 title.firstChild.textContent = title.firstChild.textContent.substring(1);
116 revisionNumber: revisionNumber,
121 description: description
125 _loaded: function(dataDocument)
127 var recordedRevisionNumbers = this.recordedCommits.reduce(function(previousResult, commit) {
128 previousResult[commit.revisionNumber] = commit;
129 return previousResult;
132 var knownCommitsWereUpdated = false;
135 var commitInfoElements = dataDocument.evaluate("/rss/channel/item", dataDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
136 var commitInfoElement;
137 while (commitInfoElement = commitInfoElements.iterateNext()) {
138 var commit = this._convertCommitInfoElementToObject(dataDocument, commitInfoElement);
139 if (commit.revisionNumber in recordedRevisionNumbers) {
140 // Author could have changed, as commit queue replaces it after the fact.
141 console.assert(recordedRevisionNumbers[commit.revisionNumber].revisionNumber === commit.revisionNumber);
142 if (recordedRevisionNumbers[commit.revisionNumber].author != commit.author) {
143 recordedRevisionNumbers[commit.revisionNumber].author = commit.author;
144 knownCommitWasUpdated = true;
147 newCommits.push(commit);
150 if (newCommits.length)
151 this.recordedCommits = newCommits.concat(this.recordedCommits).sort(function(a, b) { return a.revisionNumber - b.revisionNumber; });
153 if (newCommits.length || knownCommitsWereUpdated)
154 this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
157 load: function(fromDate, toDate)
159 loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
160 this._loaded(dataDocument);
161 this.dispatchEventToListeners(Trac.Event.Loaded, [fromDate, toDate]);
162 }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
167 loadXML(this._xmlTimelineURL(), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
170 startPeriodicUpdates: function()
173 this.updateTimer = setInterval(this.update.bind(this), Trac.UpdateInterval);