Support Trac 1.0.x XML timeline link format
[WebKit-https.git] / Tools / BuildSlaveSupport / build.webkit.org-config / public_html / dashboard / Scripts / Trac.js
index ba73443..77bad79 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 Apple Inc. All rights reserved.
+ * Copyright (C) 2013, 2014 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-Trac = function(baseURL)
+Trac = function(baseURL, options)
 {
     BaseObject.call(this);
 
     console.assert(baseURL);
 
     this.baseURL = baseURL;
-    this.recordedCommits = []; // Will be sorted in ascending order.
+    this._needsAuthentication = (typeof options === "object") && options[Trac.NeedsAuthentication] === true;
 
-    this.update();
-    this.updateTimer = setInterval(this.update.bind(this), Trac.UpdateInterval);
+    this.recordedCommits = []; // Will be sorted in ascending order.
 };
 
 BaseObject.addConstructorFunctions(Trac);
 
+Trac.NeedsAuthentication = "needsAuthentication";
 Trac.UpdateInterval = 45000; // 45 seconds
 
 Trac.Event = {
-    NewCommitsRecorded: "new-commits-recorded"
+    CommitsUpdated: "commits-updated",
+    Loaded: "loaded"
 };
 
 Trac.prototype = {
     constructor: Trac,
     __proto__: BaseObject.prototype,
 
+    get oldestRecordedRevisionNumber()
+    {
+        if (!this.recordedCommits.length)
+            return undefined;
+        return this.recordedCommits[0].revisionNumber;
+    },
+
     get latestRecordedRevisionNumber()
     {
         if (!this.recordedCommits.length)
@@ -55,20 +63,41 @@ Trac.prototype = {
         return this.recordedCommits[this.recordedCommits.length - 1].revisionNumber;
     },
 
+    commitsOnBranch: function(branch, filter)
+    {
+        return this.recordedCommits.filter(function(commit) {
+            return (!commit.containsBranchLocation || commit.branch === branch) && filter(commit);
+        });
+    },
+
     revisionURL: function(revision)
     {
         return this.baseURL + "changeset/" + encodeURIComponent(revision);
     },
 
-    _xmlTimelineURL: function()
+    _xmlTimelineURL: function(fromDate, toDate)
     {
-        return this.baseURL + "timeline?changeset=on&max=50&format=rss";
+        console.assert(fromDate <= toDate);
+
+        var fromDay = new Date(fromDate.getFullYear(), fromDate.getMonth(), fromDate.getDate());
+        var toDay = new Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate());
+
+        return this.baseURL + "timeline?changeset=on&format=rss&max=0" +
+            "&from=" +  (toDay.getMonth() + 1) + "%2F" + toDay.getDate() + "%2F" + (toDay.getFullYear() % 100) +
+            "&daysback=" + ((toDay - fromDay) / 1000 / 60 / 60 / 24);
     },
 
     _convertCommitInfoElementToObject: function(doc, commitElement)
     {
         var link = doc.evaluate("./link", commitElement, null, XPathResult.STRING_TYPE).stringValue;
-        var revisionNumber = parseInt(/\d+$/.exec(link))
+
+        // There are multiple link formats for Trac that we support:
+        // https://trac.webkit.org/changeset/190497
+        // http://trac.foobar.com/repository/changeset/75388/project
+        var linkComponents = link.split("/");
+        var revisionNumber = parseInt(linkComponents.pop());
+        if (!revisionNumber)
+            var revisionNumber = parseInt(linkComponents.pop());
 
         function tracNSResolver(prefix)
         {
@@ -82,11 +111,19 @@ Trac.prototype = {
         date = new Date(Date.parse(date));
         var description = doc.evaluate("./description", commitElement, null, XPathResult.STRING_TYPE).stringValue;
 
-        // The feed contains a <title>, but it's not parsed as well as what we are getting from description.
         var parsedDescription = document.createElement("div");
         parsedDescription.innerHTML = description;
+
+        var location = "";
+        if (parsedDescription.firstChild && parsedDescription.firstChild.className === "changes") {
+            // We can extract branch information when trac.ini contains "changeset_show_files=location".
+            location = doc.evaluate("//strong", parsedDescription.firstChild, null, XPathResult.STRING_TYPE).stringValue
+            parsedDescription.removeChild(parsedDescription.firstChild);
+        }
+
+        // The feed contains a <title>, but it's not parsed as well as what we are getting from description.
         var title = document.createElement("div");
-        var node = parsedDescription.firstChild.firstChild;
+        var node = parsedDescription.firstChild ? parsedDescription.firstChild.firstChild : null;
         while (node && node.tagName != "BR") {
             title.appendChild(node.cloneNode(true));
             node = node.nextSibling;
@@ -96,40 +133,128 @@ Trac.prototype = {
         if (title.firstChild && title.firstChild.nodeType == Node.TEXT_NODE && title.firstChild.textContent.length > 0 && title.firstChild.textContent[0] == "\n")
             title.firstChild.textContent = title.firstChild.textContent.substring(1);
 
-        return {
+        var result = {
             revisionNumber: revisionNumber,
             link: link,
             title: title,
             author: author,
             date: date,
-            description: description
+            description: parsedDescription.innerHTML,
+            containsBranchLocation: location !== ""
         };
+
+        if (result.containsBranchLocation) {
+            console.assert(location[location.length - 1] !== "/");
+            location = location += "/";
+            if (location.startsWith("tags/"))
+                result.tag = location.substr(5, location.indexOf("/", 5) - 5);
+            else if (location.startsWith("branches/"))
+                result.branch = location.substr(9, location.indexOf("/", 9) - 9);
+            else if (location.startsWith("releases/"))
+                result.release = location.substr(9, location.indexOf("/", 9) - 9);
+            else if (location.startsWith("trunk/"))
+                result.branch = "trunk";
+            else if (location.startsWith("submissions/"))
+                ; // These changes are never relevant to the dashboard.
+            else {
+                // result.containsBranchLocation remains true, because this commit does
+                // not match any explicitly specified branches.
+                console.assert(false);
+            }
+        }
+
+        return result;
     },
 
-    update: function()
+    _loaded: function(dataDocument)
     {
-        loadXML(this._xmlTimelineURL(), function(dataDocument) {
-            var latestKnownRevision = 0;
-            if (this.recordedCommits.length)
-                latestKnownRevision = this.recordedCommits[this.recordedCommits.length - 1].revisionNumber;
-
-            var newCommits = [];
-
-            var commitInfoElements = dataDocument.evaluate("/rss/channel/item", dataDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
-            var commitInfoElement = undefined;
-            while (commitInfoElement = commitInfoElements.iterateNext()) {
-                var commit = this._convertCommitInfoElementToObject(dataDocument, commitInfoElement);
-                if (commit.revisionNumber == latestKnownRevision)
-                    break;
+        if (!dataDocument)
+            return;
+
+        var recordedRevisionNumbers = this.recordedCommits.reduce(function(previousResult, commit) {
+            previousResult[commit.revisionNumber] = commit;
+            return previousResult;
+        }, {});
+
+        var knownCommitsWereUpdated = false;
+        var newCommits = [];
+
+        var commitInfoElements = dataDocument.evaluate("/rss/channel/item", dataDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
+        var commitInfoElement;
+        while (commitInfoElement = commitInfoElements.iterateNext()) {
+            var commit = this._convertCommitInfoElementToObject(dataDocument, commitInfoElement);
+            if (commit.revisionNumber in recordedRevisionNumbers) {
+                // Author could have changed, as commit queue replaces it after the fact.
+                console.assert(recordedRevisionNumbers[commit.revisionNumber].revisionNumber === commit.revisionNumber);
+                if (recordedRevisionNumbers[commit.revisionNumber].author != commit.author) {
+                    recordedRevisionNumbers[commit.revisionNumber].author = commit.author;
+                    knownCommitWasUpdated = true;
+                }
+            } else
                 newCommits.push(commit);
-            }
-            
-            if (!newCommits.length)
-                return;
+        }
 
-            this.recordedCommits = this.recordedCommits.concat(newCommits.reverse());
+        if (newCommits.length)
+            this.recordedCommits = newCommits.concat(this.recordedCommits).sort(function(a, b) { return a.revisionNumber - b.revisionNumber; });
+
+        if (newCommits.length || knownCommitsWereUpdated)
+            this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
+    },
+
+    load: function(fromDate, toDate)
+    {
+        loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
+            this._loaded(dataDocument);
+            this.dispatchEventToListeners(Trac.Event.Loaded, [fromDate, toDate]);
+        }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
+    },
 
-            this.dispatchEventToListeners(Trac.Event.NewCommitsRecorded, {newCommits: newCommits});
-        }.bind(this));
-    }
+    _update: function()
+    {
+        var fromDate = new Date(this._latestLoadedDate);
+        var toDate = new Date();
+
+        this._latestLoadedDate = toDate;
+
+        loadXML(this._xmlTimelineURL(fromDate, toDate), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
+    },
+
+    startPeriodicUpdates: function()
+    {
+        console.assert(!this._oldestHistoricalDate);
+
+        var today = new Date();
+
+        this._oldestHistoricalDate = today;
+        this._latestLoadedDate = today;
+
+        this._loadingHistoricalData = true;
+        loadXML(this._xmlTimelineURL(today, today), function(dataDocument) {
+            this._loadingHistoricalData = false;
+            this._loaded(dataDocument);
+        }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
+
+        this.updateTimer = setInterval(this._update.bind(this), Trac.UpdateInterval);
+    },
+
+    loadMoreHistoricalData: function()
+    {
+        console.assert(this._oldestHistoricalDate);
+
+        if (this._loadingHistoricalData)
+            return;
+
+        // Load one more day of historical data.
+        var fromDate = new Date(this._oldestHistoricalDate);
+        fromDate.setDate(fromDate.getDate() - 1);
+        var toDate = new Date(fromDate);
+
+        this._oldestHistoricalDate = fromDate;
+
+        this._loadingHistoricalData = true;
+        loadXML(this._xmlTimelineURL(fromDate, toDate), function(dataDocument) {
+            this._loadingHistoricalData = false;
+            this._loaded(dataDocument);
+        }.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
+    },
 };