blob: 09a2962437660f9f0f33399e32d2030882a6d442 [file] [log] [blame]
// 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;
const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900;
const PARENT_PATCH_NUM = 'PARENT';
const CHECK_SIGN_IN_DEBOUNCE_MS = 3 * 1000;
const CHECK_SIGN_IN_DEBOUNCER_NAME = 'checkCredentials';
const FAILED_TO_FETCH_ERROR = 'Failed to fetch';
const Requests = {
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;
Polymer({
is: 'gr-rest-api-interface',
behaviors: [
Gerrit.PathListBehavior,
Gerrit.RESTClientBehavior,
],
/**
* Fired when an server error occurs.
*
* @event server-error
*/
/**
* Fired when a network error occurs.
*
* @event network-error
*/
/**
* Fired when credentials were rejected by server (e.g. expired).
*
* @event auth-error
*/
properties: {
_cache: {
type: Object,
value: {}, // Intentional to share the object across instances.
},
_sharedFetchPromises: {
type: Object,
value: {}, // Intentional to share the object 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.
},
_auth: {
type: Object,
value: Gerrit.Auth, // Share across instances.
},
},
JSON_PREFIX,
/**
* Fetch JSON from url provided.
* Returns a Promise that resolves to a native Response.
* Doesn't do error checking. Supports cancel condition. Performs auth.
* Validates auth expiry errors.
* @param {string} url
* @param {?function(?Response, string=)=} opt_errFn
* passed as null sometimes.
* @param {?function()=} opt_cancelCondition
* passed as null sometimes.
* @param {?Object=} opt_params URL params, key-value hash.
* @param {?Object=} opt_options Fetch options.
*/
_fetchRawJSON(url, opt_errFn, opt_cancelCondition, opt_params,
opt_options) {
const urlWithParams = this._urlWithParams(url, opt_params);
return this._auth.fetch(urlWithParams, opt_options).then(response => {
if (opt_cancelCondition && opt_cancelCondition()) {
response.body.cancel();
return;
}
return response;
}).catch(err => {
const isLoggedIn = !!this._cache['/accounts/self/detail'];
if (isLoggedIn && err && err.message === FAILED_TO_FETCH_ERROR) {
if (!this.isDebouncerActive(CHECK_SIGN_IN_DEBOUNCER_NAME)) {
this.checkCredentials();
}
this.debounce(CHECK_SIGN_IN_DEBOUNCER_NAME, this.checkCredentials,
CHECK_SIGN_IN_DEBOUNCE_MS);
return;
}
if (opt_errFn) {
opt_errFn.call(undefined, null, err);
} else {
this.fire('network-error', {error: err});
}
throw err;
});
},
/**
* Fetch JSON from url provided.
* Returns a Promise that resolves to a parsed response.
* Same as {@link _fetchRawJSON}, plus error handling.
* @param {string} url
* @param {?function(?Response, string=)=} opt_errFn
* passed as null sometimes.
* @param {?function()=} opt_cancelCondition
* passed as null sometimes.
* @param {?Object=} opt_params URL params, key-value hash.
* @param {?Object=} opt_options Fetch options.
*/
fetchJSON(url, opt_errFn, opt_cancelCondition, opt_params, opt_options) {
return this._fetchRawJSON(
url, opt_errFn, opt_cancelCondition, opt_params, opt_options)
.then(response => {
if (!response) {
return;
}
if (!response.ok) {
if (opt_errFn) {
opt_errFn.call(null, response);
return;
}
this.fire('server-error', {response});
return;
}
return response && this.getResponseObject(response);
});
},
/**
* @param {string} url
* @param {?Object=} opt_params URL params, key-value hash.
* @return {string}
*/
_urlWithParams(url, opt_params) {
if (!opt_params) { return this.getBaseUrl() + url; }
const params = [];
for (const p in opt_params) {
if (!opt_params.hasOwnProperty(p)) { continue; }
if (opt_params[p] == null) {
params.push(encodeURIComponent(p));
continue;
}
for (const value of [].concat(opt_params[p])) {
params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`);
}
}
return this.getBaseUrl() + url + '?' + params.join('&');
},
/**
* @param {!Object} response
* @return {?}
*/
getResponseObject(response) {
return this._readResponsePayload(response)
.then(payload => payload.parsed);
},
/**
* @param {!Object} response
* @return {!Object}
*/
_readResponsePayload(response) {
return response.text().then(text => {
let result;
try {
result = this._parsePrefixedJSON(text);
} catch (_) {
result = null;
}
return {parsed: result, raw: text};
});
},
/**
* @param {string} source
* @return {?}
*/
_parsePrefixedJSON(source) {
return JSON.parse(source.substring(JSON_PREFIX.length));
},
getConfig() {
return this._fetchSharedCacheURL('/config/server/info');
},
getProject(project) {
return this._fetchSharedCacheURL(
'/projects/' + encodeURIComponent(project));
},
getProjectConfig(project) {
return this._fetchSharedCacheURL(
'/projects/' + encodeURIComponent(project) + '/config');
},
getProjectAccess(project) {
return this._fetchSharedCacheURL(
'/access/?project=' + encodeURIComponent(project));
},
saveProjectConfig(project, config, opt_errFn, opt_ctx) {
const encodeName = encodeURIComponent(project);
return this.send('PUT', `/projects/${encodeName}/config`, config,
opt_errFn, opt_ctx);
},
runProjectGC(project, opt_errFn, opt_ctx) {
if (!project) {
return '';
}
const encodeName = encodeURIComponent(project);
return this.send('POST', `/projects/${encodeName}/gc`, '',
opt_errFn, opt_ctx);
},
/**
* @param {?Object} config
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
createProject(config, opt_errFn, opt_ctx) {
if (!config.name) { return ''; }
const encodeName = encodeURIComponent(config.name);
return this.send('PUT', `/projects/${encodeName}`, config, opt_errFn,
opt_ctx);
},
/**
* @param {?Object} config
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
createGroup(config, opt_errFn, opt_ctx) {
if (!config.name) { return ''; }
const encodeName = encodeURIComponent(config.name);
return this.send('PUT', `/groups/${encodeName}`, config, opt_errFn,
opt_ctx);
},
getGroupConfig(group) {
const encodeName = encodeURIComponent(group);
return this.fetchJSON(`/groups/${encodeName}/detail`);
},
/**
* @param {string} project
* @param {string} ref
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
deleteProjectBranches(project, ref, opt_errFn, opt_ctx) {
if (!project || !ref) {
return '';
}
const encodeName = encodeURIComponent(project);
const encodeRef = encodeURIComponent(ref);
return this.send('DELETE',
`/projects/${encodeName}/branches/${encodeRef}`, '',
opt_errFn, opt_ctx);
},
/**
* @param {string} project
* @param {string} ref
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
deleteProjectTags(project, ref, opt_errFn, opt_ctx) {
if (!project || !ref) {
return '';
}
const encodeName = encodeURIComponent(project);
const encodeRef = encodeURIComponent(ref);
return this.send('DELETE',
`/projects/${encodeName}/tags/${encodeRef}`, '',
opt_errFn, opt_ctx);
},
/**
* @param {string} name
* @param {string} branch
* @param {string} revision
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
createProjectBranch(name, branch, revision, opt_errFn, opt_ctx) {
if (!name || !branch || !revision) { return ''; }
const encodeName = encodeURIComponent(name);
const encodeBranch = encodeURIComponent(branch);
return this.send('PUT',
`/projects/${encodeName}/branches/${encodeBranch}`,
revision, opt_errFn, opt_ctx);
},
/**
* @param {string} name
* @param {string} tag
* @param {string} revision
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
createProjectTag(name, tag, revision, opt_errFn, opt_ctx) {
if (!name || !tag || !revision) { return ''; }
const encodeName = encodeURIComponent(name);
const encodeTag = encodeURIComponent(tag);
return this.send('PUT', `/projects/${encodeName}/tags/${encodeTag}`,
revision, opt_errFn, opt_ctx);
},
/**
* @param {!string} groupName
* @returns {!Promise<boolean>}
*/
getIsGroupOwner(groupName) {
const encodeName = encodeURIComponent(groupName);
return this._fetchSharedCacheURL(`/groups/?owned&q=${encodeName}`)
.then(configs => configs.hasOwnProperty(groupName));
},
getGroupMembers(groupName) {
const encodeName = encodeURIComponent(groupName);
return this.send('GET', `/groups/${encodeName}/members/`)
.then(response => this.getResponseObject(response));
},
getIncludedGroup(groupName) {
const encodeName = encodeURIComponent(groupName);
return this.send('GET', `/groups/${encodeName}/groups/`)
.then(response => this.getResponseObject(response));
},
saveGroupName(groupId, name) {
const encodeId = encodeURIComponent(groupId);
return this.send('PUT', `/groups/${encodeId}/name`, {name});
},
saveGroupOwner(groupId, ownerId) {
const encodeId = encodeURIComponent(groupId);
return this.send('PUT', `/groups/${encodeId}/owner`, {owner: ownerId});
},
saveGroupDescription(groupId, description) {
const encodeId = encodeURIComponent(groupId);
return this.send('PUT', `/groups/${encodeId}/description`,
{description});
},
saveGroupOptions(groupId, options) {
const encodeId = encodeURIComponent(groupId);
return this.send('PUT', `/groups/${encodeId}/options`, options);
},
getGroupAuditLog(group) {
return this._fetchSharedCacheURL('/groups/' + group + '/log.audit');
},
saveGroupMembers(groupName, groupMembers) {
const encodeName = encodeURIComponent(groupName);
const encodeMember = encodeURIComponent(groupMembers);
return this.send('PUT', `/groups/${encodeName}/members/${encodeMember}`)
.then(response => this.getResponseObject(response));
},
saveIncludedGroup(groupName, includedGroup, opt_errFn) {
const encodeName = encodeURIComponent(groupName);
const encodeIncludedGroup = encodeURIComponent(includedGroup);
return this.send('PUT',
`/groups/${encodeName}/groups/${encodeIncludedGroup}`, null,
opt_errFn).then(response => {
if (response.ok) {
return this.getResponseObject(response);
}
});
},
deleteGroupMembers(groupName, groupMembers) {
const encodeName = encodeURIComponent(groupName);
const encodeMember = encodeURIComponent(groupMembers);
return this.send('DELETE',
`/groups/${encodeName}/members/${encodeMember}`);
},
deleteIncludedGroup(groupName, includedGroup) {
const encodeName = encodeURIComponent(groupName);
const encodeIncludedGroup = encodeURIComponent(includedGroup);
return this.send('DELETE',
`/groups/${encodeName}/groups/${encodeIncludedGroup}`);
},
getVersion() {
return this._fetchSharedCacheURL('/config/server/version');
},
getDiffPreferences() {
return this.getLoggedIn().then(loggedIn => {
if (loggedIn) {
return this._fetchSharedCacheURL('/accounts/self/preferences.diff');
}
// These defaults should match the defaults in
// gerrit-extension-api/src/main/jcg/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',
});
});
},
/**
* @param {?Object} prefs
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
savePreferences(prefs, opt_errFn, opt_ctx) {
// Note (Issue 5142): normalize the download scheme with lower case before
// saving.
if (prefs.download_scheme) {
prefs.download_scheme = prefs.download_scheme.toLowerCase();
}
return this.send('PUT', '/accounts/self/preferences', prefs, opt_errFn,
opt_ctx);
},
/**
* @param {?Object} prefs
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
saveDiffPreferences(prefs, opt_errFn, opt_ctx) {
// Invalidate the cache.
this._cache['/accounts/self/preferences.diff'] = undefined;
return this.send('PUT', '/accounts/self/preferences.diff', prefs,
opt_errFn, opt_ctx);
},
getAccount() {
return this._fetchSharedCacheURL('/accounts/self/detail', resp => {
if (resp.status === 403) {
this._cache['/accounts/self/detail'] = null;
}
});
},
/**
* @param {string} userId the ID of the user usch as an email address.
* @return {!Promise<!Object>}
*/
getAccountDetails(userId) {
return this.fetchJSON(`/accounts/${encodeURIComponent(userId)}/detail`);
},
getAccountEmails() {
return this._fetchSharedCacheURL('/accounts/self/emails');
},
/**
* @param {string} email
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
addAccountEmail(email, opt_errFn, opt_ctx) {
return this.send('PUT', '/accounts/self/emails/' +
encodeURIComponent(email), null, opt_errFn, opt_ctx);
},
/**
* @param {string} email
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
deleteAccountEmail(email, opt_errFn, opt_ctx) {
return this.send('DELETE', '/accounts/self/emails/' +
encodeURIComponent(email), null, opt_errFn, opt_ctx);
},
/**
* @param {string} email
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
setPreferredAccountEmail(email, opt_errFn, opt_ctx) {
return this.send('PUT', '/accounts/self/emails/' +
encodeURIComponent(email) + '/preferred', null,
opt_errFn, opt_ctx).then(() => {
// If result of getAccountEmails is in cache, update it in the cache
// so we don't have to invalidate it.
const cachedEmails = this._cache['/accounts/self/emails'];
if (cachedEmails) {
const emails = cachedEmails.map(entry => {
if (entry.email === email) {
return {email, preferred: true};
} else {
return {email};
}
});
this._cache['/accounts/self/emails'] = emails;
}
});
},
/**
* @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['/accounts/self/detail'];
if (cachedAccount) {
// Replace object in cache with new object to force UI updates.
this._cache['/accounts/self/detail'] =
Object.assign({}, cachedAccount, obj);
}
},
/**
* @param {string} name
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
setAccountName(name, opt_errFn, opt_ctx) {
return this.send('PUT', '/accounts/self/name', {name}, opt_errFn, opt_ctx)
.then(response => this.getResponseObject(response)
.then(newName => this._updateCachedAccount({name: newName})));
},
/**
* @param {string} username
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
setAccountUsername(username, opt_errFn, opt_ctx) {
return this.send('PUT', '/accounts/self/username', {username}, opt_errFn,
opt_ctx).then(response => this.getResponseObject(response)
.then(newName => this._updateCachedAccount({username: newName})));
},
/**
* @param {string} status
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
setAccountStatus(status, opt_errFn, opt_ctx) {
return this.send('PUT', '/accounts/self/status', {status},
opt_errFn, opt_ctx).then(response => this.getResponseObject(response)
.then(newStatus => this._updateCachedAccount(
{status: newStatus})));
},
getAccountStatus(userId) {
return this.fetchJSON(`/accounts/${encodeURIComponent(userId)}/status`);
},
getAccountGroups() {
return this._fetchSharedCacheURL('/accounts/self/groups');
},
getAccountAgreements() {
return this._fetchSharedCacheURL('/accounts/self/agreements');
},
/**
* @param {string=} opt_params
*/
getAccountCapabilities(opt_params) {
let queryString = '';
if (opt_params) {
queryString = '?q=' + opt_params
.map(param => { return encodeURIComponent(param); })
.join('&q=');
}
return this._fetchSharedCacheURL('/accounts/self/capabilities' +
queryString);
},
getLoggedIn() {
return this.getAccount().then(account => {
return account != null;
});
},
getIsAdmin() {
return this.getLoggedIn().then(isLoggedIn => {
if (isLoggedIn) {
return this.getAccountCapabilities();
} else {
return Promise.resolve();
}
}).then(capabilities => {
return capabilities && capabilities.administrateServer;
});
},
checkCredentials() {
// Skip the REST response cache.
return this._fetchRawJSON('/accounts/self/detail').then(response => {
if (!response) { return; }
if (response.status === 403) {
this.fire('auth-error');
this._cache['/accounts/self/detail'] = null;
} else if (response.ok) {
return this.getResponseObject(response);
}
}).then(response => {
if (response) {
this._cache['/accounts/self/detail'] = response;
}
return response;
});
},
getDefaultPreferences() {
return this._fetchSharedCacheURL('/config/server/preferences');
},
getPreferences() {
return this.getLoggedIn().then(loggedIn => {
if (loggedIn) {
return this._fetchSharedCacheURL('/accounts/self/preferences').then(
res => {
if (this._isNarrowScreen()) {
res.default_diff_view = DiffViewMode.UNIFIED;
} else {
res.default_diff_view = res.diff_view;
}
return Promise.resolve(res);
});
}
return Promise.resolve({
changes_per_page: 25,
default_diff_view: this._isNarrowScreen() ?
DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE,
diff_view: 'SIDE_BY_SIDE',
});
});
},
getWatchedProjects() {
return this._fetchSharedCacheURL('/accounts/self/watched.projects');
},
/**
* @param {string} projects
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
saveWatchedProjects(projects, opt_errFn, opt_ctx) {
return this.send('POST', '/accounts/self/watched.projects', projects,
opt_errFn, opt_ctx)
.then(response => {
return this.getResponseObject(response);
});
},
/**
* @param {string} projects
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
deleteWatchedProjects(projects, opt_errFn, opt_ctx) {
return this.send('POST', '/accounts/self/watched.projects:delete',
projects, opt_errFn, opt_ctx);
},
/**
* @param {string} url
* @param {function(?Response, string=)=} opt_errFn
*/
_fetchSharedCacheURL(url, opt_errFn) {
if (this._sharedFetchPromises[url]) {
return this._sharedFetchPromises[url];
}
// TODO(andybons): Periodic cache invalidation.
if (this._cache[url] !== undefined) {
return Promise.resolve(this._cache[url]);
}
this._sharedFetchPromises[url] = this.fetchJSON(url, opt_errFn)
.then(response => {
if (response !== undefined) {
this._cache[url] = response;
}
this._sharedFetchPromises[url] = undefined;
return response;
}).catch(err => {
this._sharedFetchPromises[url] = undefined;
throw err;
});
return this._sharedFetchPromises[url];
},
_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);
}
};
return this.fetchJSON('/changes/', null, null, params).then(response => {
// Response may be an array of changes OR an array of arrays of
// changes.
if (opt_query instanceof Array) {
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) {
const options = this.listChangesOptionsToHex(
this.ListChangesOption.ALL_COMMITS,
this.ListChangesOption.ALL_REVISIONS,
this.ListChangesOption.CHANGE_ACTIONS,
this.ListChangesOption.CURRENT_ACTIONS,
this.ListChangesOption.DOWNLOAD_COMMANDS,
this.ListChangesOption.SUBMITTABLE,
this.ListChangesOption.WEB_LINKS
);
return this._getChangeDetail(
changeNum, options, opt_errFn, opt_cancelCondition)
.then(GrReviewerUpdatesParser.parse);
},
/**
* @param {number|string} changeNum
* @param {function(?Response, string=)=} opt_errFn
* @param {function()=} opt_cancelCondition
*/
getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
const params = this.listChangesOptionsToHex(
this.ListChangesOption.ALL_REVISIONS
);
return this._getChangeDetail(changeNum, params, opt_errFn,
opt_cancelCondition);
},
/**
* @param {number|string} changeNum
* @param {function(?Response, string=)=} opt_errFn
* @param {function()=} opt_cancelCondition
*/
_getChangeDetail(changeNum, params, opt_errFn,
opt_cancelCondition) {
return this.getChangeActionURL(changeNum, null, '/detail').then(url => {
const urlWithParams = this._urlWithParams(url, params);
return this._fetchRawJSON(
url,
opt_errFn,
opt_cancelCondition,
{O: params},
this._etags.getOptions(urlWithParams))
.then(response => {
if (response && response.status === 304) {
return Promise.resolve(this._parsePrefixedJSON(
this._etags.getCachedPayload(urlWithParams)));
}
if (response && !response.ok) {
if (opt_errFn) {
opt_errFn.call(null, response);
} else {
this.fire('server-error', {response});
}
return;
}
const payloadPromise = response ?
this._readResponsePayload(response) :
Promise.resolve(null);
return payloadPromise.then(payload => {
if (!payload) { return null; }
this._etags.collect(urlWithParams, response, payload.raw);
this._maybeInsertInLookup(payload);
return payload.parsed;
});
});
});
},
/**
* @param {number|string} changeNum
* @param {number|string} patchNum
*/
getChangeCommitInfo(changeNum, patchNum) {
return this._getChangeURLAndFetch(changeNum, '/commit?links', patchNum);
},
/**
* @param {number|string} changeNum
* @param {!Promise<?Object>} patchRange
*/
getChangeFiles(changeNum, patchRange) {
let endpoint = '/files';
if (patchRange.basePatchNum !== 'PARENT') {
endpoint += '?base=' + encodeURIComponent(patchRange.basePatchNum);
}
return this._getChangeURLAndFetch(changeNum, endpoint,
patchRange.patchNum);
},
/**
* @param {number|string} changeNum
* @param {!Promise<?Object>} patchRange
*/
getChangeEditFiles(changeNum, patchRange) {
let endpoint = '/edit?list';
if (patchRange.basePatchNum !== 'PARENT') {
endpoint += '?base=' + encodeURIComponent(patchRange.basePatchNum);
}
return this._getChangeURLAndFetch(changeNum, endpoint);
},
getChangeFilesAsSpeciallySortedArray(changeNum, patchRange) {
return this.getChangeFiles(changeNum, patchRange).then(
this._normalizeChangeFilesResponse.bind(this));
},
getChangeEditFilesAsSpeciallySortedArray(changeNum, patchRange) {
return this.getChangeEditFiles(changeNum, patchRange).then(files =>
this._normalizeChangeFilesResponse(files.files));
},
/**
* The closure compiler doesn't realize this.specialFilePathCompare is
* valid.
* @suppress {checkTypes}
*/
getChangeFilePathsAsSpeciallySortedArray(changeNum, patchRange) {
return this.getChangeFiles(changeNum, patchRange).then(files => {
return Object.keys(files).sort(this.specialFilePathCompare);
});
},
/**
* The closure compiler doesn't realize this.specialFilePathCompare is
* valid.
* @suppress {checkTypes}
*/
_normalizeChangeFilesResponse(response) {
if (!response) { return []; }
const paths = Object.keys(response).sort(this.specialFilePathCompare);
const files = [];
for (let i = 0; i < paths.length; i++) {
const info = response[paths[i]];
info.__path = paths[i];
info.lines_inserted = info.lines_inserted || 0;
info.lines_deleted = info.lines_deleted || 0;
files.push(info);
}
return files;
},
getChangeRevisionActions(changeNum, patchNum) {
return this._getChangeURLAndFetch(changeNum, '/actions', patchNum)
.then(revisionActions => {
// The rebase button on change screen is always enabled.
if (revisionActions.rebase) {
revisionActions.rebase.rebaseOnCurrent =
!!revisionActions.rebase.enabled;
revisionActions.rebase.enabled = true;
}
return revisionActions;
});
},
/**
* @param {number|string} changeNum
* @param {string} inputVal
* @param {function(?Response, string=)=} opt_errFn
*/
getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) {
const params = {n: 10};
if (inputVal) { params.q = inputVal; }
return this._getChangeURLAndFetch(changeNum, '/suggest_reviewers', null,
opt_errFn, null, params);
},
/**
* @param {number|string} changeNum
*/
getChangeIncludedIn(changeNum) {
return this._getChangeURLAndFetch(changeNum, '/in', null);
},
_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
* @return {!Promise<?Object>}
*/
getGroups(filter, groupsPerPage, opt_offset) {
const offset = opt_offset || 0;
return this._fetchSharedCacheURL(
`/groups/?n=${groupsPerPage + 1}&S=${offset}` +
this._computeFilter(filter)
);
},
/**
* @param {string} filter
* @param {number} projectsPerPage
* @param {number=} opt_offset
* @return {!Promise<?Object>}
*/
getProjects(filter, projectsPerPage, opt_offset) {
const offset = opt_offset || 0;
return this._fetchSharedCacheURL(
`/projects/?d&n=${projectsPerPage + 1}&S=${offset}` +
this._computeFilter(filter)
);
},
setProjectHead(project, ref) {
return this.send(
'PUT', `/projects/${encodeURIComponent(project)}/HEAD`, {ref});
},
/**
* @param {string} filter
* @param {string} project
* @param {number} projectsBranchesPerPage
* @param {number=} opt_offset
* @return {!Promise<?Object>}
*/
getProjectBranches(filter, project, projectsBranchesPerPage, opt_offset) {
const offset = opt_offset || 0;
return this.fetchJSON(
`/projects/${encodeURIComponent(project)}/branches` +
`?n=${projectsBranchesPerPage + 1}&S=${offset}` +
this._computeFilter(filter)
);
},
/**
* @param {string} filter
* @param {string} project
* @param {number} projectsTagsPerPage
* @param {number=} opt_offset
* @return {!Promise<?Object>}
*/
getProjectTags(filter, project, projectsTagsPerPage, opt_offset) {
const offset = opt_offset || 0;
return this.fetchJSON(
`/projects/${encodeURIComponent(project)}/tags` +
`?n=${projectsTagsPerPage + 1}&S=${offset}` +
this._computeFilter(filter)
);
},
/**
* @param {string} filter
* @param {number} pluginsPerPage
* @param {number=} opt_offset
* @return {!Promise<?Object>}
*/
getPlugins(filter, pluginsPerPage, opt_offset) {
const offset = opt_offset || 0;
return this.fetchJSON(
`/plugins/?all&n=${pluginsPerPage + 1}&S=${offset}` +
this._computeFilter(filter)
);
},
getProjectAccessRights(projectName) {
return this._fetchSharedCacheURL(
`/projects/${encodeURIComponent(projectName)}/access`);
},
setProjectAccessRights(projectName, projectInfo) {
return this.send(
'POST', `/projects/${encodeURIComponent(projectName)}/access`,
projectInfo);
},
/**
* @param {string} inputVal
* @param {number} opt_n
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
getSuggestedGroups(inputVal, opt_n, opt_errFn, opt_ctx) {
const params = {s: inputVal};
if (opt_n) { params.n = opt_n; }
return this.fetchJSON('/groups/', opt_errFn, opt_ctx, params);
},
/**
* @param {string} inputVal
* @param {number} opt_n
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
getSuggestedProjects(inputVal, opt_n, opt_errFn, opt_ctx) {
const params = {
m: inputVal,
n: MAX_PROJECT_RESULTS,
type: 'ALL',
};
if (opt_n) { params.n = opt_n; }
return this.fetchJSON('/projects/', opt_errFn, opt_ctx, params);
},
/**
* @param {string} inputVal
* @param {number} opt_n
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
getSuggestedAccounts(inputVal, opt_n, opt_errFn, opt_ctx) {
if (!inputVal) {
return Promise.resolve([]);
}
const params = {suggest: null, q: inputVal};
if (opt_n) { params.n = opt_n; }
return this.fetchJSON('/accounts/', opt_errFn, opt_ctx, params);
},
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.send(method, url, body);
});
},
getRelatedChanges(changeNum, patchNum) {
return this._getChangeURLAndFetch(changeNum, '/related', patchNum);
},
getChangesSubmittedTogether(changeNum) {
return this._getChangeURLAndFetch(changeNum, '/submitted_together', null);
},
getChangeConflicts(changeNum) {
const options = this.listChangesOptionsToHex(
this.ListChangesOption.CURRENT_REVISION,
this.ListChangesOption.CURRENT_COMMIT
);
const params = {
O: options,
q: 'status:open is:mergeable conflicts:' + changeNum,
};
return this.fetchJSON('/changes/', null, null, params);
},
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.fetchJSON('/changes/', null, null, params);
},
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.fetchJSON('/changes/', null, null, params);
},
getReviewedFiles(changeNum, patchNum) {
return this._getChangeURLAndFetch(changeNum, '/files?reviewed', patchNum);
},
/**
* @param {number|string} changeNum
* @param {number|string} patchNum
* @param {string} path
* @param {boolean} reviewed
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn, opt_ctx) {
const method = reviewed ? 'PUT' : 'DELETE';
const e = `/files/${encodeURIComponent(path)}/reviewed`;
return this.getChangeURLAndSend(changeNum, method, patchNum, e, null,
opt_errFn, opt_ctx);
},
/**
* @param {number|string} changeNum
* @param {number|string} patchNum
* @param {!Object} review
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
saveChangeReview(changeNum, patchNum, review, opt_errFn, opt_ctx) {
const promises = [
this.awaitPendingDiffDrafts(),
this.getChangeActionURL(changeNum, patchNum, '/review'),
];
return Promise.all(promises).then(([, url]) => {
return this.send('POST', url, review, opt_errFn, opt_ctx);
});
},
getChangeEdit(changeNum, opt_download_commands) {
const params = opt_download_commands ? {'download-commands': true} : null;
return this.getLoggedIn().then(loggedIn => {
return loggedIn ?
this._getChangeURLAndFetch(changeNum, '/edit/', null, null, null,
params) :
false;
});
},
/**
* @param {!string} project
* @param {!string} branch
* @param {!string} subject
* @param {!string} topic
* @param {!boolean} isPrivate
* @param {!boolean} workInProgress
*/
createChange(project, branch, subject, topic, isPrivate,
workInProgress) {
return this.send('POST', '/changes/',
{project, branch, subject, topic, is_private: isPrivate,
work_in_progress: workInProgress})
.then(response => this.getResponseObject(response));
},
getFileInChangeEdit(changeNum, path) {
const e = '/edit/' + encodeURIComponent(path);
return this.getChangeURLAndSend(changeNum, 'GET', null, e);
},
rebaseChangeEdit(changeNum) {
return this.getChangeURLAndSend(changeNum, 'POST', null, '/edit:rebase');
},
deleteChangeEdit(changeNum) {
return this.getChangeURLAndSend(changeNum, 'DELETE', null, '/edit');
},
restoreFileInChangeEdit(changeNum, restore_path) {
const p = {restore_path};
return this.getChangeURLAndSend(changeNum, 'POST', null, '/edit', p);
},
renameFileInChangeEdit(changeNum, old_path, new_path) {
const p = {old_path, new_path};
return this.getChangeURLAndSend(changeNum, 'POST', null, '/edit', p);
},
deleteFileInChangeEdit(changeNum, path) {
const e = '/edit/' + encodeURIComponent(path);
return this.getChangeURLAndSend(changeNum, 'DELETE', null, e);
},
saveChangeEdit(changeNum, path, contents) {
const e = '/edit/' + encodeURIComponent(path);
return this.getChangeURLAndSend(changeNum, 'PUT', null, e, contents);
},
// Deprecated, prefer to use putChangeCommitMessage instead.
saveChangeCommitMessageEdit(changeNum, message) {
const p = {message};
return this.getChangeURLAndSend(changeNum, 'PUT', null, '/edit:message',
p);
},
publishChangeEdit(changeNum) {
return this.getChangeURLAndSend(changeNum, 'POST', null,
'/edit:publish');
},
putChangeCommitMessage(changeNum, message) {
const p = {message};
return this.getChangeURLAndSend(changeNum, 'PUT', null, '/message', p);
},
saveChangeStarred(changeNum, starred) {
const url = '/accounts/self/starred.changes/' + changeNum;
const method = starred ? 'PUT' : 'DELETE';
return this.send(method, url);
},
/**
* @param {string} method
* @param {string} url
* @param {?string|number|Object=} opt_body passed as null sometimes
* and also apparently a number. TODO (beckysiegel) remove need for
* number at least.
* @param {?function(?Response, string=)=} opt_errFn
* passed as null sometimes.
* @param {?=} opt_ctx
* @param {?string=} opt_contentType
*/
send(method, url, opt_body, opt_errFn, opt_ctx, opt_contentType) {
const options = {method};
if (opt_body) {
options.headers = new Headers();
options.headers.set(
'Content-Type', opt_contentType || 'application/json');
if (typeof opt_body !== 'string') {
opt_body = JSON.stringify(opt_body);
}
options.body = opt_body;
}
if (!url.startsWith('http')) {
url = this.getBaseUrl() + url;
}
return this._auth.fetch(url, options).then(response => {
if (!response.ok) {
if (opt_errFn) {
return opt_errFn.call(opt_ctx || null, response);
}
this.fire('server-error', {response});
}
return response;
}).catch(err => {
this.fire('network-error', {error: err});
if (opt_errFn) {
return opt_errFn.call(opt_ctx, null, err);
} else {
throw err;
}
});
},
/**
* @param {number|string} changeNum
* @param {number|string} basePatchNum
* @param {number|string} patchNum
* @param {string} path
* @param {function(?Response, string=)=} opt_errFn
* @param {function()=} opt_cancelCondition
*/
getDiff(changeNum, basePatchNum, patchNum, path,
opt_errFn, opt_cancelCondition) {
const params = {
context: 'ALL',
intraline: null,
whitespace: 'IGNORE_NONE',
};
if (basePatchNum != PARENT_PATCH_NUM) {
params.base = basePatchNum;
}
const endpoint = `/files/${encodeURIComponent(path)}/diff`;
return this._getChangeURLAndFetch(changeNum, endpoint, patchNum,
opt_errFn, opt_cancelCondition, params);
},
/**
* @param {number|string} changeNum
* @param {number|string=} opt_basePatchNum
* @param {number|string=} opt_patchNum
* @param {string=} opt_path
*/
getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
return this._getDiffComments(changeNum, '/comments', opt_basePatchNum,
opt_patchNum, opt_path);
},
getDiffRobotComments(changeNum, basePatchNum, patchNum, opt_path) {
return this._getDiffComments(changeNum, '/robotcomments', basePatchNum,
patchNum, opt_path);
},
/**
* 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) => {
return 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
*/
_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 {!Object} Diff comments response.
*/
const fetchComments = opt_patchNum => {
return this._getChangeURLAndFetch(changeNum, endpoint, opt_patchNum);
};
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(() => {
return 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';
if (draft.id) {
endpoint += '/' + draft.id;
}
let body;
if (method === 'PUT') {
body = draft;
}
if (!this._pendingRequests[Requests.SEND_DIFF_DRAFT]) {
this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
}
const promise = this.getChangeURLAndSend(changeNum, method, patchNum,
endpoint, body);
this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise);
if (isCreate) {
return this._failForCreate200(promise);
}
return promise;
},
getCommitInfo(project, commit) {
return this.fetchJSON(
'/projects/' + encodeURIComponent(project) +
'/commits/' + encodeURIComponent(commit));
},
_fetchB64File(url) {
return this._auth.fetch(this.getBaseUrl() + url)
.then(response => {
if (!response.ok) { return Promise.reject(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
*/
getChangeFileContents(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.getChangeFileContents(changeNum, patchRange.patchNum,
diff.meta_a.name, 1);
} else {
promiseA = this.getChangeFileContents(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.getChangeFileContents(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) {
const p = {topic};
return this.getChangeURLAndSend(changeNum, 'PUT', null, '/topic', p)
.then(this.getResponseObject.bind(this));
},
/**
* @suppress {checkTypes}
* Resulted in error: Promise.prototype.then does not match formal
* parameter.
*/
setChangeHashtag(changeNum, hashtag) {
return this.getChangeURLAndSend(changeNum, 'POST', null, '/hashtags',
hashtag).then(this.getResponseObject.bind(this));
},
deleteAccountHttpPassword() {
return this.send('DELETE', '/accounts/self/password.http');
},
/**
* @suppress {checkTypes}
* Resulted in error: Promise.prototype.then does not match formal
* parameter.
*/
generateAccountHttpPassword() {
return this.send('PUT', '/accounts/self/password.http', {generate: true})
.then(this.getResponseObject.bind(this));
},
getAccountSSHKeys() {
return this._fetchSharedCacheURL('/accounts/self/sshkeys');
},
addAccountSSHKey(key) {
return this.send('POST', '/accounts/self/sshkeys', key, null, null,
'plain/text')
.then(response => {
if (response.status < 200 && response.status >= 300) {
return Promise.reject();
}
return this.getResponseObject(response);
})
.then(obj => {
if (!obj.valid) { return Promise.reject(); }
return obj;
});
},
deleteAccountSSHKey(id) {
return this.send('DELETE', '/accounts/self/sshkeys/' + id);
},
deleteVote(changeNum, account, label) {
const e = `/reviewers/${account}/votes/${encodeURIComponent(label)}`;
return this.getChangeURLAndSend(changeNum, 'DELETE', null, e);
},
setDescription(changeNum, patchNum, desc) {
const p = {description: desc};
return this.getChangeURLAndSend(changeNum, 'PUT', patchNum,
'/description', p);
},
confirmEmail(token) {
return this.send('PUT', '/config/server/email.confirm', {token})
.then(response => {
if (response.status === 204) {
return 'Email confirmed successfully.';
}
return null;
});
},
getCapabilities(token) {
return this.fetchJSON('/config/server/capabilities');
},
setAssignee(changeNum, assignee) {
const p = {assignee};
return this.getChangeURLAndSend(changeNum, 'PUT', null, '/assignee', p);
},
deleteAssignee(changeNum) {
return this.getChangeURLAndSend(changeNum, 'DELETE', null, '/assignee');
},
probePath(path) {
return fetch(new Request(path, {method: 'HEAD'}))
.then(response => {
return response.ok;
});
},
/**
* @param {number|string} changeNum
* @param {number|string=} opt_message
*/
startWorkInProgress(changeNum, opt_message) {
const payload = {};
if (opt_message) {
payload.message = opt_message;
}
return this.getChangeURLAndSend(changeNum, 'POST', null, '/wip', payload)
.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, 'POST', null, '/ready',
opt_body, opt_errFn);
},
/**
* @suppress {checkTypes}
* Resulted in error: Promise.prototype.then does not match formal
* parameter.
*/
deleteComment(changeNum, patchNum, commentID, reason) {
const endpoint = `/comments/${commentID}/delete`;
const payload = {reason};
return this.getChangeURLAndSend(changeNum, 'POST', patchNum, endpoint,
payload).then(this.getResponseObject.bind(this));
},
/**
* 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.fetchJSON(`/changes/${changeNum}`, opt_errFn);
},
/**
* @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 {string|number} changeNum
* @param {string} method
* @param {?string|number} patchNum gets passed as null.
* @param {?string} endpoint gets passed as null.
* @param {?Object|number|string=} opt_payload gets passed as null, string,
* Object, or number.
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
* @param {?=} opt_contentType
* @return {!Promise<!Object>}
*/
getChangeURLAndSend(changeNum, method, patchNum, endpoint, opt_payload,
opt_errFn, opt_ctx, opt_contentType) {
return this._changeBaseURL(changeNum, patchNum).then(url => {
return this.send(method, url + endpoint, opt_payload, opt_errFn,
opt_ctx, opt_contentType);
});
},
/**
* Alias for _changeBaseURL.then(fetchJSON).
* @todo(beckysiegel) clean up comments
* @param {string|number} changeNum
* @param {string} endpoint
* @param {?string|number=} opt_patchNum gets passed as null.
* @param {?function(?Response, string=)=} opt_errFn gets passed as null.
* @param {?function()=} opt_cancelCondition gets passed as null.
* @param {?Object=} opt_params gets passed as null.
* @param {!Object=} opt_options
* @return {!Promise<!Object>}
*/
_getChangeURLAndFetch(changeNum, endpoint, opt_patchNum, opt_errFn,
opt_cancelCondition, opt_params, opt_options) {
return this._changeBaseURL(changeNum, opt_patchNum).then(url => {
return this.fetchJSON(url + endpoint, opt_errFn, opt_cancelCondition,
opt_params, opt_options);
});
},
/**
* 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,
`/files/${encodedPath}/blame`, patchNum, undefined, undefined,
opt_base ? {base: 't'} : undefined);
},
/**
* 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;
});
},
});
})();