Revert "Make chumpdetector plugin compatible with Polymer2"
This reverts commit 158f1f51260ca59a2dc8b9461bfdfe9ca510d710.
Reason for revert: copybara import failed. need to fix
Change-Id: Iad07565b4d2abde9207900617023d53775efb7b3
Reviewed-on: https://chromium-review.googlesource.com/c/infra/gerrit-plugins/chumpdetector/+/1762801
Reviewed-by: Edward Lesmes <ehmaldonado@chromium.org>
diff --git a/BUILD b/BUILD
index 891be4d..305398c 100644
--- a/BUILD
+++ b/BUILD
@@ -14,6 +14,6 @@
polygerrit_plugin(
name = "chumpdetector_ui",
- app = "src/main/resources/static/chumpdetector.html",
+ app = "src/main/resources/static/chumpdetector.js",
plugin_name = "chumpdetector",
)
diff --git a/src/main/java/com/googlesource/chromium/plugins/chumpdetector/ChumpDetectorModule.java b/src/main/java/com/googlesource/chromium/plugins/chumpdetector/ChumpDetectorModule.java
index b95077a..66efd39 100644
--- a/src/main/java/com/googlesource/chromium/plugins/chumpdetector/ChumpDetectorModule.java
+++ b/src/main/java/com/googlesource/chromium/plugins/chumpdetector/ChumpDetectorModule.java
@@ -14,7 +14,7 @@
public class ChumpDetectorModule extends RestApiModule {
@Override
protected void configure() {
- DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(WebUiPlugin.js("chumpdetector.html"));
+ DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(WebUiPlugin.js("chumpdetector.js"));
get(PROJECT_KIND, "config").to(GetProjectChumpConfig.class);
}
diff --git a/src/main/resources/static/chumpdetector.html b/src/main/resources/static/chumpdetector.html
deleted file mode 100644
index a564a9d..0000000
--- a/src/main/resources/static/chumpdetector.html
+++ /dev/null
@@ -1,530 +0,0 @@
-// Copyright 2019 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-<dom-module id="common-helpers">
- <script>
- // Milliseconds between tree status checks.
- const STATUS_INTERVAL_MS = 60 * 1000;
-
- // Date used in status if real one can't be fetched.
- const UNKNOWN_DATE = new Date(0);
-
- // Gerrit JSON response prefix.
- const JSON_PREFIX = ')]}\'';
-
- // Placeholder status when actual one is unknown yet.
- const UNKNOWN_TREE_STATUS = {
- isOpen: false,
- generalState: 'unknown',
- message: 'Tree status unknown.',
- username: 'Pending',
- date: UNKNOWN_DATE,
- url: null
- };
-
- // Placeholder status when we're waiting for an answer.
- const PENDING_TREE_STATUS = {
- isOpen: false,
- generalState: 'unknown',
- message: 'Waiting for tree status...',
- username: 'Pending',
- date: UNKNOWN_DATE,
- url: null
- };
-
- // Placeholder status when the current branch is not the trunk.
- const DISABLED_BRANCH_TREE_STATUS = {
- isOpen: true,
- generalState: 'unknown',
- message: 'No tree status for this branch.',
- username: 'Warning',
- date: UNKNOWN_DATE,
- url: null
- };
-
-
- // A message div to add inside the confirm submit dialog.
- let confirmSubmitMessage = document.createElement('p');
-
- const MESSAGE_STYLES = {
- styleConfirmSubmit: 'color: red; width: 400px;',
- styleHidden: 'display: none;',
- }
-
- /**
- * Discard any the message in the confirm submit dialog.
- * (No message to show.)
- */
- const disableConfirmSubmitMessage = function() {
- confirmSubmitMessage.style = MESSAGE_STYLES.styleHidden;
- confirmSubmitMessage.textContent = '';
- }
-
- /**
- * Set the message that will be shown in the confirm submit dialog.
- * @param {string} message The message to display.
- */
- const setConfirmSubmitMessage = function(message) {
- confirmSubmitMessage.style = MESSAGE_STYLES.styleConfirmSubmit;
- confirmSubmitMessage.textContent = message;
- }
-
- // Cache of StatusFetchers. Config's treeName -> StatusFetcher.
- let fetchers = {};
- // TreeStatus object that correspond to current change.
- let installedTreeStatus = null;
-
- // Fetches JSON from URL, returns Promise.
- const fetchJSON = async (url, options) => {
- const response = await fetch(url, options);
- if (response.status == 204 || !response.ok) {
- console.error(`${url} returned with status ${response.status}.`);
- return null;
- }
- let responseText = await response.text();
- try {
- if (responseText.startsWith(JSON_PREFIX)) {
- responseText = responseText.substring(JSON_PREFIX.length);
- }
- return JSON.parse(responseText);
- } catch (e) {
- console.error(`${url} returned invalid JSON '${responseText}'`);
- return null;
- }
- };
-
- /**
- * Returns configuration for a tree status (status URL to use, login URL, etc.)
- * @param {string} project Gerrit project name to search config for
- * @return {Promise} Promise, resolving to project config.
- */
- const getProjectConfig = function(project) {
- // Expected format of a project config:
- // {
- // treeName: English label to describe the project, eg: 'Chrome'.
- // viewURL: URL to the user facing status, eg: 'http://foo/'
- // statusURL: URL to the tree status, eg: 'http://foo/current?format=json'
- // withCredentials: True if status requests require HTTP cookies.
- // disabledBranchPattern: Regex object or string pattern to exclude a branch,
- // enforceCommitQueue: True if project is using Commit Queue
- // }
-
- // If we've fetched it already, use the config set on the window.
- let config = window.chumpDetectorConfig;
- if (config !== undefined) {
- return Promise.resolve(config);
- }
-
- //Otherwise, fetch it and store it for reuse.
- let chumpdetectorURL = `/projects/${encodeURIComponent(project)}` +
- `/chumpdetector~config`;
- return fetchJSON(chumpdetectorURL).then(function(cfg) {
- window.chumpDetectorConfig = cfg || null;
- if (!window.chumpDetectorConfig) {
- console.log('Chump detector is not configured.');
- return null;
- }
- return getProjectConfig(project);
- });
- }
-
- /**
- * Creates a new one or returns existing StatusFetcher object.
- * @param {Object} config Project configuration as returned by getProjectConfig
- * @return {Object} StatusFetcher object
- */
- const getStatusFetcher = function(config) {
- if (!fetchers.hasOwnProperty(config.treeName)) {
- fetchers[config.treeName] = new StatusFetcher(config.viewURL,
- config.statusURL,
- config.withCredentials,
- config.loginURL);
- }
- return fetchers[config.treeName];
- }
- </script>
-</dom-module>
-
-<dom-module id="tree-status">
- <script>
- const TREE_STYLES = {
- styleOuterDiv:
- 'align-items: center;' +
- 'display: flex;' +
- 'font-size: 1.1em;' +
- 'justify-content: center;' +
- 'margin: 5px 0 8px;' +
- 'padding: .4em;' +
- 'max-width: 20em;',
- unknownStatus:
- 'color: black;' +
- 'background-color: #FFFC6C;',
- openStatus:
- 'color: black;' +
- 'background-color: #BCE889;',
- closedStatus:
- 'color: white;' +
- 'background-color: #E98080;',
- };
-
- /**
- * Tree status UI for some single change.
- * @param {Object} change Change object provided by Gerrit
- * @param {Object} config Project config this change belongs to
- */
- const TreeStatus = function(change, config) {
- this.change = change;
- this.config = config;
- this.treeStatus = null;
-
- this.fetcher = null;
- this.fetcherCallback = null;
- this.installed = false;
-
- // Build DOM for tree status UI.
- this.outerDiv = document.createElement('div');
- this.outerDiv.style = TREE_STYLES.styleOuterDiv;
- this.innerDiv = document.createElement('div');
- this.outerDiv.appendChild(this.innerDiv);
-
- // Set default state to 'Unknown'.
- this.setTreeStatus(UNKNOWN_TREE_STATUS);
- };
-
- /**
- * Add DOM elements to the document, subscribe to status fetcher
- * notifications.
- * @param {Element} element to hook the tree-status to.
- * @param {Object} fetcher StatusFetcher object that fetches tree status
- */
- TreeStatus.prototype.install = function(hookElement, fetcher) {
- if (!this.installed) {
- console.log('Installing tree status for change:', this.change.id);
- hookElement.appendChild(this.outerDiv);
- this.fetcher = fetcher;
- if (this.fetcher) {
- this.fetcherCallback = this.setTreeStatus.bind(this);
- this.fetcher.subscribe(this.fetcherCallback);
- }
- this.installed = true;
- }
- };
-
- /**
- * Remove DOM elements from the document, unsubscribe from status fetcher.
- */
- TreeStatus.prototype.uninstall = function() {
- if (this.installed) {
- console.log('Uninstalling tree status for change:', this.change.id);
- if (this.fetcher) {
- this.fetcher.unsubscribe(this.fetcherCallback);
- this.fetcher = null;
- this.fetcherCallback = null;
- }
- if (this.outerDiv.parentNode) {
- this.outerDiv.parentNode.removeChild(this.outerDiv);
- }
- this.installed = false;
- }
- };
-
- /**
- * Update DOM to display given tree status.
- * @param {Object} treeStatus Tree status object produced by StatusFetcher
- */
- TreeStatus.prototype.setTreeStatus = function(treeStatus) {
- this.treeStatus = treeStatus;
- // Update content.
- if (!treeStatus.url) {
- this.innerDiv.textContent = `${treeStatus.username}: ${treeStatus.message}`;
- } else {
- // Create <a></a>.
- let anchor = document.createElement('a');
- anchor.href = treeStatus.url;
- anchor.target = '_blank';
- anchor.innerText = treeStatus.message;
- // Replace whatever in innerDiv with that link.
- this.innerDiv.textContent = '';
- this.innerDiv.appendChild(anchor);
- }
-
- // Update CSS style.
- let style = (treeStatus.generalState || 'unknown') + 'Status';
- style = TREE_STYLES[style] || TREE_STYLES.unknownStatus;
- this.outerDiv.style = TREE_STYLES.styleOuterDiv + style;
- }
- </script>
-</dom-module>
-
-<dom-module id="status-fetcher">
- <script>
- /**
- * Periodically fetches tree status for some project.
- * @param {string} viewURL URL to link to normally for users to navigate to
- * @param {string} statusURL URL to fetch status from
- * @param {boolean} withCredentials True to send cookies with the request
- * @param {string} loginURL URL to use for login (if required)
- */
- const StatusFetcher = function(viewURL, statusURL, withCredentials, loginURL) {
- this.viewURL = viewURL;
- this.statusURL = statusURL;
- this.withCredentials = withCredentials;
- this.loginURL = loginURL;
- this.lastKnownStatus = PENDING_TREE_STATUS;
- this.callbacks = [];
- this.request = null;
- this.timer = null;
- };
-
- /**
- * Register callback to be called when tree status is fetched.
- * @param {Function} callback Will be called with single treeStatus argument
- */
- StatusFetcher.prototype.subscribe = function(callback) {
- this.callbacks.push(callback);
- };
-
- /**
- * Removes previously registered callback.
- * @param {Function} callback Exact same function object that was registered
- */
- StatusFetcher.prototype.unsubscribe = function(callback) {
- const index = this.callbacks.indexOf(callback);
- if (index != -1) {
- this.callbacks.splice(index, 1);
- }
- };
-
- /**
- * Asynchronously fetches (or refetches) tree status and calls callbacks.
- */
- StatusFetcher.prototype.fetch = function() {
- // Already fetching?
- if (this.request) {
- return;
- }
- // Fetch restarts refetch timer (by launching it again when request finishes).
- if (this.timer) {
- clearTimeout(this.timer);
- this.timer = null;
- }
- // Launch async GET.
- this.lastKnownStatus = PENDING_TREE_STATUS;
- this.request = new XMLHttpRequest();
- var that = this;
- this.request.onreadystatechange = function() {
- if (this.readyState == 4) {
- console.log('Fetched', that.statusURL);
- that.request = null;
- that.onFetchCompleted.call(that, this);
- }
- };
- this.request.open('GET', this.statusURL);
- this.request.withCredentials = this.withCredentials;
- this.request.send();
- };
-
- /**
- * Called when HTTP request finishes (successfully or not).
- * @param {Object} request Completed XMLHttpRequest object.
- */
- StatusFetcher.prototype.onFetchCompleted = function(request) {
- let treeStatus;
- if (request.status == 200) {
- try {
- let result = JSON.parse(request.responseText);
- treeStatus = {
- isOpen: result.can_commit_freely,
- generalState: result.general_state,
- message: result.message,
- username: result.username,
- date: new Date(result.date.replace(' ', 'T')),
- url: this.viewURL
- };
- } catch (ex) {
- console.log('Error parsing json: ' + ex);
- treeStatus = {
- isOpen: false,
- generalState: 'unknown',
- message: String(ex),
- username: 'ParseError',
- date: UNKNOWN_DATE,
- url: this.viewURL
- };
- }
- } else {
- let message;
- let url;
- if (this.withCredentials) {
- url = this.loginURL;
- message = 'Login required';
- } else {
- url = this.viewURL;
- message = (request.statusText ||
- 'Error ' + request.status + ' requesting tree status');
- }
- treeStatus = {
- isOpen: false,
- generalState: 'unknown',
- message: message,
- username: 'RequestError',
- date: UNKNOWN_DATE,
- url: url
- };
- }
- // Remember this status until next refetch cycle.
- this.lastKnownStatus = treeStatus;
- // Iterate over a copy. Callbacks may modify the list during iteration.
- let copy = this.callbacks.slice(0);
- for (let i = 0; i < copy.length; i++) {
- copy[i](treeStatus);
- }
- // Auto refetch status later (if still have subscribers).
- var that = this;
- this.timer = setTimeout(function() {
- that.timer = null;
- // Known status is too old. Better to forget it.
- that.lastKnownStatus = PENDING_TREE_STATUS;
- if (that.callbacks.length) {
- that.fetch();
- }
- }, STATUS_INTERVAL_MS);
- };
-
- </script>
-</dom-module>
-
-<dom-module>
- <script>
- const installTreeStatus = function(element) {
- const change = element.change;
- const revision = element.revision;
-
- // Uninstall previous one (if any).
- if (installedTreeStatus) {
- installedTreeStatus.uninstall();
- installedTreeStatus = null;
- }
-
- getProjectConfig(change.project).then(config => {
- if (!config || !config.statusURL) {
- console.log('No status url for this project.');
- return;
- } else if (config.disabled) {
- console.log('Chumpdetector is disabled for this project.');
- return;
- }
- // Install new one if the project is supported.
- let fetcher;
- if (config.disabledBranchPattern &&
- new RegExp(config.disabledBranchPattern).test(change.branch)) {
- console.log('Disabling chump detector based on branch name.');
- fetcher = null;
- } else {
- console.log(`Using status url: ${config.statusURL}`);
- fetcher = getStatusFetcher(config);
- }
-
- // Bind tree status to status fetcher, perform initial fetch.
- installedTreeStatus = new TreeStatus(change, config);
- installedTreeStatus.install(element, fetcher);
- if (fetcher) {
- installedTreeStatus.setTreeStatus(fetcher.lastKnownStatus);
- fetcher.fetch();
- } else {
- installedTreeStatus.setTreeStatus(DISABLED_BRANCH_TREE_STATUS);
- }
- }, function() {
- console.log('Error fetching config.');
- });
- }
- </script>
-</dom-module>
-
-<dom-module id="uninstall-tree">
- <script>
- const maybeUninstallTreeStatus = function(token) {
- // Navigated away from a change page? Uninstall tree status UI.
- if (token.substring(0, 2) != '/c' && installedTreeStatus) {
- installedTreeStatus.uninstall();
- installedTreeStatus = null;
- }
- }
- </script>
-</dom-module>
-
-<dom-module id="confirm-message">
- <script>
- const installConfirmMessage = function(element) {
- element.appendChild(confirmSubmitMessage);
- }
- </script>
-</dom-module>
-
-<dom-module id="check-tree">
- <script>
- const checkTreeBeforeSubmit = function() {
- if (!installedTreeStatus) {
- disableConfirmSubmitMessage();
- return true;
- }
-
- let treeStatus = installedTreeStatus.treeStatus;
- let config = installedTreeStatus.config;
-
- // Change is in unsupported branch, do not block it.
- if (treeStatus == DISABLED_BRANCH_TREE_STATUS) {
- disableConfirmSubmitMessage();
- return true;
- }
-
- // Change should be submitted via CQ (not directly via Gerrit).
- //if (config.enforceCommitQueue) {
- if (config.enforceCommitQueue) {
- setConfirmSubmitMessage(
- `The ${config.treeName} project uses the commit queue (CQ). ` +
- `You should submit your change by clicking "Reply" and setting the ` +
- `"Commit-Queue" label to the appropriate value.\n` +
- `Do you want to commit the change directly, bypassing CQ (dangerous)?`);
- return true;
- }
-
- // Tree is open, do not block the change.
- if (treeStatus.isOpen) {
- disableConfirmSubmitMessage();
- return true;
- }
-
- // Unknown tree state. Double check with the user before submitting.
- if (treeStatus.generalState == 'unknown') {
- setConfirmSubmitMessage(
- `${config.treeName} tree status is unknown, submitting this ` +
- `change now might be dangerous. Submit anyway?`);
- return true;
- }
-
- // Tree is closed (or in some entirely unexpected state). Warn the user.
- setConfirmSubmitMessage(
- `The ${config.treeName} tree is closed, submitting this ` +
- `change is dangerous. Submit anyway?`);
- return true;
- }
- </script>
-</dom-module>
-
-<dom-module id="install-plugin">
- <script>
- Gerrit.install(plugin => {
- plugin.hook('change-metadata-item').onAttached(installTreeStatus);
- // Called by Gerrit whenever document.location changes.
- // @param {string} token Fragment part of a page URL
- plugin.on('history', maybeUninstallTreeStatus);
- plugin.hook('confirm-submit-change').onAttached(
- installConfirmMessage);
- // Called by gerrit whenever 'Submit' is clicked.
- // @return {boolean} False to block submit
- plugin.on('submitchange', checkTreeBeforeSubmit);
- });
- </script>
-</dom-module>
diff --git a/src/main/resources/static/chumpdetector.js b/src/main/resources/static/chumpdetector.js
new file mode 100644
index 0000000..1d865e0
--- /dev/null
+++ b/src/main/resources/static/chumpdetector.js
@@ -0,0 +1,577 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+Gerrit.install(function(self) {
+
+
+/**
+ * Milliseconds between tree status checks.
+ */
+var STATUS_INTERVAL_MS = 60 * 1000;
+
+
+/**
+ * Date used in status if real one can't be fetched.
+ */
+var UNKNOWN_DATE = new Date(0);
+
+
+/**
+ * Gerrit JSON response prefix.
+ */
+var JSON_PREFIX = ')]}\'';
+
+
+/**
+ * Placeholder status when actual one is unknown yet.
+ */
+var UNKNOWN_TREE_STATUS = {
+ isOpen: false,
+ generalState: 'unknown',
+ message: 'Tree status unknown.',
+ username: 'Pending',
+ date: UNKNOWN_DATE,
+ url: null
+};
+
+
+/**
+ * Placeholder status when we're waiting for an answer.
+ */
+var PENDING_TREE_STATUS = {
+ isOpen: false,
+ generalState: 'unknown',
+ message: 'Waiting for tree status...',
+ username: 'Pending',
+ date: UNKNOWN_DATE,
+ url: null
+};
+
+
+/**
+ * Placeholder status when the current branch is not the trunk.
+ */
+var DISABLED_BRANCH_TREE_STATUS = {
+ isOpen: true,
+ generalState: 'unknown',
+ message: 'No tree status for this branch.',
+ username: 'Warning',
+ date: UNKNOWN_DATE,
+ url: null
+};
+
+
+// Register CSS classes only once, during plugin loading.
+var STYLES = {
+ // Base style for div that contains tree status box.
+ outerDiv: Gerrit.css(
+ 'align-items: center;' +
+ 'display: flex;' +
+ 'font-size: 1.1em;' +
+ 'justify-content: center;' +
+ 'margin: 5px 0 8px;' +
+ 'padding: .4em;'),
+
+ // Additional styles for PolyGerrit UI.
+ outerDivPolyGerrit: Gerrit.css('max-width: 20em;'),
+
+ // Tree status is not yet known.
+ unknownStatus: Gerrit.css(
+ 'color: black;' +
+ 'background-color: #FFFC6C;'),
+
+ // Tree is open.
+ openStatus: Gerrit.css(
+ 'color: black;' +
+ 'background-color: #BCE889;'),
+
+ // Tree is closed.
+ closedStatus: Gerrit.css(
+ 'color: white;' +
+ 'background-color: #E98080;'),
+
+ confirmSubmitMessage: Gerrit.css(
+ 'color: red;' +
+ 'width: 400px;'),
+
+ hiddenConfirmSubmitMessage: Gerrit.css(
+ 'display: none;')
+};
+
+
+/**
+ * Tree status UI for some single change.
+ * @param {Object} change Change object provided by Gerrit
+ * @param {Object} config Project config this change belongs to
+ */
+var TreeStatus = function(change, config) {
+ this.change = change;
+ this.config = config;
+ this.treeStatus = null;
+
+ this.fetcher = null;
+ this.fetcherCallback = null;
+ this.installed = false;
+
+ // Build DOM for tree status UI.
+ this.outerDiv = document.createElement('div');
+ this.outerDiv.classList.add(STYLES.outerDiv);
+ if (window.Polymer) {
+ this.outerDiv.classList.add(STYLES.outerDivPolyGerrit);
+ }
+ this.innerDiv = document.createElement('div');
+ this.outerDiv.appendChild(this.innerDiv);
+
+ // Set default state to 'Unknown'.
+ this.setTreeStatus(UNKNOWN_TREE_STATUS);
+};
+
+
+/**
+ * Add DOM elements to the document, subscribe to status fetcher notifications.
+ * @param {Object} fetcher StatusFetcher object that fetches tree status
+ */
+TreeStatus.prototype.install = function(fetcher) {
+ if (!this.installed) {
+ console.log('Installing tree status for change:', this.change.id);
+ document.getElementById('change_plugins').appendChild(this.outerDiv);
+ this.fetcher = fetcher;
+ if (this.fetcher) {
+ this.fetcherCallback = this.setTreeStatus.bind(this);
+ this.fetcher.subscribe(this.fetcherCallback);
+ }
+ this.installed = true;
+ }
+};
+
+
+/**
+ * Remove DOM elements from the document, unsubscribe from status fetcher.
+ */
+TreeStatus.prototype.uninstall = function() {
+ if (this.installed) {
+ console.log('Uninstalling tree status for change:', this.change.id);
+ if (this.fetcher) {
+ this.fetcher.unsubscribe(this.fetcherCallback);
+ this.fetcher = null;
+ this.fetcherCallback = null;
+ }
+ if (this.outerDiv.parentNode)
+ this.outerDiv.parentNode.removeChild(this.outerDiv);
+ this.installed = false;
+ }
+};
+
+
+/**
+ * Update DOM to display given tree status.
+ * @param {Object} treeStatus Tree status object produced by StatusFetcher
+ */
+TreeStatus.prototype.setTreeStatus = function(treeStatus) {
+ this.treeStatus = treeStatus;
+
+ // Update content.
+ if (!treeStatus.url) {
+ this.innerDiv.textContent = treeStatus.username + ': ' + treeStatus.message;
+ } else {
+ // Create <a></a>.
+ var anchor = document.createElement('a');
+ anchor.href = treeStatus.url;
+ anchor.target = '_blank';
+ anchor.innerText = treeStatus.message;
+ // Replace whatever in innerDiv with that link.
+ this.innerDiv.textContent = '';
+ this.innerDiv.appendChild(anchor);
+ }
+
+ // Update CSS style.
+ var styleName = (treeStatus.generalState || 'unknown') + 'Status';
+ var cssClass = STYLES[styleName] || STYLES.unknownStatus;
+ this.outerDiv.className = STYLES.outerDiv + ' ' + cssClass;
+ if (window.Polymer) {
+ this.outerDiv.classList.add(STYLES.outerDivPolyGerrit);
+ }
+};
+
+
+/**
+ * Periodically fetches tree status for some project.
+ * @param {string} viewURL URL to link to normally for users to navigate to
+ * @param {string} statusURL URL to fetch status from
+ * @param {boolean} withCredentials True to send cookies with the request
+ */
+var StatusFetcher = function(viewURL, statusURL, withCredentials) {
+ this.viewURL = viewURL;
+ this.statusURL = statusURL;
+ this.withCredentials = withCredentials;
+ this.lastKnownStatus = PENDING_TREE_STATUS;
+
+ this.callbacks = [];
+ this.request = null;
+ this.timer = null;
+};
+
+
+/**
+ * Register callback to be called when tree status is fetched.
+ * @param {Function} callback Will be called with single treeStatus argument
+ */
+StatusFetcher.prototype.subscribe = function(callback) {
+ this.callbacks.push(callback);
+};
+
+
+/**
+ * Removes previously registered callback.
+ * @param {Function} callback Exact same function object that was registered
+ */
+StatusFetcher.prototype.unsubscribe = function(callback) {
+ var index = this.callbacks.indexOf(callback);
+ if (index != -1) {
+ this.callbacks.splice(index, 1);
+ }
+};
+
+
+/**
+ * Asynchronously fetches (or refetches) tree status and calls callbacks.
+ */
+StatusFetcher.prototype.fetch = function() {
+ // Already fetching?
+ if (this.request)
+ return;
+
+ // Fetch restarts refetch timer (by launching it again when request finishes).
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+
+ // Launch async GET.
+ this.lastKnownStatus = PENDING_TREE_STATUS;
+ this.request = new XMLHttpRequest();
+
+ var that = this;
+ this.request.onreadystatechange = function() {
+ if (this.readyState == 4) {
+ console.log('Fetched', that.statusURL);
+ that.request = null;
+ that.onFetchCompleted.call(that, this);
+ }
+ };
+
+ this.request.open('GET', this.statusURL);
+ this.request.withCredentials = this.withCredentials;
+ this.request.send();
+};
+
+
+/**
+ * Called when HTTP request finishes (successfully or not).
+ * @param {Object} request Completed XMLHttpRequest object.
+ */
+StatusFetcher.prototype.onFetchCompleted = function(request) {
+ var treeStatus;
+ if (request.status == 200) {
+ try {
+ var result = JSON.parse(request.responseText);
+ treeStatus = {
+ isOpen: result.can_commit_freely,
+ generalState: result.general_state,
+ message: result.message,
+ username: result.username,
+ date: new Date(result.date.replace(' ', 'T')),
+ url: this.viewURL
+ };
+ } catch (ex) {
+ console.log('Error parsing json: ' + ex);
+ treeStatus = {
+ isOpen: false,
+ generalState: 'unknown',
+ message: String(ex),
+ username: 'ParseError',
+ date: UNKNOWN_DATE,
+ url: this.viewURL
+ };
+ }
+ } else {
+ var message;
+ var url;
+ if (this.withCredentials) {
+ message = 'Login required';
+ } else {
+ url = this.viewURL;
+ message = (request.statusText ||
+ 'Error ' + request.status + ' requesting tree status');
+ }
+ treeStatus = {
+ isOpen: false,
+ generalState: 'unknown',
+ message: message,
+ username: 'RequestError',
+ date: UNKNOWN_DATE,
+ url: url
+ };
+ }
+
+ // Remember this status until next refetch cycle.
+ this.lastKnownStatus = treeStatus;
+
+ // Iterate over a copy. Callbacks may modify the list during iteration.
+ var copy = this.callbacks.slice(0);
+ for (var i = 0; i < copy.length; i++) {
+ copy[i](treeStatus);
+ }
+
+ // Auto refetch status later (if still have subscribers).
+ var that = this;
+ this.timer = setTimeout(function() {
+ that.timer = null;
+ // Known status is too old. Better to forget it.
+ that.lastKnownStatus = PENDING_TREE_STATUS;
+ if (that.callbacks.length) {
+ that.fetch();
+ }
+ }, STATUS_INTERVAL_MS);
+};
+
+
+
+// Cache of StatusFetchers. Config's treeName -> StatusFetcher.
+var fetchers = {};
+// TreeStatus object that correspond to current change.
+var installedTreeStatus = null;
+
+// Fetches JSON from URL, returns Promise.
+function fetchJSON(url) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url);
+ xhr.onload = function() {
+ var result = null;
+ if (this.status === 204) {
+ resolve(null);
+ } else if (this.status >= 200 && this.status < 300) {
+ try {
+ result = JSON.parse(xhr.response.substring(JSON_PREFIX.length));
+ resolve(result);
+ } catch (_) {
+ reject("Unable to parse JSON");
+ }
+ } else {
+ reject("Got non-200 response");
+ }
+ };
+ xhr.onerror = function() {
+ reject("Unhandled error");
+ };
+ xhr.send();
+ });
+}
+
+/**
+ * Returns configuration for a tree status (status URL to use, login URL, etc.)
+ * @param {string} project Gerrit project name to search config for
+ * @return {Promise} Promise, resolving to project config.
+ */
+function getProjectConfig(project) {
+ /*
+ Expected format of a project config:
+ {
+ treeName: English label to describe the project, eg: 'Chrome'.
+ viewURL: URL to the user facing status, eg: 'http://foo/'
+ statusURL: URL to the tree status, eg: 'http://foo/current?format=json'
+ withCredentials: True if status requests require HTTP cookies.
+ disabledBranchPattern: Regex object or string pattern to exclude a branch,
+ enforceCommitQueue: True if project is using Commit Queue
+ }
+ */
+ // If we've fetched it already, use the config set on the window.
+ var config = window.chumpDetectorConfig;
+ if (config !== undefined) {
+ return Promise.resolve(config);
+ }
+
+ // Otherwise, fetch it and store it for reuse.
+ var chumpdetectorURL = '/projects/' + encodeURIComponent(project) +
+ '/chumpdetector~config';
+ return fetchJSON(chumpdetectorURL).then(function(cfg) {
+ window.chumpDetectorConfig = cfg || null;
+ if (!window.chumpDetectorConfig) {
+ console.log('Chump detector is not configured.');
+ return null;
+ }
+ return getProjectConfig(project);
+ });
+}
+
+
+/**
+ * Creates a new one or returns existing StatusFetcher object.
+ * @param {Object} config Project configuration as returned by getProjectConfig
+ * @return {Object} StatusFetcher object
+ */
+function getStatusFetcher(config) {
+ if (!fetchers.hasOwnProperty(config.treeName)) {
+ fetchers[config.treeName] = new StatusFetcher(config.viewURL,
+ config.statusURL,
+ config.withCredentials);
+ }
+ return fetchers[config.treeName];
+}
+
+
+self.on('showchange',
+ /**
+ * Called by Gerrit when change screen is shown.
+ * @param {Object} change Object with change details
+ * @param {Object} revision Object with revision details
+ */
+ function(change, revision) {
+ // Uninstall previous one (if any).
+ if (installedTreeStatus) {
+ installedTreeStatus.uninstall();
+ installedTreeStatus = null;
+ }
+
+ getProjectConfig(change.project).then(function(config) {
+ if (!config || !config.statusURL) {
+ console.log('No status url for this project.');
+ return;
+ } else if (config.disabled) {
+ console.log('Chumpdetector is disabled for this project.');
+ return;
+ }
+ // Install new one if the project is supported.
+ var fetcher;
+ if (config.disabledBranchPattern &&
+ new RegExp(config.disabledBranchPattern).test(change.branch)) {
+ console.log('Disabling chump detector based on branch name.');
+ fetcher = null;
+ } else {
+ console.log('Using status url: ' + config.statusURL);
+ fetcher = getStatusFetcher(config);
+ }
+
+ function setupTreeStatus() {
+ // Bind tree status to status fetcher, perform initial fetch.
+ installedTreeStatus = new TreeStatus(change, config);
+ installedTreeStatus.install(fetcher);
+ if (fetcher) {
+ installedTreeStatus.setTreeStatus(fetcher.lastKnownStatus);
+ fetcher.fetch();
+ } else {
+ installedTreeStatus.setTreeStatus(DISABLED_BRANCH_TREE_STATUS);
+ }
+ }
+
+ // If the configuration specifies loading an image URL first then
+ // request the image. Whether it succeeds or fails immediately
+ // move into setting up the tree status request. This allows users
+ // the opportunity to use the image load to establish cookies the
+ // browser may need in order to make the status request which can
+ // work even without the image request succeeding.
+ if (config.preloadImageURL) {
+ var img = new Image();
+ img.addEventListener("load", setupTreeStatus);
+ img.addEventListener("error", setupTreeStatus);
+ img.src = config.preloadImageURL;
+ } else {
+ setupTreeStatus();
+ }
+ }, function() {
+ console.log('Error fetching config.');
+ });
+});
+
+
+self.on('history',
+ /**
+ * Called by Gerrit whenever document.location changes.
+ * @param {string} token Fragment part of a page URL
+ */
+ function(token) {
+ // Navigated away from a change page? Uninstall tree status UI.
+ if (token.substring(0, 2) != '/c' && installedTreeStatus) {
+ installedTreeStatus.uninstall();
+ installedTreeStatus = null;
+ }
+});
+
+// Add a message div to appear inside the confirm submit dialog.
+var confirmSubmitMessage = document.createElement('p');
+self.hook('confirm-submit-change').onAttached(function(element) {
+ element.appendChild(confirmSubmitMessage);
+});
+
+/**
+ * Discard any the message in the confirm submit dialog. (No message to show.)
+ */
+function disableConfirmSubmitMessage() {
+ confirmSubmitMessage.style = STYLES.hiddenConfirmSubmitMessage;
+ confirmSubmitMessage.textContent = '';
+}
+
+/**
+ * Set the message that will be shown in the confirm submit tialog.
+ * @param {string} message The message to appear.
+ */
+function setConfirmSubmitMessage(message) {
+ confirmSubmitMessage.style = STYLES.confirmSubmitMessage;
+ confirmSubmitMessage.textContent = message;
+}
+
+self.on('submitchange',
+ /**
+ * Called by Gerrit whenever 'Submit' is clicked.
+ * @return {boolean} False to block submit
+ */
+ function() {
+ // Change is in unsupported project, do not block it.
+ if (!installedTreeStatus) {
+ disableConfirmSubmitMessage();
+ return true;
+ }
+
+ var treeStatus = installedTreeStatus.treeStatus;
+ var config = installedTreeStatus.config;
+
+ // Change is in unsupported branch, do not block it.
+ if (treeStatus == DISABLED_BRANCH_TREE_STATUS) {
+ disableConfirmSubmitMessage();
+ return true;
+ }
+
+ // Change should be submitted via CQ (not directly via Gerrit).
+ if (config.enforceCommitQueue) {
+ setConfirmSubmitMessage(
+ 'The ' + config.treeName + ' project uses the commit queue (CQ). ' +
+ 'You should submit your change by clicking "Reply" and setting the ' +
+ '"Commit-Queue" label to the appropriate value.\n' +
+ 'Do you want to commit the change directly, bypassing CQ (dangerous)?');
+ return true;
+ }
+
+ // Tree is open, do not block the change.
+ if (treeStatus.isOpen) {
+ disableConfirmSubmitMessage();
+ return true;
+ }
+
+ // Unknown tree state. Double check with the user before submitting.
+ if (treeStatus.generalState == 'unknown') {
+ setConfirmSubmitMessage(
+ config.treeName + ' tree status is unknown, submitting this ' +
+ 'change now might be dangerous. Submit anyway?');
+ return true;
+ }
+
+ // Tree is closed (or in some entirely unexpected state). Warn the user.
+ setConfirmSubmitMessage(
+ 'The ' + config.treeName + ' tree is closed, submitting this ' +
+ 'change is dangerous. Submit anyway?');
+ return true;
+});
+
+});