196 lines
5.4 KiB
TypeScript
196 lines
5.4 KiB
TypeScript
// @generated — DO NOT EDIT. Source: packages/shared/pr-stack.ts
|
|
import type { DiffResult, ReviewGitRuntime } from "./review-core";
|
|
import type {
|
|
PRDiffScopeOption,
|
|
PRMetadata,
|
|
PRStackInfo,
|
|
PRStackTree,
|
|
PRStackNode,
|
|
} from "./pr-provider";
|
|
export type { PRDiffScope, PRDiffScopeOption, PRStackInfo, PRStackTree, PRStackNode } from "./pr-provider";
|
|
|
|
function branchNameIsSafe(branch: string): boolean {
|
|
return branch.trim().length > 0 && !branch.startsWith("-") && !branch.includes("\0");
|
|
}
|
|
|
|
export function getPRStackInfo(metadata: PRMetadata | undefined): PRStackInfo | null {
|
|
if (!metadata?.defaultBranch) return null;
|
|
if (metadata.baseBranch === metadata.defaultBranch) return null;
|
|
|
|
return {
|
|
isStacked: true,
|
|
baseBranch: metadata.baseBranch,
|
|
defaultBranch: metadata.defaultBranch,
|
|
label: `${metadata.headBranch} stacked on ${metadata.baseBranch}`,
|
|
source: "branch-inferred",
|
|
};
|
|
}
|
|
|
|
export function resolveStackInfo(
|
|
metadata: PRMetadata,
|
|
stackTree: PRStackTree | null,
|
|
existing?: PRStackInfo | null,
|
|
): PRStackInfo | null {
|
|
if (existing) return existing;
|
|
if (!stackTree || stackTree.nodes.filter(n => !n.isDefaultBranch).length <= 1) return null;
|
|
return getPRStackInfo(metadata) ?? {
|
|
isStacked: true,
|
|
baseBranch: metadata.baseBranch,
|
|
defaultBranch: metadata.defaultBranch!,
|
|
label: `Root of stack — ${metadata.headBranch}`,
|
|
source: "tree-discovered",
|
|
};
|
|
}
|
|
|
|
export function getPRDiffScopeOptions(
|
|
metadata: PRMetadata | undefined,
|
|
hasLocalCheckout: boolean,
|
|
): PRDiffScopeOption[] {
|
|
const stackInfo = getPRStackInfo(metadata);
|
|
|
|
return [
|
|
{
|
|
id: "layer",
|
|
label: "Layer",
|
|
description: metadata?.baseBranch
|
|
? `Only changes relative to ${metadata.baseBranch}.`
|
|
: "Only changes from this review.",
|
|
enabled: true,
|
|
},
|
|
{
|
|
id: "full-stack",
|
|
label: "Full stack",
|
|
description: stackInfo?.defaultBranch
|
|
? `All changes from ${stackInfo.defaultBranch} to HEAD in the local checkout.`
|
|
: "All changes from the default branch to HEAD in the local checkout.",
|
|
enabled: Boolean(stackInfo && hasLocalCheckout),
|
|
},
|
|
];
|
|
}
|
|
|
|
export async function resolvePRFullStackBaseRef(
|
|
runtime: ReviewGitRuntime,
|
|
defaultBranch: string,
|
|
cwd?: string,
|
|
): Promise<string | null> {
|
|
const remoteRef = `origin/${defaultBranch}`;
|
|
const remote = await runtime.runGit(
|
|
["show-ref", "--verify", "--quiet", `refs/remotes/${remoteRef}`],
|
|
{ cwd },
|
|
);
|
|
if (remote.exitCode === 0) return remoteRef;
|
|
|
|
const local = await runtime.runGit(
|
|
["show-ref", "--verify", "--quiet", `refs/heads/${defaultBranch}`],
|
|
{ cwd },
|
|
);
|
|
if (local.exitCode === 0) return defaultBranch;
|
|
|
|
return null;
|
|
}
|
|
|
|
export async function runPRFullStackDiff(
|
|
runtime: ReviewGitRuntime,
|
|
metadata: PRMetadata,
|
|
cwd?: string,
|
|
): Promise<DiffResult> {
|
|
const defaultBranch = metadata.defaultBranch;
|
|
if (!defaultBranch || !branchNameIsSafe(defaultBranch)) {
|
|
return {
|
|
patch: "",
|
|
label: "Full stack diff unavailable",
|
|
error: "Could not determine a safe default branch for this review.",
|
|
};
|
|
}
|
|
|
|
const baseRef = await resolvePRFullStackBaseRef(runtime, defaultBranch, cwd);
|
|
if (!baseRef) {
|
|
return {
|
|
patch: "",
|
|
label: "Full stack diff unavailable",
|
|
error: `Could not find origin/${defaultBranch} or local ${defaultBranch} in this checkout.`,
|
|
};
|
|
}
|
|
|
|
const diffArgs = [
|
|
"diff",
|
|
"--no-ext-diff",
|
|
"--src-prefix=a/",
|
|
"--dst-prefix=b/",
|
|
"--end-of-options",
|
|
`${baseRef}...HEAD`,
|
|
];
|
|
const diff = await runtime.runGit(diffArgs, { cwd });
|
|
if (diff.exitCode !== 0) {
|
|
const message = diff.stderr.trim() || `git ${diffArgs.join(" ")} failed`;
|
|
return {
|
|
patch: "",
|
|
label: "Full stack diff unavailable",
|
|
error: message.split("\n").find((line) => line.trim().length > 0) ?? message,
|
|
};
|
|
}
|
|
|
|
return {
|
|
patch: diff.stdout,
|
|
label: `Full stack diff vs ${baseRef}`,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fetch and checkout a PR/MR head in a local worktree.
|
|
* Returns true if the checkout succeeded, false otherwise.
|
|
*/
|
|
export async function checkoutPRHead(
|
|
runtime: ReviewGitRuntime,
|
|
metadata: PRMetadata,
|
|
cwd: string,
|
|
): Promise<boolean> {
|
|
const refSpec = metadata.platform === "github"
|
|
? `refs/pull/${metadata.number}/head`
|
|
: `refs/merge-requests/${metadata.iid}/head`;
|
|
|
|
const fetch = await runtime.runGit(["fetch", "origin", refSpec], { cwd });
|
|
if (fetch.exitCode !== 0) return false;
|
|
|
|
const checkout = await runtime.runGit(["checkout", "FETCH_HEAD"], { cwd });
|
|
return checkout.exitCode === 0;
|
|
}
|
|
|
|
/**
|
|
* Build a minimal stack tree from existing metadata (no API calls).
|
|
* Used as a fallback when the full stack tree hasn't loaded yet.
|
|
*/
|
|
export function buildMinimalStackTree(
|
|
metadata: PRMetadata,
|
|
stackInfo: PRStackInfo,
|
|
): PRStackTree {
|
|
const nodes: PRStackNode[] = [];
|
|
|
|
if (stackInfo.defaultBranch) {
|
|
nodes.push({
|
|
branch: stackInfo.defaultBranch,
|
|
isCurrent: false,
|
|
isDefaultBranch: true,
|
|
});
|
|
}
|
|
|
|
if (stackInfo.baseBranch !== stackInfo.defaultBranch) {
|
|
nodes.push({
|
|
branch: stackInfo.baseBranch,
|
|
isCurrent: false,
|
|
isDefaultBranch: false,
|
|
});
|
|
}
|
|
|
|
nodes.push({
|
|
branch: metadata.headBranch,
|
|
number: metadata.platform === "github" ? metadata.number : metadata.iid,
|
|
title: metadata.title,
|
|
url: metadata.url,
|
|
isCurrent: true,
|
|
isDefaultBranch: false,
|
|
});
|
|
|
|
return { nodes };
|
|
}
|