blob: 7a5cd45146dc112d274bf4725282fa1855be4e04 [file] [log] [blame]
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';
}
}