summaryrefslogtreecommitdiffstats
path: root/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
diff options
context:
space:
mode:
Diffstat (limited to 'polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js')
-rw-r--r--polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js2298
1 files changed, 1700 insertions, 598 deletions
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 09a2962437..09eff805c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -1,19 +1,118 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
(function() {
'use strict';
+ const Defs = {};
+
+ /**
+ * @typedef {{
+ * basePatchNum: (string|number),
+ * patchNum: (number),
+ * }}
+ */
+ Defs.patchRange;
+
+ /**
+ * @typedef {{
+ * url: string,
+ * fetchOptions: (Object|null|undefined),
+ * anonymizedUrl: (string|undefined),
+ * }}
+ */
+ Defs.FetchRequest;
+
+ /**
+ * Object to describe a request for passing into _fetchJSON or _fetchRawJSON.
+ * - url is the URL for the request (excluding get params)
+ * - errFn is a function to invoke when the request fails.
+ * - cancelCondition is a function that, if provided and returns true, will
+ * cancel the response after it resolves.
+ * - params is a key-value hash to specify get params for the request URL.
+ * @typedef {{
+ * url: string,
+ * errFn: (function(?Response, string=)|null|undefined),
+ * cancelCondition: (function()|null|undefined),
+ * params: (Object|null|undefined),
+ * fetchOptions: (Object|null|undefined),
+ * anonymizedUrl: (string|undefined),
+ * reportUrlAsIs: (boolean|undefined),
+ * }}
+ */
+ Defs.FetchJSONRequest;
+
+ /**
+ * @typedef {{
+ * changeNum: (string|number),
+ * endpoint: string,
+ * patchNum: (string|number|null|undefined),
+ * errFn: (function(?Response, string=)|null|undefined),
+ * params: (Object|null|undefined),
+ * fetchOptions: (Object|null|undefined),
+ * anonymizedEndpoint: (string|undefined),
+ * reportEndpointAsIs: (boolean|undefined),
+ * }}
+ */
+ Defs.ChangeFetchRequest;
+
+ /**
+ * Object to describe a request for passing into _send.
+ * - method is the HTTP method to use in the request.
+ * - url is the URL for the request
+ * - body is a request payload.
+ * TODO (beckysiegel) remove need for number at least.
+ * - errFn is a function to invoke when the request fails.
+ * - cancelCondition is a function that, if provided and returns true, will
+ * cancel the response after it resolves.
+ * - contentType is the content type of the body.
+ * - headers is a key-value hash to describe HTTP headers for the request.
+ * - parseResponse states whether the result should be parsed as a JSON
+ * object using getResponseObject.
+ * @typedef {{
+ * method: string,
+ * url: string,
+ * body: (string|number|Object|null|undefined),
+ * errFn: (function(?Response, string=)|null|undefined),
+ * contentType: (string|null|undefined),
+ * headers: (Object|undefined),
+ * parseResponse: (boolean|undefined),
+ * anonymizedUrl: (string|undefined),
+ * reportUrlAsIs: (boolean|undefined),
+ * }}
+ */
+ Defs.SendRequest;
+
+ /**
+ * @typedef {{
+ * changeNum: (string|number),
+ * method: string,
+ * patchNum: (string|number|undefined),
+ * endpoint: string,
+ * body: (string|number|Object|null|undefined),
+ * errFn: (function(?Response, string=)|null|undefined),
+ * contentType: (string|null|undefined),
+ * headers: (Object|undefined),
+ * parseResponse: (boolean|undefined),
+ * anonymizedEndpoint: (string|undefined),
+ * reportEndpointAsIs: (boolean|undefined),
+ * }}
+ */
+ Defs.ChangeSendRequest;
+
const DiffViewMode = {
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
UNIFIED: 'UNIFIED_DIFF',
@@ -22,8 +121,6 @@
const MAX_PROJECT_RESULTS = 25;
const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900;
const PARENT_PATCH_NUM = 'PARENT';
- const CHECK_SIGN_IN_DEBOUNCE_MS = 3 * 1000;
- const CHECK_SIGN_IN_DEBOUNCER_NAME = 'checkCredentials';
const FAILED_TO_FETCH_ERROR = 'Failed to fetch';
const Requests = {
@@ -34,12 +131,62 @@
'Saving draft resulted in HTTP 200 (OK) but expected HTTP 201 (Created)';
const HEADER_REPORTING_BLACKLIST = /^set-cookie$/i;
+ const ANONYMIZED_CHANGE_BASE_URL = '/changes/*~*';
+ const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL +
+ '/revisions/*';
+
+ /**
+ * Wrapper around Map for caching server responses. Site-based so that
+ * changes to CANONICAL_PATH will result in a different cache going into
+ * effect.
+ */
+ class SiteBasedCache {
+ constructor() {
+ // Container of per-canonical-path caches.
+ this._data = new Map();
+ }
+
+ // Returns the cache for the current canonical path.
+ _cache() {
+ if (!this._data.has(window.CANONICAL_PATH)) {
+ this._data.set(window.CANONICAL_PATH, new Map());
+ }
+ return this._data.get(window.CANONICAL_PATH);
+ }
+
+ has(key) {
+ return this._cache().has(key);
+ }
+
+ get(key) {
+ return this._cache().get(key);
+ }
+
+ set(key, value) {
+ this._cache().set(key, value);
+ }
+
+ delete(key) {
+ this._cache().delete(key);
+ }
+
+ invalidatePrefix(prefix) {
+ const newMap = new Map();
+ for (const [key, value] of this._cache().entries()) {
+ if (!key.startsWith(prefix)) {
+ newMap.set(key, value);
+ }
+ }
+ this._data.set(window.CANONICAL_PATH, newMap);
+ }
+ }
Polymer({
is: 'gr-rest-api-interface',
behaviors: [
Gerrit.PathListBehavior,
+ Gerrit.PatchSetBehavior,
Gerrit.RESTClientBehavior,
],
@@ -61,10 +208,20 @@
* @event auth-error
*/
+ /**
+ * Fired after an RPC completes.
+ *
+ * @event rpc-log
+ */
+
properties: {
_cache: {
type: Object,
- value: {}, // Intentional to share the object across instances.
+ value: new SiteBasedCache(), // Shared across instances.
+ },
+ _credentialCheck: {
+ type: Object,
+ value: {checking: false}, // Shared across instances.
},
_sharedFetchPromises: {
type: Object,
@@ -94,39 +251,76 @@
JSON_PREFIX,
/**
+ * Wraps calls to the underlying authenticated fetch function (_auth.fetch)
+ * with timing and logging.
+ * @param {Defs.FetchRequest} req
+ */
+ _fetch(req) {
+ const start = Date.now();
+ const xhr = this._auth.fetch(req.url, req.fetchOptions);
+
+ // Log the call after it completes.
+ xhr.then(res => this._logCall(req, start, res.status));
+
+ // Return the XHR directly (without the log).
+ return xhr;
+ },
+
+ /**
+ * Log information about a REST call. Because the elapsed time is determined
+ * by this method, it should be called immediately after the request
+ * finishes.
+ * @param {Defs.FetchRequest} req
+ * @param {number} startTime the time that the request was started.
+ * @param {number} status the HTTP status of the response. The status value
+ * is used here rather than the response object so there is no way this
+ * method can read the body stream.
+ */
+ _logCall(req, startTime, status) {
+ const method = (req.fetchOptions && req.fetchOptions.method) ?
+ req.fetchOptions.method : 'GET';
+ const elapsed = (Date.now() - startTime);
+ console.log([
+ 'HTTP',
+ status,
+ method,
+ elapsed + 'ms',
+ req.anonymizedUrl || req.url,
+ ].join(' '));
+ if (req.anonymizedUrl) {
+ this.fire('rpc-log',
+ {status, method, elapsed, anonymizedUrl: req.anonymizedUrl});
+ }
+ },
+
+ /**
* Fetch JSON from url provided.
* Returns a Promise that resolves to a native Response.
* Doesn't do error checking. Supports cancel condition. Performs auth.
* Validates auth expiry errors.
- * @param {string} url
- * @param {?function(?Response, string=)=} opt_errFn
- * passed as null sometimes.
- * @param {?function()=} opt_cancelCondition
- * passed as null sometimes.
- * @param {?Object=} opt_params URL params, key-value hash.
- * @param {?Object=} opt_options Fetch options.
- */
- _fetchRawJSON(url, opt_errFn, opt_cancelCondition, opt_params,
- opt_options) {
- const urlWithParams = this._urlWithParams(url, opt_params);
- return this._auth.fetch(urlWithParams, opt_options).then(response => {
- if (opt_cancelCondition && opt_cancelCondition()) {
- response.body.cancel();
+ * @param {Defs.FetchJSONRequest} req
+ */
+ _fetchRawJSON(req) {
+ const urlWithParams = this._urlWithParams(req.url, req.params);
+ const fetchReq = {
+ url: urlWithParams,
+ fetchOptions: req.fetchOptions,
+ anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl,
+ };
+ return this._fetch(fetchReq).then(res => {
+ if (req.cancelCondition && req.cancelCondition()) {
+ res.body.cancel();
return;
}
- return response;
+ return res;
}).catch(err => {
- const isLoggedIn = !!this._cache['/accounts/self/detail'];
+ const isLoggedIn = !!this._cache.get('/accounts/self/detail');
if (isLoggedIn && err && err.message === FAILED_TO_FETCH_ERROR) {
- if (!this.isDebouncerActive(CHECK_SIGN_IN_DEBOUNCER_NAME)) {
- this.checkCredentials();
- }
- this.debounce(CHECK_SIGN_IN_DEBOUNCER_NAME, this.checkCredentials,
- CHECK_SIGN_IN_DEBOUNCE_MS);
+ this.checkCredentials();
return;
}
- if (opt_errFn) {
- opt_errFn.call(undefined, null, err);
+ if (req.errFn) {
+ req.errFn.call(undefined, null, err);
} else {
this.fire('network-error', {error: err});
}
@@ -138,31 +332,23 @@
* Fetch JSON from url provided.
* Returns a Promise that resolves to a parsed response.
* Same as {@link _fetchRawJSON}, plus error handling.
- * @param {string} url
- * @param {?function(?Response, string=)=} opt_errFn
- * passed as null sometimes.
- * @param {?function()=} opt_cancelCondition
- * passed as null sometimes.
- * @param {?Object=} opt_params URL params, key-value hash.
- * @param {?Object=} opt_options Fetch options.
+ * @param {Defs.FetchJSONRequest} req
*/
- fetchJSON(url, opt_errFn, opt_cancelCondition, opt_params, opt_options) {
- return this._fetchRawJSON(
- url, opt_errFn, opt_cancelCondition, opt_params, opt_options)
- .then(response => {
- if (!response) {
- return;
- }
- if (!response.ok) {
- if (opt_errFn) {
- opt_errFn.call(null, response);
- return;
- }
- this.fire('server-error', {response});
- return;
- }
- return response && this.getResponseObject(response);
- });
+ _fetchJSON(req) {
+ return this._fetchRawJSON(req).then(response => {
+ if (!response) {
+ return;
+ }
+ if (!response.ok) {
+ if (req.errFn) {
+ req.errFn.call(null, response);
+ return;
+ }
+ this.fire('server-error', {request: req, response});
+ return;
+ }
+ return response && this.getResponseObject(response);
+ });
},
/**
@@ -220,101 +406,166 @@
return JSON.parse(source.substring(JSON_PREFIX.length));
},
- getConfig() {
- return this._fetchSharedCacheURL('/config/server/info');
+ getConfig(noCache) {
+ if (!noCache) {
+ return this._fetchSharedCacheURL({
+ url: '/config/server/info',
+ reportUrlAsIs: true,
+ });
+ }
+
+ return this._fetchJSON({
+ url: '/config/server/info',
+ reportUrlAsIs: true,
+ });
+ },
+
+ getRepo(repo, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: '/projects/' + encodeURIComponent(repo),
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*',
+ });
},
- getProject(project) {
- return this._fetchSharedCacheURL(
- '/projects/' + encodeURIComponent(project));
+ getProjectConfig(repo, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: '/projects/' + encodeURIComponent(repo) + '/config',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/config',
+ });
},
- getProjectConfig(project) {
- return this._fetchSharedCacheURL(
- '/projects/' + encodeURIComponent(project) + '/config');
+ getRepoAccess(repo) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: '/access/?project=' + encodeURIComponent(repo),
+ anonymizedUrl: '/access/?project=*',
+ });
},
- getProjectAccess(project) {
- return this._fetchSharedCacheURL(
- '/access/?project=' + encodeURIComponent(project));
+ getRepoDashboards(repo, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: `/projects/${encodeURIComponent(repo)}/dashboards?inherited`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/dashboards?inherited',
+ });
},
- saveProjectConfig(project, config, opt_errFn, opt_ctx) {
- const encodeName = encodeURIComponent(project);
- return this.send('PUT', `/projects/${encodeName}/config`, config,
- opt_errFn, opt_ctx);
+ saveRepoConfig(repo, config, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(repo);
+ return this._send({
+ method: 'PUT',
+ url: `/projects/${encodeName}/config`,
+ body: config,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/config',
+ });
},
- runProjectGC(project, opt_errFn, opt_ctx) {
- if (!project) {
- return '';
- }
- const encodeName = encodeURIComponent(project);
- return this.send('POST', `/projects/${encodeName}/gc`, '',
- opt_errFn, opt_ctx);
+ runRepoGC(repo, opt_errFn) {
+ if (!repo) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(repo);
+ return this._send({
+ method: 'POST',
+ url: `/projects/${encodeName}/gc`,
+ body: '',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/gc',
+ });
},
/**
* @param {?Object} config
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- createProject(config, opt_errFn, opt_ctx) {
+ createRepo(config, opt_errFn) {
if (!config.name) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
const encodeName = encodeURIComponent(config.name);
- return this.send('PUT', `/projects/${encodeName}`, config, opt_errFn,
- opt_ctx);
+ return this._send({
+ method: 'PUT',
+ url: `/projects/${encodeName}`,
+ body: config,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*',
+ });
},
/**
* @param {?Object} config
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- createGroup(config, opt_errFn, opt_ctx) {
+ createGroup(config, opt_errFn) {
if (!config.name) { return ''; }
const encodeName = encodeURIComponent(config.name);
- return this.send('PUT', `/groups/${encodeName}`, config, opt_errFn,
- opt_ctx);
+ return this._send({
+ method: 'PUT',
+ url: `/groups/${encodeName}`,
+ body: config,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*',
+ });
},
- getGroupConfig(group) {
- const encodeName = encodeURIComponent(group);
- return this.fetchJSON(`/groups/${encodeName}/detail`);
+ getGroupConfig(group, opt_errFn) {
+ return this._fetchJSON({
+ url: `/groups/${encodeURIComponent(group)}/detail`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/detail',
+ });
},
/**
- * @param {string} project
+ * @param {string} repo
* @param {string} ref
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- deleteProjectBranches(project, ref, opt_errFn, opt_ctx) {
- if (!project || !ref) {
- return '';
- }
- const encodeName = encodeURIComponent(project);
+ deleteRepoBranches(repo, ref, opt_errFn) {
+ if (!repo || !ref) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(repo);
const encodeRef = encodeURIComponent(ref);
- return this.send('DELETE',
- `/projects/${encodeName}/branches/${encodeRef}`, '',
- opt_errFn, opt_ctx);
+ return this._send({
+ method: 'DELETE',
+ url: `/projects/${encodeName}/branches/${encodeRef}`,
+ body: '',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/branches/*',
+ });
},
/**
- * @param {string} project
+ * @param {string} repo
* @param {string} ref
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- deleteProjectTags(project, ref, opt_errFn, opt_ctx) {
- if (!project || !ref) {
- return '';
- }
- const encodeName = encodeURIComponent(project);
+ deleteRepoTags(repo, ref, opt_errFn) {
+ if (!repo || !ref) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(repo);
const encodeRef = encodeURIComponent(ref);
- return this.send('DELETE',
- `/projects/${encodeName}/tags/${encodeRef}`, '',
- opt_errFn, opt_ctx);
+ return this._send({
+ method: 'DELETE',
+ url: `/projects/${encodeName}/tags/${encodeRef}`,
+ body: '',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/tags/*',
+ });
},
/**
@@ -322,15 +573,20 @@
* @param {string} branch
* @param {string} revision
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- createProjectBranch(name, branch, revision, opt_errFn, opt_ctx) {
+ createRepoBranch(name, branch, revision, opt_errFn) {
if (!name || !branch || !revision) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
const encodeName = encodeURIComponent(name);
const encodeBranch = encodeURIComponent(branch);
- return this.send('PUT',
- `/projects/${encodeName}/branches/${encodeBranch}`,
- revision, opt_errFn, opt_ctx);
+ return this._send({
+ method: 'PUT',
+ url: `/projects/${encodeName}/branches/${encodeBranch}`,
+ body: revision,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/branches/*',
+ });
},
/**
@@ -338,14 +594,20 @@
* @param {string} tag
* @param {string} revision
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- createProjectTag(name, tag, revision, opt_errFn, opt_ctx) {
+ createRepoTag(name, tag, revision, opt_errFn) {
if (!name || !tag || !revision) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
const encodeName = encodeURIComponent(name);
const encodeTag = encodeURIComponent(tag);
- return this.send('PUT', `/projects/${encodeName}/tags/${encodeTag}`,
- revision, opt_errFn, opt_ctx);
+ return this._send({
+ method: 'PUT',
+ url: `/projects/${encodeName}/tags/${encodeTag}`,
+ body: revision,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/tags/*',
+ });
},
/**
@@ -354,91 +616,142 @@
*/
getIsGroupOwner(groupName) {
const encodeName = encodeURIComponent(groupName);
- return this._fetchSharedCacheURL(`/groups/?owned&q=${encodeName}`)
+ const req = {
+ url: `/groups/?owned&q=${encodeName}`,
+ anonymizedUrl: '/groups/owned&q=*',
+ };
+ return this._fetchSharedCacheURL(req)
.then(configs => configs.hasOwnProperty(groupName));
},
- getGroupMembers(groupName) {
+ getGroupMembers(groupName, opt_errFn) {
const encodeName = encodeURIComponent(groupName);
- return this.send('GET', `/groups/${encodeName}/members/`)
- .then(response => this.getResponseObject(response));
+ return this._fetchJSON({
+ url: `/groups/${encodeName}/members/`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/members',
+ });
},
getIncludedGroup(groupName) {
- const encodeName = encodeURIComponent(groupName);
- return this.send('GET', `/groups/${encodeName}/groups/`)
- .then(response => this.getResponseObject(response));
+ return this._fetchJSON({
+ url: `/groups/${encodeURIComponent(groupName)}/groups/`,
+ anonymizedUrl: '/groups/*/groups',
+ });
},
saveGroupName(groupId, name) {
const encodeId = encodeURIComponent(groupId);
- return this.send('PUT', `/groups/${encodeId}/name`, {name});
+ return this._send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/name`,
+ body: {name},
+ anonymizedUrl: '/groups/*/name',
+ });
},
saveGroupOwner(groupId, ownerId) {
const encodeId = encodeURIComponent(groupId);
- return this.send('PUT', `/groups/${encodeId}/owner`, {owner: ownerId});
+ return this._send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/owner`,
+ body: {owner: ownerId},
+ anonymizedUrl: '/groups/*/owner',
+ });
},
saveGroupDescription(groupId, description) {
const encodeId = encodeURIComponent(groupId);
- return this.send('PUT', `/groups/${encodeId}/description`,
- {description});
+ return this._send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/description`,
+ body: {description},
+ anonymizedUrl: '/groups/*/description',
+ });
},
saveGroupOptions(groupId, options) {
const encodeId = encodeURIComponent(groupId);
- return this.send('PUT', `/groups/${encodeId}/options`, options);
+ return this._send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/options`,
+ body: options,
+ anonymizedUrl: '/groups/*/options',
+ });
},
- getGroupAuditLog(group) {
- return this._fetchSharedCacheURL('/groups/' + group + '/log.audit');
+ getGroupAuditLog(group, opt_errFn) {
+ return this._fetchSharedCacheURL({
+ url: '/groups/' + group + '/log.audit',
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/log.audit',
+ });
},
saveGroupMembers(groupName, groupMembers) {
const encodeName = encodeURIComponent(groupName);
const encodeMember = encodeURIComponent(groupMembers);
- return this.send('PUT', `/groups/${encodeName}/members/${encodeMember}`)
- .then(response => this.getResponseObject(response));
+ return this._send({
+ method: 'PUT',
+ url: `/groups/${encodeName}/members/${encodeMember}`,
+ parseResponse: true,
+ anonymizedUrl: '/groups/*/members/*',
+ });
},
saveIncludedGroup(groupName, includedGroup, opt_errFn) {
const encodeName = encodeURIComponent(groupName);
const encodeIncludedGroup = encodeURIComponent(includedGroup);
- return this.send('PUT',
- `/groups/${encodeName}/groups/${encodeIncludedGroup}`, null,
- opt_errFn).then(response => {
- if (response.ok) {
- return this.getResponseObject(response);
- }
- });
+ const req = {
+ method: 'PUT',
+ url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/groups/*',
+ };
+ return this._send(req).then(response => {
+ if (response.ok) {
+ return this.getResponseObject(response);
+ }
+ });
},
deleteGroupMembers(groupName, groupMembers) {
const encodeName = encodeURIComponent(groupName);
const encodeMember = encodeURIComponent(groupMembers);
- return this.send('DELETE',
- `/groups/${encodeName}/members/${encodeMember}`);
+ return this._send({
+ method: 'DELETE',
+ url: `/groups/${encodeName}/members/${encodeMember}`,
+ anonymizedUrl: '/groups/*/members/*',
+ });
},
deleteIncludedGroup(groupName, includedGroup) {
const encodeName = encodeURIComponent(groupName);
const encodeIncludedGroup = encodeURIComponent(includedGroup);
- return this.send('DELETE',
- `/groups/${encodeName}/groups/${encodeIncludedGroup}`);
+ return this._send({
+ method: 'DELETE',
+ url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
+ anonymizedUrl: '/groups/*/groups/*',
+ });
},
getVersion() {
- return this._fetchSharedCacheURL('/config/server/version');
+ return this._fetchSharedCacheURL({
+ url: '/config/server/version',
+ reportUrlAsIs: true,
+ });
},
getDiffPreferences() {
return this.getLoggedIn().then(loggedIn => {
if (loggedIn) {
- return this._fetchSharedCacheURL('/accounts/self/preferences.diff');
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/preferences.diff',
+ reportUrlAsIs: true,
+ });
}
// These defaults should match the defaults in
- // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
+ // java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
// NOTE: There are some settings that don't apply to PolyGerrit
// (Render mode being at least one of them).
return Promise.resolve({
@@ -460,39 +773,127 @@
});
},
+ getEditPreferences() {
+ return this.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/preferences.edit',
+ reportUrlAsIs: true,
+ });
+ }
+ // These defaults should match the defaults in
+ // java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
+ return Promise.resolve({
+ auto_close_brackets: false,
+ cursor_blink_rate: 0,
+ hide_line_numbers: false,
+ hide_top_menu: false,
+ indent_unit: 2,
+ indent_with_tabs: false,
+ key_map_type: 'DEFAULT',
+ line_length: 100,
+ line_wrapping: false,
+ match_brackets: true,
+ show_base: false,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ tab_size: 8,
+ theme: 'DEFAULT',
+ });
+ });
+ },
+
/**
* @param {?Object} prefs
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- savePreferences(prefs, opt_errFn, opt_ctx) {
+ savePreferences(prefs, opt_errFn) {
// Note (Issue 5142): normalize the download scheme with lower case before
// saving.
if (prefs.download_scheme) {
prefs.download_scheme = prefs.download_scheme.toLowerCase();
}
- return this.send('PUT', '/accounts/self/preferences', prefs, opt_errFn,
- opt_ctx);
+ return this._send({
+ method: 'PUT',
+ url: '/accounts/self/preferences',
+ body: prefs,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ },
+
+ /**
+ * @param {?Object} prefs
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ saveDiffPreferences(prefs, opt_errFn) {
+ // Invalidate the cache.
+ this._cache.delete('/accounts/self/preferences.diff');
+ return this._send({
+ method: 'PUT',
+ url: '/accounts/self/preferences.diff',
+ body: prefs,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
},
/**
* @param {?Object} prefs
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- saveDiffPreferences(prefs, opt_errFn, opt_ctx) {
+ saveEditPreferences(prefs, opt_errFn) {
// Invalidate the cache.
- this._cache['/accounts/self/preferences.diff'] = undefined;
- return this.send('PUT', '/accounts/self/preferences.diff', prefs,
- opt_errFn, opt_ctx);
+ this._cache.delete('/accounts/self/preferences.edit');
+ return this._send({
+ method: 'PUT',
+ url: '/accounts/self/preferences.edit',
+ body: prefs,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
},
getAccount() {
- return this._fetchSharedCacheURL('/accounts/self/detail', resp => {
- if (resp.status === 403) {
- this._cache['/accounts/self/detail'] = null;
- }
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/detail',
+ reportUrlAsIs: true,
+ errFn: resp => {
+ if (!resp || resp.status === 403) {
+ this._cache.delete('/accounts/self/detail');
+ }
+ },
+ });
+ },
+
+ getAvatarChangeUrl() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/avatar.change.url',
+ reportUrlAsIs: true,
+ errFn: resp => {
+ if (!resp || resp.status === 403) {
+ this._cache.delete('/accounts/self/avatar.change.url');
+ }
+ },
+ });
+ },
+
+ getExternalIds() {
+ return this._fetchJSON({
+ url: '/accounts/self/external.ids',
+ reportUrlAsIs: true,
+ });
+ },
+
+ deleteAccountIdentity(id) {
+ return this._send({
+ method: 'POST',
+ url: '/accounts/self/external.ids:delete',
+ body: id,
+ parseResponse: true,
+ reportUrlAsIs: true,
});
},
@@ -501,56 +902,72 @@
* @return {!Promise<!Object>}
*/
getAccountDetails(userId) {
- return this.fetchJSON(`/accounts/${encodeURIComponent(userId)}/detail`);
+ return this._fetchJSON({
+ url: `/accounts/${encodeURIComponent(userId)}/detail`,
+ anonymizedUrl: '/accounts/*/detail',
+ });
},
getAccountEmails() {
- return this._fetchSharedCacheURL('/accounts/self/emails');
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/emails',
+ reportUrlAsIs: true,
+ });
},
/**
* @param {string} email
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- addAccountEmail(email, opt_errFn, opt_ctx) {
- return this.send('PUT', '/accounts/self/emails/' +
- encodeURIComponent(email), null, opt_errFn, opt_ctx);
+ addAccountEmail(email, opt_errFn) {
+ return this._send({
+ method: 'PUT',
+ url: '/accounts/self/emails/' + encodeURIComponent(email),
+ errFn: opt_errFn,
+ anonymizedUrl: '/account/self/emails/*',
+ });
},
/**
* @param {string} email
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- deleteAccountEmail(email, opt_errFn, opt_ctx) {
- return this.send('DELETE', '/accounts/self/emails/' +
- encodeURIComponent(email), null, opt_errFn, opt_ctx);
+ deleteAccountEmail(email, opt_errFn) {
+ return this._send({
+ method: 'DELETE',
+ url: '/accounts/self/emails/' + encodeURIComponent(email),
+ errFn: opt_errFn,
+ anonymizedUrl: '/accounts/self/email/*',
+ });
},
/**
* @param {string} email
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
- */
- setPreferredAccountEmail(email, opt_errFn, opt_ctx) {
- return this.send('PUT', '/accounts/self/emails/' +
- encodeURIComponent(email) + '/preferred', null,
- opt_errFn, opt_ctx).then(() => {
- // If result of getAccountEmails is in cache, update it in the cache
- // so we don't have to invalidate it.
- const cachedEmails = this._cache['/accounts/self/emails'];
- if (cachedEmails) {
- const emails = cachedEmails.map(entry => {
- if (entry.email === email) {
- return {email, preferred: true};
- } else {
- return {email};
- }
- });
- this._cache['/accounts/self/emails'] = emails;
+ */
+ setPreferredAccountEmail(email, opt_errFn) {
+ const encodedEmail = encodeURIComponent(email);
+ const req = {
+ method: 'PUT',
+ url: `/accounts/self/emails/${encodedEmail}/preferred`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/accounts/self/emails/*/preferred',
+ };
+ return this._send(req).then(() => {
+ // If result of getAccountEmails is in cache, update it in the cache
+ // so we don't have to invalidate it.
+ const cachedEmails = this._cache.get('/accounts/self/emails');
+ if (cachedEmails) {
+ const emails = cachedEmails.map(entry => {
+ if (entry.email === email) {
+ return {email, preferred: true};
+ } else {
+ return {email};
}
});
+ this._cache.set('/accounts/self/emails', emails);
+ }
+ });
},
/**
@@ -559,58 +976,93 @@
_updateCachedAccount(obj) {
// If result of getAccount is in cache, update it in the cache
// so we don't have to invalidate it.
- const cachedAccount = this._cache['/accounts/self/detail'];
+ const cachedAccount = this._cache.get('/accounts/self/detail');
if (cachedAccount) {
// Replace object in cache with new object to force UI updates.
- this._cache['/accounts/self/detail'] =
- Object.assign({}, cachedAccount, obj);
+ this._cache.set('/accounts/self/detail',
+ Object.assign({}, cachedAccount, obj));
}
},
/**
* @param {string} name
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- setAccountName(name, opt_errFn, opt_ctx) {
- return this.send('PUT', '/accounts/self/name', {name}, opt_errFn, opt_ctx)
- .then(response => this.getResponseObject(response)
- .then(newName => this._updateCachedAccount({name: newName})));
+ setAccountName(name, opt_errFn) {
+ const req = {
+ method: 'PUT',
+ url: '/accounts/self/name',
+ body: {name},
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ };
+ return this._send(req)
+ .then(newName => this._updateCachedAccount({name: newName}));
},
/**
* @param {string} username
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- setAccountUsername(username, opt_errFn, opt_ctx) {
- return this.send('PUT', '/accounts/self/username', {username}, opt_errFn,
- opt_ctx).then(response => this.getResponseObject(response)
- .then(newName => this._updateCachedAccount({username: newName})));
+ setAccountUsername(username, opt_errFn) {
+ const req = {
+ method: 'PUT',
+ url: '/accounts/self/username',
+ body: {username},
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ };
+ return this._send(req)
+ .then(newName => this._updateCachedAccount({username: newName}));
},
/**
* @param {string} status
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- setAccountStatus(status, opt_errFn, opt_ctx) {
- return this.send('PUT', '/accounts/self/status', {status},
- opt_errFn, opt_ctx).then(response => this.getResponseObject(response)
- .then(newStatus => this._updateCachedAccount(
- {status: newStatus})));
+ setAccountStatus(status, opt_errFn) {
+ const req = {
+ method: 'PUT',
+ url: '/accounts/self/status',
+ body: {status},
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ };
+ return this._send(req)
+ .then(newStatus => this._updateCachedAccount({status: newStatus}));
},
getAccountStatus(userId) {
- return this.fetchJSON(`/accounts/${encodeURIComponent(userId)}/status`);
+ return this._fetchJSON({
+ url: `/accounts/${encodeURIComponent(userId)}/status`,
+ anonymizedUrl: '/accounts/*/status',
+ });
},
getAccountGroups() {
- return this._fetchSharedCacheURL('/accounts/self/groups');
+ return this._fetchJSON({
+ url: '/accounts/self/groups',
+ reportUrlAsIs: true,
+ });
},
getAccountAgreements() {
- return this._fetchSharedCacheURL('/accounts/self/agreements');
+ return this._fetchJSON({
+ url: '/accounts/self/agreements',
+ reportUrlAsIs: true,
+ });
+ },
+
+ saveAccountAgreement(name) {
+ return this._send({
+ method: 'PUT',
+ url: '/accounts/self/agreements',
+ body: name,
+ reportUrlAsIs: true,
+ });
},
/**
@@ -623,8 +1075,10 @@
.map(param => { return encodeURIComponent(param); })
.join('&q=');
}
- return this._fetchSharedCacheURL('/accounts/self/capabilities' +
- queryString);
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/capabilities' + queryString,
+ anonymizedUrl: '/accounts/self/capabilities?q=*',
+ });
},
getLoggedIn() {
@@ -646,39 +1100,50 @@
},
checkCredentials() {
+ if (this._credentialCheck.checking) {
+ return;
+ }
+ this._credentialCheck.checking = true;
+ const req = {url: '/accounts/self/detail', reportUrlAsIs: true};
// Skip the REST response cache.
- return this._fetchRawJSON('/accounts/self/detail').then(response => {
- if (!response) { return; }
- if (response.status === 403) {
+ return this._fetchRawJSON(req).then(res => {
+ if (!res) { return; }
+ if (res.status === 403) {
this.fire('auth-error');
- this._cache['/accounts/self/detail'] = null;
- } else if (response.ok) {
- return this.getResponseObject(response);
+ this._cache.delete('/accounts/self/detail');
+ } else if (res.ok) {
+ return this.getResponseObject(res);
}
- }).then(response => {
- if (response) {
- this._cache['/accounts/self/detail'] = response;
+ }).then(res => {
+ this._credentialCheck.checking = false;
+ if (res) {
+ this._cache.delete('/accounts/self/detail');
}
- return response;
+ return res;
+ }).catch(err => {
+ this._credentialCheck.checking = false;
});
},
getDefaultPreferences() {
- return this._fetchSharedCacheURL('/config/server/preferences');
+ return this._fetchSharedCacheURL({
+ url: '/config/server/preferences',
+ reportUrlAsIs: true,
+ });
},
getPreferences() {
return this.getLoggedIn().then(loggedIn => {
if (loggedIn) {
- return this._fetchSharedCacheURL('/accounts/self/preferences').then(
- res => {
- if (this._isNarrowScreen()) {
- res.default_diff_view = DiffViewMode.UNIFIED;
- } else {
- res.default_diff_view = res.diff_view;
- }
- return Promise.resolve(res);
- });
+ const req = {url: '/accounts/self/preferences', reportUrlAsIs: true};
+ return this._fetchSharedCacheURL(req).then(res => {
+ if (this._isNarrowScreen()) {
+ res.default_diff_view = DiffViewMode.UNIFIED;
+ } else {
+ res.default_diff_view = res.diff_view;
+ }
+ return Promise.resolve(res);
+ });
}
return Promise.resolve({
@@ -686,61 +1151,84 @@
default_diff_view: this._isNarrowScreen() ?
DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE,
diff_view: 'SIDE_BY_SIDE',
+ size_bar_in_change_table: true,
});
});
},
getWatchedProjects() {
- return this._fetchSharedCacheURL('/accounts/self/watched.projects');
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/watched.projects',
+ reportUrlAsIs: true,
+ });
},
/**
* @param {string} projects
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- saveWatchedProjects(projects, opt_errFn, opt_ctx) {
- return this.send('POST', '/accounts/self/watched.projects', projects,
- opt_errFn, opt_ctx)
- .then(response => {
- return this.getResponseObject(response);
- });
+ saveWatchedProjects(projects, opt_errFn) {
+ return this._send({
+ method: 'POST',
+ url: '/accounts/self/watched.projects',
+ body: projects,
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
},
/**
* @param {string} projects
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- deleteWatchedProjects(projects, opt_errFn, opt_ctx) {
- return this.send('POST', '/accounts/self/watched.projects:delete',
- projects, opt_errFn, opt_ctx);
+ deleteWatchedProjects(projects, opt_errFn) {
+ return this._send({
+ method: 'POST',
+ url: '/accounts/self/watched.projects:delete',
+ body: projects,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
},
/**
- * @param {string} url
- * @param {function(?Response, string=)=} opt_errFn
+ * @param {Defs.FetchJSONRequest} req
*/
- _fetchSharedCacheURL(url, opt_errFn) {
- if (this._sharedFetchPromises[url]) {
- return this._sharedFetchPromises[url];
+ _fetchSharedCacheURL(req) {
+ if (this._sharedFetchPromises[req.url]) {
+ return this._sharedFetchPromises[req.url];
}
// TODO(andybons): Periodic cache invalidation.
- if (this._cache[url] !== undefined) {
- return Promise.resolve(this._cache[url]);
+ if (this._cache.has(req.url)) {
+ return Promise.resolve(this._cache.get(req.url));
}
- this._sharedFetchPromises[url] = this.fetchJSON(url, opt_errFn)
+ this._sharedFetchPromises[req.url] = this._fetchJSON(req)
.then(response => {
if (response !== undefined) {
- this._cache[url] = response;
+ this._cache.set(req.url, response);
}
- this._sharedFetchPromises[url] = undefined;
+ this._sharedFetchPromises[req.url] = undefined;
return response;
}).catch(err => {
- this._sharedFetchPromises[url] = undefined;
+ this._sharedFetchPromises[req.url] = undefined;
throw err;
});
- return this._sharedFetchPromises[url];
+ return this._sharedFetchPromises[req.url];
+ },
+
+ /**
+ * @param {string} prefix
+ */
+ _invalidateSharedFetchPromisesPrefix(prefix) {
+ const newObject = {};
+ Object.entries(this._sharedFetchPromises).forEach(([key, value]) => {
+ if (!key.startsWith(prefix)) {
+ newObject[key] = value;
+ }
+ });
+ this._sharedFetchPromises = newObject;
+ this._cache.invalidatePrefix(prefix);
},
_isNarrowScreen() {
@@ -753,8 +1241,8 @@
* @param {number|string=} opt_offset
* @param {!Object=} opt_options
* @return {?Array<!Object>|?Array<!Array<!Object>>} If opt_query is an
- * array, fetchJSON will return an array of arrays of changeInfos. If it
- * is unspecified or a string, fetchJSON will return an array of
+ * array, _fetchJSON will return an array of arrays of changeInfos. If it
+ * is unspecified or a string, _fetchJSON will return an array of
* changeInfos.
*/
getChanges(opt_changesPerPage, opt_query, opt_offset, opt_options) {
@@ -779,10 +1267,20 @@
this._maybeInsertInLookup(change);
}
};
- return this.fetchJSON('/changes/', null, null, params).then(response => {
+ const req = {
+ url: '/changes/',
+ params,
+ reportUrlAsIs: true,
+ };
+ return this._fetchJSON(req).then(response => {
// Response may be an array of changes OR an array of arrays of
// changes.
if (opt_query instanceof Array) {
+ // Normalize the response to look like a multi-query response
+ // when there is only one query.
+ if (opt_query.length === 1) {
+ response = [response];
+ }
for (const arr of response) {
iterateOverChanges(arr);
}
@@ -823,18 +1321,27 @@
* @param {function()=} opt_cancelCondition
*/
getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
- const options = this.listChangesOptionsToHex(
- this.ListChangesOption.ALL_COMMITS,
- this.ListChangesOption.ALL_REVISIONS,
- this.ListChangesOption.CHANGE_ACTIONS,
- this.ListChangesOption.CURRENT_ACTIONS,
- this.ListChangesOption.DOWNLOAD_COMMANDS,
- this.ListChangesOption.SUBMITTABLE,
- this.ListChangesOption.WEB_LINKS
- );
- return this._getChangeDetail(
- changeNum, options, opt_errFn, opt_cancelCondition)
- .then(GrReviewerUpdatesParser.parse);
+ const options = [
+ this.ListChangesOption.ALL_COMMITS,
+ this.ListChangesOption.ALL_REVISIONS,
+ this.ListChangesOption.CHANGE_ACTIONS,
+ this.ListChangesOption.CURRENT_ACTIONS,
+ this.ListChangesOption.DETAILED_LABELS,
+ this.ListChangesOption.DOWNLOAD_COMMANDS,
+ this.ListChangesOption.MESSAGES,
+ this.ListChangesOption.SUBMITTABLE,
+ this.ListChangesOption.WEB_LINKS,
+ this.ListChangesOption.SKIP_MERGEABLE,
+ ];
+ return this.getConfig(false).then(config => {
+ if (config.receive && config.receive.enable_signed_push) {
+ options.push(this.ListChangesOption.PUSH_CERTIFICATES);
+ }
+ const optionsHex = this.listChangesOptionsToHex(...options);
+ return this._getChangeDetail(
+ changeNum, optionsHex, opt_errFn, opt_cancelCondition)
+ .then(GrReviewerUpdatesParser.parse);
+ });
},
/**
@@ -844,7 +1351,9 @@
*/
getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
const params = this.listChangesOptionsToHex(
- this.ListChangesOption.ALL_REVISIONS
+ this.ListChangesOption.ALL_COMMITS,
+ this.ListChangesOption.ALL_REVISIONS,
+ this.ListChangesOption.SKIP_MERGEABLE
);
return this._getChangeDetail(changeNum, params, opt_errFn,
opt_cancelCondition);
@@ -855,44 +1364,44 @@
* @param {function(?Response, string=)=} opt_errFn
* @param {function()=} opt_cancelCondition
*/
- _getChangeDetail(changeNum, params, opt_errFn,
- opt_cancelCondition) {
+ _getChangeDetail(changeNum, params, opt_errFn, opt_cancelCondition) {
return this.getChangeActionURL(changeNum, null, '/detail').then(url => {
const urlWithParams = this._urlWithParams(url, params);
- return this._fetchRawJSON(
- url,
- opt_errFn,
- opt_cancelCondition,
- {O: params},
- this._etags.getOptions(urlWithParams))
- .then(response => {
- if (response && response.status === 304) {
- return Promise.resolve(this._parsePrefixedJSON(
- this._etags.getCachedPayload(urlWithParams)));
- }
-
- if (response && !response.ok) {
- if (opt_errFn) {
- opt_errFn.call(null, response);
- } else {
- this.fire('server-error', {response});
- }
- return;
- }
+ const req = {
+ url,
+ errFn: opt_errFn,
+ cancelCondition: opt_cancelCondition,
+ params: {O: params},
+ fetchOptions: this._etags.getOptions(urlWithParams),
+ anonymizedUrl: '/changes/*~*/detail?O=' + params,
+ };
+ return this._fetchRawJSON(req).then(response => {
+ if (response && response.status === 304) {
+ return Promise.resolve(this._parsePrefixedJSON(
+ this._etags.getCachedPayload(urlWithParams)));
+ }
- const payloadPromise = response ?
- this._readResponsePayload(response) :
- Promise.resolve(null);
+ if (response && !response.ok) {
+ if (opt_errFn) {
+ opt_errFn.call(null, response);
+ } else {
+ this.fire('server-error', {request: req, response});
+ }
+ return;
+ }
- return payloadPromise.then(payload => {
- if (!payload) { return null; }
+ const payloadPromise = response ?
+ this._readResponsePayload(response) :
+ Promise.resolve(null);
- this._etags.collect(urlWithParams, response, payload.raw);
- this._maybeInsertInLookup(payload);
+ return payloadPromise.then(payload => {
+ if (!payload) { return null; }
+ this._etags.collect(urlWithParams, response, payload.raw);
+ this._maybeInsertInLookup(payload.parsed);
- return payload.parsed;
- });
- });
+ return payload.parsed;
+ });
+ });
});
},
@@ -901,42 +1410,79 @@
* @param {number|string} patchNum
*/
getChangeCommitInfo(changeNum, patchNum) {
- return this._getChangeURLAndFetch(changeNum, '/commit?links', patchNum);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/commit?links',
+ patchNum,
+ reportEndpointAsIs: true,
+ });
},
/**
* @param {number|string} changeNum
- * @param {!Promise<?Object>} patchRange
+ * @param {Defs.patchRange} patchRange
+ * @param {number=} opt_parentIndex
*/
- getChangeFiles(changeNum, patchRange) {
- let endpoint = '/files';
- if (patchRange.basePatchNum !== 'PARENT') {
- endpoint += '?base=' + encodeURIComponent(patchRange.basePatchNum);
+ getChangeFiles(changeNum, patchRange, opt_parentIndex) {
+ let params = undefined;
+ if (this.isMergeParent(patchRange.basePatchNum)) {
+ params = {parent: this.getParentIndex(patchRange.basePatchNum)};
+ } else if (!this.patchNumEquals(patchRange.basePatchNum, 'PARENT')) {
+ params = {base: patchRange.basePatchNum};
}
- return this._getChangeURLAndFetch(changeNum, endpoint,
- patchRange.patchNum);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/files',
+ patchNum: patchRange.patchNum,
+ params,
+ reportEndpointAsIs: true,
+ });
},
/**
* @param {number|string} changeNum
- * @param {!Promise<?Object>} patchRange
+ * @param {Defs.patchRange} patchRange
*/
getChangeEditFiles(changeNum, patchRange) {
let endpoint = '/edit?list';
+ let anonymizedEndpoint = endpoint;
if (patchRange.basePatchNum !== 'PARENT') {
- endpoint += '?base=' + encodeURIComponent(patchRange.basePatchNum);
+ endpoint += '&base=' + encodeURIComponent(patchRange.basePatchNum + '');
+ anonymizedEndpoint += '&base=*';
}
- return this._getChangeURLAndFetch(changeNum, endpoint);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint,
+ anonymizedEndpoint,
+ });
},
- getChangeFilesAsSpeciallySortedArray(changeNum, patchRange) {
- return this.getChangeFiles(changeNum, patchRange).then(
- this._normalizeChangeFilesResponse.bind(this));
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string} patchNum
+ * @param {string} query
+ * @return {!Promise<!Object>}
+ */
+ queryChangeFiles(changeNum, patchNum, query) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: `/files?q=${encodeURIComponent(query)}`,
+ patchNum,
+ anonymizedEndpoint: '/files?q=*',
+ });
},
- getChangeEditFilesAsSpeciallySortedArray(changeNum, patchRange) {
- return this.getChangeEditFiles(changeNum, patchRange).then(files =>
- this._normalizeChangeFilesResponse(files.files));
+ /**
+ * @param {number|string} changeNum
+ * @param {Defs.patchRange} patchRange
+ * @return {!Promise<!Array<!Object>>}
+ */
+ getChangeOrEditFiles(changeNum, patchRange) {
+ if (this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME)) {
+ return this.getChangeEditFiles(changeNum, patchRange).then(res =>
+ res.files);
+ }
+ return this.getChangeFiles(changeNum, patchRange);
},
/**
@@ -950,36 +1496,22 @@
});
},
- /**
- * The closure compiler doesn't realize this.specialFilePathCompare is
- * valid.
- * @suppress {checkTypes}
- */
- _normalizeChangeFilesResponse(response) {
- if (!response) { return []; }
- const paths = Object.keys(response).sort(this.specialFilePathCompare);
- const files = [];
- for (let i = 0; i < paths.length; i++) {
- const info = response[paths[i]];
- info.__path = paths[i];
- info.lines_inserted = info.lines_inserted || 0;
- info.lines_deleted = info.lines_deleted || 0;
- files.push(info);
- }
- return files;
- },
-
getChangeRevisionActions(changeNum, patchNum) {
- return this._getChangeURLAndFetch(changeNum, '/actions', patchNum)
- .then(revisionActions => {
- // The rebase button on change screen is always enabled.
- if (revisionActions.rebase) {
- revisionActions.rebase.rebaseOnCurrent =
- !!revisionActions.rebase.enabled;
- revisionActions.rebase.enabled = true;
- }
- return revisionActions;
- });
+ const req = {
+ changeNum,
+ endpoint: '/actions',
+ patchNum,
+ reportEndpointAsIs: true,
+ };
+ return this._getChangeURLAndFetch(req).then(revisionActions => {
+ // The rebase button on change screen is always enabled.
+ if (revisionActions.rebase) {
+ revisionActions.rebase.rebaseOnCurrent =
+ !!revisionActions.rebase.enabled;
+ revisionActions.rebase.enabled = true;
+ }
+ return revisionActions;
+ });
},
/**
@@ -990,15 +1522,24 @@
getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) {
const params = {n: 10};
if (inputVal) { params.q = inputVal; }
- return this._getChangeURLAndFetch(changeNum, '/suggest_reviewers', null,
- opt_errFn, null, params);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/suggest_reviewers',
+ errFn: opt_errFn,
+ params,
+ reportEndpointAsIs: true,
+ });
},
/**
* @param {number|string} changeNum
*/
getChangeIncludedIn(changeNum) {
- return this._getChangeURLAndFetch(changeNum, '/in', null);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/in',
+ reportEndpointAsIs: true,
+ });
},
_computeFilter(filter) {
@@ -1016,138 +1557,251 @@
* @param {string} filter
* @param {number} groupsPerPage
* @param {number=} opt_offset
- * @return {!Promise<?Object>}
*/
- getGroups(filter, groupsPerPage, opt_offset) {
+ _getGroupsUrl(filter, groupsPerPage, opt_offset) {
const offset = opt_offset || 0;
- return this._fetchSharedCacheURL(
- `/groups/?n=${groupsPerPage + 1}&S=${offset}` +
- this._computeFilter(filter)
- );
+ return `/groups/?n=${groupsPerPage + 1}&S=${offset}` +
+ this._computeFilter(filter);
},
/**
* @param {string} filter
- * @param {number} projectsPerPage
+ * @param {number} reposPerPage
* @param {number=} opt_offset
- * @return {!Promise<?Object>}
*/
- getProjects(filter, projectsPerPage, opt_offset) {
+ _getReposUrl(filter, reposPerPage, opt_offset) {
+ const defaultFilter = 'state:active OR state:read-only';
+ const namePartDelimiters = /[@.\-\s\/_]/g;
const offset = opt_offset || 0;
- return this._fetchSharedCacheURL(
- `/projects/?d&n=${projectsPerPage + 1}&S=${offset}` +
- this._computeFilter(filter)
- );
+ if (filter && !filter.includes(':') && filter.match(namePartDelimiters)) {
+ // The query language specifies hyphens as operators. Split the string
+ // by hyphens and 'AND' the parts together as 'inname:' queries.
+ // If the filter includes a semicolon, the user is using a more complex
+ // query so we trust them and don't do any magic under the hood.
+ const originalFilter = filter;
+ filter = '';
+ originalFilter.split(namePartDelimiters).forEach(part => {
+ if (part) {
+ filter += (filter === '' ? 'inname:' : ' AND inname:') + part;
+ }
+ });
+ }
+ // Check if filter is now empty which could be either because the user did
+ // not provide it or because the user provided only a split character.
+ if (!filter) {
+ filter = defaultFilter;
+ }
+
+ filter = filter.trim();
+ const encodedFilter = encodeURIComponent(filter);
+
+ return `/projects/?n=${reposPerPage + 1}&S=${offset}` +
+ `&query=${encodedFilter}`;
},
- setProjectHead(project, ref) {
- return this.send(
- 'PUT', `/projects/${encodeURIComponent(project)}/HEAD`, {ref});
+ invalidateGroupsCache() {
+ this._invalidateSharedFetchPromisesPrefix('/groups/?');
+ },
+
+ invalidateReposCache() {
+ this._invalidateSharedFetchPromisesPrefix('/projects/?');
},
/**
* @param {string} filter
- * @param {string} project
- * @param {number} projectsBranchesPerPage
+ * @param {number} groupsPerPage
* @param {number=} opt_offset
* @return {!Promise<?Object>}
*/
- getProjectBranches(filter, project, projectsBranchesPerPage, opt_offset) {
- const offset = opt_offset || 0;
+ getGroups(filter, groupsPerPage, opt_offset) {
+ const url = this._getGroupsUrl(filter, groupsPerPage, opt_offset);
- return this.fetchJSON(
- `/projects/${encodeURIComponent(project)}/branches` +
- `?n=${projectsBranchesPerPage + 1}&S=${offset}` +
- this._computeFilter(filter)
- );
+ return this._fetchSharedCacheURL({
+ url,
+ anonymizedUrl: '/groups/?*',
+ });
},
/**
* @param {string} filter
- * @param {string} project
- * @param {number} projectsTagsPerPage
+ * @param {number} reposPerPage
+ * @param {number=} opt_offset
+ * @return {!Promise<?Object>}
+ */
+ getRepos(filter, reposPerPage, opt_offset) {
+ const url = this._getReposUrl(filter, reposPerPage, opt_offset);
+
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url,
+ anonymizedUrl: '/projects/?*',
+ });
+ },
+
+ setRepoHead(repo, ref) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._send({
+ method: 'PUT',
+ url: `/projects/${encodeURIComponent(repo)}/HEAD`,
+ body: {ref},
+ anonymizedUrl: '/projects/*/HEAD',
+ });
+ },
+
+ /**
+ * @param {string} filter
+ * @param {string} repo
+ * @param {number} reposBranchesPerPage
* @param {number=} opt_offset
+ * @param {?function(?Response, string=)=} opt_errFn
* @return {!Promise<?Object>}
*/
- getProjectTags(filter, project, projectsTagsPerPage, opt_offset) {
+ getRepoBranches(filter, repo, reposBranchesPerPage, opt_offset, opt_errFn) {
const offset = opt_offset || 0;
+ const count = reposBranchesPerPage + 1;
+ filter = this._computeFilter(filter);
+ repo = encodeURIComponent(repo);
+ const url = `/projects/${repo}/branches?n=${count}&S=${offset}${filter}`;
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchJSON({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/branches?*',
+ });
+ },
- return this.fetchJSON(
- `/projects/${encodeURIComponent(project)}/tags` +
- `?n=${projectsTagsPerPage + 1}&S=${offset}` +
- this._computeFilter(filter)
- );
+ /**
+ * @param {string} filter
+ * @param {string} repo
+ * @param {number} reposTagsPerPage
+ * @param {number=} opt_offset
+ * @param {?function(?Response, string=)=} opt_errFn
+ * @return {!Promise<?Object>}
+ */
+ getRepoTags(filter, repo, reposTagsPerPage, opt_offset, opt_errFn) {
+ const offset = opt_offset || 0;
+ const encodedRepo = encodeURIComponent(repo);
+ const n = reposTagsPerPage + 1;
+ const encodedFilter = this._computeFilter(filter);
+ const url = `/projects/${encodedRepo}/tags` + `?n=${n}&S=${offset}` +
+ encodedFilter;
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchJSON({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/tags',
+ });
},
/**
* @param {string} filter
* @param {number} pluginsPerPage
* @param {number=} opt_offset
+ * @param {?function(?Response, string=)=} opt_errFn
* @return {!Promise<?Object>}
*/
- getPlugins(filter, pluginsPerPage, opt_offset) {
+ getPlugins(filter, pluginsPerPage, opt_offset, opt_errFn) {
const offset = opt_offset || 0;
+ const encodedFilter = this._computeFilter(filter);
+ const n = pluginsPerPage + 1;
+ const url = `/plugins/?all&n=${n}&S=${offset}${encodedFilter}`;
+ return this._fetchJSON({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/plugins/?all',
+ });
+ },
- return this.fetchJSON(
- `/plugins/?all&n=${pluginsPerPage + 1}&S=${offset}` +
- this._computeFilter(filter)
- );
+ getRepoAccessRights(repoName, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchJSON({
+ url: `/projects/${encodeURIComponent(repoName)}/access`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/access',
+ });
},
- getProjectAccessRights(projectName) {
- return this._fetchSharedCacheURL(
- `/projects/${encodeURIComponent(projectName)}/access`);
+ setRepoAccessRights(repoName, repoInfo) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._send({
+ method: 'POST',
+ url: `/projects/${encodeURIComponent(repoName)}/access`,
+ body: repoInfo,
+ anonymizedUrl: '/projects/*/access',
+ });
},
- setProjectAccessRights(projectName, projectInfo) {
- return this.send(
- 'POST', `/projects/${encodeURIComponent(projectName)}/access`,
- projectInfo);
+ setRepoAccessRightsForReview(projectName, projectInfo) {
+ return this._send({
+ method: 'PUT',
+ url: `/projects/${encodeURIComponent(projectName)}/access:review`,
+ body: projectInfo,
+ parseResponse: true,
+ anonymizedUrl: '/projects/*/access:review',
+ });
},
/**
* @param {string} inputVal
* @param {number} opt_n
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- getSuggestedGroups(inputVal, opt_n, opt_errFn, opt_ctx) {
+ getSuggestedGroups(inputVal, opt_n, opt_errFn) {
const params = {s: inputVal};
if (opt_n) { params.n = opt_n; }
- return this.fetchJSON('/groups/', opt_errFn, opt_ctx, params);
+ return this._fetchJSON({
+ url: '/groups/',
+ errFn: opt_errFn,
+ params,
+ reportUrlAsIs: true,
+ });
},
/**
* @param {string} inputVal
* @param {number} opt_n
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- getSuggestedProjects(inputVal, opt_n, opt_errFn, opt_ctx) {
+ getSuggestedProjects(inputVal, opt_n, opt_errFn) {
const params = {
m: inputVal,
n: MAX_PROJECT_RESULTS,
type: 'ALL',
};
if (opt_n) { params.n = opt_n; }
- return this.fetchJSON('/projects/', opt_errFn, opt_ctx, params);
+ return this._fetchJSON({
+ url: '/projects/',
+ errFn: opt_errFn,
+ params,
+ reportUrlAsIs: true,
+ });
},
/**
* @param {string} inputVal
* @param {number} opt_n
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- getSuggestedAccounts(inputVal, opt_n, opt_errFn, opt_ctx) {
+ getSuggestedAccounts(inputVal, opt_n, opt_errFn) {
if (!inputVal) {
return Promise.resolve([]);
}
const params = {suggest: null, q: inputVal};
if (opt_n) { params.n = opt_n; }
- return this.fetchJSON('/accounts/', opt_errFn, opt_ctx, params);
+ return this._fetchJSON({
+ url: '/accounts/',
+ errFn: opt_errFn,
+ params,
+ anonymizedUrl: '/accounts/?n=*',
+ });
},
addChangeReviewer(changeNum, reviewerID) {
@@ -1173,16 +1827,25 @@
throw Error('Unsupported HTTP method: ' + method);
}
- return this.send(method, url, body);
+ return this._send({method, url, body});
});
},
getRelatedChanges(changeNum, patchNum) {
- return this._getChangeURLAndFetch(changeNum, '/related', patchNum);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/related',
+ patchNum,
+ reportEndpointAsIs: true,
+ });
},
getChangesSubmittedTogether(changeNum) {
- return this._getChangeURLAndFetch(changeNum, '/submitted_together', null);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/submitted_together?o=NON_VISIBLE_CHANGES',
+ reportEndpointAsIs: true,
+ });
},
getChangeConflicts(changeNum) {
@@ -1194,7 +1857,11 @@
O: options,
q: 'status:open is:mergeable conflicts:' + changeNum,
};
- return this.fetchJSON('/changes/', null, null, params);
+ return this._fetchJSON({
+ url: '/changes/',
+ params,
+ anonymizedUrl: '/changes/conflicts:*',
+ });
},
getChangeCherryPicks(project, changeID, changeNum) {
@@ -1212,7 +1879,11 @@
O: options,
q: query,
};
- return this.fetchJSON('/changes/', null, null, params);
+ return this._fetchJSON({
+ url: '/changes/',
+ params,
+ anonymizedUrl: '/changes/change:*',
+ });
},
getChangesWithSameTopic(topic, changeNum) {
@@ -1225,17 +1896,26 @@
const query = [
'status:open',
'-change:' + changeNum,
- 'topic:' + topic,
+ `topic:"${topic}"`,
].join(' ');
const params = {
O: options,
q: query,
};
- return this.fetchJSON('/changes/', null, null, params);
+ return this._fetchJSON({
+ url: '/changes/',
+ params,
+ anonymizedUrl: '/changes/topic:*',
+ });
},
getReviewedFiles(changeNum, patchNum) {
- return this._getChangeURLAndFetch(changeNum, '/files?reviewed', patchNum);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/files?reviewed',
+ patchNum,
+ reportEndpointAsIs: true,
+ });
},
/**
@@ -1244,13 +1924,16 @@
* @param {string} path
* @param {boolean} reviewed
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn, opt_ctx) {
- const method = reviewed ? 'PUT' : 'DELETE';
- const e = `/files/${encodeURIComponent(path)}/reviewed`;
- return this.getChangeURLAndSend(changeNum, method, patchNum, e, null,
- opt_errFn, opt_ctx);
+ saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: reviewed ? 'PUT' : 'DELETE',
+ patchNum,
+ endpoint: `/files/${encodeURIComponent(path)}/reviewed`,
+ errFn: opt_errFn,
+ anonymizedEndpoint: '/files/*/reviewed',
+ });
},
/**
@@ -1258,165 +1941,346 @@
* @param {number|string} patchNum
* @param {!Object} review
* @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
*/
- saveChangeReview(changeNum, patchNum, review, opt_errFn, opt_ctx) {
+ saveChangeReview(changeNum, patchNum, review, opt_errFn) {
const promises = [
this.awaitPendingDiffDrafts(),
this.getChangeActionURL(changeNum, patchNum, '/review'),
];
return Promise.all(promises).then(([, url]) => {
- return this.send('POST', url, review, opt_errFn, opt_ctx);
+ return this._send({
+ method: 'POST',
+ url,
+ body: review,
+ errFn: opt_errFn,
+ });
});
},
getChangeEdit(changeNum, opt_download_commands) {
const params = opt_download_commands ? {'download-commands': true} : null;
return this.getLoggedIn().then(loggedIn => {
- return loggedIn ?
- this._getChangeURLAndFetch(changeNum, '/edit/', null, null, null,
- params) :
- false;
+ if (!loggedIn) { return false; }
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/edit/',
+ params,
+ reportEndpointAsIs: true,
+ });
});
},
/**
- * @param {!string} project
- * @param {!string} branch
- * @param {!string} subject
- * @param {!string} topic
- * @param {!boolean} isPrivate
- * @param {!boolean} workInProgress
+ * @param {string} project
+ * @param {string} branch
+ * @param {string} subject
+ * @param {string=} opt_topic
+ * @param {boolean=} opt_isPrivate
+ * @param {boolean=} opt_workInProgress
+ * @param {string=} opt_baseChange
+ * @param {string=} opt_baseCommit
+ */
+ createChange(project, branch, subject, opt_topic, opt_isPrivate,
+ opt_workInProgress, opt_baseChange, opt_baseCommit) {
+ return this._send({
+ method: 'POST',
+ url: '/changes/',
+ body: {
+ project,
+ branch,
+ subject,
+ topic: opt_topic,
+ is_private: opt_isPrivate,
+ work_in_progress: opt_workInProgress,
+ base_change: opt_baseChange,
+ base_commit: opt_baseCommit,
+ },
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ },
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string} path
+ * @param {number|string} patchNum
+ */
+ getFileContent(changeNum, path, patchNum) {
+ // 404s indicate the file does not exist yet in the revision, so suppress
+ // them.
+ const suppress404s = res => {
+ if (res && res.status !== 404) { this.fire('server-error', {res}); }
+ return res;
+ };
+ const promise = this.patchNumEquals(patchNum, this.EDIT_NAME) ?
+ this._getFileInChangeEdit(changeNum, path) :
+ this._getFileInRevision(changeNum, path, patchNum, suppress404s);
+
+ return promise.then(res => {
+ if (!res.ok) { return res; }
+
+ // The file type (used for syntax highlighting) is identified in the
+ // X-FYI-Content-Type header of the response.
+ const type = res.headers.get('X-FYI-Content-Type');
+ return this.getResponseObject(res).then(content => {
+ return {content, type, ok: true};
+ });
+ });
+ },
+
+ /**
+ * Gets a file in a specific change and revision.
+ * @param {number|string} changeNum
+ * @param {string} path
+ * @param {number|string} patchNum
+ * @param {?function(?Response, string=)=} opt_errFn
*/
- createChange(project, branch, subject, topic, isPrivate,
- workInProgress) {
- return this.send('POST', '/changes/',
- {project, branch, subject, topic, is_private: isPrivate,
- work_in_progress: workInProgress})
- .then(response => this.getResponseObject(response));
+ _getFileInRevision(changeNum, path, patchNum, opt_errFn) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'GET',
+ patchNum,
+ endpoint: `/files/${encodeURIComponent(path)}/content`,
+ errFn: opt_errFn,
+ headers: {Accept: 'application/json'},
+ anonymizedEndpoint: '/files/*/content',
+ });
},
- getFileInChangeEdit(changeNum, path) {
- const e = '/edit/' + encodeURIComponent(path);
- return this.getChangeURLAndSend(changeNum, 'GET', null, e);
+ /**
+ * Gets a file in a change edit.
+ * @param {number|string} changeNum
+ * @param {string} path
+ */
+ _getFileInChangeEdit(changeNum, path) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'GET',
+ endpoint: '/edit/' + encodeURIComponent(path),
+ headers: {Accept: 'application/json'},
+ anonymizedEndpoint: '/edit/*',
+ });
},
rebaseChangeEdit(changeNum) {
- return this.getChangeURLAndSend(changeNum, 'POST', null, '/edit:rebase');
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit:rebase',
+ reportEndpointAsIs: true,
+ });
},
deleteChangeEdit(changeNum) {
- return this.getChangeURLAndSend(changeNum, 'DELETE', null, '/edit');
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: '/edit',
+ reportEndpointAsIs: true,
+ });
},
restoreFileInChangeEdit(changeNum, restore_path) {
- const p = {restore_path};
- return this.getChangeURLAndSend(changeNum, 'POST', null, '/edit', p);
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit',
+ body: {restore_path},
+ reportEndpointAsIs: true,
+ });
},
renameFileInChangeEdit(changeNum, old_path, new_path) {
- const p = {old_path, new_path};
- return this.getChangeURLAndSend(changeNum, 'POST', null, '/edit', p);
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit',
+ body: {old_path, new_path},
+ reportEndpointAsIs: true,
+ });
},
deleteFileInChangeEdit(changeNum, path) {
- const e = '/edit/' + encodeURIComponent(path);
- return this.getChangeURLAndSend(changeNum, 'DELETE', null, e);
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: '/edit/' + encodeURIComponent(path),
+ anonymizedEndpoint: '/edit/*',
+ });
},
saveChangeEdit(changeNum, path, contents) {
- const e = '/edit/' + encodeURIComponent(path);
- return this.getChangeURLAndSend(changeNum, 'PUT', null, e, contents);
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/edit/' + encodeURIComponent(path),
+ body: contents,
+ contentType: 'text/plain',
+ anonymizedEndpoint: '/edit/*',
+ });
},
// Deprecated, prefer to use putChangeCommitMessage instead.
saveChangeCommitMessageEdit(changeNum, message) {
- const p = {message};
- return this.getChangeURLAndSend(changeNum, 'PUT', null, '/edit:message',
- p);
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/edit:message',
+ body: {message},
+ reportEndpointAsIs: true,
+ });
},
publishChangeEdit(changeNum) {
- return this.getChangeURLAndSend(changeNum, 'POST', null,
- '/edit:publish');
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit:publish',
+ reportEndpointAsIs: true,
+ });
},
putChangeCommitMessage(changeNum, message) {
- const p = {message};
- return this.getChangeURLAndSend(changeNum, 'PUT', null, '/message', p);
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/message',
+ body: {message},
+ reportEndpointAsIs: true,
+ });
},
saveChangeStarred(changeNum, starred) {
- const url = '/accounts/self/starred.changes/' + changeNum;
- const method = starred ? 'PUT' : 'DELETE';
- return this.send(method, url);
+ // Some servers may require the project name to be provided
+ // alongside the change number, so resolve the project name
+ // first.
+ return this.getFromProjectLookup(changeNum).then(project => {
+ const url = '/accounts/self/starred.changes/' +
+ (project ? encodeURIComponent(project) + '~' : '') + changeNum;
+ return this._send({
+ method: starred ? 'PUT' : 'DELETE',
+ url,
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ });
+ });
+ },
+
+ saveChangeReviewed(changeNum, reviewed) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: reviewed ? '/reviewed' : '/unreviewed',
+ });
},
/**
- * @param {string} method
- * @param {string} url
- * @param {?string|number|Object=} opt_body passed as null sometimes
- * and also apparently a number. TODO (beckysiegel) remove need for
- * number at least.
- * @param {?function(?Response, string=)=} opt_errFn
- * passed as null sometimes.
- * @param {?=} opt_ctx
- * @param {?string=} opt_contentType
+ * Send an XHR.
+ * @param {Defs.SendRequest} req
+ * @return {Promise}
*/
- send(method, url, opt_body, opt_errFn, opt_ctx, opt_contentType) {
- const options = {method};
- if (opt_body) {
+ _send(req) {
+ const options = {method: req.method};
+ if (req.body) {
options.headers = new Headers();
options.headers.set(
- 'Content-Type', opt_contentType || 'application/json');
- if (typeof opt_body !== 'string') {
- opt_body = JSON.stringify(opt_body);
- }
- options.body = opt_body;
+ 'Content-Type', req.contentType || 'application/json');
+ options.body = typeof req.body === 'string' ?
+ req.body : JSON.stringify(req.body);
}
- if (!url.startsWith('http')) {
- url = this.getBaseUrl() + url;
+ if (req.headers) {
+ if (!options.headers) { options.headers = new Headers(); }
+ for (const header in req.headers) {
+ if (!req.headers.hasOwnProperty(header)) { continue; }
+ options.headers.set(header, req.headers[header]);
+ }
}
- return this._auth.fetch(url, options).then(response => {
+ const url = req.url.startsWith('http') ?
+ req.url : this.getBaseUrl() + req.url;
+ const fetchReq = {
+ url,
+ fetchOptions: options,
+ anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl,
+ };
+ const xhr = this._fetch(fetchReq).then(response => {
if (!response.ok) {
- if (opt_errFn) {
- return opt_errFn.call(opt_ctx || null, response);
+ if (req.errFn) {
+ return req.errFn.call(undefined, response);
}
- this.fire('server-error', {response});
+ this.fire('server-error', {request: fetchReq, response});
}
return response;
}).catch(err => {
this.fire('network-error', {error: err});
- if (opt_errFn) {
- return opt_errFn.call(opt_ctx, null, err);
+ if (req.errFn) {
+ return req.errFn.call(undefined, null, err);
} else {
throw err;
}
});
+
+ if (req.parseResponse) {
+ return xhr.then(res => this.getResponseObject(res));
+ }
+
+ return xhr;
+ },
+
+ /**
+ * Public version of the _send method preserved for plugins.
+ * @param {string} method
+ * @param {string} url
+ * @param {?string|number|Object=} opt_body passed as null sometimes
+ * and also apparently a number. TODO (beckysiegel) remove need for
+ * number at least.
+ * @param {?function(?Response, string=)=} opt_errFn
+ * passed as null sometimes.
+ * @param {?string=} opt_contentType
+ * @param {Object=} opt_headers
+ */
+ send(method, url, opt_body, opt_errFn, opt_contentType,
+ opt_headers) {
+ return this._send({
+ method,
+ url,
+ body: opt_body,
+ errFn: opt_errFn,
+ contentType: opt_contentType,
+ headers: opt_headers,
+ });
},
/**
* @param {number|string} changeNum
- * @param {number|string} basePatchNum
+ * @param {number|string} basePatchNum Negative values specify merge parent
+ * index.
* @param {number|string} patchNum
* @param {string} path
+ * @param {string=} opt_whitespace the ignore-whitespace level for the diff
+ * algorithm.
* @param {function(?Response, string=)=} opt_errFn
- * @param {function()=} opt_cancelCondition
*/
- getDiff(changeNum, basePatchNum, patchNum, path,
- opt_errFn, opt_cancelCondition) {
+ getDiff(changeNum, basePatchNum, patchNum, path, opt_whitespace,
+ opt_errFn) {
const params = {
context: 'ALL',
intraline: null,
- whitespace: 'IGNORE_NONE',
+ whitespace: opt_whitespace || 'IGNORE_NONE',
};
- if (basePatchNum != PARENT_PATCH_NUM) {
+ if (this.isMergeParent(basePatchNum)) {
+ params.parent = this.getParentIndex(basePatchNum);
+ } else if (!this.patchNumEquals(basePatchNum, PARENT_PATCH_NUM)) {
params.base = basePatchNum;
}
const endpoint = `/files/${encodeURIComponent(path)}/diff`;
- return this._getChangeURLAndFetch(changeNum, endpoint, patchNum,
- opt_errFn, opt_cancelCondition, params);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint,
+ patchNum,
+ errFn: opt_errFn,
+ params,
+ anonymizedEndpoint: '/files/*/diff',
+ });
},
/**
@@ -1424,15 +2288,23 @@
* @param {number|string=} opt_basePatchNum
* @param {number|string=} opt_patchNum
* @param {string=} opt_path
+ * @return {!Promise<!Object>}
*/
getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
return this._getDiffComments(changeNum, '/comments', opt_basePatchNum,
opt_patchNum, opt_path);
},
- getDiffRobotComments(changeNum, basePatchNum, patchNum, opt_path) {
- return this._getDiffComments(changeNum, '/robotcomments', basePatchNum,
- patchNum, opt_path);
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string=} opt_basePatchNum
+ * @param {number|string=} opt_patchNum
+ * @param {string=} opt_path
+ * @return {!Promise<!Object>}
+ */
+ getDiffRobotComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
+ return this._getDiffComments(changeNum, '/robotcomments',
+ opt_basePatchNum, opt_patchNum, opt_path);
},
/**
@@ -1444,7 +2316,7 @@
* @param {number|string=} opt_basePatchNum
* @param {number|string=} opt_patchNum
* @param {string=} opt_path
- * @return {!Promise<?Object>}
+ * @return {!Promise<!Object>}
*/
getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
return this.getLoggedIn().then(loggedIn => {
@@ -1483,6 +2355,7 @@
* @param {number|string=} opt_basePatchNum
* @param {number|string=} opt_patchNum
* @param {string=} opt_path
+ * @return {!Promise<!Object>}
*/
_getDiffComments(changeNum, endpoint, opt_basePatchNum,
opt_patchNum, opt_path) {
@@ -1491,10 +2364,15 @@
* Helper function to make promises more legible.
*
* @param {string|number=} opt_patchNum
- * @return {!Object} Diff comments response.
+ * @return {!Promise<!Object>} Diff comments response.
*/
const fetchComments = opt_patchNum => {
- return this._getChangeURLAndFetch(changeNum, endpoint, opt_patchNum);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint,
+ patchNum: opt_patchNum,
+ reportEndpointAsIs: true,
+ });
};
if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
@@ -1584,8 +2462,10 @@
_sendDiffDraftRequest(method, changeNum, patchNum, draft) {
const isCreate = !draft.id && method === 'PUT';
let endpoint = '/drafts';
+ let anonymizedEndpoint = endpoint;
if (draft.id) {
endpoint += '/' + draft.id;
+ anonymizedEndpoint += '/*';
}
let body;
if (method === 'PUT') {
@@ -1596,8 +2476,16 @@
this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
}
- const promise = this.getChangeURLAndSend(changeNum, method, patchNum,
- endpoint, body);
+ const req = {
+ changeNum,
+ method,
+ patchNum,
+ endpoint,
+ body,
+ anonymizedEndpoint,
+ };
+
+ const promise = this._getChangeURLAndSend(req);
this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise);
if (isCreate) {
@@ -1608,13 +2496,15 @@
},
getCommitInfo(project, commit) {
- return this.fetchJSON(
- '/projects/' + encodeURIComponent(project) +
- '/commits/' + encodeURIComponent(commit));
+ return this._fetchJSON({
+ url: '/projects/' + encodeURIComponent(project) +
+ '/commits/' + encodeURIComponent(commit),
+ anonymizedUrl: '/projects/*/comments/*',
+ });
},
_fetchB64File(url) {
- return this._auth.fetch(this.getBaseUrl() + url)
+ return this._fetch({url: this.getBaseUrl() + url})
.then(response => {
if (!response.ok) { return Promise.reject(response.statusText); }
const type = response.headers.get('X-FYI-Content-Type');
@@ -1631,7 +2521,7 @@
* @param {string} path
* @param {number=} opt_parentIndex
*/
- getChangeFileContents(changeId, patchNum, path, opt_parentIndex) {
+ getB64FileContents(changeId, patchNum, path, opt_parentIndex) {
const parent = typeof opt_parentIndex === 'number' ?
'?parent=' + opt_parentIndex : '';
return this._changeBaseURL(changeId, patchNum).then(url => {
@@ -1647,10 +2537,10 @@
if (diff.meta_a && diff.meta_a.content_type.startsWith('image/')) {
if (patchRange.basePatchNum === 'PARENT') {
// Note: we only attempt to get the image from the first parent.
- promiseA = this.getChangeFileContents(changeNum, patchRange.patchNum,
+ promiseA = this.getB64FileContents(changeNum, patchRange.patchNum,
diff.meta_a.name, 1);
} else {
- promiseA = this.getChangeFileContents(changeNum,
+ promiseA = this.getB64FileContents(changeNum,
patchRange.basePatchNum, diff.meta_a.name);
}
} else {
@@ -1658,7 +2548,7 @@
}
if (diff.meta_b && diff.meta_b.content_type.startsWith('image/')) {
- promiseB = this.getChangeFileContents(changeNum, patchRange.patchNum,
+ promiseB = this.getB64FileContents(changeNum, patchRange.patchNum,
diff.meta_b.name);
} else {
promiseB = Promise.resolve(null);
@@ -1709,9 +2599,14 @@
* parameter.
*/
setChangeTopic(changeNum, topic) {
- const p = {topic};
- return this.getChangeURLAndSend(changeNum, 'PUT', null, '/topic', p)
- .then(this.getResponseObject.bind(this));
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/topic',
+ body: {topic},
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
},
/**
@@ -1720,12 +2615,22 @@
* parameter.
*/
setChangeHashtag(changeNum, hashtag) {
- return this.getChangeURLAndSend(changeNum, 'POST', null, '/hashtags',
- hashtag).then(this.getResponseObject.bind(this));
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/hashtags',
+ body: hashtag,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
},
deleteAccountHttpPassword() {
- return this.send('DELETE', '/accounts/self/password.http');
+ return this._send({
+ method: 'DELETE',
+ url: '/accounts/self/password.http',
+ reportUrlAsIs: true,
+ });
},
/**
@@ -1734,17 +2639,31 @@
* parameter.
*/
generateAccountHttpPassword() {
- return this.send('PUT', '/accounts/self/password.http', {generate: true})
- .then(this.getResponseObject.bind(this));
+ return this._send({
+ method: 'PUT',
+ url: '/accounts/self/password.http',
+ body: {generate: true},
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
},
getAccountSSHKeys() {
- return this._fetchSharedCacheURL('/accounts/self/sshkeys');
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/sshkeys',
+ reportUrlAsIs: true,
+ });
},
addAccountSSHKey(key) {
- return this.send('POST', '/accounts/self/sshkeys', key, null, null,
- 'plain/text')
+ const req = {
+ method: 'POST',
+ url: '/accounts/self/sshkeys',
+ body: key,
+ contentType: 'plain/text',
+ reportUrlAsIs: true,
+ };
+ return this._send(req)
.then(response => {
if (response.status < 200 && response.status >= 300) {
return Promise.reject();
@@ -1758,41 +2677,115 @@
},
deleteAccountSSHKey(id) {
- return this.send('DELETE', '/accounts/self/sshkeys/' + id);
+ return this._send({
+ method: 'DELETE',
+ url: '/accounts/self/sshkeys/' + id,
+ anonymizedUrl: '/accounts/self/sshkeys/*',
+ });
+ },
+
+ getAccountGPGKeys() {
+ return this._fetchJSON({
+ url: '/accounts/self/gpgkeys',
+ reportUrlAsIs: true,
+ });
+ },
+
+ addAccountGPGKey(key) {
+ const req = {
+ method: 'POST',
+ url: '/accounts/self/gpgkeys',
+ body: key,
+ reportUrlAsIs: true,
+ };
+ return this._send(req)
+ .then(response => {
+ if (response.status < 200 && response.status >= 300) {
+ return Promise.reject();
+ }
+ return this.getResponseObject(response);
+ })
+ .then(obj => {
+ if (!obj) { return Promise.reject(); }
+ return obj;
+ });
+ },
+
+ deleteAccountGPGKey(id) {
+ return this._send({
+ method: 'DELETE',
+ url: '/accounts/self/gpgkeys/' + id,
+ anonymizedUrl: '/accounts/self/gpgkeys/*',
+ });
},
deleteVote(changeNum, account, label) {
- const e = `/reviewers/${account}/votes/${encodeURIComponent(label)}`;
- return this.getChangeURLAndSend(changeNum, 'DELETE', null, e);
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: `/reviewers/${account}/votes/${encodeURIComponent(label)}`,
+ anonymizedEndpoint: '/reviewers/*/votes/*',
+ });
},
setDescription(changeNum, patchNum, desc) {
- const p = {description: desc};
- return this.getChangeURLAndSend(changeNum, 'PUT', patchNum,
- '/description', p);
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT', patchNum,
+ endpoint: '/description',
+ body: {description: desc},
+ reportUrlAsIs: true,
+ });
},
confirmEmail(token) {
- return this.send('PUT', '/config/server/email.confirm', {token})
- .then(response => {
- if (response.status === 204) {
- return 'Email confirmed successfully.';
- }
- return null;
- });
+ const req = {
+ method: 'PUT',
+ url: '/config/server/email.confirm',
+ body: {token},
+ reportUrlAsIs: true,
+ };
+ return this._send(req).then(response => {
+ if (response.status === 204) {
+ return 'Email confirmed successfully.';
+ }
+ return null;
+ });
},
- getCapabilities(token) {
- return this.fetchJSON('/config/server/capabilities');
+ getCapabilities(token, opt_errFn) {
+ return this._fetchJSON({
+ url: '/config/server/capabilities',
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ },
+
+ getTopMenus(opt_errFn) {
+ return this._fetchJSON({
+ url: '/config/server/top-menus',
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
},
setAssignee(changeNum, assignee) {
- const p = {assignee};
- return this.getChangeURLAndSend(changeNum, 'PUT', null, '/assignee', p);
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/assignee',
+ body: {assignee},
+ reportUrlAsIs: true,
+ });
},
deleteAssignee(changeNum) {
- return this.getChangeURLAndSend(changeNum, 'DELETE', null, '/assignee');
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: '/assignee',
+ reportUrlAsIs: true,
+ });
},
probePath(path) {
@@ -1807,16 +2800,22 @@
* @param {number|string=} opt_message
*/
startWorkInProgress(changeNum, opt_message) {
- const payload = {};
+ const body = {};
if (opt_message) {
- payload.message = opt_message;
+ body.message = opt_message;
}
- return this.getChangeURLAndSend(changeNum, 'POST', null, '/wip', payload)
- .then(response => {
- if (response.status === 204) {
- return 'Change marked as Work In Progress.';
- }
- });
+ const req = {
+ changeNum,
+ method: 'POST',
+ endpoint: '/wip',
+ body,
+ reportUrlAsIs: true,
+ };
+ return this._getChangeURLAndSend(req).then(response => {
+ if (response.status === 204) {
+ return 'Change marked as Work In Progress.';
+ }
+ });
},
/**
@@ -1825,8 +2824,14 @@
* @param {function(?Response, string=)=} opt_errFn
*/
startReview(changeNum, opt_body, opt_errFn) {
- return this.getChangeURLAndSend(changeNum, 'POST', null, '/ready',
- opt_body, opt_errFn);
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/ready',
+ body: opt_body,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
},
/**
@@ -1835,10 +2840,15 @@
* parameter.
*/
deleteComment(changeNum, patchNum, commentID, reason) {
- const endpoint = `/comments/${commentID}/delete`;
- const payload = {reason};
- return this.getChangeURLAndSend(changeNum, 'POST', patchNum, endpoint,
- payload).then(this.getResponseObject.bind(this));
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ patchNum,
+ endpoint: `/comments/${commentID}/delete`,
+ body: {reason},
+ parseResponse: true,
+ anonymizedEndpoint: '/comments/*/delete',
+ });
},
/**
@@ -1850,7 +2860,14 @@
*/
getChange(changeNum, opt_errFn) {
// Cannot use _changeBaseURL, as this function is used by _projectLookup.
- return this.fetchJSON(`/changes/${changeNum}`, opt_errFn);
+ return this._fetchJSON({
+ url: `/changes/?q=change:${changeNum}`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/changes/?q=change:*',
+ }).then(res => {
+ if (!res || !res.length) { return null; }
+ return res[0];
+ });
},
/**
@@ -1893,42 +2910,71 @@
/**
* Alias for _changeBaseURL.then(send).
* @todo(beckysiegel) clean up comments
- * @param {string|number} changeNum
- * @param {string} method
- * @param {?string|number} patchNum gets passed as null.
- * @param {?string} endpoint gets passed as null.
- * @param {?Object|number|string=} opt_payload gets passed as null, string,
- * Object, or number.
- * @param {function(?Response, string=)=} opt_errFn
- * @param {?=} opt_ctx
- * @param {?=} opt_contentType
+ * @param {Defs.ChangeSendRequest} req
* @return {!Promise<!Object>}
*/
- getChangeURLAndSend(changeNum, method, patchNum, endpoint, opt_payload,
- opt_errFn, opt_ctx, opt_contentType) {
- return this._changeBaseURL(changeNum, patchNum).then(url => {
- return this.send(method, url + endpoint, opt_payload, opt_errFn,
- opt_ctx, opt_contentType);
+ _getChangeURLAndSend(req) {
+ const anonymizedBaseUrl = req.patchNum ?
+ ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
+ const anonymizedEndpoint = req.reportEndpointAsIs ?
+ req.endpoint : req.anonymizedEndpoint;
+
+ return this._changeBaseURL(req.changeNum, req.patchNum).then(url => {
+ return this._send({
+ method: req.method,
+ url: url + req.endpoint,
+ body: req.body,
+ errFn: req.errFn,
+ contentType: req.contentType,
+ headers: req.headers,
+ parseResponse: req.parseResponse,
+ anonymizedUrl: anonymizedEndpoint ?
+ (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
+ });
});
},
- /**
- * Alias for _changeBaseURL.then(fetchJSON).
- * @todo(beckysiegel) clean up comments
- * @param {string|number} changeNum
- * @param {string} endpoint
- * @param {?string|number=} opt_patchNum gets passed as null.
- * @param {?function(?Response, string=)=} opt_errFn gets passed as null.
- * @param {?function()=} opt_cancelCondition gets passed as null.
- * @param {?Object=} opt_params gets passed as null.
- * @param {!Object=} opt_options
- * @return {!Promise<!Object>}
- */
- _getChangeURLAndFetch(changeNum, endpoint, opt_patchNum, opt_errFn,
- opt_cancelCondition, opt_params, opt_options) {
- return this._changeBaseURL(changeNum, opt_patchNum).then(url => {
- return this.fetchJSON(url + endpoint, opt_errFn, opt_cancelCondition,
- opt_params, opt_options);
+ /**
+ * Alias for _changeBaseURL.then(_fetchJSON).
+ * @param {Defs.ChangeFetchRequest} req
+ * @return {!Promise<!Object>}
+ */
+ _getChangeURLAndFetch(req) {
+ const anonymizedEndpoint = req.reportEndpointAsIs ?
+ req.endpoint : req.anonymizedEndpoint;
+ const anonymizedBaseUrl = req.patchNum ?
+ ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
+ return this._changeBaseURL(req.changeNum, req.patchNum).then(url => {
+ return this._fetchJSON({
+ url: url + req.endpoint,
+ errFn: req.errFn,
+ params: req.params,
+ fetchOptions: req.fetchOptions,
+ anonymizedUrl: anonymizedEndpoint ?
+ (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
+ });
+ });
+ },
+
+ /**
+ * Execute a change action or revision action on a change.
+ * @param {number} changeNum
+ * @param {string} method
+ * @param {string} endpoint
+ * @param {string|number|undefined} opt_patchNum
+ * @param {Object=} opt_payload
+ * @param {?function(?Response, string=)=} opt_errFn
+ * @return {Promise}
+ */
+ executeChangeAction(changeNum, method, endpoint, opt_patchNum, opt_payload,
+ opt_errFn) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method,
+ patchNum: opt_patchNum,
+ endpoint,
+ body: opt_payload,
+ errFn: opt_errFn,
});
},
@@ -1943,9 +2989,13 @@
*/
getBlame(changeNum, patchNum, path, opt_base) {
const encodedPath = encodeURIComponent(path);
- return this._getChangeURLAndFetch(changeNum,
- `/files/${encodedPath}/blame`, patchNum, undefined, undefined,
- opt_base ? {base: 't'} : undefined);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: `/files/${encodedPath}/blame`,
+ patchNum,
+ params: opt_base ? {base: 't'} : undefined,
+ anonymizedEndpoint: '/files/*/blame',
+ });
},
/**
@@ -1976,5 +3026,57 @@
return result;
});
},
+
+ /**
+ * Fetch a project dashboard definition.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-dashboard
+ * @param {string} project
+ * @param {string} dashboard
+ * @param {function(?Response, string=)=} opt_errFn
+ * passed as null sometimes.
+ * @return {!Promise<!Object>}
+ */
+ getDashboard(project, dashboard, opt_errFn) {
+ const url = '/projects/' + encodeURIComponent(project) + '/dashboards/' +
+ encodeURIComponent(dashboard);
+ return this._fetchSharedCacheURL({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/dashboards/*',
+ });
+ },
+
+ /**
+ * @param {string} filter
+ * @return {!Promise<?Object>}
+ */
+ getDocumentationSearches(filter) {
+ filter = filter.trim();
+ const encodedFilter = encodeURIComponent(filter);
+
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: `/Documentation/?q=${encodedFilter}`,
+ anonymizedUrl: '/Documentation/?*',
+ });
+ },
+
+ getMergeable(changeNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/revisions/current/mergeable',
+ parseResponse: true,
+ reportEndpointAsIs: true,
+ });
+ },
+
+ deleteDraftComments(query) {
+ return this._send({
+ method: 'POST',
+ url: '/accounts/self/drafts:delete',
+ body: {query},
+ });
+ },
});
})();