2 * Copyright (C) 2013 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 this.updateTimer = setInterval(this.update.bind(this), Trac.UpdateInterval);
41 BaseObject.addConstructorFunctions(Trac);
43 Trac.NeedsAuthentication = "needsAuthentication";
44 Trac.UpdateInterval = 45000; // 45 seconds
47 CommitsUpdated: "commits-updated"
52 __proto__: BaseObject.prototype,
54 get latestRecordedRevisionNumber()
56 if (!this.recordedCommits.length)
58 return this.recordedCommits[this.recordedCommits.length - 1].revisionNumber;
61 revisionURL: function(revision)
63 return this.baseURL + "changeset/" + encodeURIComponent(revision);
66 _xmlTimelineURL: function(fromDate, toDate)
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")
76 console.assert(fromDate <= toDate);
78 var fromDay = new Date(fromDate.getFullYear(), fromDate.getMonth(), fromDate.getDate());
79 var toDay = new Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate());
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);
86 _convertCommitInfoElementToObject: function(doc, commitElement)
88 var link = doc.evaluate("./link", commitElement, null, XPathResult.STRING_TYPE).stringValue;
89 var revisionNumber = parseInt(/\d+$/.exec(link))
91 function tracNSResolver(prefix)
94 return "http://purl.org/dc/elements/1.1/";
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;
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;
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);
118 revisionNumber: revisionNumber,
123 description: description
127 _loaded: function(dataDocument)
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;
136 var knownCommitsWereUpdated = false;
138 var newCommitsBeforeEarliestKnownRevision = [];
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);
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;
163 console.assert(indexInRecordedCommits >= 0);
164 console.assert(this.recordedCommits[indexInRecordedCommits].revisionNumber === commit.revisionNumber);
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;
171 --indexInRecordedCommits;
175 if (newCommits.length || newCommitsBeforeEarliestKnownRevision.length)
176 this.recordedCommits = newCommitsBeforeEarliestKnownRevision.reverse().concat(this.recordedCommits, newCommits.reverse());
178 if (newCommits.length || newCommitsBeforeEarliestKnownRevision.length || knownCommitsWereUpdated)
179 this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
184 loadXML(this._xmlTimelineURL(), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});