From 101651b107aa209166a6b6040e1d92936856d0b9 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 10 Apr 2026 10:02:23 -0700 Subject: [PATCH] Extract shared gitError helper to deduplicate stderr formatting Also clear activity on failed merge and improve error context for conflicted file reads. Co-Authored-By: Claude Opus 4.6 --- src/commands/helpers.ts | 8 +++++--- src/git.ts | 30 ++++++++++++++---------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index d29bdf0..19beb06 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -120,7 +120,9 @@ export async function resolveConflicts( continue } - const content = await Bun.file(join(cwd, file)).text() + const content = await Bun.file(join(cwd, file)).text().catch(() => { + throw new Error(`Failed to read conflicted file: ${file}`) + }) const resolved = await vm.claudePipe( content, @@ -128,8 +130,7 @@ export async function resolveConflicts( ) if (resolved.exitCode !== 0 || !resolved.stdout.trim()) { - const stderr = resolved.stderr.trim() - throw new Error(`Claude failed to resolve ${file}: ${stderr || "(no output)"}`) + throw new Error(`Claude failed to resolve ${file}: ${resolved.stderr.trim() || "(no output)"}`) } await Bun.write(join(cwd, file), resolved.stdout.trimEnd() + "\n") @@ -187,6 +188,7 @@ export async function mergeAndClose(branch: string, opts?: { squash?: boolean; f } catch (err) { const message = err instanceof Error ? err.message : String(err) spin.fail(message) + if (session) await vm.clearActivity(session.worktree, branch) await git.abortMerge(root) process.exit(1) } finally { diff --git a/src/git.ts b/src/git.ts index 1871287..162de00 100644 --- a/src/git.ts +++ b/src/git.ts @@ -2,6 +2,12 @@ import { existsSync } from "fs" import { rm } from "fs/promises" import { $ } from "bun" +/** Format a git error with a fallback for empty stderr. */ +function gitError(action: string, stderr: Buffer | string): Error { + const msg = stderr.toString().trim() + return new Error(`${action}: ${msg || "(no output)"}`) +} + /** Get the repo root from a working directory. */ export async function repoRoot(cwd?: string): Promise { const result = await $`git rev-parse --show-toplevel`.cwd(cwd ?? ".").nothrow().quiet() @@ -72,8 +78,7 @@ export async function createWorktree(branch: string, worktreePath: string, cwd: } if (result.exitCode !== 0) { if (switchedFromBranch) await checkout(branch, cwd).catch(() => {}) - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to create worktree for "${branch}": ${stderr || "(no output)"}`) + throw gitError(`Failed to create worktree for "${branch}"`, result.stderr) } return { branchCreated: exists !== "local" } } @@ -99,8 +104,7 @@ export async function deleteLocalBranch(branch: string, cwd: string): Promise { const result = await $`git checkout ${branch}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to checkout branch "${branch}": ${stderr || "(no output)"}`) + throw gitError(`Failed to checkout branch "${branch}"`, result.stderr) } } @@ -119,8 +123,7 @@ export async function merge(branch: string, cwd: string, opts?: { squash?: boole // Not a conflict — some other merge failure const label = opts?.squash ? "squash-merge" : "merge" - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to ${label} branch "${branch}": ${stderr || "(no output)"}`) + throw gitError(`Failed to ${label} branch "${branch}"`, result.stderr) } /** Return the staged diff as text. */ @@ -132,8 +135,7 @@ export async function diffStaged(cwd: string): Promise { export async function commit(message: string, cwd: string): Promise { const result = await $`git commit -m ${message}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to commit: ${stderr || "(no output)"}`) + throw gitError("Failed to commit", result.stderr) } } @@ -141,8 +143,7 @@ export async function commit(message: string, cwd: string): Promise { export async function checkoutTheirs(file: string, cwd: string): Promise { const result = await $`git checkout --theirs -- ${file}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to checkout theirs for ${file}: ${stderr || "(no output)"}`) + throw gitError(`Failed to checkout theirs for ${file}`, result.stderr) } } @@ -150,8 +151,7 @@ export async function checkoutTheirs(file: string, cwd: string): Promise { export async function stageFile(file: string, cwd: string): Promise { const result = await $`git add ${file}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to stage ${file}: ${stderr || "(no output)"}`) + throw gitError(`Failed to stage ${file}`, result.stderr) } } @@ -159,8 +159,7 @@ export async function stageFile(file: string, cwd: string): Promise { export async function commitMerge(cwd: string): Promise { const result = await $`git commit --no-edit`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to commit merge: ${stderr || "(no output)"}`) + throw gitError("Failed to commit merge", result.stderr) } } @@ -198,8 +197,7 @@ export async function rebaseContinue(cwd: string): Promise { const files = unmerged.trim().split("\n").filter(Boolean) if (files.length > 0) return files - const stderr = result.stderr.toString().trim() - throw new Error(`Rebase --continue failed: ${stderr || "(no output)"}`) + throw gitError("Rebase --continue failed", result.stderr) } /** Abort an in-progress rebase. */