|  | /** | 
|  | * @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: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 */ | 
|  | /* NB: Order is important, because of namespaced classes. */ | 
|  | /* | 
|  | FIXME(polymer-modulizer): the above comments were extracted | 
|  | from HTML and may be out of place here. Review them and | 
|  | then delete this comment! | 
|  | */ | 
|  | import '../../../scripts/bundled-polymer.js'; | 
|  |  | 
|  | 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 'es6-promise/lib/es6-promise.js'; | 
|  | import 'whatwg-fetch/fetch.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 {util} from '../../../scripts/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_BLACKLIST = /^set-cookie$/i; | 
|  |  | 
|  | const ANONYMIZED_CHANGE_BASE_URL = '/changes/*~*'; | 
|  | const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL + | 
|  | '/revisions/*'; | 
|  |  | 
|  | /** | 
|  | * @extends Polymer.Element | 
|  | */ | 
|  | 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: new SiteBasedCache(), // Shared across instances. | 
|  | }, | 
|  | _sharedFetchPromises: { | 
|  | type: Object, | 
|  | value: new FetchPromisesCache(), // Shared across instances. | 
|  | }, | 
|  | _pendingRequests: { | 
|  | type: Object, | 
|  | value: {}, // Intentional to share the object across instances. | 
|  | }, | 
|  | _etags: { | 
|  | type: Object, | 
|  | value: new GrEtagDecorator(), // Share across instances. | 
|  | }, | 
|  | /** | 
|  | * Used to maintain a mapping of changeNums to project names. | 
|  | */ | 
|  | _projectLookup: { | 
|  | type: Object, | 
|  | value: {}, // Intentional to share the object across instances. | 
|  | }, | 
|  | }; | 
|  | } | 
|  |  | 
|  | /** @override */ | 
|  | created() { | 
|  | super.created(); | 
|  | this._auth = 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._getChangeURLAndSend({ | 
|  | changeNum, | 
|  | method: 'PUT', | 
|  | endpoint: reviewed ? '/reviewed' : '/unreviewed', | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Public version of the _restApiHelper.send method preserved for plugins. | 
|  | * | 
|  | * @param {string} method | 
|  | * @param {string} url | 
|  | * @param {?string|number|Object=} opt_body passed as null sometimes | 
|  | *    and also apparently a number. TODO (beckysiegel) remove need for | 
|  | *    number at least. | 
|  | * @param {?function(?Response, string=)=} opt_errFn | 
|  | *    passed as null sometimes. | 
|  | * @param {?string=} opt_contentType | 
|  | * @param {Object=} opt_headers | 
|  | */ | 
|  | send(method, url, opt_body, opt_errFn, opt_contentType, | 
|  | opt_headers) { | 
|  | return this._restApiHelper.send({ | 
|  | method, | 
|  | url, | 
|  | body: opt_body, | 
|  | errFn: opt_errFn, | 
|  | contentType: opt_contentType, | 
|  | headers: opt_headers, | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {number|string} changeNum | 
|  | * @param {number|string} basePatchNum Negative values specify merge parent | 
|  | *     index. | 
|  | * @param {number|string} patchNum | 
|  | * @param {string} path | 
|  | * @param {string=} opt_whitespace the ignore-whitespace level for the diff | 
|  | *     algorithm. | 
|  | * @param {function(?Response, string=)=} opt_errFn | 
|  | */ | 
|  | getDiff(changeNum, basePatchNum, patchNum, path, opt_whitespace, | 
|  | opt_errFn) { | 
|  | const params = { | 
|  | context: 'ALL', | 
|  | intraline: null, | 
|  | whitespace: opt_whitespace || 'IGNORE_NONE', | 
|  | }; | 
|  | if (this.isMergeParent(basePatchNum)) { | 
|  | params.parent = this.getParentIndex(basePatchNum); | 
|  | } else if (!this.patchNumEquals(basePatchNum, PARENT_PATCH_NUM)) { | 
|  | params.base = basePatchNum; | 
|  | } | 
|  | const endpoint = `/files/${encodeURIComponent(path)}/diff`; | 
|  | const req = { | 
|  | changeNum, | 
|  | endpoint, | 
|  | patchNum, | 
|  | errFn: opt_errFn, | 
|  | params, | 
|  | anonymizedEndpoint: '/files/*/diff', | 
|  | }; | 
|  |  | 
|  | // Invalidate the cache if its edit patch to make sure we always get latest. | 
|  | if (patchNum === this.EDIT_NAME) { | 
|  | if (!req.fetchOptions) req.fetchOptions = {}; | 
|  | if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers(); | 
|  | req.fetchOptions.headers.append('Cache-Control', 'no-cache'); | 
|  | } | 
|  |  | 
|  | return this._getChangeURLAndFetch(req); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {number|string} changeNum | 
|  | * @param {number|string=} opt_basePatchNum | 
|  | * @param {number|string=} opt_patchNum | 
|  | * @param {string=} opt_path | 
|  | * @return {!Promise<!Object>} | 
|  | */ | 
|  | getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { | 
|  | return this._getDiffComments(changeNum, '/comments', opt_basePatchNum, | 
|  | opt_patchNum, opt_path); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {number|string} changeNum | 
|  | * @param {number|string=} opt_basePatchNum | 
|  | * @param {number|string=} opt_patchNum | 
|  | * @param {string=} opt_path | 
|  | * @return {!Promise<!Object>} | 
|  | */ | 
|  | getDiffRobotComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { | 
|  | return this._getDiffComments(changeNum, '/robotcomments', | 
|  | opt_basePatchNum, opt_patchNum, opt_path); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * If the user is logged in, fetch the user's draft diff comments. If there | 
|  | * is no logged in user, the request is not made and the promise yields an | 
|  | * empty object. | 
|  | * | 
|  | * @param {number|string} changeNum | 
|  | * @param {number|string=} opt_basePatchNum | 
|  | * @param {number|string=} opt_patchNum | 
|  | * @param {string=} opt_path | 
|  | * @return {!Promise<!Object>} | 
|  | */ | 
|  | getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { | 
|  | return this.getLoggedIn().then(loggedIn => { | 
|  | if (!loggedIn) { return Promise.resolve({}); } | 
|  | return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum, | 
|  | opt_patchNum, opt_path); | 
|  | }); | 
|  | } | 
|  |  | 
|  | _setRange(comments, comment) { | 
|  | if (comment.in_reply_to && !comment.range) { | 
|  | for (let i = 0; i < comments.length; i++) { | 
|  | if (comments[i].id === comment.in_reply_to) { | 
|  | comment.range = comments[i].range; | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  | return comment; | 
|  | } | 
|  |  | 
|  | _setRanges(comments) { | 
|  | comments = comments || []; | 
|  | comments.sort( | 
|  | (a, b) => util.parseDate(a.updated) - util.parseDate(b.updated) | 
|  | ); | 
|  | for (const comment of comments) { | 
|  | this._setRange(comments, comment); | 
|  | } | 
|  | return comments; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {number|string} changeNum | 
|  | * @param {string} endpoint | 
|  | * @param {number|string=} opt_basePatchNum | 
|  | * @param {number|string=} opt_patchNum | 
|  | * @param {string=} opt_path | 
|  | * @return {!Promise<!Object>} | 
|  | */ | 
|  | _getDiffComments(changeNum, endpoint, opt_basePatchNum, | 
|  | opt_patchNum, opt_path) { | 
|  | /** | 
|  | * Fetches the comments for a given patchNum. | 
|  | * Helper function to make promises more legible. | 
|  | * | 
|  | * @param {string|number=} opt_patchNum | 
|  | * @return {!Promise<!Object>} Diff comments response. | 
|  | */ | 
|  | // 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; | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @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_BLACKLIST.test(key)) { | 
|  | obj[key] = val; | 
|  | } | 
|  | return obj; | 
|  | }, {}); | 
|  | const err = new Error([ | 
|  | CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE, | 
|  | JSON.stringify(headers), | 
|  | ].join('\n')); | 
|  | // Throw the error so that it is caught by gr-reporting. | 
|  | throw err; | 
|  | } | 
|  | return result; | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Fetch a project dashboard definition. | 
|  | * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-dashboard | 
|  | * | 
|  | * @param {string} project | 
|  | * @param {string} dashboard | 
|  | * @param {function(?Response, string=)=} opt_errFn | 
|  | *    passed as null sometimes. | 
|  | * @return {!Promise<!Object>} | 
|  | */ | 
|  | getDashboard(project, dashboard, opt_errFn) { | 
|  | const url = '/projects/' + encodeURIComponent(project) + '/dashboards/' + | 
|  | encodeURIComponent(dashboard); | 
|  | return this._fetchSharedCacheURL({ | 
|  | url, | 
|  | errFn: opt_errFn, | 
|  | anonymizedUrl: '/projects/*/dashboards/*', | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} filter | 
|  | * @return {!Promise<?Object>} | 
|  | */ | 
|  | getDocumentationSearches(filter) { | 
|  | filter = filter.trim(); | 
|  | const encodedFilter = encodeURIComponent(filter); | 
|  |  | 
|  | // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend | 
|  | // supports it. | 
|  | return this._fetchSharedCacheURL({ | 
|  | url: `/Documentation/?q=${encodedFilter}`, | 
|  | anonymizedUrl: '/Documentation/?*', | 
|  | }); | 
|  | } | 
|  |  | 
|  | getMergeable(changeNum) { | 
|  | return this._getChangeURLAndFetch({ | 
|  | changeNum, | 
|  | endpoint: '/revisions/current/mergeable', | 
|  | parseResponse: true, | 
|  | reportEndpointAsIs: true, | 
|  | }); | 
|  | } | 
|  |  | 
|  | deleteDraftComments(query) { | 
|  | return this._restApiHelper.send({ | 
|  | method: 'POST', | 
|  | url: '/accounts/self/drafts:delete', | 
|  | body: {query}, | 
|  | }); | 
|  | } | 
|  | } | 
|  |  | 
|  | customElements.define(GrRestApiInterface.is, GrRestApiInterface); |