| /** |
| * @license |
| * Copyright (C) 2021 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. |
| */ |
| |
| // TODO: Try to remove it. The ResponseError and getErrorMessage duplicates |
| // code from the gr-plugin-rest-api.ts. This code is required because |
| // we want custom error processing in some functions. For details see |
| // the original gr-plugin-rest-api.ts file/ |
| class ResponseError extends Error { |
| constructor(response) { |
| super(); |
| this.response = response; |
| } |
| } |
| |
| export class ServerConfigurationError extends Error { |
| constructor(msg) { |
| super(msg); |
| } |
| } |
| |
| async function getErrorMessage(response) { |
| const text = await response.text(); |
| return text ? |
| `${response.status}: ${text}` : |
| `${response.status}`; |
| } |
| |
| /** |
| * Responsible for communicating with the rest-api |
| * |
| * @see resources/Documentation/rest-api.md |
| */ |
| export class CodeOwnersApi { |
| constructor(restApi) { |
| this.restApi = restApi; |
| } |
| |
| /** |
| * Send a get request and provides custom response-code handling |
| */ |
| async _get(url) { |
| const errFn = (response, error) => { |
| if (error) throw error; |
| if (response) throw new ResponseError(response); |
| throw new Error('Generic REST API error'); |
| }; |
| try { |
| return await this.restApi.send( |
| 'GET', |
| url, |
| undefined, |
| errFn |
| ); |
| } catch (err) { |
| if (err instanceof ResponseError && err.response.status === 409) { |
| return getErrorMessage(err.response).then(msg => { |
| throw new ServerConfigurationError(msg); |
| }); |
| } |
| throw err; |
| } |
| } |
| |
| /** |
| * Returns a promise fetching the owner statuses for all files within the change. |
| * |
| * @doc https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#change-endpoints |
| * @param {string} changeId |
| */ |
| listOwnerStatus(changeId) { |
| return this._get(`/changes/${changeId}/code_owners.status?limit=100000`); |
| } |
| |
| /** |
| * Returns a promise fetching the owners for a given path. |
| * |
| * @doc https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#list-code-owners-for-path-in-branch |
| * @param {string} changeId |
| * @param {string} path |
| */ |
| listOwnersForPath(changeId, path, limit) { |
| return this._get( |
| `/changes/${changeId}/revisions/current/code_owners` + |
| `/${encodeURIComponent(path)}?limit=${limit}&o=DETAILS` |
| ); |
| } |
| |
| /** |
| * Returns a promise fetching the owners config for a given path. |
| * |
| * @doc https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#branch-endpoints |
| * @param {string} project |
| * @param {string} branch |
| * @param {string} path |
| */ |
| getConfigForPath(project, branch, path) { |
| return this._get( |
| `/projects/${encodeURIComponent(project)}/` + |
| `branches/${encodeURIComponent(branch)}/` + |
| `code_owners.config/${encodeURIComponent(path)}` |
| ); |
| } |
| |
| /** |
| * Returns a promise fetching the owners config for a given branch. |
| * |
| * @doc https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#branch-endpoints |
| * @param {string} project |
| * @param {string} branch |
| */ |
| async getBranchConfig(project, branch) { |
| try { |
| const config = await this._get( |
| `/projects/${encodeURIComponent(project)}/` + |
| `branches/${encodeURIComponent(branch)}/` + |
| `code_owners.branch_config` |
| ); |
| if (config.override_approval && |
| !(config.override_approval instanceof Array)) { |
| // In the upcoming backend changes, the override_approval will be changed |
| // to array with (possible) multiple items. |
| // While this transition is in progress, the frontend supports both API - |
| // the old one and the new one. |
| return {...config, override_approval: [config.override_approval]}; |
| } |
| return config; |
| } catch (err) { |
| if (err instanceof ResponseError) { |
| if (err.response.status === 404) { |
| // The 404 error means that the branch doesn't exist and |
| // the plugin should be disabled. |
| return {disabled: true}; |
| } |
| return getErrorMessage(err.response).then(msg => { |
| throw new Error(msg); |
| }); |
| } |
| throw err; |
| } |
| } |
| } |
| |
| /** |
| * Wrapper around codeOwnerApi, sends each requests only once and then cache |
| * the response. A new CodeOwnersCacheApi instance is created every time when a |
| * new change object is assigned. |
| * Gerrit never updates existing change object, but instead always assigns a new |
| * change object. Particularly, a new change object is assigned when a change |
| * is updated and user clicks reload toasts to see the updated change. |
| * As a result, the lifetime of a cache is the same as a lifetime of an assigned |
| * change object. |
| * Periodical cache invalidation can lead to inconsistency in UI, i.e. |
| * user can see the old reviewers list (reflects a state when a change was |
| * loaded) and code-owners status for the current reviewer list. To avoid |
| * this inconsistency, the cache doesn't invalidate. |
| */ |
| export class CodeOwnersCacheApi { |
| constructor(codeOwnerApi, change) { |
| this.codeOwnerApi = codeOwnerApi; |
| this.change = change; |
| this.promises = {}; |
| } |
| |
| _fetchOnce(cacheKey, asyncFn) { |
| if (!this.promises[cacheKey]) { |
| this.promises[cacheKey] = asyncFn(); |
| } |
| return this.promises[cacheKey]; |
| } |
| |
| getAccount() { |
| return this._fetchOnce('getAccount', () => this._getAccount()); |
| } |
| |
| async _getAccount() { |
| const loggedIn = await this.codeOwnerApi.restApi.getLoggedIn(); |
| if (!loggedIn) return undefined; |
| return await this.codeOwnerApi.restApi.getAccount(); |
| } |
| |
| listOwnerStatus() { |
| return this._fetchOnce('listOwnerStatus', |
| () => this.codeOwnerApi.listOwnerStatus(this.change._number)); |
| } |
| |
| getBranchConfig() { |
| return this._fetchOnce('getBranchConfig', |
| () => this.codeOwnerApi.getBranchConfig(this.change.project, |
| this.change.branch)); |
| } |
| } |