blob: 40e3eef0ca0055d31a178a46e508125db3dde74e [file] [log] [blame]
import {
RevisionInfo,
ChangeInfo,
PatchSetNum,
EditPatchSetNum,
BrandType,
ParentPatchSetNum,
} from '../types/common';
import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
import {EditRevisionInfo, ParsedChangeInfo} from '../types/types';
/**
* @license
* Copyright (C) 2016 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.
*/
// 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: PatchSetNum;
desc: string | undefined;
sha: string;
wip?: boolean;
}
interface PatchRange {
patchNum?: PatchSetNum;
basePatchNum?: PatchSetNum;
}
/**
* 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 === ParentPatchSetNum || isMergeParent(n);
}
export function isPatchSetNum(patchset: string) {
if (!isNaN(Number(patchset))) return true;
return patchset === EditPatchSetNum || patchset === ParentPatchSetNum;
}
export function convertToPatchSetNum(
patchset: string | undefined
): PatchSetNum | undefined {
if (patchset === undefined) return patchset;
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 BrandType<number, '_patchSet'> {
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[],
patchNum: PatchSetNum
) {
for (const rev of revisions) {
if (rev._number === patchNum) {
return rev;
}
}
console.warn('no revision found');
return;
}
/**
* 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 = revisions.find(info => info._number === EditPatchSetNum);
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 === EditPatchSetNum
? 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,
};
});
}
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[]
): PatchSetNum | undefined {
if (!allPatchSets || !allPatchSets.length) {
return undefined;
}
if (allPatchSets[0].num === EditPatchSetNum) {
return allPatchSets[1].num;
}
return allPatchSets[0].num;
}
export function computePredecessor(
patchset?: PatchSetNum
): PatchSetNum | undefined {
if (
!patchset ||
patchset === ParentPatchSetNum ||
patchset === EditPatchSetNum
) {
return undefined;
}
if (patchset === 1) return ParentPatchSetNum;
return (Number(patchset) - 1) as PatchSetNum;
}
export function hasEditBasedOnCurrentPatchSet(allPatchSets: PatchSet[]) {
if (!allPatchSets || allPatchSets.length < 2) {
return false;
}
return allPatchSets[0].num === EditPatchSetNum;
}
export function hasEditPatchsetLoaded(patchRange: PatchRange) {
return (
patchRange.patchNum === EditPatchSetNum ||
patchRange.basePatchNum === EditPatchSetNum
);
}
/**
* Check whether there is no newer patch than the latest patch that was
* available when this change was loaded.
*
* @return A promise that yields true if the latest patch
* has been loaded, and false if a newer patch has been uploaded in the
* meantime. The promise is rejected on network error.
*/
export function fetchChangeUpdates(
change: ChangeInfo | ParsedChangeInfo,
restAPI: RestApiService
) {
const knownLatest = computeLatestPatchNum(computeAllPatchSets(change));
return restAPI.getChangeDetail(change._number).then(detail => {
if (!detail) {
const error = new Error('Change detail not found.');
return Promise.reject(error);
}
const actualLatest = computeLatestPatchNum(computeAllPatchSets(detail));
if (!actualLatest || !knownLatest) {
const error = new Error('Unable to check for latest patchset.');
return Promise.reject(error);
}
return {
isLatest: actualLatest <= knownLatest,
newStatus: change.status !== detail.status ? detail.status : null,
newMessages:
(change.messages || []).length < (detail.messages || []).length
? detail.messages![detail.messages!.length - 1]
: undefined,
};
});
}
/**
* @param revisions A sorted array of revisions.
*
* @return the index of the revision with the given patchNum.
*/
export function findSortedIndex(
patchNum: PatchSetNum,
revisions: RevisionInfo[]
) {
revisions = revisions || [];
const findNum = (rev: RevisionInfo) => `${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}`);
}