blob: 6fd5b352f7b37a85fac1392d3647fb33caaba29e [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.
*/
import {CodeOwnersApi} from './code-owners-api.js';
/**
* All statuses returned for owner status.
*
* @enum
*/
export const OwnerStatus = {
INSUFFICIENT_REVIEWERS: 'INSUFFICIENT_REVIEWERS',
PENDING: 'PENDING',
APPROVED: 'APPROVED',
};
/**
* @enum
*/
export const FetchStatus = {
/** Fetch hasn't been started */
NOT_STARTED: 0,
/**
* Fetch has been started, but not all files has been finished.
* Pausing during fetching doesn't change state.
*/
FETCHING: 1,
/**
* All owners has been loaded. resume/pause call doesn't change state.
*/
FINISHED: 2,
};
/**
* Fetch owners for files. The class fetches owners in parallel and allows to
* pause/resume fetch.
*/
class OwnersFetcher {
/**
* Creates a fetcher in paused state. Actual fetching starts after resume()
* is called.
*
* @param {Array<string>} filesToFetch - Files paths for loading owners.
* @param {number} ownersLimit - number of requested owners per file.
* @param {number} maxConcurrentRequest - max number of concurrent requests to server.
*/
constructor(codeOwnerApi, changeId, filesToFetch, ownersLimit,
maxConcurrentRequest) {
this._fetchedOwners = new Map();
this._ownersLimit = ownersLimit;
this._paused = true;
this._pausedFilesFetcher = [];
this._filesToFetch = filesToFetch;
this._fetchFilesPromises = [];
this._codeOwnerApi = codeOwnerApi;
this._changeId = changeId;
for (let i = 0; i < maxConcurrentRequest; i++) {
this._fetchFilesPromises.push(this._fetchFiles());
}
}
async _fetchFiles() {
for (;;) {
const filePath = await this._getNextFilePath();
if (!filePath) return;
try {
this._fetchedOwners.set(filePath, {
owners: await this._codeOwnerApi.listOwnersForPath(this._changeId,
filePath, this._ownersLimit),
});
} catch (error) {
this._fetchedOwners.set(filePath, {error});
}
}
}
async _getNextFilePath() {
if (this._paused) {
await new Promise(resolve => this._pausedFilesFetcher.push(resolve));
}
if (this._filesToFetch.length === 0) return null;
return this._filesToFetch.splice(0, 1)[0];
}
async waitFetchComplete() {
await Promise.allSettled(this._fetchFilesPromises);
}
resume() {
if (!this._paused) return;
this._paused = false;
for (const fetcher of this._pausedFilesFetcher.splice(0,
this._pausedFilesFetcher.length)) {
fetcher();
}
}
pause() {
this._paused = true;
}
getFetchedOwners() {
return this._fetchedOwners;
}
getFiles() {
const result = [];
for (const [path, info] of this._fetchedOwners.entries()) {
result.push({path, info});
}
return result;
}
}
export class OwnersProvider {
constructor(restApi, change, options) {
this.change = change;
this.options = options;
this._totalFetchCount = 0;
this._status = FetchStatus.NOT_STARTED;
this._codeOwnerApi = new CodeOwnersApi(restApi);
}
getStatus() {
return this._status;
}
getProgressString() {
return !this._ownersFetcher || this._totalFetchCount === 0 ?
`Loading suggested owners ...` :
`${this._ownersFetcher.getFetchedOwners().size} out of ` +
`${this._totalFetchCount} files have returned suggested owners.`;
}
getFiles() {
if (!this._ownersFetcher) return [];
return this._ownersFetcher.getFiles();
}
async fetchSuggestedOwners(codeOwnerStatusMap) {
if (this._status !== FetchStatus.NOT_STARTED) {
await this._ownersFetcher.waitFetchComplete();
return;
}
const filesToFetch = this._getFilesToFetch(codeOwnerStatusMap);
this._totalFetchCount = filesToFetch.length;
this._ownersFetcher = new OwnersFetcher(this._codeOwnerApi, this.change.id,
filesToFetch,
this.options.ownersLimit, this.options.maxConcurrentRequests);
this._status = FetchStatus.FETCHING;
this._ownersFetcher.resume();
await this._ownersFetcher.waitFetchComplete(filesToFetch);
this._status = FetchStatus.FINISHED;
}
_getFilesToFetch(codeOwnerStatusMap) {
// only fetch those not approved yet
const filesGroupByStatus = [...codeOwnerStatusMap.entries()].reduce(
(list, [file, fileInfo]) => {
if (list[fileInfo.status]) list[fileInfo.status].push(file);
return list;
}
, {
[OwnerStatus.PENDING]: [],
[OwnerStatus.INSUFFICIENT_REVIEWERS]: [],
[OwnerStatus.APPROVED]: [],
}
);
// always fetch INSUFFICIENT_REVIEWERS first, then pending and then approved
return filesGroupByStatus[OwnerStatus.INSUFFICIENT_REVIEWERS]
.concat(filesGroupByStatus[OwnerStatus.PENDING])
.concat(filesGroupByStatus[OwnerStatus.APPROVED]);
}
pause() {
if (!this._ownersFetcher) return;
this._ownersFetcher.pause();
}
resume() {
if (!this._ownersFetcher) return;
this._ownersFetcher.resume();
}
reset() {
this._totalFetchCount = 0;
this.ownersFetcher = null;
this._status = FetchStatus.NOT_STARTED;
}
}