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 var parsedDescription = document.createElement("div");
102 parsedDescription.innerHTML = description;
105 if (parsedDescription.firstChild && parsedDescription.firstChild.className === "changes") {
106 // We can extract branch information when trac.ini contains "changeset_show_files=location".
107 location = doc.evaluate("//strong", parsedDescription.firstChild, null, XPathResult.STRING_TYPE).stringValue
108 parsedDescription.removeChild(parsedDescription.firstChild);
111 // The feed contains a <title>, but it's not parsed as well as what we are getting from description.
112 var title = document.createElement("div");
113 var node = parsedDescription.firstChild ? parsedDescription.firstChild.firstChild : null;
114 while (node && node.tagName != "BR") {
115 title.appendChild(node.cloneNode(true));
116 node = node.nextSibling;
119 // For some reason, trac titles start with a newline. Delete it.
120 if (title.firstChild && title.firstChild.nodeType == Node.TEXT_NODE && title.firstChild.textContent.length > 0 && title.firstChild.textContent[0] == "\n")
121 title.firstChild.textContent = title.firstChild.textContent.substring(1);
124 revisionNumber: revisionNumber,
129 description: parsedDescription.innerHTML,
130 containsBranchLocation: location !== ""
133 if (result.containsBranchLocation) {
134 console.assert(location[location.length - 1] !== "/");
135 location = location += "/";
136 if (location.startsWith("tags/"))
137 result.tag = location.substr(5, location.indexOf("/", 5) - 5);
138 else if (location.startsWith("branches/"))
139 result.branch = location.substr(9, location.indexOf("/", 9) - 9);
140 else if (location.startsWith("releases/"))
141 result.release = location.substr(9, location.indexOf("/", 9) - 9);
142 else if (location.startsWith("trunk/"))
143 result.branch = "trunk";
145 console.assert(false);
146 result.containsBranchLocation = false;
153 _loaded: function(dataDocument)
158 var recordedRevisionNumbers = this.recordedCommits.reduce(function(previousResult, commit) {
159 previousResult[commit.revisionNumber] = commit;
160 return previousResult;
163 var knownCommitsWereUpdated = false;
166 var commitInfoElements = dataDocument.evaluate("/rss/channel/item", dataDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
167 var commitInfoElement;
168 while (commitInfoElement = commitInfoElements.iterateNext()) {
169 var commit = this._convertCommitInfoElementToObject(dataDocument, commitInfoElement);
170 if (commit.revisionNumber in recordedRevisionNumbers) {
171 // Author could have changed, as commit queue replaces it after the fact.
172 console.assert(recordedRevisionNumbers[commit.revisionNumber].revisionNumber === commit.revisionNumber);
173 if (recordedRevisionNumbers[commit.revisionNumber].author != commit.author) {
174 recordedRevisionNumbers[commit.revisionNumber].author = commit.author;
175 knownCommitWasUpdated = true;
178 newCommits.push(commit);
181 if (newCommits.length)
182 this.recordedCommits = newCommits.concat(this.recordedCommits).sort(function(a, b) { return a.revisionNumber - b.revisionNumber; });
184 if (newCommits.length || knownCommitsWereUpdated)
185 this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
188 load: function(fromDate, toDate)
190 loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
191 this._loaded(dataDocument);
192 this.dispatchEventToListeners(Trac.Event.Loaded, [fromDate, toDate]);
193 }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
198 loadXML(this._xmlTimelineURL(), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
201 startPeriodicUpdates: function()
204 this.updateTimer = setInterval(this.update.bind(this), Trac.UpdateInterval);