blob: e186edead301603ce8b6f1e17e6810cd80b3db39 [file] [log] [blame]
/**
* @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`);
}
/**
* 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));
}
}