// 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_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900; const PARENT_PATCH_NUM = 'PARENT'; const Requests = { SEND_DIFF_DRAFT: 'sendDiffDraft', }; // Must be kept in sync with the ListChangesOption enum and protobuf. const ListChangesOption = { LABELS: 0, DETAILED_LABELS: 8, // Return information on the current patch set of the change. CURRENT_REVISION: 1, ALL_REVISIONS: 2, // If revisions are included, parse the commit object. CURRENT_COMMIT: 3, ALL_COMMITS: 4, // If a patch set is included, include the files of the patch set. CURRENT_FILES: 5, ALL_FILES: 6, // If accounts are included, include detailed account info. DETAILED_ACCOUNTS: 7, // Include messages associated with the change. MESSAGES: 9, // Include allowed actions client could perform. CURRENT_ACTIONS: 10, // Set the reviewed boolean for the caller. REVIEWED: 11, // Include download commands for the caller. DOWNLOAD_COMMANDS: 13, // Include patch set weblinks. WEB_LINKS: 14, // Include consistency check results. CHECK: 15, // Include allowed change actions client could perform. CHANGE_ACTIONS: 16, // Include a copy of commit messages including review footers. COMMIT_FOOTERS: 17, // Include push certificate information along with any patch sets. PUSH_CERTIFICATES: 18, // Include change's reviewer updates. REVIEWER_UPDATES: 19, // Set the submittable boolean. SUBMITTABLE: 20, }; Polymer({ is: 'gr-rest-api-interface', behaviors: [ Gerrit.BaseUrlBehavior, Gerrit.PathListBehavior, ], /** * Fired when an server error occurs. * * @event server-error */ /** * Fired when a network error occurs. * * @event network-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. }, }, fetchJSON(url, opt_errFn, opt_cancelCondition, opt_params, opt_opts) { opt_opts = opt_opts || {}; // Issue 5715, This can be reverted back once // iOS 10.3 and mac os 10.12.4 has the fetch api fix. const fetchOptions = { credentials: 'same-origin', }; if (opt_opts.headers !== undefined) { fetchOptions['headers'] = opt_opts.headers; } const urlWithParams = this._urlWithParams(url, opt_params); return fetch(urlWithParams, fetchOptions).then(response => { if (opt_cancelCondition && opt_cancelCondition()) { response.body.cancel(); return; } if (!response.ok) { if (opt_errFn) { opt_errFn.call(null, response); return; } this.fire('server-error', {response}); return; } return this.getResponseObject(response); }).catch(err => { if (opt_errFn) { opt_errFn.call(null, null, err); } else { this.fire('network-error', {error: err}); throw err; } throw err; }); }, _urlWithParams(url, opt_params) { if (!opt_params) { return this.getBaseUrl() + url; } const params = []; for (const p in opt_params) { 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('&'); }, getResponseObject(response) { return response.text().then(text => { let result; try { result = JSON.parse(text.substring(JSON_PREFIX.length)); } catch (_) { result = null; } return result; }); }, getConfig() { return this._fetchSharedCacheURL('/config/server/info'); }, getProjectConfig(project) { return this._fetchSharedCacheURL( '/projects/' + encodeURIComponent(project) + '/config'); }, 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', }); }); }, 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); }, 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; } }); }, getAccountEmails() { return this._fetchSharedCacheURL('/accounts/self/emails'); }, addAccountEmail(email, opt_errFn, opt_ctx) { return this.send('PUT', '/accounts/self/emails/' + encodeURIComponent(email), null, opt_errFn, opt_ctx); }, deleteAccountEmail(email, opt_errFn, opt_ctx) { return this.send('DELETE', '/accounts/self/emails/' + encodeURIComponent(email), null, opt_errFn, 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; } }); }, setAccountName(name, opt_errFn, opt_ctx) { return this.send('PUT', '/accounts/self/name', {name}, opt_errFn, opt_ctx).then(response => { // 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) { return this.getResponseObject(response).then(newName => { // Replace object in cache with new object to force UI updates. // TODO(logan): Polyfill for Object.assign in IE this._cache['/accounts/self/detail'] = Object.assign( {}, cachedAccount, {name: newName}); }); } }); }, setAccountStatus(status, opt_errFn, opt_ctx) { return this.send('PUT', '/accounts/self/status', {status}, opt_errFn, opt_ctx).then(response => { // 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) { return this.getResponseObject(response).then(newStatus => { // Replace object in cache with new object to force UI updates. // TODO(logan): Polyfill for Object.assign in IE this._cache['/accounts/self/detail'] = Object.assign( {}, cachedAccount, {status: newStatus}); }); } }); }, getAccountGroups() { return this._fetchSharedCacheURL('/accounts/self/groups'); }, 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.fetchJSON('/accounts/self/detail'); }, 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'); }, 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); }); }, deleteWatchedProjects(projects, opt_errFn, opt_ctx) { return this.send('POST', '/accounts/self/watched.projects:delete', projects, opt_errFn, opt_ctx); }, _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; }, getChanges(changesPerPage, opt_query, opt_offset) { const options = this._listChangesOptionsToHex( ListChangesOption.LABELS, ListChangesOption.DETAILED_ACCOUNTS ); // Issue 4524: respect legacy token with max sortkey. if (opt_offset === 'n,z') { opt_offset = 0; } const params = { n: changesPerPage, O: options, S: opt_offset || 0, }; if (opt_query && opt_query.length > 0) { params.q = opt_query; } return this.fetchJSON('/changes/', null, null, params); }, getDashboardChanges() { const options = this._listChangesOptionsToHex( ListChangesOption.LABELS, ListChangesOption.DETAILED_ACCOUNTS, ListChangesOption.REVIEWED ); const params = { O: options, q: [ 'is:open owner:self', 'is:open ((reviewer:self -owner:self -is:ignored) OR assignee:self)', 'is:closed (owner:self OR reviewer:self OR assignee:self) -age:4w ' + 'limit:10', ], }; return this.fetchJSON('/changes/', null, null, params); }, getChangeActionURL(changeNum, opt_patchNum, endpoint) { return this._changeBaseURL(changeNum, opt_patchNum) + endpoint; }, getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) { const options = this._listChangesOptionsToHex( ListChangesOption.ALL_REVISIONS, ListChangesOption.CHANGE_ACTIONS, ListChangesOption.CURRENT_ACTIONS, ListChangesOption.CURRENT_COMMIT, ListChangesOption.DOWNLOAD_COMMANDS, ListChangesOption.SUBMITTABLE, ListChangesOption.WEB_LINKS ); return this._getChangeDetail( changeNum, options, opt_errFn, opt_cancelCondition) .then(GrReviewerUpdatesParser.parse); }, getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) { const options = this._listChangesOptionsToHex( ListChangesOption.ALL_REVISIONS ); return this._getChangeDetail(changeNum, options, opt_errFn, opt_cancelCondition); }, _getChangeDetail(changeNum, options, opt_errFn, opt_cancelCondition) { return this.fetchJSON( this.getChangeActionURL(changeNum, null, '/detail'), opt_errFn, opt_cancelCondition, {O: options}); }, getChangeCommitInfo(changeNum, patchNum) { return this.fetchJSON( this.getChangeActionURL(changeNum, patchNum, '/commit?links')); }, getChangeFiles(changeNum, patchRange) { let endpoint = '/files'; if (patchRange.basePatchNum !== 'PARENT') { endpoint += '?base=' + encodeURIComponent(patchRange.basePatchNum); } return this.fetchJSON( this.getChangeActionURL(changeNum, patchRange.patchNum, endpoint)); }, getChangeFilesAsSpeciallySortedArray(changeNum, patchRange) { return this.getChangeFiles(changeNum, patchRange).then( this._normalizeChangeFilesResponse.bind(this)); }, getChangeFilePathsAsSpeciallySortedArray(changeNum, patchRange) { return this.getChangeFiles(changeNum, patchRange).then(files => { return Object.keys(files).sort(this.specialFilePathCompare); }); }, _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.fetchJSON( this.getChangeActionURL(changeNum, patchNum, '/actions')).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; }); }, getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn, opt_ctx) { const url = this.getChangeActionURL(changeNum, null, '/suggest_reviewers'); return this.fetchJSON(url, opt_errFn, opt_ctx, { n: 10, // Return max 10 results q: inputVal, }); }, 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); }, getSuggestedProjects(inputVal, opt_n, opt_errFn, opt_ctx) { const params = {p: inputVal}; if (opt_n) { params.n = opt_n; } return this.fetchJSON('/projects/', opt_errFn, opt_ctx, params); }, getSuggestedAccounts(inputVal, opt_n, opt_errFn, opt_ctx) { const params = {q: inputVal, suggest: null}; 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) { let url = this.getChangeActionURL(changeNum, null, '/reviewers'); 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.fetchJSON( this.getChangeActionURL(changeNum, patchNum, '/related')); }, getChangesSubmittedTogether(changeNum) { return this.fetchJSON( this.getChangeActionURL(changeNum, null, '/submitted_together')); }, getChangeConflicts(changeNum) { const options = this._listChangesOptionsToHex( ListChangesOption.CURRENT_REVISION, 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( ListChangesOption.CURRENT_REVISION, 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) { const options = this._listChangesOptionsToHex( ListChangesOption.LABELS, ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT, ListChangesOption.DETAILED_LABELS ); const params = { O: options, q: 'status:open topic:' + topic, }; return this.fetchJSON('/changes/', null, null, params); }, getReviewedFiles(changeNum, patchNum) { return this.fetchJSON( this.getChangeActionURL(changeNum, patchNum, '/files?reviewed')); }, saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn, opt_ctx) { const method = reviewed ? 'PUT' : 'DELETE'; const url = this.getChangeActionURL(changeNum, patchNum, '/files/' + encodeURIComponent(path) + '/reviewed'); return this.send(method, url, null, opt_errFn, opt_ctx); }, saveChangeReview(changeNum, patchNum, review, opt_errFn, opt_ctx) { const url = this.getChangeActionURL(changeNum, patchNum, '/review'); return this.send('POST', url, review, opt_errFn, opt_ctx); }, getFileInChangeEdit(changeNum, path) { return this.send('GET', this.getChangeActionURL(changeNum, null, '/edit/' + encodeURIComponent(path) )); }, rebaseChangeEdit(changeNum) { return this.send('POST', this.getChangeActionURL(changeNum, null, '/edit:rebase' )); }, deleteChangeEdit(changeNum) { return this.send('DELETE', this.getChangeActionURL(changeNum, null, '/edit' )); }, restoreFileInChangeEdit(changeNum, restore_path) { return this.send('POST', this.getChangeActionURL(changeNum, null, '/edit'), {restore_path} ); }, renameFileInChangeEdit(changeNum, old_path, new_path) { return this.send('POST', this.getChangeActionURL(changeNum, null, '/edit'), {old_path}, {new_path} ); }, deleteFileInChangeEdit(changeNum, path) { return this.send('DELETE', this.getChangeActionURL(changeNum, null, '/edit/' + encodeURIComponent(path) )); }, saveChangeEdit(changeNum, path, contents) { return this.send('PUT', this.getChangeActionURL(changeNum, null, '/edit/' + encodeURIComponent(path) ), contents ); }, saveChangeCommitMessageEdit(changeNum, message) { const url = this.getChangeActionURL(changeNum, null, '/edit:message'); return this.send('PUT', url, {message}); }, publishChangeEdit(changeNum) { return this.send('POST', this.getChangeActionURL(changeNum, null, '/edit:publish')); }, saveChangeStarred(changeNum, starred) { const url = '/accounts/self/starred.changes/' + changeNum; const method = starred ? 'PUT' : 'DELETE'; return this.send(method, url); }, send(method, url, opt_body, opt_errFn, opt_ctx, opt_contentType) { const headers = new Headers({ 'X-Gerrit-Auth': this._getCookie('XSRF_TOKEN'), }); const options = { method, headers, credentials: 'same-origin', }; if (opt_body) { headers.append('Content-Type', opt_contentType || 'application/json'); if (typeof opt_body !== 'string') { opt_body = JSON.stringify(opt_body); } options.body = opt_body; } return fetch(this.getBaseUrl() + url, options).then(response => { if (!response.ok) { if (opt_errFn) { opt_errFn.call(opt_ctx || null, response); return undefined; } this.fire('server-error', {response}); } return response; }).catch(err => { this.fire('network-error', {error: err}); if (opt_errFn) { opt_errFn.call(opt_ctx, null, err); } else { throw err; } }); }, getDiff(changeNum, basePatchNum, patchNum, path, opt_errFn, opt_cancelCondition) { const url = this._getDiffFetchURL(changeNum, patchNum, path); const params = { context: 'ALL', intraline: null, whitespace: 'IGNORE_NONE', }; if (basePatchNum != PARENT_PATCH_NUM) { params.base = basePatchNum; } return this.fetchJSON(url, opt_errFn, opt_cancelCondition, params); }, _getDiffFetchURL(changeNum, patchNum, path) { return this._changeBaseURL(changeNum, patchNum) + '/files/' + encodeURIComponent(path) + '/diff'; }, 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); }, getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { 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; }, _getDiffComments(changeNum, endpoint, opt_basePatchNum, opt_patchNum, opt_path) { if (!opt_basePatchNum && !opt_patchNum && !opt_path) { return this.fetchJSON( this._getDiffCommentsFetchURL(changeNum, endpoint)); } 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; const url = this._getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum); promises.push(this.fetchJSON(url).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); })); if (opt_basePatchNum != PARENT_PATCH_NUM) { const baseURL = this._getDiffCommentsFetchURL(changeNum, endpoint, opt_basePatchNum); promises.push(this.fetchJSON(baseURL).then(response => { baseComments = (response[opt_path] || []).filter(withoutParent); baseComments = this._setRanges(baseComments); baseComments.forEach(setPath); })); } return Promise.all(promises).then(() => { return Promise.resolve({ baseComments, comments, }); }); }, _getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum) { return this._changeBaseURL(changeNum, opt_patchNum) + endpoint; }, saveDiffDraft(changeNum, patchNum, draft) { return this._sendDiffDraftRequest('PUT', changeNum, patchNum, draft); }, deleteDiffDraft(changeNum, patchNum, draft) { return this._sendDiffDraftRequest('DELETE', changeNum, patchNum, draft); }, hasPendingDiffDrafts() { return !!this._pendingRequests[Requests.SEND_DIFF_DRAFT]; }, _sendDiffDraftRequest(method, changeNum, patchNum, draft) { let url = this.getChangeActionURL(changeNum, patchNum, '/drafts'); if (draft.id) { url += '/' + draft.id; } let body; if (method === 'PUT') { body = draft; } if (!this._pendingRequests[Requests.SEND_DIFF_DRAFT]) { this._pendingRequests[Requests.SEND_DIFF_DRAFT] = 0; } this._pendingRequests[Requests.SEND_DIFF_DRAFT]++; return this.send(method, url, body).then(res => { this._pendingRequests[Requests.SEND_DIFF_DRAFT]--; return res; }); }, _changeBaseURL(changeNum, opt_patchNum) { let v = '/changes/' + changeNum; if (opt_patchNum) { v += '/revisions/' + opt_patchNum; } return v; }, // Derived from // gerrit-extension-api/src/main/j/c/g/gerrit/extensions/client/ListChangesOption.java _listChangesOptionsToHex(...args) { let v = 0; for (let i = 0; i < args.length; i++) { v |= 1 << args[i]; } return v.toString(16); }, _getCookie(name) { const key = name + '='; const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { let c = cookies[i]; while (c.charAt(0) == ' ') { c = c.substring(1); } if (c.startsWith(key)) { return c.substring(key.length, c.length); } } return ''; }, getCommitInfo(project, commit) { return this.fetchJSON( '/projects/' + encodeURIComponent(project) + '/commits/' + encodeURIComponent(commit)); }, _fetchB64File(url) { return fetch(this.getBaseUrl() + url, {credentials: 'same-origin'}) .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}; }); }); }, getChangeFileContents(changeId, patchNum, path, opt_parentIndex) { const parent = typeof opt_parentIndex === 'number' ? '?parent=' + opt_parentIndex : ''; return this._fetchB64File( '/changes/' + encodeURIComponent(changeId) + '/revisions/' + encodeURIComponent(patchNum) + '/files/' + encodeURIComponent(path) + '/content' + parent); }, 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}; }); }, setChangeTopic(changeNum, topic) { return this.send('PUT', '/changes/' + encodeURIComponent(changeNum) + '/topic', {topic}); }, deleteAccountHttpPassword() { return this.send('DELETE', '/accounts/self/password.http'); }, generateAccountHttpPassword() { return this.send('PUT', '/accounts/self/password.http', {generate: true}) .then(this.getResponseObject); }, 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(changeID, account, label) { return this.send('DELETE', '/changes/' + changeID + '/reviewers/' + account + '/votes/' + encodeURIComponent(label)); }, setDescription(changeNum, patchNum, desc) { return this.send('PUT', this.getChangeActionURL(changeNum, patchNum, '/description'), {description: desc}); }, confirmEmail(token) { return this.send('PUT', '/config/server/email.confirm', {token}) .then(response => { if (response.status === 204) { return 'Email confirmed successfully.'; } return null; }); }, setAssignee(changeNum, assignee) { return this.send('PUT', this.getChangeActionURL(changeNum, null, '/assignee'), {assignee}); }, deleteAssignee(changeNum) { return this.send('DELETE', this.getChangeActionURL(changeNum, null, '/assignee')); }, probePath(path) { return fetch(new Request(path, {method: 'HEAD'})) .then(response => { return response.ok; }); }, startWorkInProgress(changeNum, opt_message) { const payload = {}; if (opt_message) { payload.message = opt_message; } const url = this.getChangeActionURL(changeNum, null, '/wip'); return this.send('POST', url, payload) .then(response => { if (response.status === 204) { return 'Change marked as Work In Progress.'; } }); }, startReview(changeNum, review) { return this.send( 'POST', this.getChangeActionURL(changeNum, null, '/ready'), review); }, deleteComment(changeNum, patchNum, commentID, reason) { const url = this._changeBaseURL(changeNum, patchNum) + '/comments/' + commentID + '/delete'; return this.send('POST', url, {reason}).then(response => this.getResponseObject(response)); }, }); })();