323 lines
9.3 KiB
TypeScript
323 lines
9.3 KiB
TypeScript
import {
|
|
RevisionInfo,
|
|
ChangeInfo,
|
|
PatchSetNum,
|
|
EditPatchSetNum,
|
|
} from '../types/common';
|
|
import {RestApiService} from '../services/services/gr-rest-api/gr-rest-api';
|
|
import {ParsedChangeInfo} from '../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
|
|
|
|
/**
|
|
* @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'];
|
|
|
|
// TODO(TS): Replace usages of these constants by
|
|
// EditPatchSetNum and ParentPatchSetNum in common.ts.
|
|
export const SPECIAL_PATCH_SET_NUM = {
|
|
EDIT: 'edit',
|
|
PARENT: 'PARENT',
|
|
};
|
|
|
|
export interface PatchSet {
|
|
num: PatchSetNum;
|
|
desc: string | undefined;
|
|
sha: string;
|
|
wip?: boolean;
|
|
}
|
|
|
|
interface RevisionWithSha extends RevisionInfo {
|
|
sha: string;
|
|
}
|
|
|
|
interface PatchRange {
|
|
patchNum?: PatchSetNum;
|
|
basePatchNum?: PatchSetNum;
|
|
}
|
|
|
|
interface PatchRangeRecord {
|
|
base: PatchRange;
|
|
}
|
|
|
|
/**
|
|
* As patchNum can be either a string (e.g. 'edit', 'PARENT') OR a number,
|
|
* this function checks for patchNum equality.
|
|
*
|
|
*/
|
|
export function patchNumEquals(a?: PatchSetNum, b?: PatchSetNum) {
|
|
if (a === undefined) {
|
|
return a === b;
|
|
}
|
|
// TODO(TS): replace with a===b when the whole code is converted to ts
|
|
return `${a}` === `${b}`;
|
|
}
|
|
|
|
/**
|
|
* Whether the given patch is a numbered parent of a merge (i.e. a negative
|
|
* number).
|
|
*/
|
|
export function isMergeParent(n: PatchSetNum) {
|
|
return `${n}`[0] === '-';
|
|
}
|
|
|
|
/**
|
|
* 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 (patchNumEquals(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: RevisionInfo[]) {
|
|
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: RevisionInfo[]) {
|
|
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>(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: ParsedChangeInfo): PatchSet[] {
|
|
if (!change) {
|
|
return [];
|
|
}
|
|
|
|
let patchNums: PatchSet[] = [];
|
|
if (change.revisions && Object.keys(change.revisions).length) {
|
|
const changeRevisions = change.revisions;
|
|
const revisions: RevisionWithSha[] = Object.keys(change.revisions).map(
|
|
sha => {
|
|
return {sha, ...changeRevisions[sha]};
|
|
}
|
|
);
|
|
patchNums = sortRevisions(revisions).map((e: RevisionWithSha) => {
|
|
// 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: 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();
|
|
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[]) {
|
|
if (!allPatchSets || !allPatchSets.length) {
|
|
return undefined;
|
|
}
|
|
if (allPatchSets[0].num === EditPatchSetNum) {
|
|
return allPatchSets[1].num;
|
|
}
|
|
return allPatchSets[0].num;
|
|
}
|
|
|
|
export function hasEditBasedOnCurrentPatchSet(allPatchSets: PatchSet[]) {
|
|
if (!allPatchSets || allPatchSets.length < 2) {
|
|
return false;
|
|
}
|
|
return allPatchSets[0].num === EditPatchSetNum;
|
|
}
|
|
|
|
export function hasEditPatchsetLoaded(patchRangeRecord: PatchRangeRecord) {
|
|
const patchRange = patchRangeRecord.base;
|
|
if (!patchRange) {
|
|
return false;
|
|
}
|
|
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,
|
|
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,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @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 -parseInt(`${rangeBase}`, 10);
|
|
}
|