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 4aac4db..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
@@ -33,9 +33,6 @@ Trac = function(baseURL, options)
     this._needsAuthentication = (typeof options === "object") && options[Trac.NeedsAuthentication] === true;
 
     this.recordedCommits = []; // Will be sorted in ascending order.
-
-    this.update();
-    this.updateTimer = setInterval(this.update.bind(this), Trac.UpdateInterval);
 };
 
 BaseObject.addConstructorFunctions(Trac);
@@ -44,13 +41,21 @@ Trac.NeedsAuthentication = "needsAuthentication";
 Trac.UpdateInterval = 45000; // 45 seconds
 
 Trac.Event = {
-    CommitsUpdated: "commits-updated"
+    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)
@@ -58,6 +63,13 @@ 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);
@@ -65,20 +77,12 @@ Trac.prototype = {
 
     _xmlTimelineURL: function(fromDate, toDate)
     {
-        if (typeof fromDate === "undefined") {
-            fromDate = new Date();
-            toDate = new Date(fromDate);
-            // By default, get at least one full day of changesets, as the current day may have only begun.
-            fromDate.setDate(fromDate.getDate() - 1);
-        } else if (typeof toDate === "undefined")
-            toDate = fromDate;
-
         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=-1" +
+        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);
     },
@@ -86,7 +90,14 @@ Trac.prototype = {
     _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)
         {
@@ -100,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;
@@ -114,73 +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;
     },
 
     _loaded: function(dataDocument)
     {
-        var earliestKnownRevision = 0;
-        var latestKnownRevision = 0;
-        if (this.recordedCommits.length) {
-            earliestKnownRevision = this.recordedCommits[0].revisionNumber;
-            latestKnownRevision = this.recordedCommits[this.recordedCommits.length - 1].revisionNumber;
-        }
+        if (!dataDocument)
+            return;
+
+        var recordedRevisionNumbers = this.recordedCommits.reduce(function(previousResult, commit) {
+            previousResult[commit.revisionNumber] = commit;
+            return previousResult;
+        }, {});
 
         var knownCommitsWereUpdated = false;
         var newCommits = [];
-        var newCommitsBeforeEarliestKnownRevision = [];
 
         var commitInfoElements = dataDocument.evaluate("/rss/channel/item", dataDocument, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
-        var commitInfoElement = undefined;
-        var indexInRecordedCommits = undefined;
+        var commitInfoElement;
         while (commitInfoElement = commitInfoElements.iterateNext()) {
             var commit = this._convertCommitInfoElementToObject(dataDocument, commitInfoElement);
-            if (commit.revisionNumber > latestKnownRevision) {
-                console.assert(typeof indexInRecordedCommits === "undefined");
-                newCommits.push(commit);
-            } else if (commit.revisionNumber < earliestKnownRevision) {
-                console.assert(typeof indexInRecordedCommits === "undefined" || indexInRecordedCommits === -1);
-                newCommitsBeforeEarliestKnownRevision.push(commit);
-            } else {
-                if (typeof indexInRecordedCommits === "undefined") {
-                    // We could have started anywhere in the recorded commits array, let's find where.
-                    // With periodic updates, this will be the latest recorded commit, so starting from the end.
-                    for (var i = this.recordedCommits.length - 1; i >= 0; --i) {
-                        if (this.recordedCommits[i].revisionNumber === commit.revisionNumber) {
-                            indexInRecordedCommits = i;
-                            break;
-                        }
-                    }
-                }
-
-                console.assert(indexInRecordedCommits >= 0);
-                console.assert(this.recordedCommits[indexInRecordedCommits].revisionNumber === commit.revisionNumber);
-
+            if (commit.revisionNumber in recordedRevisionNumbers) {
                 // Author could have changed, as commit queue replaces it after the fact.
-                if (this.recordedCommits[indexInRecordedCommits].author !== commit.author) {
-                    this.recordedCommits[indexInRecordedCommits].author = commit.author;
+                console.assert(recordedRevisionNumbers[commit.revisionNumber].revisionNumber === commit.revisionNumber);
+                if (recordedRevisionNumbers[commit.revisionNumber].author != commit.author) {
+                    recordedRevisionNumbers[commit.revisionNumber].author = commit.author;
                     knownCommitWasUpdated = true;
                 }
-                --indexInRecordedCommits;
-            }
+            } else
+                newCommits.push(commit);
         }
 
-        if (newCommits.length || newCommitsBeforeEarliestKnownRevision.length)
-            this.recordedCommits = newCommitsBeforeEarliestKnownRevision.reverse().concat(this.recordedCommits, newCommits.reverse());
+        if (newCommits.length)
+            this.recordedCommits = newCommits.concat(this.recordedCommits).sort(function(a, b) { return a.revisionNumber - b.revisionNumber; });
 
-        if (newCommits.length || newCommitsBeforeEarliestKnownRevision.length || knownCommitsWereUpdated)
+        if (newCommits.length || knownCommitsWereUpdated)
             this.dispatchEventToListeners(Trac.Event.CommitsUpdated, null);
     },
 
-    update: function()
+    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 } : {});
+    },
+
+    _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()
     {
-        loadXML(this._xmlTimelineURL(), this._loaded.bind(this), this._needsAuthentication ? { withCredentials: true } : {});
-    }
+        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 } : {});
+    },
 };