| /** |
| * @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 {AccountId, GroupId} from '@gerritcodereview/typescript-api/rest-api'; |
| import {CodeOwnerInfo, CodeOwnersInfo, FetchedFile} from './code-owners-api'; |
| import {BestSuggestionsLimit} from './code-owners-model'; |
| |
| export interface GroupedFiles { |
| groupName: {name: string|undefined; prefix: string;}; |
| files: Array<FetchedFile>; |
| owners?: CodeOwnersInfo; |
| error?: Error; |
| hasSelected?: boolean; |
| expanded?: boolean; |
| } |
| |
| /** |
| * For each file calculates owners to display and group all files by those |
| * owners. The function creates "fake" groups when one or more |
| * reviewers are included in all owners of a file, but none of reviewers is |
| * included in best reviewers for the file. |
| * |
| * Such situations are possible when user turns on "Show all owners", selects |
| * one of newly displayed owners and then turns off "Show all owners". Without |
| * "fake" groups a user can see inconsistent state in dialog. |
| */ |
| export function getDisplayOwnersGroups( |
| files: Array<FetchedFile>, |
| allOwnersByPathMap: Map<string, CodeOwnersInfo|undefined>, |
| reviewersIdSet: Set<AccountId|GroupId>, |
| allowAllOwnersSubstition: boolean): Array<GroupedFiles> { |
| const getDisplayOwnersFunc = !allowAllOwnersSubstition || |
| allOwnersByPathMap.size === 0 || reviewersIdSet.size === 0 ? |
| (file: FetchedFile) => file.info.owners : |
| (file: FetchedFile) => |
| getDisplayOwners(file, allOwnersByPathMap, reviewersIdSet); |
| return groupFilesByOwners(files, getDisplayOwnersFunc); |
| } |
| |
| function getDisplayOwners( |
| file: FetchedFile, |
| allOwnersByPathMap: Map<String, CodeOwnersInfo|undefined>, |
| reviewersIdSet: Set<AccountId|GroupId>) { |
| const ownerSelected = (owner: CodeOwnerInfo) => |
| owner?.account?._account_id !== undefined && |
| reviewersIdSet.has(owner.account._account_id); |
| const defaultOwners = file.info.owners; |
| if (!defaultOwners || defaultOwners.owned_by_all_users || |
| defaultOwners.code_owners.some(ownerSelected)) { |
| return defaultOwners; |
| } |
| const allOwners = allOwnersByPathMap.get(file.path); |
| if (!allOwners) return defaultOwners; |
| if (allOwners.owned_by_all_users) return allOwners; |
| const selectedAllOwners = allOwners.code_owners.filter(ownerSelected); |
| if (selectedAllOwners.length === 0) return defaultOwners; |
| return { |
| code_owners: selectedAllOwners.slice(0, BestSuggestionsLimit), |
| }; |
| } |
| |
| function groupFilesByOwners( |
| files: Array<FetchedFile>, |
| getDisplayOwnersFunc: (file: FetchedFile) => |
| CodeOwnersInfo | undefined): Array<GroupedFiles> { |
| // Note: for renamed or moved files, they will have two entries in the map |
| // we will treat them as two entries when group as well |
| const ownersFilesMap = |
| new Map<string, {files: Array<FetchedFile>, owners: CodeOwnersInfo}>(); |
| const failedToFetchFiles = new Set<FetchedFile>(); |
| for (const file of files) { |
| // for files failed to fetch, add them to the special group |
| if (file.info.error) { |
| failedToFetchFiles.add(file); |
| continue; |
| } |
| |
| // do not include files still in fetching |
| if (!file.info.owners) { |
| continue; |
| } |
| const displayOwners = getDisplayOwnersFunc(file); |
| if (displayOwners === undefined) continue; |
| |
| const ownersKey = getOwnersGroupKey(displayOwners); |
| ownersFilesMap.set( |
| ownersKey, |
| ownersFilesMap.get(ownersKey) ?? {files: [], owners: displayOwners}); |
| ownersFilesMap.get(ownersKey)!.files.push(file); |
| } |
| const groupedItems = []; |
| for (const ownersKey of ownersFilesMap.keys()) { |
| const groupName = getGroupName(ownersFilesMap.get(ownersKey)!.files); |
| groupedItems.push({ |
| groupName, |
| files: ownersFilesMap.get(ownersKey)!.files, |
| owners: ownersFilesMap.get(ownersKey)!.owners, |
| }); |
| } |
| |
| if (failedToFetchFiles.size > 0) { |
| const failedFiles = [...failedToFetchFiles]; |
| groupedItems.push({ |
| groupName: getGroupName(failedFiles), |
| files: failedFiles, |
| error: new Error( |
| 'Failed to fetch code owner info. Try to refresh the page.'), |
| }); |
| } |
| return groupedItems; |
| } |
| |
| function getOwnersGroupKey(owners: CodeOwnersInfo) { |
| if (owners.owned_by_all_users) { |
| return '__owned_by_all_users__'; |
| } |
| const code_owners = owners.code_owners; |
| return code_owners.map(owner => owner.account?._account_id).sort().join(','); |
| } |
| |
| function getGroupName(files: Array<FetchedFile>) { |
| const path = files[0].path.split('/'); |
| const fileName = path.pop(); |
| const shortenedDirectory = path.map(d => d.charAt(0)).join('/'); |
| return { |
| name: `${shortenedDirectory}/${fileName}`, |
| prefix: files.length > 1 ? `+ ${files.length - 1} more` : '', |
| }; |
| } |