| import { |
| RevisionInfo, |
| ChangeInfo, |
| PatchSetNum, |
| EDIT, |
| PARENT, |
| PatchSetNumber, |
| BasePatchSetNum, |
| RevisionPatchSetNum, |
| BranchName, |
| CommitId, |
| } from '../types/common'; |
| import {EditRevisionInfo, ParsedChangeInfo} from '../types/types'; |
| import {assert} from './common-util'; |
| |
| /** |
| * @license |
| * Copyright 2016 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| // Tags identifying ChangeMessages that move change into WIP state. |
| const WIP_TAGS = [ |
| 'autogenerated:gerrit:newWipPatchSet', |
| 'autogenerated:gerrit:setWorkInProgress', |
| ]; |
| |
| // Tags identifying ChangeMessages that move change out of WIP state. |
| const READY_TAGS = ['autogenerated:gerrit:setReadyForReview']; |
| |
| export const CURRENT = 'current'; |
| |
| export interface PatchSet { |
| num: RevisionPatchSetNum; |
| desc: string | undefined; |
| sha: CommitId; |
| wip?: boolean; |
| } |
| |
| /** |
| * Whether the given patch is a numbered parent of a merge (i.e. a negative |
| * number). |
| */ |
| export function isMergeParent(n: PatchSetNum) { |
| return `${n}`[0] === '-'; |
| } |
| |
| /** |
| * Whether the given patch is a parent, either a regular parent or a merge |
| * parent. |
| */ |
| export function isAParent(n: PatchSetNum) { |
| return n === PARENT || isMergeParent(n); |
| } |
| |
| export function isPatchSetNum(patchset: string) { |
| if (!isNaN(Number(patchset))) return true; |
| return patchset === EDIT || patchset === PARENT; |
| } |
| |
| export function convertToPatchSetNum( |
| patchset: string | undefined |
| ): PatchSetNum | undefined { |
| if (!patchset) return undefined; |
| if (!isPatchSetNum(patchset)) { |
| console.error('string is not of type PatchSetNum'); |
| } |
| const value = Number(patchset); |
| if (!isNaN(value)) return value as PatchSetNum; |
| return patchset as PatchSetNum; |
| } |
| |
| export function isNumber(psn: PatchSetNum): psn is PatchSetNumber { |
| return typeof psn === 'number'; |
| } |
| |
| /** |
| * Given an object of revisions, get a particular revision based on patch |
| * num. |
| * |
| * @return The correspondent revision obj from {revisions} |
| */ |
| export function getRevisionByPatchNum( |
| revisions: (RevisionInfo | EditRevisionInfo)[], |
| patchNum: PatchSetNum |
| ) { |
| for (const rev of revisions) { |
| if (rev._number === patchNum) { |
| return rev; |
| } |
| } |
| if (revisions.length > 0) console.warn('no revision found'); |
| return; |
| } |
| |
| export function getShaByPatchNum( |
| revisions: {[revisionId: string]: RevisionInfo | EditRevisionInfo}, |
| patchNum: RevisionPatchSetNum |
| ) { |
| for (const [sha, rev] of Object.entries(revisions)) { |
| if (rev._number === patchNum) return sha; |
| } |
| return undefined; |
| } |
| |
| /** |
| * Find change edit revision if change edit exists. |
| */ |
| export function findEdit( |
| revisions: Array<RevisionInfo | EditRevisionInfo> |
| ): EditRevisionInfo | undefined { |
| const editRev = revisions.find(info => info._number === EDIT); |
| return editRev as EditRevisionInfo | undefined; |
| } |
| |
| /** |
| * Find change edit base revision if change edit exists. |
| * |
| * @return change edit parent revision or null if change edit |
| * doesn't exist. |
| */ |
| export function findEditParentRevision( |
| revisions: Array<RevisionInfo | EditRevisionInfo> |
| ) { |
| const editInfo = findEdit(revisions); |
| if (!editInfo) return null; |
| |
| return revisions.find(info => info._number === editInfo.basePatchNum) || null; |
| } |
| |
| /** |
| * Find change edit base patch set number if change edit exists. |
| * |
| * @return Change edit patch set number or -1. |
| * |
| */ |
| export function findEditParentPatchNum( |
| revisions: Array<RevisionInfo | EditRevisionInfo> |
| ) { |
| const revisionInfo = findEditParentRevision(revisions); |
| // finding parent of EDIT patchset, hence revisionInfo._number cannot be |
| // EDIT and must be a number |
| // TODO(TS): find a way to avoid 'as' |
| return revisionInfo ? (revisionInfo._number as number) : -1; |
| } |
| |
| /** |
| * Sort given revisions array according to the patch set number, in |
| * descending order. |
| * The sort algorithm is change edit aware. Change edit has patch set number |
| * equals EDIT, but must appear after the patch set it was based on. |
| * Example: change edit is based on patch set 2, and another patch set was |
| * uploaded after change edit creation, the sorted order should be: |
| * 3, edit, 2, 1. |
| * |
| */ |
| export function sortRevisions<T extends RevisionInfo | EditRevisionInfo>( |
| revisions: T[] |
| ): T[] { |
| const editParent: number = findEditParentPatchNum(revisions); |
| // Map a normal patchNum to 2 * (patchNum - 1) + 1... I.e. 1 -> 1, |
| // 2 -> 3, 3 -> 5, etc. |
| // Map an edit to the patchNum of parent*2... I.e. edit on 2 -> 4. |
| // TODO(TS): find a way to avoid 'as' |
| const num = (r: T) => |
| r._number === EDIT ? 2 * editParent : 2 * ((r._number as number) - 1) + 1; |
| return revisions.sort((a, b) => num(b) - num(a)); |
| } |
| |
| /** |
| * Construct a chronological list of patch sets derived from change details. |
| * Each element of this list is an object with the following properties: |
| * |
| * * num The number identifying the patch set |
| * * desc Optional patch set description |
| * * wip If true, this patch set was never subject to review. |
| * * sha hash of the commit |
| * |
| * The wip property is determined by the change's current work_in_progress |
| * property and its log of change messages. |
| * |
| * @return Sorted list of patch set objects, as described |
| * above |
| */ |
| export function computeAllPatchSets( |
| change: ChangeInfo | ParsedChangeInfo | undefined |
| ): PatchSet[] { |
| if (!change) return []; |
| |
| let patchNums: PatchSet[] = []; |
| if (change.revisions && Object.keys(change.revisions).length) { |
| const changeRevisions = change.revisions; |
| const revisions = Object.keys(change.revisions).map(sha => { |
| return {sha, ...changeRevisions[sha]}; |
| }); |
| patchNums = sortRevisions(revisions).map(e => { |
| // TODO(kaspern): Mark which patchset an edit was made on, if an |
| // edit exists -- perhaps with a temporary description. |
| return { |
| num: e._number, |
| desc: e.description, |
| sha: e.sha as CommitId, |
| }; |
| }); |
| } |
| return computeWipForPatchSets(change, patchNums); |
| } |
| |
| /** |
| * Populate the wip properties of the given list of patch sets. |
| * |
| * @param change The change details |
| * @param patchNums Sorted list of patch set objects, as |
| * generated by computeAllPatchSets |
| * @return The given list of patch set objects, with the |
| * wip property set on each of them |
| */ |
| function computeWipForPatchSets( |
| change: ChangeInfo | ParsedChangeInfo, |
| patchNums: PatchSet[] |
| ) { |
| if (!change.messages || !change.messages.length) { |
| return patchNums; |
| } |
| // TODO(TS): replace with Map<PatchNum, boolean> |
| const psWip: Map<string, boolean> = new Map<string, boolean>(); |
| let wip = !!change.work_in_progress; |
| for (let i = 0; i < change.messages.length; i++) { |
| const msg = change.messages[i]; |
| if (msg.tag && WIP_TAGS.includes(msg.tag)) { |
| wip = true; |
| } else if (msg.tag && READY_TAGS.includes(msg.tag)) { |
| wip = false; |
| } |
| if ( |
| msg._revision_number && |
| psWip.get(`${msg._revision_number}`) !== false |
| ) { |
| psWip.set(`${msg._revision_number}`, wip); |
| } |
| } |
| |
| for (let i = 0; i < patchNums.length; i++) { |
| patchNums[i].wip = psWip.get(`${patchNums[i].num}`); |
| } |
| return patchNums; |
| } |
| |
| export const _testOnly_computeWipForPatchSets = computeWipForPatchSets; |
| |
| export function computeLatestPatchNum( |
| allPatchSets?: PatchSet[] |
| ): PatchSetNumber | undefined { |
| if (!allPatchSets || !allPatchSets.length) { |
| return undefined; |
| } |
| let latest = allPatchSets[0].num; |
| if (latest === EDIT) { |
| latest = allPatchSets[1].num; |
| } |
| assert(isNumber(latest), 'Latest patchset cannot be EDIT or PARENT.'); |
| return latest; |
| } |
| |
| // Basically is computeLatestPatchNum but allows "edits". |
| export function computeLatestPatchNumWithEdit( |
| allPatchSets?: PatchSet[] |
| ): RevisionPatchSetNum | undefined { |
| if (!allPatchSets || !allPatchSets.length) { |
| return undefined; |
| } |
| return allPatchSets[0].num; |
| } |
| |
| export function computePredecessor( |
| patchset?: PatchSetNum |
| ): BasePatchSetNum | undefined { |
| if (!patchset || patchset === PARENT || patchset === EDIT) { |
| return undefined; |
| } |
| if (patchset === 1) return PARENT; |
| return (Number(patchset) - 1) as BasePatchSetNum; |
| } |
| |
| export function hasEditBasedOnCurrentPatchSet( |
| allPatchSets: PatchSet[] |
| ): boolean { |
| if (!allPatchSets || allPatchSets.length < 2) { |
| return false; |
| } |
| return allPatchSets[0].num === EDIT; |
| } |
| |
| /** |
| * @param revisions A sorted array of revisions. |
| * |
| * @return the index of the revision with the given patchNum. |
| */ |
| export function findSortedIndex( |
| patchNum: PatchSetNum, |
| revisions: (RevisionInfo | EditRevisionInfo)[] |
| ) { |
| revisions = revisions || []; |
| const findNum = (rev: RevisionInfo | EditRevisionInfo) => |
| `${rev._number}` === `${patchNum}`; |
| return revisions.findIndex(findNum); |
| } |
| |
| /** |
| * Convert parent indexes from patch range expressions to numbers. |
| * For example, in a patch range expression `"-3"` becomes `3`. |
| */ |
| export function getParentIndex(rangeBase: PatchSetNum) { |
| return -Number(`${rangeBase}`); |
| } |
| |
| export function shorten(sha?: string) { |
| // Using the first 7 characters of the 40 chars commit sha is standard Git convention. |
| return sha?.substring(0, 7); |
| } |
| |
| export function branchName(branch?: string | BranchName): BranchName { |
| if (!branch) return '' as BranchName; |
| if (branch.startsWith('refs/heads/')) { |
| return branch.substring('refs/heads/'.length) as BranchName; |
| } |
| return branch as BranchName; |
| } |
| |
| export function getParentCommit( |
| rev?: RevisionInfo | EditRevisionInfo, |
| index?: number |
| ) { |
| const parents = rev?.parents_data ?? []; |
| const parent = parents[index ?? 0]; |
| if (!parent) return ''; |
| return shorten(parent.commit_id) ?? ''; |
| } |
| |
| export function getParentInfoString( |
| rev?: RevisionInfo | EditRevisionInfo, |
| index?: number |
| ) { |
| const parents = rev?.parents_data ?? []; |
| const parent = parents[index ?? 0]; |
| if (!parent || parent.is_merged_in_target_branch) return ''; |
| |
| if (index === 0) { |
| if (parent.change_number) { |
| return `Patchset ${parent.patch_set_number} of Change ${parent.change_number}`; |
| } else { |
| return 'Warning: The base commit is not known (aka reachable) in the target branch.'; |
| } |
| } else { |
| // For merge changes the parents with index > 0 are expected to be from a different branch. |
| return 'Other branch'; |
| } |
| } |