Files
backnotprop__plannotator/packages/shared/review-core.ts
T
Michael Ramos f2f713a655 fix: truncate git error messages in code review UI
Git dumps its entire --help output on some failures (e.g. running
code review from a non-git directory). The error was passed raw to
the UI, rendering hundreds of lines of usage text. Extract only the
first meaningful line and cap at 200 chars. Also add line-clamp on
the UI error display as a safety net.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 08:43:03 -07:00

485 lines
12 KiB
TypeScript

/**
* 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"
| `worktree:${string}`;
export interface DiffOption {
id: string;
label: string;
}
export interface WorktreeInfo {
path: string;
branch: string | null;
head: string;
}
export interface GitContext {
currentBranch: string;
defaultBranch: string;
diffOptions: DiffOption[];
worktrees: WorktreeInfo[];
}
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 },
) => Promise<GitCommandResult>;
readTextFile: (path: string) => Promise<string | null>;
}
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> {
const remoteHead = await runtime.runGit(
["symbolic-ref", "refs/remotes/origin/HEAD"],
{ cwd },
);
if (remoteHead.exitCode === 0) {
const ref = remoteHead.stdout.trim();
if (ref) return ref.replace("refs/remotes/origin/", "");
}
const mainBranch = await runtime.runGit(
["show-ref", "--verify", "refs/heads/main"],
{ cwd },
);
if (mainBranch.exitCode === 0) return "main";
return "master";
}
export async function getWorktrees(
runtime: ReviewGitRuntime,
): Promise<WorktreeInfo[]> {
const result = await runtime.runGit(["worktree", "list", "--porcelain"]);
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,
): Promise<GitContext> {
const [currentBranch, defaultBranch] = await Promise.all([
getCurrentBranch(runtime),
getDefaultBranch(runtime),
]);
const diffOptions: DiffOption[] = [
{ id: "uncommitted", label: "Uncommitted changes" },
{ id: "staged", label: "Staged changes" },
{ id: "unstaged", label: "Unstaged changes" },
{ id: "last-commit", label: "Last commit" },
];
if (currentBranch !== defaultBranch) {
diffOptions.push({ id: "branch", label: `vs ${defaultBranch}` });
}
const [worktrees, currentTreePathResult] = await Promise.all([
getWorktrees(runtime),
runtime.runGit(["rev-parse", "--show-toplevel"]),
]);
const currentTreePath =
currentTreePathResult.exitCode === 0
? currentTreePathResult.stdout.trim()
: null;
return {
currentBranch,
defaultBranch,
diffOptions,
worktrees: worktrees.filter((wt) => wt.path !== currentTreePath),
};
}
async function getUntrackedFileDiffs(
runtime: ReviewGitRuntime,
srcPrefix = "a/",
dstPrefix = "b/",
cwd?: string,
): Promise<string> {
const lsResult = await runtime.runGit(
["ls-files", "--others", "--exclude-standard"],
{ cwd },
);
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-index",
`--src-prefix=${srcPrefix}`,
`--dst-prefix=${dstPrefix}`,
"/dev/null",
file,
],
{ cwd },
);
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",
]);
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",
): Promise<DiffResult> {
let patch = "";
let label = "";
let cwd: string | undefined;
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;
}
try {
switch (effectiveDiffType) {
case "uncommitted": {
const trackedDiffArgs = [
"diff",
"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,
);
patch = trackedPatch + untrackedDiff;
label = "Uncommitted changes";
break;
}
case "staged": {
const stagedDiffArgs = [
"diff",
"--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", "--src-prefix=a/", "--dst-prefix=b/"];
const trackedDiff = assertGitSuccess(
await runtime.runGit(trackedDiffArgs, { cwd }),
trackedDiffArgs,
);
const untrackedDiff = await getUntrackedFileDiffs(
runtime,
"a/",
"b/",
cwd,
);
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", "HEAD~1..HEAD", "--src-prefix=a/", "--dst-prefix=b/"]
: ["diff", "--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": {
const branchDiffArgs = [
"diff",
`${defaultBranch}..HEAD`,
"--src-prefix=a/",
"--dst-prefix=b/",
];
const branchDiff = assertGitSuccess(
await runtime.runGit(branchDiffArgs, { cwd }),
branchDiffArgs,
);
patch = branchDiff.stdout;
label = `Changes vs ${defaultBranch}`;
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 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> {
const result = await runtime.runGit(["show", `${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),
};
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);
}