Add plannotator extension v0.19.10
This commit is contained in:
748
extensions/plannotator/generated/review-core.ts
Normal file
748
extensions/plannotator/generated/review-core.ts
Normal file
@@ -0,0 +1,748 @@
|
||||
// @generated — DO NOT EDIT. Source: packages/shared/review-core.ts
|
||||
/**
|
||||
* Runtime-agnostic code-review core shared by Bun runtimes and Pi.
|
||||
*
|
||||
* Pi consumes a build-time copy of this file so its published package stays
|
||||
* self-contained while review diff logic remains sourced from one module.
|
||||
*/
|
||||
|
||||
import { resolve as resolvePath } from "node:path";
|
||||
|
||||
export type DiffType =
|
||||
| "uncommitted"
|
||||
| "staged"
|
||||
| "unstaged"
|
||||
| "last-commit"
|
||||
| "branch"
|
||||
| "merge-base"
|
||||
| "all"
|
||||
| `worktree:${string}`
|
||||
| "p4-default"
|
||||
| `p4-changelist:${string}`;
|
||||
|
||||
export interface DiffOption {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface WorktreeInfo {
|
||||
path: string;
|
||||
branch: string | null;
|
||||
head: string;
|
||||
}
|
||||
|
||||
export interface AvailableBranches {
|
||||
local: string[];
|
||||
remote: string[];
|
||||
}
|
||||
|
||||
export interface GitContext {
|
||||
currentBranch: string;
|
||||
defaultBranch: string;
|
||||
diffOptions: DiffOption[];
|
||||
worktrees: WorktreeInfo[];
|
||||
availableBranches: AvailableBranches;
|
||||
cwd?: string;
|
||||
vcsType?: "git" | "p4";
|
||||
}
|
||||
|
||||
export interface DiffResult {
|
||||
patch: string;
|
||||
label: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface GitCommandResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
}
|
||||
|
||||
export interface ReviewGitRuntime {
|
||||
runGit: (
|
||||
args: string[],
|
||||
options?: { cwd?: string; timeoutMs?: number },
|
||||
) => Promise<GitCommandResult>;
|
||||
readTextFile: (path: string) => Promise<string | null>;
|
||||
}
|
||||
|
||||
export interface GitDiffOptions {
|
||||
hideWhitespace?: boolean;
|
||||
}
|
||||
|
||||
export async function getCurrentBranch(
|
||||
runtime: ReviewGitRuntime,
|
||||
cwd?: string,
|
||||
): Promise<string> {
|
||||
const result = await runtime.runGit(
|
||||
["rev-parse", "--abbrev-ref", "HEAD"],
|
||||
{ cwd },
|
||||
);
|
||||
return result.exitCode === 0 ? result.stdout.trim() || "HEAD" : "HEAD";
|
||||
}
|
||||
|
||||
export async function getDefaultBranch(
|
||||
runtime: ReviewGitRuntime,
|
||||
cwd?: string,
|
||||
): Promise<string> {
|
||||
// Prefer the remote tracking ref (e.g. `origin/main`) so diffs run against
|
||||
// the upstream tip, not a potentially stale local copy. Only fall back to
|
||||
// a local ref when there's no remote configured at all.
|
||||
const remoteHead = await runtime.runGit(
|
||||
["symbolic-ref", "refs/remotes/origin/HEAD"],
|
||||
{ cwd },
|
||||
);
|
||||
if (remoteHead.exitCode === 0) {
|
||||
const ref = remoteHead.stdout.trim();
|
||||
if (ref) {
|
||||
// `symbolic-ref` only tells us what origin/HEAD *points at* — it does
|
||||
// not guarantee that the target ref was actually fetched. In narrow
|
||||
// or partial clones the pointer can be set while the target is
|
||||
// missing, in which case a later `git diff origin/main..HEAD` would
|
||||
// error. Verify the target exists before trusting it.
|
||||
const verify = await runtime.runGit(
|
||||
["show-ref", "--verify", "--quiet", ref],
|
||||
{ cwd },
|
||||
);
|
||||
if (verify.exitCode === 0) return ref.replace("refs/remotes/", "");
|
||||
}
|
||||
}
|
||||
|
||||
const mainBranch = await runtime.runGit(
|
||||
["show-ref", "--verify", "refs/heads/main"],
|
||||
{ cwd },
|
||||
);
|
||||
if (mainBranch.exitCode === 0) return "main";
|
||||
|
||||
return "master";
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the remote for its default branch via `ls-remote --symref`. Returns
|
||||
* `origin/<name>` if the remote answers and the tracking ref exists locally,
|
||||
* otherwise `null`. Designed to run in the background at server startup — the
|
||||
* caller fires it with `.then()` and uses the result if/when it arrives.
|
||||
*
|
||||
* Timeout-guarded: if the network is slow or absent, the promise resolves
|
||||
* (with `null`) once the timeout fires. Never throws.
|
||||
*/
|
||||
export async function detectRemoteDefaultBranch(
|
||||
runtime: ReviewGitRuntime,
|
||||
cwd?: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const lsRemote = await runtime.runGit(
|
||||
["ls-remote", "--symref", "origin", "HEAD"],
|
||||
{ cwd, timeoutMs: 5000 },
|
||||
);
|
||||
if (lsRemote.exitCode !== 0) return null;
|
||||
const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/(\S+)\s+HEAD/m);
|
||||
if (!match) return null;
|
||||
const remoteBranch = `origin/${match[1]}`;
|
||||
const refExists = await runtime.runGit(
|
||||
["show-ref", "--verify", "--quiet", `refs/remotes/${remoteBranch}`],
|
||||
{ cwd },
|
||||
);
|
||||
return refExists.exitCode === 0 ? remoteBranch : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listBranches(
|
||||
runtime: ReviewGitRuntime,
|
||||
cwd?: string,
|
||||
): Promise<AvailableBranches> {
|
||||
// Emit `<full-refname>\t<short-name>` so we can classify by ref prefix
|
||||
// without guessing from the short form — local branches can contain `/`
|
||||
// (e.g. `feature/foo`), so `name.includes("/")` would misclassify them.
|
||||
const result = await runtime.runGit(
|
||||
[
|
||||
"for-each-ref",
|
||||
"--format=%(refname)\t%(refname:short)",
|
||||
"refs/heads",
|
||||
"refs/remotes",
|
||||
],
|
||||
{ cwd },
|
||||
);
|
||||
if (result.exitCode !== 0) return { local: [], remote: [] };
|
||||
|
||||
const local: string[] = [];
|
||||
const remote: string[] = [];
|
||||
|
||||
for (const line of result.stdout.split("\n")) {
|
||||
const [fullRef, shortName] = line.split("\t");
|
||||
if (!fullRef || !shortName) continue;
|
||||
if (shortName.endsWith("/HEAD")) continue;
|
||||
if (fullRef.startsWith("refs/heads/")) {
|
||||
local.push(shortName);
|
||||
} else if (fullRef.startsWith("refs/remotes/")) {
|
||||
remote.push(shortName);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep both local and remote refs — they can point to different commits
|
||||
// (stale local tracking branches are common) and users need to be able to
|
||||
// pick either explicitly. The picker groups them separately for clarity.
|
||||
local.sort();
|
||||
remote.sort();
|
||||
|
||||
return { local, remote };
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a safe base branch. Trusts the caller verbatim if they supplied one,
|
||||
* otherwise falls back to the detected default. Shared by Bun (`review.ts`)
|
||||
* and Pi (`serverReview.ts`) so both runtimes behave identically.
|
||||
*
|
||||
* Why trust the caller: the UI picker only ever sends refs from the known
|
||||
* list, and external/programmatic callers may pass tags, SHAs, or refs under
|
||||
* non-`origin` remotes that we must not silently rewrite (a tag `release` is
|
||||
* not the same commit as a branch `origin/release`). Invalid refs surface as
|
||||
* git errors on the next diff call, which is better than silently producing
|
||||
* a patch against the wrong commit.
|
||||
*/
|
||||
export function resolveBaseBranch(
|
||||
requested: string | undefined,
|
||||
detected: string,
|
||||
): string {
|
||||
return requested || detected;
|
||||
}
|
||||
|
||||
export async function getWorktrees(
|
||||
runtime: ReviewGitRuntime,
|
||||
cwd?: string,
|
||||
): Promise<WorktreeInfo[]> {
|
||||
const result = await runtime.runGit(["worktree", "list", "--porcelain"], { cwd });
|
||||
if (result.exitCode !== 0) return [];
|
||||
|
||||
const entries: WorktreeInfo[] = [];
|
||||
let current: Partial<WorktreeInfo> = {};
|
||||
|
||||
for (const line of result.stdout.split("\n")) {
|
||||
if (line.startsWith("worktree ")) {
|
||||
if (current.path) {
|
||||
entries.push({
|
||||
path: current.path,
|
||||
head: current.head || "",
|
||||
branch: current.branch ?? null,
|
||||
});
|
||||
}
|
||||
current = { path: line.slice("worktree ".length) };
|
||||
} else if (line.startsWith("HEAD ")) {
|
||||
current.head = line.slice("HEAD ".length);
|
||||
} else if (line.startsWith("branch ")) {
|
||||
current.branch = line
|
||||
.slice("branch ".length)
|
||||
.replace("refs/heads/", "");
|
||||
} else if (line === "detached") {
|
||||
current.branch = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (current.path) {
|
||||
entries.push({
|
||||
path: current.path,
|
||||
head: current.head || "",
|
||||
branch: current.branch ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
export async function getGitContext(
|
||||
runtime: ReviewGitRuntime,
|
||||
cwd?: string,
|
||||
): Promise<GitContext> {
|
||||
const [currentBranch, defaultBranch, availableBranches] = await Promise.all([
|
||||
getCurrentBranch(runtime, cwd),
|
||||
getDefaultBranch(runtime, cwd),
|
||||
listBranches(runtime, cwd),
|
||||
]);
|
||||
|
||||
const diffOptions: DiffOption[] = [
|
||||
{ id: "uncommitted", label: "Uncommitted changes" },
|
||||
{ id: "staged", label: "Staged changes" },
|
||||
{ id: "unstaged", label: "Unstaged changes" },
|
||||
{ id: "last-commit", label: "Last commit" },
|
||||
];
|
||||
|
||||
// Always offer Branch diff / PR Diff when a default branch exists. The
|
||||
// older guard hid them when the reviewer was on the default branch (the
|
||||
// `vs <default>` diff from the default branch itself is always empty), but
|
||||
// the base picker now lets reviewers compare against any branch from any
|
||||
// branch, so there's no meaningless-by-construction option. Also: preserving
|
||||
// diff mode across worktree switches and Pi's `initialBase` can land the
|
||||
// reviewer on the default branch with branch/merge-base already active — the
|
||||
// old guard hid the active mode's option, trapping them. Unconditional
|
||||
// emission keeps the active option reachable in every flow.
|
||||
if (defaultBranch) {
|
||||
diffOptions.push({ id: "merge-base", label: "Committed changes" });
|
||||
}
|
||||
|
||||
diffOptions.push({ id: "all", label: "All files (HEAD)" });
|
||||
|
||||
const [worktrees, currentTreePathResult] = await Promise.all([
|
||||
getWorktrees(runtime, cwd),
|
||||
runtime.runGit(["rev-parse", "--show-toplevel"], { cwd }),
|
||||
]);
|
||||
|
||||
const currentTreePath =
|
||||
currentTreePathResult.exitCode === 0
|
||||
? currentTreePathResult.stdout.trim()
|
||||
: null;
|
||||
|
||||
return {
|
||||
currentBranch,
|
||||
defaultBranch,
|
||||
diffOptions,
|
||||
worktrees: worktrees.filter((wt) => wt.path !== currentTreePath),
|
||||
availableBranches,
|
||||
cwd,
|
||||
};
|
||||
}
|
||||
|
||||
async function getUntrackedFileDiffs(
|
||||
runtime: ReviewGitRuntime,
|
||||
srcPrefix = "a/",
|
||||
dstPrefix = "b/",
|
||||
cwd?: string,
|
||||
options?: GitDiffOptions,
|
||||
): Promise<string> {
|
||||
// git ls-files scopes to the CWD subtree and returns CWD-relative paths,
|
||||
// unlike git diff HEAD which always covers the full repo with root-relative
|
||||
// paths. Resolve the repo root so untracked files from the entire repo are
|
||||
// included and their paths match the tracked-diff output.
|
||||
const toplevelResult = await runtime.runGit(
|
||||
["rev-parse", "--show-toplevel"],
|
||||
{ cwd },
|
||||
);
|
||||
const rootCwd =
|
||||
toplevelResult.exitCode === 0 ? toplevelResult.stdout.trim() : cwd;
|
||||
|
||||
const lsResult = await runtime.runGit(
|
||||
["ls-files", "--others", "--exclude-standard"],
|
||||
{ cwd: rootCwd },
|
||||
);
|
||||
if (lsResult.exitCode !== 0) return "";
|
||||
|
||||
const files = lsResult.stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((file) => file.length > 0);
|
||||
|
||||
if (files.length === 0) return "";
|
||||
|
||||
const diffs = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const diffResult = await runtime.runGit(
|
||||
[
|
||||
"diff",
|
||||
"--no-ext-diff",
|
||||
...(options?.hideWhitespace ? ["-w"] : []),
|
||||
"--no-index",
|
||||
`--src-prefix=${srcPrefix}`,
|
||||
`--dst-prefix=${dstPrefix}`,
|
||||
"/dev/null",
|
||||
file,
|
||||
],
|
||||
{ cwd: rootCwd },
|
||||
);
|
||||
return diffResult.stdout;
|
||||
}),
|
||||
);
|
||||
|
||||
return diffs.join("");
|
||||
}
|
||||
|
||||
function assertGitSuccess(
|
||||
result: GitCommandResult,
|
||||
args: string[],
|
||||
): GitCommandResult {
|
||||
if (result.exitCode === 0) return result;
|
||||
|
||||
const command = `git ${args.join(" ")}`;
|
||||
const stderr = result.stderr.trim();
|
||||
throw new Error(
|
||||
stderr
|
||||
? `${command} failed: ${stderr}`
|
||||
: `${command} failed with exit code ${result.exitCode}`,
|
||||
);
|
||||
}
|
||||
|
||||
const WORKTREE_SUB_TYPES = new Set([
|
||||
"uncommitted",
|
||||
"staged",
|
||||
"unstaged",
|
||||
"last-commit",
|
||||
"branch",
|
||||
"merge-base",
|
||||
"all",
|
||||
]);
|
||||
|
||||
export function parseWorktreeDiffType(
|
||||
diffType: string,
|
||||
): { path: string; subType: string } | null {
|
||||
if (!diffType.startsWith("worktree:")) return null;
|
||||
|
||||
const rest = diffType.slice("worktree:".length);
|
||||
const lastColon = rest.lastIndexOf(":");
|
||||
if (lastColon !== -1) {
|
||||
const maybeSub = rest.slice(lastColon + 1);
|
||||
if (WORKTREE_SUB_TYPES.has(maybeSub)) {
|
||||
return { path: rest.slice(0, lastColon), subType: maybeSub };
|
||||
}
|
||||
}
|
||||
|
||||
return { path: rest, subType: "uncommitted" };
|
||||
}
|
||||
|
||||
export async function runGitDiff(
|
||||
runtime: ReviewGitRuntime,
|
||||
diffType: DiffType,
|
||||
defaultBranch: string = "main",
|
||||
externalCwd?: string,
|
||||
options?: GitDiffOptions,
|
||||
): Promise<DiffResult> {
|
||||
let patch = "";
|
||||
let label = "";
|
||||
let cwd: string | undefined = externalCwd;
|
||||
let effectiveDiffType = diffType as string;
|
||||
|
||||
if (diffType.startsWith("worktree:")) {
|
||||
const parsed = parseWorktreeDiffType(diffType);
|
||||
if (!parsed) {
|
||||
return {
|
||||
patch: "",
|
||||
label: "Worktree error",
|
||||
error: "Could not parse worktree diff type",
|
||||
};
|
||||
}
|
||||
cwd = parsed.path;
|
||||
effectiveDiffType = parsed.subType;
|
||||
}
|
||||
|
||||
const wFlag = options?.hideWhitespace ? ["-w"] : [];
|
||||
|
||||
try {
|
||||
switch (effectiveDiffType) {
|
||||
case "uncommitted": {
|
||||
const trackedDiffArgs = [
|
||||
"diff",
|
||||
"--no-ext-diff",
|
||||
...wFlag,
|
||||
"HEAD",
|
||||
"--src-prefix=a/",
|
||||
"--dst-prefix=b/",
|
||||
];
|
||||
const hasHead =
|
||||
(await runtime.runGit(["rev-parse", "--verify", "HEAD"], { cwd }))
|
||||
.exitCode === 0;
|
||||
const trackedPatch = hasHead
|
||||
? assertGitSuccess(
|
||||
await runtime.runGit(trackedDiffArgs, { cwd }),
|
||||
trackedDiffArgs,
|
||||
).stdout
|
||||
: "";
|
||||
const untrackedDiff = await getUntrackedFileDiffs(
|
||||
runtime,
|
||||
"a/",
|
||||
"b/",
|
||||
cwd,
|
||||
options,
|
||||
);
|
||||
patch = trackedPatch + untrackedDiff;
|
||||
label = "Uncommitted changes";
|
||||
break;
|
||||
}
|
||||
|
||||
case "staged": {
|
||||
const stagedDiffArgs = [
|
||||
"diff",
|
||||
"--no-ext-diff",
|
||||
...wFlag,
|
||||
"--staged",
|
||||
"--src-prefix=a/",
|
||||
"--dst-prefix=b/",
|
||||
];
|
||||
const stagedDiff = assertGitSuccess(
|
||||
await runtime.runGit(stagedDiffArgs, { cwd }),
|
||||
stagedDiffArgs,
|
||||
);
|
||||
patch = stagedDiff.stdout;
|
||||
label = "Staged changes";
|
||||
break;
|
||||
}
|
||||
|
||||
case "unstaged": {
|
||||
const trackedDiffArgs = [
|
||||
"diff",
|
||||
"--no-ext-diff",
|
||||
...wFlag,
|
||||
"--src-prefix=a/",
|
||||
"--dst-prefix=b/",
|
||||
];
|
||||
const trackedDiff = assertGitSuccess(
|
||||
await runtime.runGit(trackedDiffArgs, { cwd }),
|
||||
trackedDiffArgs,
|
||||
);
|
||||
const untrackedDiff = await getUntrackedFileDiffs(
|
||||
runtime,
|
||||
"a/",
|
||||
"b/",
|
||||
cwd,
|
||||
options,
|
||||
);
|
||||
patch = trackedDiff.stdout + untrackedDiff;
|
||||
label = "Unstaged changes";
|
||||
break;
|
||||
}
|
||||
|
||||
case "last-commit": {
|
||||
const hasParent = await runtime.runGit(
|
||||
["rev-parse", "--verify", "HEAD~1"],
|
||||
{ cwd },
|
||||
);
|
||||
const args =
|
||||
hasParent.exitCode === 0
|
||||
? ["diff", "--no-ext-diff", ...wFlag, "HEAD~1..HEAD", "--src-prefix=a/", "--dst-prefix=b/"]
|
||||
: ["diff", "--no-ext-diff", ...wFlag, "--root", "HEAD", "--src-prefix=a/", "--dst-prefix=b/"];
|
||||
const lastCommitDiff = assertGitSuccess(
|
||||
await runtime.runGit(args, { cwd }),
|
||||
args,
|
||||
);
|
||||
patch = lastCommitDiff.stdout;
|
||||
label = "Last commit";
|
||||
break;
|
||||
}
|
||||
|
||||
case "branch": {
|
||||
// `--end-of-options` hardens against a caller-supplied `defaultBranch`
|
||||
// that starts with `-` being parsed as a git flag (e.g. `--output=...`
|
||||
// would redirect diff output to an attacker-chosen path). Same pattern
|
||||
// applied wherever user-controlled refs flow into a git argv.
|
||||
const branchDiffArgs = [
|
||||
"diff",
|
||||
"--no-ext-diff",
|
||||
...wFlag,
|
||||
"--src-prefix=a/",
|
||||
"--dst-prefix=b/",
|
||||
"--end-of-options",
|
||||
`${defaultBranch}..HEAD`,
|
||||
];
|
||||
const branchDiff = assertGitSuccess(
|
||||
await runtime.runGit(branchDiffArgs, { cwd }),
|
||||
branchDiffArgs,
|
||||
);
|
||||
patch = branchDiff.stdout;
|
||||
label = `Changes vs ${defaultBranch}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case "merge-base": {
|
||||
const mergeBaseLookupArgs = ["merge-base", "--end-of-options", defaultBranch, "HEAD"];
|
||||
const mergeBaseResult = assertGitSuccess(
|
||||
await runtime.runGit(mergeBaseLookupArgs, { cwd }),
|
||||
mergeBaseLookupArgs,
|
||||
);
|
||||
const mergeBase = mergeBaseResult.stdout.trim();
|
||||
const mergeBaseDiffArgs = [
|
||||
"diff",
|
||||
"--no-ext-diff",
|
||||
...wFlag,
|
||||
"--src-prefix=a/",
|
||||
"--dst-prefix=b/",
|
||||
"--end-of-options",
|
||||
`${mergeBase}..HEAD`,
|
||||
];
|
||||
const mergeBaseDiff = assertGitSuccess(
|
||||
await runtime.runGit(mergeBaseDiffArgs, { cwd }),
|
||||
mergeBaseDiffArgs,
|
||||
);
|
||||
patch = mergeBaseDiff.stdout;
|
||||
label = `PR diff vs ${defaultBranch}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case "all": {
|
||||
// Diff from the empty tree to HEAD — shows every tracked file as an addition.
|
||||
const emptyTreeResult = await runtime.runGit(["hash-object", "-t", "tree", "/dev/null"], { cwd });
|
||||
const emptyTree = emptyTreeResult.exitCode === 0
|
||||
? emptyTreeResult.stdout.trim()
|
||||
: "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
||||
const allDiffArgs = [
|
||||
"diff",
|
||||
"--no-ext-diff",
|
||||
...wFlag,
|
||||
"--src-prefix=a/",
|
||||
"--dst-prefix=b/",
|
||||
"--end-of-options",
|
||||
`${emptyTree}..HEAD`,
|
||||
];
|
||||
const allDiff = assertGitSuccess(
|
||||
await runtime.runGit(allDiffArgs, { cwd }),
|
||||
allDiffArgs,
|
||||
);
|
||||
patch = allDiff.stdout;
|
||||
label = "All files";
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return { patch: "", label: "Unknown diff type" };
|
||||
}
|
||||
} catch (error) {
|
||||
const raw = error instanceof Error ? error.message : String(error);
|
||||
// Git dumps its entire --help output on some failures; keep only the
|
||||
// first meaningful line so the UI doesn't vomit a wall of text.
|
||||
const firstLine = raw.split("\n").find((l) => l.trim().length > 0) ?? raw;
|
||||
const message = firstLine.length > 200 ? firstLine.slice(0, 200) + "…" : firstLine;
|
||||
return {
|
||||
patch: "",
|
||||
label: cwd ? "Worktree error" : `Error: ${diffType}`,
|
||||
error: message,
|
||||
};
|
||||
}
|
||||
|
||||
if (cwd) {
|
||||
const branch = await getCurrentBranch(runtime, cwd);
|
||||
label =
|
||||
branch && branch !== "HEAD"
|
||||
? `${branch}: ${label}`
|
||||
: `${cwd.split("/").pop()}: ${label}`;
|
||||
}
|
||||
|
||||
return { patch, label };
|
||||
}
|
||||
|
||||
export async function runGitDiffWithContext(
|
||||
runtime: ReviewGitRuntime,
|
||||
diffType: DiffType,
|
||||
gitContext: GitContext,
|
||||
options?: GitDiffOptions,
|
||||
): Promise<DiffResult> {
|
||||
return runGitDiff(runtime, diffType, gitContext.defaultBranch, gitContext.cwd, options);
|
||||
}
|
||||
|
||||
export async function getFileContentsForDiff(
|
||||
runtime: ReviewGitRuntime,
|
||||
diffType: DiffType,
|
||||
defaultBranch: string,
|
||||
filePath: string,
|
||||
oldPath?: string,
|
||||
cwd?: string,
|
||||
): Promise<{ oldContent: string | null; newContent: string | null }> {
|
||||
const oldFilePath = oldPath || filePath;
|
||||
|
||||
let effectiveDiffType = diffType as string;
|
||||
if (diffType.startsWith("worktree:")) {
|
||||
const parsed = parseWorktreeDiffType(diffType);
|
||||
if (!parsed) return { oldContent: null, newContent: null };
|
||||
cwd = parsed.path;
|
||||
effectiveDiffType = parsed.subType;
|
||||
}
|
||||
|
||||
async function gitShow(ref: string, path: string): Promise<string | null> {
|
||||
// `--end-of-options` hardens against user-supplied refs starting with `-`.
|
||||
const result = await runtime.runGit(["show", "--end-of-options", `${ref}:${path}`], { cwd });
|
||||
return result.exitCode === 0 ? result.stdout : null;
|
||||
}
|
||||
|
||||
async function readWorkingTree(path: string): Promise<string | null> {
|
||||
const fullPath = cwd ? resolvePath(cwd, path) : path;
|
||||
return runtime.readTextFile(fullPath);
|
||||
}
|
||||
|
||||
switch (effectiveDiffType) {
|
||||
case "uncommitted":
|
||||
return {
|
||||
oldContent: await gitShow("HEAD", oldFilePath),
|
||||
newContent: await readWorkingTree(filePath),
|
||||
};
|
||||
case "staged":
|
||||
return {
|
||||
oldContent: await gitShow("HEAD", oldFilePath),
|
||||
newContent: await gitShow(":0", filePath),
|
||||
};
|
||||
case "unstaged":
|
||||
return {
|
||||
oldContent: await gitShow(":0", oldFilePath),
|
||||
newContent: await readWorkingTree(filePath),
|
||||
};
|
||||
case "last-commit":
|
||||
return {
|
||||
oldContent: await gitShow("HEAD~1", oldFilePath),
|
||||
newContent: await gitShow("HEAD", filePath),
|
||||
};
|
||||
case "branch":
|
||||
return {
|
||||
oldContent: await gitShow(defaultBranch, oldFilePath),
|
||||
newContent: await gitShow("HEAD", filePath),
|
||||
};
|
||||
case "merge-base": {
|
||||
const mbResult = await runtime.runGit(["merge-base", "--end-of-options", defaultBranch, "HEAD"], { cwd });
|
||||
const mb = mbResult.exitCode === 0 ? mbResult.stdout.trim() : defaultBranch;
|
||||
return {
|
||||
oldContent: await gitShow(mb, oldFilePath),
|
||||
newContent: await gitShow("HEAD", filePath),
|
||||
};
|
||||
}
|
||||
case "all":
|
||||
return {
|
||||
oldContent: null,
|
||||
newContent: await gitShow("HEAD", filePath),
|
||||
};
|
||||
default:
|
||||
return { oldContent: null, newContent: null };
|
||||
}
|
||||
}
|
||||
|
||||
export function validateFilePath(filePath: string): void {
|
||||
if (filePath.includes("..") || filePath.startsWith("/")) {
|
||||
throw new Error("Invalid file path");
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureGitSuccess(
|
||||
runtime: ReviewGitRuntime,
|
||||
args: string[],
|
||||
cwd?: string,
|
||||
): Promise<void> {
|
||||
const result = await runtime.runGit(args, { cwd });
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(result.stderr.trim() || `git ${args.join(" ")} failed`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitAddFile(
|
||||
runtime: ReviewGitRuntime,
|
||||
filePath: string,
|
||||
cwd?: string,
|
||||
): Promise<void> {
|
||||
validateFilePath(filePath);
|
||||
await ensureGitSuccess(runtime, ["add", "--", filePath], cwd);
|
||||
}
|
||||
|
||||
export async function gitResetFile(
|
||||
runtime: ReviewGitRuntime,
|
||||
filePath: string,
|
||||
cwd?: string,
|
||||
): Promise<void> {
|
||||
validateFilePath(filePath);
|
||||
await ensureGitSuccess(runtime, ["reset", "HEAD", "--", filePath], cwd);
|
||||
}
|
||||
|
||||
export function parseP4DiffType(
|
||||
diffType: string,
|
||||
): { changelist: string | "default" } | null {
|
||||
if (diffType === "p4-default") return { changelist: "default" };
|
||||
if (diffType.startsWith("p4-changelist:")) {
|
||||
return { changelist: diffType.slice("p4-changelist:".length) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isP4DiffType(diffType: string): boolean {
|
||||
return parseP4DiffType(diffType) !== null;
|
||||
}
|
||||
Reference in New Issue
Block a user