blob: 3c76ee1f0faa9a397400f1f29726ee752e7e46c8 [file] [log] [blame]
/**
* @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.
*/
/* NB: Order is important, because of namespaced classes. */
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
import {GrEtagDecorator} from './gr-etag-decorator.js';
import {SiteBasedCache, FetchPromisesCache, GrRestApiHelper} from './gr-rest-apis/gr-rest-api-helper.js';
import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
import {parseDate} from '../../../utils/date-util.js';
import {authService} from './gr-auth.js';
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_BLOCK_REGEX = /^set-cookie$/i;
const ANONYMIZED_CHANGE_BASE_URL = '/changes/*~*';
const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL +
'/revisions/*';
let siteBasedCache = new SiteBasedCache(); // Shared across instances.
let fetchPromisesCache = new FetchPromisesCache(); // Shared across instances.
let pendingRequest = {}; // Shared across instances.
let grEtagDecorator = new GrEtagDecorator; // Shared across instances.
let projectLookup = {}; // Shared across instances.
export function _testOnlyResetGrRestApiSharedObjects() {
for (const key in fetchPromisesCache._data) {
if (fetchPromisesCache._data.hasOwnProperty(key)) {
// reject already fulfilled promise does nothing
fetchPromisesCache._data[key].reject();
}
}
for (const key in pendingRequest) {
if (!pendingRequest.hasOwnProperty(key)) {
continue;
}
for (const req of pendingRequest[key]) {
// reject already fulfilled promise does nothing
req.reject();
}
}
siteBasedCache = new SiteBasedCache();
fetchPromisesCache = new FetchPromisesCache();
pendingRequest = {};
grEtagDecorator = new GrEtagDecorator;
projectLookup = {};
authService.clearCache();
}
/**
* @extends PolymerElement
*/
class GrRestApiInterface extends mixinBehaviors( [
PathListBehavior,
PatchSetBehavior,
RESTClientBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
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: siteBasedCache, // Shared across instances.
},
_sharedFetchPromises: {
type: Object,
value: fetchPromisesCache, // Shared across instances.
},
_pendingRequests: {
type: Object,
value: pendingRequest, // Intentional to share the object across instances.
},
_etags: {
type: Object,
value: grEtagDecorator, // Share across instances.
},
/**
* Used to maintain a mapping of changeNums to project names.
*/
_projectLookup: {
type: Object,
value: projectLookup, // Intentional to share the object across instances.
},
};
}
/** @override */
created() {
super.created();
this._auth = authService;
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} displayName
* @param {function(?Response, string=)=} opt_errFn
*/
setAccountDisplayName(displayName, opt_errFn) {
const req = {
method: 'PUT',
url: '/accounts/self/displayname',
body: {display_name: displayName},
errFn: opt_errFn,
parseResponse: true,
reportUrlAsIs: true,
};
return this._restApiHelper.send(req)
.then(newName => this._updateCachedAccount({displayName: 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) {
return this.getConfig(false)
.then(config => {
const options = opt_options || this._getChangesOptionsHex(config);
// 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;
}
return {
url: '/changes/',
params,
reportUrlAsIs: true,
};
})
.then(req => this._restApiHelper.fetchJSON(req))
.then(response => {
const iterateOverChanges = arr => {
for (const change of (arr || [])) {
this._maybeInsertInLookup(change);
}
};
// 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) {
return this.getConfig(false).then(config => {
const optionsHex = this._getChangeOptionsHex(config);
return this._getChangeDetail(
changeNum, optionsHex, opt_errFn, opt_cancelCondition)
.then(GrReviewerUpdatesParser.parse);
});
}
_getChangesOptionsHex(config) {
const options = [
this.ListChangesOption.LABELS,
this.ListChangesOption.DETAILED_ACCOUNTS,
];
if (config && config.change && config.change.enable_attention_set) {
options.push(this.ListChangesOption.DETAILED_LABELS);
} else {
options.push(this.ListChangesOption.REVIEWED);
}
return this.listChangesOptionsToHex(...options);
}
_getChangeOptionsHex(config) {
if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.changePage
&& !(config.receive && config.receive.enable_signed_push)) {
return window.DEFAULT_DETAIL_HEXES.changePage;
}
// 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,
];
if (config.receive && config.receive.enable_signed_push) {
options.push(this.ListChangesOption.PUSH_CERTIFICATES);
}
return this.listChangesOptionsToHex(...options);
}
/**
* @param {number|string} changeNum
* @param {function(?Response, string=)=} opt_errFn
* @param {function()=} opt_cancelCondition
*/
getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
let optionsHex = '';
if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.diffPage) {
optionsHex = window.DEFAULT_DETAIL_HEXES.diffPage;
} else {
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};
const req = {
url,
errFn: opt_errFn,
cancelCondition: opt_cancelCondition,
params,
fetchOptions: this._etags.getOptions(urlWithParams),
anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
};
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.dispatchEvent(new CustomEvent('server-error', {
detail: {request: req, response},
composed: true, bubbles: true,
}));
}
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);
}
getChangeRevisionActions(changeNum, patchNum) {
const req = {
changeNum,
endpoint: '/actions',
patchNum,
reportEndpointAsIs: true,
};
return this._getChangeURLAndFetch(req);
}
/**
* @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,
}, 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.dispatchEvent(new CustomEvent('server-error', {
detail: {res},
composed: true, bubbles: true,
}));
}
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/*',
});
}
saveFileUploadChangeEdit(changeNum, path, content) {
return this._getChangeURLAndSend({
changeNum,
method: 'PUT',
endpoint: '/edit/' + encodeURIComponent(path),
body: {binary_content: content},
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,
});
}
deleteChangeCommitMessage(changeNum, messageId) {
return this._getChangeURLAndSend({
changeNum,
method: 'DELETE',
endpoint: '/messages/' + messageId,
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.getConfig().then(config => {
const isAttentionSetEnabled = !!config && !!config.change
&& config.change.enable_attention_set;
if (isAttentionSetEnabled) return Promise.resolve();
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) => parseDate(a.updated) - 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.
*/
// We don't want to add accept header, since preloading of comments is
// working only without accept header.
const noAcceptHeader = true;
const fetchComments = opt_patchNum => this._getChangeURLAndFetch({
changeNum,
endpoint,
patchNum: opt_patchNum,
reportEndpointAsIs: true,
}, noAcceptHeader);
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;
});
}
addToAttentionSet(changeNum, user, reason) {
return this._getChangeURLAndSend({
changeNum,
method: 'POST',
endpoint: '/attention',
body: {user, reason},
reportUrlAsIs: true,
});
}
removeFromAttentionSet(changeNum, user, reason) {
return this._getChangeURLAndSend({
changeNum,
method: 'DELETE',
endpoint: `/attention/${user}`,
anonymizedEndpoint: '/attention/*',
body: {reason},
});
}
/**
* @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.dispatchEvent(new CustomEvent('page-error', {
detail: {response},
composed: true, bubbles: true,
}));
};
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, noAcceptHeader) {
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,
}, noAcceptHeader));
}
/**
* 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_BLOCK_REGEX.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);