Files
gerrit/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
Patrick Hiesel a4824dbce2 Clean up configs around mergeability behavior
Gerrit now lets admins decide if they want to optimize speed of
operations by omitting 'mergeable' from the change index and ChangeInfo
in API responses. Omitting 'mergeable' from the change index also means
that Gerrit will not reindex all open changes when the target ref
advances. This operation is especially costly on repositories with a
large number of open changes.

Prior to this commit, this was controlled by the following switches:

gerrit.config:
  - change.api.excludeChangeInMergeable: Skip 'mergable' in API
  responses.
  - index.change.indexMergeable: Skip mergeable when indexing.

ListChangeOptions:
  - SKIP_MERGEABLE: Skip 'mergeable' on-demand in the query at hand.

These three knobs make it hard to get this right. In addition, they
allow for potentially dangerous behavior when
change.api.excludeChangeInMergeable=false and
index.change.indexMergeable=false since this forces Gerrit to recompute
the 'mergeability' bit potentially many times when returning query
responses.

This commit cleans up this landscape by deprecating the SKIP_MERGEABLE
change list option. The idea is that Gerrit admins need to be
prescriptive about the performance behavior of their systems. So the
behavior should be controlled system-wide instead of per-request.

The two Gerrit configs are unified into an enum that allows us to get
rid of the dangerous behavior. The possible states of
change.mergeabilityComputationBehavior are:
  - API_REF_UPDATED_AND_CHANGE_REINDEX
  - REF_UPDATED_AND_CHANGE_REINDEX
  - NEVER

Change-Id: I01c87f6f6d731121b51301f403fa92de3e4c72f7
2020-01-27 14:09:27 +01:00

2757 lines
81 KiB
JavaScript

/**
* @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 DiffViewMode = {
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
UNIFIED: 'UNIFIED_DIFF',
};
const JSON_PREFIX = ')]}\'';
const MAX_PROJECT_RESULTS = 25;
// This value is somewhat arbitrary and not based on research or calculations.
const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
const PARENT_PATCH_NUM = 'PARENT';
const Requests = {
SEND_DIFF_DRAFT: 'sendDiffDraft',
};
const CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE =
'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/*';
/**
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.PathListMixin
* @appliesMixin Gerrit.PatchSetMixin
* @appliesMixin Gerrit.RESTClientMixin
* @extends Polymer.Element
*/
class GrRestApiInterface extends Polymer.mixinBehaviors( [
Gerrit.FireBehavior,
Gerrit.PathListBehavior,
Gerrit.PatchSetBehavior,
Gerrit.RESTClientBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-rest-api-interface'; }
/**
* Fired when an server error occurs.
*
* @event server-error
*/
/**
* Fired when a network error occurs.
*
* @event network-error
*/
/**
* Fired after an RPC completes.
*
* @event rpc-log
*/
constructor() {
super();
this.JSON_PREFIX = JSON_PREFIX;
}
static get properties() {
return {
_cache: {
type: Object,
value: new SiteBasedCache(), // Shared across instances.
},
_sharedFetchPromises: {
type: Object,
value: new FetchPromisesCache(), // Shared across instances.
},
_pendingRequests: {
type: Object,
value: {}, // Intentional to share the object across instances.
},
_etags: {
type: Object,
value: new GrEtagDecorator(), // Share across instances.
},
/**
* Used to maintain a mapping of changeNums to project names.
*/
_projectLookup: {
type: Object,
value: {}, // Intentional to share the object across instances.
},
};
}
/** @override */
created() {
super.created();
this._auth = Gerrit.Auth;
this._initRestApiHelper();
}
_initRestApiHelper() {
if (this._restApiHelper) {
return;
}
if (this._cache && this._auth && this._sharedFetchPromises) {
this._restApiHelper = new GrRestApiHelper(this._cache, this._auth,
this._sharedFetchPromises, this);
}
}
_fetchSharedCacheURL(req) {
// Cache is shared across instances
return this._restApiHelper.fetchCacheURL(req);
}
/**
* @param {!Object} response
* @return {?}
*/
getResponseObject(response) {
return this._restApiHelper.getResponseObject(response);
}
getConfig(noCache) {
if (!noCache) {
return this._fetchSharedCacheURL({
url: '/config/server/info',
reportUrlAsIs: true,
});
}
return this._restApiHelper.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/*',
});
}
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',
});
}
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=*',
});
}
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',
});
}
saveRepoConfig(repo, config, opt_errFn) {
// TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
// supports it.
const url = `/projects/${encodeURIComponent(repo)}/config`;
this._cache.delete(url);
return this._restApiHelper.send({
method: 'PUT',
url,
body: config,
errFn: opt_errFn,
anonymizedUrl: '/projects/*/config',
});
}
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._restApiHelper.send({
method: 'POST',
url: `/projects/${encodeName}/gc`,
body: '',
errFn: opt_errFn,
anonymizedUrl: '/projects/*/gc',
});
}
/**
* @param {?Object} config
* @param {function(?Response, string=)=} opt_errFn
*/
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._restApiHelper.send({
method: 'PUT',
url: `/projects/${encodeName}`,
body: config,
errFn: opt_errFn,
anonymizedUrl: '/projects/*',
});
}
/**
* @param {?Object} config
* @param {function(?Response, string=)=} opt_errFn
*/
createGroup(config, opt_errFn) {
if (!config.name) { return ''; }
const encodeName = encodeURIComponent(config.name);
return this._restApiHelper.send({
method: 'PUT',
url: `/groups/${encodeName}`,
body: config,
errFn: opt_errFn,
anonymizedUrl: '/groups/*',
});
}
getGroupConfig(group, opt_errFn) {
return this._restApiHelper.fetchJSON({
url: `/groups/${encodeURIComponent(group)}/detail`,
errFn: opt_errFn,
anonymizedUrl: '/groups/*/detail',
});
}
/**
* @param {string} repo
* @param {string} ref
* @param {function(?Response, string=)=} opt_errFn
*/
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._restApiHelper.send({
method: 'DELETE',
url: `/projects/${encodeName}/branches/${encodeRef}`,
body: '',
errFn: opt_errFn,
anonymizedUrl: '/projects/*/branches/*',
});
}
/**
* @param {string} repo
* @param {string} ref
* @param {function(?Response, string=)=} opt_errFn
*/
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._restApiHelper.send({
method: 'DELETE',
url: `/projects/${encodeName}/tags/${encodeRef}`,
body: '',
errFn: opt_errFn,
anonymizedUrl: '/projects/*/tags/*',
});
}
/**
* @param {string} name
* @param {string} branch
* @param {string} revision
* @param {function(?Response, string=)=} opt_errFn
*/
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._restApiHelper.send({
method: 'PUT',
url: `/projects/${encodeName}/branches/${encodeBranch}`,
body: revision,
errFn: opt_errFn,
anonymizedUrl: '/projects/*/branches/*',
});
}
/**
* @param {string} name
* @param {string} tag
* @param {string} revision
* @param {function(?Response, string=)=} opt_errFn
*/
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._restApiHelper.send({
method: 'PUT',
url: `/projects/${encodeName}/tags/${encodeTag}`,
body: revision,
errFn: opt_errFn,
anonymizedUrl: '/projects/*/tags/*',
});
}
/**
* @param {!string} groupName
* @returns {!Promise<boolean>}
*/
getIsGroupOwner(groupName) {
const encodeName = encodeURIComponent(groupName);
const req = {
url: `/groups/?owned&g=${encodeName}`,
anonymizedUrl: '/groups/owned&g=*',
};
return this._fetchSharedCacheURL(req)
.then(configs => configs.hasOwnProperty(groupName));
}
getGroupMembers(groupName, opt_errFn) {
const encodeName = encodeURIComponent(groupName);
return this._restApiHelper.fetchJSON({
url: `/groups/${encodeName}/members/`,
errFn: opt_errFn,
anonymizedUrl: '/groups/*/members',
});
}
getIncludedGroup(groupName) {
return this._restApiHelper.fetchJSON({
url: `/groups/${encodeURIComponent(groupName)}/groups/`,
anonymizedUrl: '/groups/*/groups',
});
}
saveGroupName(groupId, name) {
const encodeId = encodeURIComponent(groupId);
return this._restApiHelper.send({
method: 'PUT',
url: `/groups/${encodeId}/name`,
body: {name},
anonymizedUrl: '/groups/*/name',
});
}
saveGroupOwner(groupId, ownerId) {
const encodeId = encodeURIComponent(groupId);
return this._restApiHelper.send({
method: 'PUT',
url: `/groups/${encodeId}/owner`,
body: {owner: ownerId},
anonymizedUrl: '/groups/*/owner',
});
}
saveGroupDescription(groupId, description) {
const encodeId = encodeURIComponent(groupId);
return this._restApiHelper.send({
method: 'PUT',
url: `/groups/${encodeId}/description`,
body: {description},
anonymizedUrl: '/groups/*/description',
});
}
saveGroupOptions(groupId, options) {
const encodeId = encodeURIComponent(groupId);
return this._restApiHelper.send({
method: 'PUT',
url: `/groups/${encodeId}/options`,
body: options,
anonymizedUrl: '/groups/*/options',
});
}
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._restApiHelper.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);
const req = {
method: 'PUT',
url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
errFn: opt_errFn,
anonymizedUrl: '/groups/*/groups/*',
};
return this._restApiHelper.send(req).then(response => {
if (response.ok) {
return this.getResponseObject(response);
}
});
}
deleteGroupMembers(groupName, groupMembers) {
const encodeName = encodeURIComponent(groupName);
const encodeMember = encodeURIComponent(groupMembers);
return this._restApiHelper.send({
method: 'DELETE',
url: `/groups/${encodeName}/members/${encodeMember}`,
anonymizedUrl: '/groups/*/members/*',
});
}
deleteIncludedGroup(groupName, includedGroup) {
const encodeName = encodeURIComponent(groupName);
const encodeIncludedGroup = encodeURIComponent(includedGroup);
return this._restApiHelper.send({
method: 'DELETE',
url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
anonymizedUrl: '/groups/*/groups/*',
});
}
getVersion() {
return this._fetchSharedCacheURL({
url: '/config/server/version',
reportUrlAsIs: true,
});
}
getDiffPreferences() {
return this.getLoggedIn().then(loggedIn => {
if (loggedIn) {
return this._fetchSharedCacheURL({
url: '/accounts/self/preferences.diff',
reportUrlAsIs: true,
});
}
// These defaults should match the defaults in
// 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({
auto_hide_diff_table_header: true,
context: 10,
cursor_blink_rate: 0,
font_size: 12,
ignore_whitespace: 'IGNORE_NONE',
intraline_difference: true,
line_length: 100,
line_wrapping: false,
show_line_endings: true,
show_tabs: true,
show_whitespace_errors: true,
syntax_highlighting: true,
tab_size: 8,
theme: 'DEFAULT',
});
});
}
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
*/
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._restApiHelper.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._restApiHelper.send({
method: 'PUT',
url: '/accounts/self/preferences.diff',
body: prefs,
errFn: opt_errFn,
reportUrlAsIs: true,
});
}
/**
* @param {?Object} prefs
* @param {function(?Response, string=)=} opt_errFn
*/
saveEditPreferences(prefs, opt_errFn) {
// Invalidate the cache.
this._cache.delete('/accounts/self/preferences.edit');
return this._restApiHelper.send({
method: 'PUT',
url: '/accounts/self/preferences.edit',
body: prefs,
errFn: opt_errFn,
reportUrlAsIs: true,
});
}
getAccount() {
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._restApiHelper.fetchJSON({
url: '/accounts/self/external.ids',
reportUrlAsIs: true,
});
}
deleteAccountIdentity(id) {
return this._restApiHelper.send({
method: 'POST',
url: '/accounts/self/external.ids:delete',
body: id,
parseResponse: true,
reportUrlAsIs: true,
});
}
/**
* @param {string} userId the ID of the user usch as an email address.
* @return {!Promise<!Object>}
*/
getAccountDetails(userId) {
return this._restApiHelper.fetchJSON({
url: `/accounts/${encodeURIComponent(userId)}/detail`,
anonymizedUrl: '/accounts/*/detail',
});
}
getAccountEmails() {
return this._fetchSharedCacheURL({
url: '/accounts/self/emails',
reportUrlAsIs: true,
});
}
/**
* @param {string} email
* @param {function(?Response, string=)=} opt_errFn
*/
addAccountEmail(email, opt_errFn) {
return this._restApiHelper.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
*/
deleteAccountEmail(email, opt_errFn) {
return this._restApiHelper.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
*/
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._restApiHelper.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);
}
});
}
/**
* @param {?Object} obj
*/
_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.get('/accounts/self/detail');
if (cachedAccount) {
// Replace object in cache with new object to force UI updates.
this._cache.set('/accounts/self/detail',
Object.assign({}, cachedAccount, obj));
}
}
/**
* @param {string} name
* @param {function(?Response, string=)=} opt_errFn
*/
setAccountName(name, opt_errFn) {
const req = {
method: 'PUT',
url: '/accounts/self/name',
body: {name},
errFn: opt_errFn,
parseResponse: true,
reportUrlAsIs: true,
};
return this._restApiHelper.send(req)
.then(newName => this._updateCachedAccount({name: newName}));
}
/**
* @param {string} username
* @param {function(?Response, string=)=} opt_errFn
*/
setAccountUsername(username, opt_errFn) {
const req = {
method: 'PUT',
url: '/accounts/self/username',
body: {username},
errFn: opt_errFn,
parseResponse: true,
reportUrlAsIs: true,
};
return this._restApiHelper.send(req)
.then(newName => this._updateCachedAccount({username: newName}));
}
/**
* @param {string} status
* @param {function(?Response, string=)=} opt_errFn
*/
setAccountStatus(status, opt_errFn) {
const req = {
method: 'PUT',
url: '/accounts/self/status',
body: {status},
errFn: opt_errFn,
parseResponse: true,
reportUrlAsIs: true,
};
return this._restApiHelper.send(req)
.then(newStatus => this._updateCachedAccount({status: newStatus}));
}
getAccountStatus(userId) {
return this._restApiHelper.fetchJSON({
url: `/accounts/${encodeURIComponent(userId)}/status`,
anonymizedUrl: '/accounts/*/status',
});
}
getAccountGroups() {
return this._restApiHelper.fetchJSON({
url: '/accounts/self/groups',
reportUrlAsIs: true,
});
}
getAccountAgreements() {
return this._restApiHelper.fetchJSON({
url: '/accounts/self/agreements',
reportUrlAsIs: true,
});
}
saveAccountAgreement(name) {
return this._restApiHelper.send({
method: 'PUT',
url: '/accounts/self/agreements',
body: name,
reportUrlAsIs: true,
});
}
/**
* @param {string=} opt_params
*/
getAccountCapabilities(opt_params) {
let queryString = '';
if (opt_params) {
queryString = '?q=' + opt_params
.map(param => encodeURIComponent(param))
.join('&q=');
}
return this._fetchSharedCacheURL({
url: '/accounts/self/capabilities' + queryString,
anonymizedUrl: '/accounts/self/capabilities?q=*',
});
}
getLoggedIn() {
return this._auth.authCheck();
}
getIsAdmin() {
return this.getLoggedIn()
.then(isLoggedIn => {
if (isLoggedIn) {
return this.getAccountCapabilities();
} else {
return Promise.resolve();
}
})
.then(
capabilities => capabilities && capabilities.administrateServer
);
}
getDefaultPreferences() {
return this._fetchSharedCacheURL({
url: '/config/server/preferences',
reportUrlAsIs: true,
});
}
getPreferences() {
return this.getLoggedIn().then(loggedIn => {
if (loggedIn) {
const req = {url: '/accounts/self/preferences', reportUrlAsIs: true};
return this._fetchSharedCacheURL(req).then(res => {
if (this._isNarrowScreen()) {
// Note that this can be problematic, because the diff will stay
// unified even after increasing the window width.
res.default_diff_view = DiffViewMode.UNIFIED;
} else {
res.default_diff_view = res.diff_view;
}
return Promise.resolve(res);
});
}
return Promise.resolve({
changes_per_page: 25,
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({
url: '/accounts/self/watched.projects',
reportUrlAsIs: true,
});
}
/**
* @param {string} projects
* @param {function(?Response, string=)=} opt_errFn
*/
saveWatchedProjects(projects, opt_errFn) {
return this._restApiHelper.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
*/
deleteWatchedProjects(projects, opt_errFn) {
return this._restApiHelper.send({
method: 'POST',
url: '/accounts/self/watched.projects:delete',
body: projects,
errFn: opt_errFn,
reportUrlAsIs: true,
});
}
_isNarrowScreen() {
return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX;
}
/**
* @param {number=} opt_changesPerPage
* @param {string|!Array<string>=} opt_query A query or an array of queries.
* @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
* changeInfos.
*/
getChanges(opt_changesPerPage, opt_query, opt_offset, opt_options) {
const options = opt_options || this.listChangesOptionsToHex(
this.ListChangesOption.LABELS,
this.ListChangesOption.DETAILED_ACCOUNTS
);
// Issue 4524: respect legacy token with max sortkey.
if (opt_offset === 'n,z') {
opt_offset = 0;
}
const params = {
O: options,
S: opt_offset || 0,
};
if (opt_changesPerPage) { params.n = opt_changesPerPage; }
if (opt_query && opt_query.length > 0) {
params.q = opt_query;
}
const iterateOverChanges = arr => {
for (const change of (arr || [])) {
this._maybeInsertInLookup(change);
}
};
const req = {
url: '/changes/',
params,
reportUrlAsIs: true,
};
return this._restApiHelper.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);
}
} else {
iterateOverChanges(response);
}
return response;
});
}
/**
* Inserts a change into _projectLookup iff it has a valid structure.
*
* @param {?{ _number: (number|string) }} change
*/
_maybeInsertInLookup(change) {
if (change && change.project && change._number) {
this.setInProjectLookup(change._number, change.project);
}
}
/**
* TODO (beckysiegel) this needs to be rewritten with the optional param
* at the end.
*
* @param {number|string} changeNum
* @param {?number|string=} opt_patchNum passed as null sometimes.
* @param {?=} endpoint
* @return {!Promise<string>}
*/
getChangeActionURL(changeNum, opt_patchNum, endpoint) {
return this._changeBaseURL(changeNum, opt_patchNum)
.then(url => url + endpoint);
}
/**
* @param {number|string} changeNum
* @param {function(?Response, string=)=} opt_errFn
* @param {function()=} opt_cancelCondition
*/
getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
// This list MUST be kept in sync with
// ChangeIT#changeDetailsDoesNotRequireIndex
const options = [
this.ListChangesOption.ALL_COMMITS,
this.ListChangesOption.ALL_REVISIONS,
this.ListChangesOption.CHANGE_ACTIONS,
this.ListChangesOption.DETAILED_LABELS,
this.ListChangesOption.DOWNLOAD_COMMANDS,
this.ListChangesOption.MESSAGES,
this.ListChangesOption.SUBMITTABLE,
this.ListChangesOption.WEB_LINKS,
this.ListChangesOption.SKIP_DIFFSTAT,
];
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);
});
}
/**
* @param {number|string} changeNum
* @param {function(?Response, string=)=} opt_errFn
* @param {function()=} opt_cancelCondition
*/
getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
const optionsHex = this.listChangesOptionsToHex(
this.ListChangesOption.ALL_COMMITS,
this.ListChangesOption.ALL_REVISIONS,
this.ListChangesOption.SKIP_DIFFSTAT
);
return this._getChangeDetail(changeNum, optionsHex, opt_errFn,
opt_cancelCondition);
}
/**
* @param {number|string} changeNum
* @param {string|undefined} optionsHex list changes options in hex
* @param {function(?Response, string=)=} opt_errFn
* @param {function()=} opt_cancelCondition
*/
_getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition) {
return this.getChangeActionURL(changeNum, null, '/detail').then(url => {
const urlWithParams = this._restApiHelper
.urlWithParams(url, optionsHex);
const params = {O: optionsHex};
let req = {
url,
errFn: opt_errFn,
cancelCondition: opt_cancelCondition,
params,
fetchOptions: this._etags.getOptions(urlWithParams),
anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
};
req = this._restApiHelper.addAcceptJsonHeader(req);
return this._restApiHelper.fetchRawJSON(req).then(response => {
if (response && response.status === 304) {
return Promise.resolve(this._restApiHelper.parsePrefixedJSON(
this._etags.getCachedPayload(urlWithParams)));
}
if (response && !response.ok) {
if (opt_errFn) {
opt_errFn.call(null, response);
} else {
this.fire('server-error', {request: req, response});
}
return;
}
const payloadPromise = response ?
this._restApiHelper.readResponsePayload(response) :
Promise.resolve(null);
return payloadPromise.then(payload => {
if (!payload) { return null; }
this._etags.collect(urlWithParams, response, payload.raw);
this._maybeInsertInLookup(payload.parsed);
return payload.parsed;
});
});
});
}
/**
* @param {number|string} changeNum
* @param {number|string} patchNum
*/
getChangeCommitInfo(changeNum, patchNum) {
return this._getChangeURLAndFetch({
changeNum,
endpoint: '/commit?links',
patchNum,
reportEndpointAsIs: true,
});
}
/**
* @param {number|string} changeNum
* @param {Gerrit.PatchRange} patchRange
* @param {number=} opt_parentIndex
*/
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: '/files',
patchNum: patchRange.patchNum,
params,
reportEndpointAsIs: true,
});
}
/**
* @param {number|string} changeNum
* @param {Gerrit.PatchRange} patchRange
*/
getChangeEditFiles(changeNum, patchRange) {
let endpoint = '/edit?list';
let anonymizedEndpoint = endpoint;
if (patchRange.basePatchNum !== 'PARENT') {
endpoint += '&base=' + encodeURIComponent(patchRange.basePatchNum + '');
anonymizedEndpoint += '&base=*';
}
return this._getChangeURLAndFetch({
changeNum,
endpoint,
anonymizedEndpoint,
});
}
/**
* @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=*',
});
}
/**
* @param {number|string} changeNum
* @param {Gerrit.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);
}
/**
* The closure compiler doesn't realize this.specialFilePathCompare is
* valid.
*
* @suppress {checkTypes}
*/
getChangeFilePathsAsSpeciallySortedArray(changeNum, patchRange) {
return this.getChangeFiles(changeNum, patchRange).then(files => {
if (!files) return;
return Object.keys(files).sort(this.specialFilePathCompare);
});
}
getChangeRevisionActions(changeNum, patchNum) {
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;
});
}
/**
* @param {number|string} changeNum
* @param {string} inputVal
* @param {function(?Response, string=)=} opt_errFn
*/
getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) {
return this._getChangeSuggestedGroup('REVIEWER', changeNum, inputVal,
opt_errFn);
}
/**
* @param {number|string} changeNum
* @param {string} inputVal
* @param {function(?Response, string=)=} opt_errFn
*/
getChangeSuggestedCCs(changeNum, inputVal, opt_errFn) {
return this._getChangeSuggestedGroup('CC', changeNum, inputVal,
opt_errFn);
}
_getChangeSuggestedGroup(reviewerState, changeNum, inputVal, opt_errFn) {
// More suggestions may obscure content underneath in the reply dialog,
// see issue 10793.
const params = {'n': 6, 'reviewer-state': reviewerState};
if (inputVal) { params.q = inputVal; }
return this._getChangeURLAndFetch({
changeNum,
endpoint: '/suggest_reviewers',
errFn: opt_errFn,
params,
reportEndpointAsIs: true,
});
}
/**
* @param {number|string} changeNum
*/
getChangeIncludedIn(changeNum) {
return this._getChangeURLAndFetch({
changeNum,
endpoint: '/in',
reportEndpointAsIs: true,
});
}
_computeFilter(filter) {
if (filter && filter.startsWith('^')) {
filter = '&r=' + encodeURIComponent(filter);
} else if (filter) {
filter = '&m=' + encodeURIComponent(filter);
} else {
filter = '';
}
return filter;
}
/**
* @param {string} filter
* @param {number} groupsPerPage
* @param {number=} opt_offset
*/
_getGroupsUrl(filter, groupsPerPage, opt_offset) {
const offset = opt_offset || 0;
return `/groups/?n=${groupsPerPage + 1}&S=${offset}` +
this._computeFilter(filter);
}
/**
* @param {string} filter
* @param {number} reposPerPage
* @param {number=} opt_offset
*/
_getReposUrl(filter, reposPerPage, opt_offset) {
const defaultFilter = 'state:active OR state:read-only';
const namePartDelimiters = /[@.\-\s\/_]/g;
const offset = opt_offset || 0;
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}`;
}
invalidateGroupsCache() {
this._restApiHelper.invalidateFetchPromisesPrefix('/groups/?');
}
invalidateReposCache() {
this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?');
}
invalidateAccountsCache() {
this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/');
}
/**
* @param {string} filter
* @param {number} groupsPerPage
* @param {number=} opt_offset
* @return {!Promise<?Object>}
*/
getGroups(filter, groupsPerPage, opt_offset) {
const url = this._getGroupsUrl(filter, groupsPerPage, opt_offset);
return this._fetchSharedCacheURL({
url,
anonymizedUrl: '/groups/?*',
});
}
/**
* @param {string} filter
* @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._restApiHelper.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>}
*/
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._restApiHelper.fetchJSON({
url,
errFn: opt_errFn,
anonymizedUrl: '/projects/*/branches?*',
});
}
/**
* @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._restApiHelper.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, 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._restApiHelper.fetchJSON({
url,
errFn: opt_errFn,
anonymizedUrl: '/plugins/?all',
});
}
getRepoAccessRights(repoName, opt_errFn) {
// TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
// supports it.
return this._restApiHelper.fetchJSON({
url: `/projects/${encodeURIComponent(repoName)}/access`,
errFn: opt_errFn,
anonymizedUrl: '/projects/*/access',
});
}
setRepoAccessRights(repoName, repoInfo) {
// TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
// supports it.
return this._restApiHelper.send({
method: 'POST',
url: `/projects/${encodeURIComponent(repoName)}/access`,
body: repoInfo,
anonymizedUrl: '/projects/*/access',
});
}
setRepoAccessRightsForReview(projectName, projectInfo) {
return this._restApiHelper.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
*/
getSuggestedGroups(inputVal, opt_n, opt_errFn) {
const params = {s: inputVal};
if (opt_n) { params.n = opt_n; }
return this._restApiHelper.fetchJSON({
url: '/groups/',
errFn: opt_errFn,
params,
reportUrlAsIs: true,
});
}
/**
* @param {string} inputVal
* @param {number} opt_n
* @param {function(?Response, string=)=} opt_errFn
*/
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._restApiHelper.fetchJSON({
url: '/projects/',
errFn: opt_errFn,
params,
reportUrlAsIs: true,
});
}
/**
* @param {string} inputVal
* @param {number} opt_n
* @param {function(?Response, string=)=} opt_errFn
*/
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._restApiHelper.fetchJSON({
url: '/accounts/',
errFn: opt_errFn,
params,
anonymizedUrl: '/accounts/?n=*',
});
}
addChangeReviewer(changeNum, reviewerID) {
return this._sendChangeReviewerRequest('POST', changeNum, reviewerID);
}
removeChangeReviewer(changeNum, reviewerID) {
return this._sendChangeReviewerRequest('DELETE', changeNum, reviewerID);
}
_sendChangeReviewerRequest(method, changeNum, reviewerID) {
return this.getChangeActionURL(changeNum, null, '/reviewers')
.then(url => {
let body;
switch (method) {
case 'POST':
body = {reviewer: reviewerID};
break;
case 'DELETE':
url += '/' + encodeURIComponent(reviewerID);
break;
default:
throw Error('Unsupported HTTP method: ' + method);
}
return this._restApiHelper.send({method, url, body});
});
}
getRelatedChanges(changeNum, patchNum) {
return this._getChangeURLAndFetch({
changeNum,
endpoint: '/related',
patchNum,
reportEndpointAsIs: true,
});
}
getChangesSubmittedTogether(changeNum) {
return this._getChangeURLAndFetch({
changeNum,
endpoint: '/submitted_together?o=NON_VISIBLE_CHANGES',
reportEndpointAsIs: true,
});
}
getChangeConflicts(changeNum) {
const options = this.listChangesOptionsToHex(
this.ListChangesOption.CURRENT_REVISION,
this.ListChangesOption.CURRENT_COMMIT
);
const params = {
O: options,
q: 'status:open conflicts:' + changeNum,
};
return this._restApiHelper.fetchJSON({
url: '/changes/',
params,
anonymizedUrl: '/changes/conflicts:*',
});
}
getChangeCherryPicks(project, changeID, changeNum) {
const options = this.listChangesOptionsToHex(
this.ListChangesOption.CURRENT_REVISION,
this.ListChangesOption.CURRENT_COMMIT
);
const query = [
'project:' + project,
'change:' + changeID,
'-change:' + changeNum,
'-is:abandoned',
].join(' ');
const params = {
O: options,
q: query,
};
return this._restApiHelper.fetchJSON({
url: '/changes/',
params,
anonymizedUrl: '/changes/change:*',
});
}
getChangesWithSameTopic(topic, changeNum) {
const options = this.listChangesOptionsToHex(
this.ListChangesOption.LABELS,
this.ListChangesOption.CURRENT_REVISION,
this.ListChangesOption.CURRENT_COMMIT,
this.ListChangesOption.DETAILED_LABELS
);
const query = [
'status:open',
'-change:' + changeNum,
`topic:"${topic}"`,
].join(' ');
const params = {
O: options,
q: query,
};
return this._restApiHelper.fetchJSON({
url: '/changes/',
params,
anonymizedUrl: '/changes/topic:*',
});
}
getReviewedFiles(changeNum, patchNum) {
return this._getChangeURLAndFetch({
changeNum,
endpoint: '/files?reviewed',
patchNum,
reportEndpointAsIs: true,
});
}
/**
* @param {number|string} changeNum
* @param {number|string} patchNum
* @param {string} path
* @param {boolean} reviewed
* @param {function(?Response, string=)=} opt_errFn
*/
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',
});
}
/**
* @param {number|string} changeNum
* @param {number|string} patchNum
* @param {!Object} review
* @param {function(?Response, string=)=} opt_errFn
*/
saveChangeReview(changeNum, patchNum, review, opt_errFn) {
const promises = [
this.awaitPendingDiffDrafts(),
this.getChangeActionURL(changeNum, patchNum, '/review'),
];
return Promise.all(promises).then(([, url]) => this._restApiHelper.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 => {
if (!loggedIn) { return false; }
return this._getChangeURLAndFetch({
changeNum,
endpoint: '/edit/',
params,
reportEndpointAsIs: true,
});
});
}
/**
* @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._restApiHelper.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
*/
_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',
});
}
/**
* 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,
method: 'POST',
endpoint: '/edit:rebase',
reportEndpointAsIs: true,
});
}
deleteChangeEdit(changeNum) {
return this._getChangeURLAndSend({
changeNum,
method: 'DELETE',
endpoint: '/edit',
reportEndpointAsIs: true,
});
}
restoreFileInChangeEdit(changeNum, restore_path) {
return this._getChangeURLAndSend({
changeNum,
method: 'POST',
endpoint: '/edit',
body: {restore_path},
reportEndpointAsIs: true,
});
}
renameFileInChangeEdit(changeNum, old_path, new_path) {
return this._getChangeURLAndSend({
changeNum,
method: 'POST',
endpoint: '/edit',
body: {old_path, new_path},
reportEndpointAsIs: true,
});
}
deleteFileInChangeEdit(changeNum, path) {
return this._getChangeURLAndSend({
changeNum,
method: 'DELETE',
endpoint: '/edit/' + encodeURIComponent(path),
anonymizedEndpoint: '/edit/*',
});
}
saveChangeEdit(changeNum, path, contents) {
return this._getChangeURLAndSend({
changeNum,
method: 'PUT',
endpoint: '/edit/' + encodeURIComponent(path),
body: contents,
contentType: 'text/plain',
anonymizedEndpoint: '/edit/*',
});
}
getRobotCommentFixPreview(changeNum, patchNum, fixId) {
return this._getChangeURLAndFetch({
changeNum,
patchNum,
endpoint: `/fixes/${encodeURIComponent(fixId)}/preview`,
reportEndpointAsId: true,
});
}
applyFixSuggestion(changeNum, patchNum, fixId) {
return this._getChangeURLAndSend({
method: 'POST',
changeNum,
patchNum,
endpoint: `/fixes/${encodeURIComponent(fixId)}/apply`,
reportEndpointAsId: true,
});
}
// Deprecated, prefer to use putChangeCommitMessage instead.
saveChangeCommitMessageEdit(changeNum, message) {
return this._getChangeURLAndSend({
changeNum,
method: 'PUT',
endpoint: '/edit:message',
body: {message},
reportEndpointAsIs: true,
});
}
publishChangeEdit(changeNum) {
return this._getChangeURLAndSend({
changeNum,
method: 'POST',
endpoint: '/edit:publish',
reportEndpointAsIs: true,
});
}
putChangeCommitMessage(changeNum, message) {
return this._getChangeURLAndSend({
changeNum,
method: 'PUT',
endpoint: '/message',
body: {message},
reportEndpointAsIs: true,
});
}
saveChangeStarred(changeNum, starred) {
// 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._restApiHelper.send({
method: starred ? 'PUT' : 'DELETE',
url,
anonymizedUrl: '/accounts/self/starred.changes/*',
});
});
}
saveChangeReviewed(changeNum, reviewed) {
return this._getChangeURLAndSend({
changeNum,
method: 'PUT',
endpoint: reviewed ? '/reviewed' : '/unreviewed',
});
}
/**
* Public version of the _restApiHelper.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._restApiHelper.send({
method,
url,
body: opt_body,
errFn: opt_errFn,
contentType: opt_contentType,
headers: opt_headers,
});
}
/**
* @param {number|string} changeNum
* @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
*/
getDiff(changeNum, basePatchNum, patchNum, path, opt_whitespace,
opt_errFn) {
const params = {
context: 'ALL',
intraline: null,
whitespace: opt_whitespace || 'IGNORE_NONE',
};
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`;
const req = {
changeNum,
endpoint,
patchNum,
errFn: opt_errFn,
params,
anonymizedEndpoint: '/files/*/diff',
};
// Invalidate the cache if its edit patch to make sure we always get latest.
if (patchNum === this.EDIT_NAME) {
if (!req.fetchOptions) req.fetchOptions = {};
if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
req.fetchOptions.headers.append('Cache-Control', 'no-cache');
}
return this._getChangeURLAndFetch(req);
}
/**
* @param {number|string} changeNum
* @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);
}
/**
* @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);
}
/**
* If the user is logged in, fetch the user's draft diff comments. If there
* is no logged in user, the request is not made and the promise yields an
* empty object.
*
* @param {number|string} changeNum
* @param {number|string=} opt_basePatchNum
* @param {number|string=} opt_patchNum
* @param {string=} opt_path
* @return {!Promise<!Object>}
*/
getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
return this.getLoggedIn().then(loggedIn => {
if (!loggedIn) { return Promise.resolve({}); }
return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum,
opt_patchNum, opt_path);
});
}
_setRange(comments, comment) {
if (comment.in_reply_to && !comment.range) {
for (let i = 0; i < comments.length; i++) {
if (comments[i].id === comment.in_reply_to) {
comment.range = comments[i].range;
break;
}
}
}
return comment;
}
_setRanges(comments) {
comments = comments || [];
comments.sort(
(a, b) => util.parseDate(a.updated) - util.parseDate(b.updated)
);
for (const comment of comments) {
this._setRange(comments, comment);
}
return comments;
}
/**
* @param {number|string} changeNum
* @param {string} endpoint
* @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) {
/**
* Fetches the comments for a given patchNum.
* Helper function to make promises more legible.
*
* @param {string|number=} opt_patchNum
* @return {!Promise<!Object>} Diff comments response.
*/
const fetchComments = opt_patchNum => this._getChangeURLAndFetch({
changeNum,
endpoint,
patchNum: opt_patchNum,
reportEndpointAsIs: true,
});
if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
return fetchComments();
}
function onlyParent(c) { return c.side == PARENT_PATCH_NUM; }
function withoutParent(c) { return c.side != PARENT_PATCH_NUM; }
function setPath(c) { c.path = opt_path; }
const promises = [];
let comments;
let baseComments;
let fetchPromise;
fetchPromise = fetchComments(opt_patchNum).then(response => {
comments = response[opt_path] || [];
// TODO(kaspern): Implement this on in the backend so this can
// be removed.
// Sort comments by date so that parent ranges can be propagated
// in a single pass.
comments = this._setRanges(comments);
if (opt_basePatchNum == PARENT_PATCH_NUM) {
baseComments = comments.filter(onlyParent);
baseComments.forEach(setPath);
}
comments = comments.filter(withoutParent);
comments.forEach(setPath);
});
promises.push(fetchPromise);
if (opt_basePatchNum != PARENT_PATCH_NUM) {
fetchPromise = fetchComments(opt_basePatchNum).then(response => {
baseComments = (response[opt_path] || [])
.filter(withoutParent);
baseComments = this._setRanges(baseComments);
baseComments.forEach(setPath);
});
promises.push(fetchPromise);
}
return Promise.all(promises).then(() => Promise.resolve({
baseComments,
comments,
}));
}
/**
* @param {number|string} changeNum
* @param {string} endpoint
* @param {number|string=} opt_patchNum
*/
_getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum) {
return this._changeBaseURL(changeNum, opt_patchNum)
.then(url => url + endpoint);
}
saveDiffDraft(changeNum, patchNum, draft) {
return this._sendDiffDraftRequest('PUT', changeNum, patchNum, draft);
}
deleteDiffDraft(changeNum, patchNum, draft) {
return this._sendDiffDraftRequest('DELETE', changeNum, patchNum, draft);
}
/**
* @returns {boolean} Whether there are pending diff draft sends.
*/
hasPendingDiffDrafts() {
const promises = this._pendingRequests[Requests.SEND_DIFF_DRAFT];
return promises && promises.length;
}
/**
* @returns {!Promise<undefined>} A promise that resolves when all pending
* diff draft sends have resolved.
*/
awaitPendingDiffDrafts() {
return Promise.all(this._pendingRequests[Requests.SEND_DIFF_DRAFT] || [])
.then(() => {
this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
});
}
_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') {
body = draft;
}
if (!this._pendingRequests[Requests.SEND_DIFF_DRAFT]) {
this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
}
const req = {
changeNum,
method,
patchNum,
endpoint,
body,
anonymizedEndpoint,
};
const promise = this._getChangeURLAndSend(req);
this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise);
if (isCreate) {
return this._failForCreate200(promise);
}
return promise;
}
getCommitInfo(project, commit) {
return this._restApiHelper.fetchJSON({
url: '/projects/' + encodeURIComponent(project) +
'/commits/' + encodeURIComponent(commit),
anonymizedUrl: '/projects/*/comments/*',
});
}
_fetchB64File(url) {
return this._restApiHelper.fetch({url: this.getBaseUrl() + url})
.then(response => {
if (!response.ok) {
return Promise.reject(new Error(response.statusText));
}
const type = response.headers.get('X-FYI-Content-Type');
return response.text()
.then(text => {
return {body: text, type};
});
});
}
/**
* @param {string} changeId
* @param {string|number} patchNum
* @param {string} path
* @param {number=} opt_parentIndex
*/
getB64FileContents(changeId, patchNum, path, opt_parentIndex) {
const parent = typeof opt_parentIndex === 'number' ?
'?parent=' + opt_parentIndex : '';
return this._changeBaseURL(changeId, patchNum).then(url => {
url = `${url}/files/${encodeURIComponent(path)}/content${parent}`;
return this._fetchB64File(url);
});
}
getImagesForDiff(changeNum, diff, patchRange) {
let promiseA;
let promiseB;
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.getB64FileContents(changeNum, patchRange.patchNum,
diff.meta_a.name, 1);
} else {
promiseA = this.getB64FileContents(changeNum,
patchRange.basePatchNum, diff.meta_a.name);
}
} else {
promiseA = Promise.resolve(null);
}
if (diff.meta_b && diff.meta_b.content_type.startsWith('image/')) {
promiseB = this.getB64FileContents(changeNum, patchRange.patchNum,
diff.meta_b.name);
} else {
promiseB = Promise.resolve(null);
}
return Promise.all([promiseA, promiseB]).then(results => {
const baseImage = results[0];
const revisionImage = results[1];
// Sometimes the server doesn't send back the content type.
if (baseImage) {
baseImage._expectedType = diff.meta_a.content_type;
baseImage._name = diff.meta_a.name;
}
if (revisionImage) {
revisionImage._expectedType = diff.meta_b.content_type;
revisionImage._name = diff.meta_b.name;
}
return {baseImage, revisionImage};
});
}
/**
* @param {number|string} changeNum
* @param {?number|string=} opt_patchNum passed as null sometimes.
* @param {string=} opt_project
* @return {!Promise<string>}
*/
_changeBaseURL(changeNum, opt_patchNum, opt_project) {
// TODO(kaspern): For full slicer migration, app should warn with a call
// stack every time _changeBaseURL is called without a project.
const projectPromise = opt_project ?
Promise.resolve(opt_project) :
this.getFromProjectLookup(changeNum);
return projectPromise.then(project => {
let url = `/changes/${encodeURIComponent(project)}~${changeNum}`;
if (opt_patchNum) {
url += `/revisions/${opt_patchNum}`;
}
return url;
});
}
/**
* @suppress {checkTypes}
* Resulted in error: Promise.prototype.then does not match formal
* parameter.
*/
setChangeTopic(changeNum, topic) {
return this._getChangeURLAndSend({
changeNum,
method: 'PUT',
endpoint: '/topic',
body: {topic},
parseResponse: true,
reportUrlAsIs: true,
});
}
/**
* @suppress {checkTypes}
* Resulted in error: Promise.prototype.then does not match formal
* parameter.
*/
setChangeHashtag(changeNum, hashtag) {
return this._getChangeURLAndSend({
changeNum,
method: 'POST',
endpoint: '/hashtags',
body: hashtag,
parseResponse: true,
reportUrlAsIs: true,
});
}
deleteAccountHttpPassword() {
return this._restApiHelper.send({
method: 'DELETE',
url: '/accounts/self/password.http',
reportUrlAsIs: true,
});
}
/**
* @suppress {checkTypes}
* Resulted in error: Promise.prototype.then does not match formal
* parameter.
*/
generateAccountHttpPassword() {
return this._restApiHelper.send({
method: 'PUT',
url: '/accounts/self/password.http',
body: {generate: true},
parseResponse: true,
reportUrlAsIs: true,
});
}
getAccountSSHKeys() {
return this._fetchSharedCacheURL({
url: '/accounts/self/sshkeys',
reportUrlAsIs: true,
});
}
addAccountSSHKey(key) {
const req = {
method: 'POST',
url: '/accounts/self/sshkeys',
body: key,
contentType: 'text/plain',
reportUrlAsIs: true,
};
return this._restApiHelper.send(req)
.then(response => {
if (response.status < 200 && response.status >= 300) {
return Promise.reject(new Error('error'));
}
return this.getResponseObject(response);
})
.then(obj => {
if (!obj.valid) { return Promise.reject(new Error('error')); }
return obj;
});
}
deleteAccountSSHKey(id) {
return this._restApiHelper.send({
method: 'DELETE',
url: '/accounts/self/sshkeys/' + id,
anonymizedUrl: '/accounts/self/sshkeys/*',
});
}
getAccountGPGKeys() {
return this._restApiHelper.fetchJSON({
url: '/accounts/self/gpgkeys',
reportUrlAsIs: true,
});
}
addAccountGPGKey(key) {
const req = {
method: 'POST',
url: '/accounts/self/gpgkeys',
body: key,
reportUrlAsIs: true,
};
return this._restApiHelper.send(req)
.then(response => {
if (response.status < 200 && response.status >= 300) {
return Promise.reject(new Error('error'));
}
return this.getResponseObject(response);
})
.then(obj => {
if (!obj) { return Promise.reject(new Error('error')); }
return obj;
});
}
deleteAccountGPGKey(id) {
return this._restApiHelper.send({
method: 'DELETE',
url: '/accounts/self/gpgkeys/' + id,
anonymizedUrl: '/accounts/self/gpgkeys/*',
});
}
deleteVote(changeNum, account, label) {
return this._getChangeURLAndSend({
changeNum,
method: 'DELETE',
endpoint: `/reviewers/${account}/votes/${encodeURIComponent(label)}`,
anonymizedEndpoint: '/reviewers/*/votes/*',
});
}
setDescription(changeNum, patchNum, desc) {
return this._getChangeURLAndSend({
changeNum,
method: 'PUT', patchNum,
endpoint: '/description',
body: {description: desc},
reportUrlAsIs: true,
});
}
confirmEmail(token) {
const req = {
method: 'PUT',
url: '/config/server/email.confirm',
body: {token},
reportUrlAsIs: true,
};
return this._restApiHelper.send(req).then(response => {
if (response.status === 204) {
return 'Email confirmed successfully.';
}
return null;
});
}
getCapabilities(opt_errFn) {
return this._restApiHelper.fetchJSON({
url: '/config/server/capabilities',
errFn: opt_errFn,
reportUrlAsIs: true,
});
}
getTopMenus(opt_errFn) {
return this._fetchSharedCacheURL({
url: '/config/server/top-menus',
errFn: opt_errFn,
reportUrlAsIs: true,
});
}
setAssignee(changeNum, assignee) {
return this._getChangeURLAndSend({
changeNum,
method: 'PUT',
endpoint: '/assignee',
body: {assignee},
reportUrlAsIs: true,
});
}
deleteAssignee(changeNum) {
return this._getChangeURLAndSend({
changeNum,
method: 'DELETE',
endpoint: '/assignee',
reportUrlAsIs: true,
});
}
probePath(path) {
return fetch(new Request(path, {method: 'HEAD'}))
.then(response => response.ok);
}
/**
* @param {number|string} changeNum
* @param {number|string=} opt_message
*/
startWorkInProgress(changeNum, opt_message) {
const body = {};
if (opt_message) {
body.message = opt_message;
}
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.';
}
});
}
/**
* @param {number|string} changeNum
* @param {number|string=} opt_body
* @param {function(?Response, string=)=} opt_errFn
*/
startReview(changeNum, opt_body, opt_errFn) {
return this._getChangeURLAndSend({
changeNum,
method: 'POST',
endpoint: '/ready',
body: opt_body,
errFn: opt_errFn,
reportUrlAsIs: true,
});
}
/**
* @suppress {checkTypes}
* Resulted in error: Promise.prototype.then does not match formal
* parameter.
*/
deleteComment(changeNum, patchNum, commentID, reason) {
return this._getChangeURLAndSend({
changeNum,
method: 'POST',
patchNum,
endpoint: `/comments/${commentID}/delete`,
body: {reason},
parseResponse: true,
anonymizedEndpoint: '/comments/*/delete',
});
}
/**
* Given a changeNum, gets the change.
*
* @param {number|string} changeNum
* @param {function(?Response, string=)=} opt_errFn
* @return {!Promise<?Object>} The change
*/
getChange(changeNum, opt_errFn) {
// Cannot use _changeBaseURL, as this function is used by _projectLookup.
return this._restApiHelper.fetchJSON({
url: `/changes/?q=change:${changeNum}`,
errFn: opt_errFn,
anonymizedUrl: '/changes/?q=change:*',
}).then(res => {
if (!res || !res.length) { return null; }
return res[0];
});
}
/**
* @param {string|number} changeNum
* @param {string=} project
*/
setInProjectLookup(changeNum, project) {
if (this._projectLookup[changeNum] &&
this._projectLookup[changeNum] !== project) {
console.warn('Change set with multiple project nums.' +
'One of them must be invalid.');
}
this._projectLookup[changeNum] = project;
}
/**
* Checks in _projectLookup for the changeNum. If it exists, returns the
* project. If not, calls the restAPI to get the change, populates
* _projectLookup with the project for that change, and returns the project.
*
* @param {string|number} changeNum
* @return {!Promise<string|undefined>}
*/
getFromProjectLookup(changeNum) {
const project = this._projectLookup[changeNum];
if (project) { return Promise.resolve(project); }
const onError = response => {
// Fire a page error so that the visual 404 is displayed.
this.fire('page-error', {response});
};
return this.getChange(changeNum, onError).then(change => {
if (!change || !change.project) { return; }
this.setInProjectLookup(changeNum, change.project);
return change.project;
});
}
/**
* Alias for _changeBaseURL.then(send).
*
* @todo(beckysiegel) clean up comments
* @param {Gerrit.ChangeSendRequest} req
* @return {!Promise<!Object>}
*/
_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 => this._restApiHelper.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).
*
* @param {Gerrit.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 => this._restApiHelper.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,
});
}
/**
* Get blame information for the given diff.
*
* @param {string|number} changeNum
* @param {string|number} patchNum
* @param {string} path
* @param {boolean=} opt_base If true, requests blame for the base of the
* diff, rather than the revision.
* @return {!Promise<!Object>}
*/
getBlame(changeNum, patchNum, path, opt_base) {
const encodedPath = encodeURIComponent(path);
return this._getChangeURLAndFetch({
changeNum,
endpoint: `/files/${encodedPath}/blame`,
patchNum,
params: opt_base ? {base: 't'} : undefined,
anonymizedEndpoint: '/files/*/blame',
});
}
/**
* Modify the given create draft request promise so that it fails and throws
* an error if the response bears HTTP status 200 instead of HTTP 201.
*
* @see Issue 7763
* @param {Promise} promise The original promise.
* @return {Promise} The modified promise.
*/
_failForCreate200(promise) {
return promise.then(result => {
if (result.status === 200) {
// Read the response headers into an object representation.
const headers = Array.from(result.headers.entries())
.reduce((obj, [key, val]) => {
if (!HEADER_REPORTING_BLACKLIST.test(key)) {
obj[key] = val;
}
return obj;
}, {});
const err = new Error([
CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE,
JSON.stringify(headers),
].join('\n'));
// Throw the error so that it is caught by gr-reporting.
throw err;
}
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._restApiHelper.send({
method: 'POST',
url: '/accounts/self/drafts:delete',
body: {query},
});
}
}
customElements.define(GrRestApiInterface.is, GrRestApiInterface);
})();