blob: 7de5e7e3ad736a50d9f8dfd99ddd907c42151d2e [file] [log] [blame]
/**
* @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {getBaseUrl} from './url-util';
import {ChangeStatus} from '../constants/constants';
import {
NumericChangeId,
PatchSetNum,
ChangeInfo,
AccountInfo,
RelatedChangeAndCommitInfo,
ChangeStates,
} from '../types/common';
import {ParsedChangeInfo} from '../types/types';
import {getUserId, isServiceUser} from './account-util';
interface ChangeStatusesOptions {
mergeable: boolean;
/** Is there a reverting change and if so, what status has it? */
revertingChangeStatus?: ChangeStatus;
}
export const ChangeDiffType = {
ADDED: 'ADDED',
COPIED: 'COPIED',
DELETED: 'DELETED',
MODIFIED: 'MODIFIED',
RENAMED: 'RENAMED',
REWRITE: 'REWRITE',
};
// Must be kept in sync with the ListChangesOption enum and protobuf.
export const ListChangesOption = {
LABELS: 0,
DETAILED_LABELS: 8,
// Return information on the current patch set of the change.
CURRENT_REVISION: 1,
ALL_REVISIONS: 2,
// If revisions are included, parse the commit object.
CURRENT_COMMIT: 3,
ALL_COMMITS: 4,
// If a patch set is included, include the files of the patch set.
CURRENT_FILES: 5,
ALL_FILES: 6,
// If accounts are included, include detailed account info.
DETAILED_ACCOUNTS: 7,
// Include messages associated with the change.
MESSAGES: 9,
// Include allowed actions client could perform.
CURRENT_ACTIONS: 10,
// Set the reviewed boolean for the caller.
REVIEWED: 11,
// Include download commands for the caller.
DOWNLOAD_COMMANDS: 13,
// Include patch set weblinks.
WEB_LINKS: 14,
// Include consistency check results.
CHECK: 15,
// Include allowed change actions client could perform.
CHANGE_ACTIONS: 16,
// Include a copy of commit messages including review footers.
COMMIT_FOOTERS: 17,
// Include push certificate information along with any patch sets.
PUSH_CERTIFICATES: 18,
// Include change's reviewer updates.
REVIEWER_UPDATES: 19,
// Set the submittable boolean.
SUBMITTABLE: 20,
// If tracking ids are included, include detailed tracking ids info.
TRACKING_IDS: 21,
// Skip mergeability data.
SKIP_MERGEABLE: 22,
/**
* Skip diffstat computation that compute the insertions field (number of lines inserted) and
* deletions field (number of lines deleted)
*/
SKIP_DIFFSTAT: 23,
/** Include the evaluated submit requirements for the caller. */
SUBMIT_REQUIREMENTS: 24,
};
export function listChangesOptionsToHex(...args: number[]) {
let v = 0;
for (let i = 0; i < args.length; i++) {
v |= 1 << args[i];
}
return v.toString(16);
}
export function changeBaseURL(
repo: string,
changeNum: NumericChangeId,
patchNum: PatchSetNum
): string {
let v = `${getBaseUrl()}/changes/${encodeURIComponent(repo)}~${changeNum}`;
if (patchNum) {
v += `/revisions/${patchNum}`;
}
return v;
}
export function changePath(changeNum: NumericChangeId) {
return `${getBaseUrl()}/c/${changeNum}`;
}
export function changeIsOpen(change?: ChangeInfo | ParsedChangeInfo | null) {
return change?.status === ChangeStatus.NEW;
}
export function changeIsMerged(change?: ChangeInfo | ParsedChangeInfo | null) {
return change?.status === ChangeStatus.MERGED;
}
export function changeIsAbandoned(
change?: ChangeInfo | ParsedChangeInfo | null
) {
return change?.status === ChangeStatus.ABANDONED;
}
/**
* Get the change number from either a ChangeInfo (such as those included in
* SubmittedTogetherInfo responses) or get the change number from a
* RelatedChangeAndCommitInfo (such as those included in a
* RelatedChangesInfo response).
*/
export function getChangeNumber(
change: ChangeInfo | ParsedChangeInfo | RelatedChangeAndCommitInfo
): NumericChangeId {
if (isChangeInfo(change)) {
return change._number;
}
return change._change_number!;
}
export function changeStatuses(
change: ChangeInfo,
options?: ChangeStatusesOptions
): ChangeStates[] {
const states = [];
if (change.status === ChangeStatus.MERGED) {
if (options?.revertingChangeStatus === ChangeStatus.MERGED) {
return [ChangeStates.MERGED, ChangeStates.REVERT_SUBMITTED];
}
if (options?.revertingChangeStatus !== undefined) {
return [ChangeStates.MERGED, ChangeStates.REVERT_CREATED];
}
return [ChangeStates.MERGED];
}
if (change.status === ChangeStatus.ABANDONED) {
return [ChangeStates.ABANDONED];
}
if (change.mergeable === false || (options && options.mergeable === false)) {
// 'mergeable' prop may not always exist (@see Issue 6819)
states.push(ChangeStates.MERGE_CONFLICT);
} else if (change.contains_git_conflicts) {
states.push(ChangeStates.GIT_CONFLICT);
}
if (change.work_in_progress) {
states.push(ChangeStates.WIP);
}
if (change.is_private) {
states.push(ChangeStates.PRIVATE);
}
// If there are any pre-defined statuses, only return those. Otherwise,
// will determine the derived status.
if (states.length || !options) {
return states;
}
if (change.submittable) {
states.push(ChangeStates.READY_TO_SUBMIT);
} else {
states.push(ChangeStates.ACTIVE);
}
return states;
}
export function isOwner(
change?: ChangeInfo | ParsedChangeInfo,
account?: AccountInfo
): boolean {
if (!change || !account) return false;
return change.owner?._account_id === account._account_id;
}
export function isReviewer(
change?: ChangeInfo | ParsedChangeInfo,
account?: AccountInfo
): boolean {
if (!change || !account) return false;
if (isOwner(change, account)) return false;
const reviewers = change.reviewers.REVIEWER ?? [];
return reviewers.some(r => r._account_id === account._account_id);
}
export function isCc(
change?: ChangeInfo | ParsedChangeInfo,
account?: AccountInfo
): boolean {
if (!change || !account) return false;
const ccs = change.reviewers.CC ?? [];
return ccs.some(r => r._account_id === account._account_id);
}
export function isUploader(
change?: ChangeInfo | ParsedChangeInfo,
account?: AccountInfo
): boolean {
if (!change || !account) return false;
const rev = getCurrentRevision(change);
return rev?.uploader?._account_id === account._account_id;
}
export function isInvolved(
change?: ChangeInfo | ParsedChangeInfo,
account?: AccountInfo
): boolean {
const owner = isOwner(change, account);
const uploader = isUploader(change, account);
const reviewer = isReviewer(change, account);
const cc = isCc(change, account);
return owner || uploader || reviewer || cc;
}
export function roleDetails(
change?: ChangeInfo | ParsedChangeInfo,
account?: AccountInfo
) {
return {
isOwner: isOwner(change, account),
isUploader: isUploader(change, account),
isReviewer: isReviewer(change, account),
isCc: isCc(change, account),
};
}
export function getCurrentRevision(change?: ChangeInfo | ParsedChangeInfo) {
if (!change?.revisions || !change?.current_revision) return undefined;
return change.revisions[change.current_revision];
}
export function getRevisionKey(
change: ChangeInfo | ParsedChangeInfo,
patchNum: PatchSetNum
) {
return Object.keys(change.revisions ?? []).find(
rev => change?.revisions?.[rev]._number === patchNum
);
}
export function hasHumanReviewer(
change?: ChangeInfo | ParsedChangeInfo
): boolean {
if (!change) return false;
const reviewers = change.reviewers.REVIEWER ?? [];
return reviewers
.filter(r => getUserId(r) !== getUserId(change.owner))
.some(r => !isServiceUser(r));
}
export function isRemovableReviewer(
change?: ChangeInfo,
reviewer?: AccountInfo
): boolean {
if (!reviewer || !change) return false;
if (isCc(change, reviewer)) return true;
if (!change.removable_reviewers) return false;
return change.removable_reviewers.some(
account =>
account._account_id === reviewer._account_id ||
(!reviewer._account_id && account.email === reviewer.email)
);
}
export function isChangeInfo(
x: ChangeInfo | RelatedChangeAndCommitInfo | ParsedChangeInfo
): x is ChangeInfo | ParsedChangeInfo {
return (x as ChangeInfo)._number !== undefined;
}