diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 67da571..df08235 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -81,6 +81,29 @@ export async function teardownSession(root: string, branch: string, worktree: st await state.removeSession(root, branch) } +/** Generated files to skip AI resolution — accept theirs and move on (basename match so `packages/foo/bun.lock` is also covered). */ +const SKIP_RESOLVE = new Set([ + "bun.lock", + "bun.lockb", + "Cargo.lock", + "composer.lock", + "Gemfile.lock", + "go.sum", + "mix.lock", + "package-lock.json", + "Pipfile.lock", + "pnpm-lock.yaml", + "Podfile.lock", + "poetry.lock", + "pubspec.lock", + "flake.lock", + "gradle.lockfile", + "npm-shrinkwrap.json", + "Package.resolved", + "uv.lock", + "yarn.lock", +]) + /** Resolve conflict markers in files using Claude, then stage them. */ export async function resolveConflicts( files: string[], @@ -90,15 +113,24 @@ export async function resolveConflicts( for (let i = 0; i < files.length; i++) { const file = files[i] onFile(file, i + 1, files.length) - const content = await Bun.file(join(cwd, file)).text() + + if (SKIP_RESOLVE.has(basename(file))) { + await git.checkoutTheirs(file, cwd) + await git.stageFile(file, cwd) + continue + } + + 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, "resolve this merge conflict. output ONLY the resolved file content with no markdown fences, no explanation, no surrounding text.", ) - if (resolved.exitCode !== 0) { - throw new Error(`Claude failed to resolve ${file}: ${resolved.stderr}`) + if (resolved.exitCode !== 0 || !resolved.stdout.trim()) { + throw new Error(`Claude failed to resolve ${file}: ${resolved.stderr.trim() || "(no output)"}`) } await Bun.write(join(cwd, file), resolved.stdout.trimEnd() + "\n") @@ -145,6 +177,7 @@ export async function mergeAndClose(branch: string, opts?: { force?: boolean }): } catch (err) { const message = err instanceof Error ? err.message : String(err) spin.fail(message) + if (session) await vm.clearActivity(session.worktree, branch) // process.exit below skips finally await git.abortMerge(root) process.exit(1) } finally { diff --git a/src/git.ts b/src/git.ts index 9e05e42..c899136 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,7 +78,7 @@ export async function createWorktree(branch: string, worktreePath: string, cwd: } if (result.exitCode !== 0) { if (switchedFromBranch) await checkout(branch, cwd).catch(() => {}) - throw new Error(`Failed to create worktree for "${branch}": ${result.stderr.toString().trim()}`) + throw gitError(`Failed to create worktree for "${branch}"`, result.stderr) } return { branchCreated: exists !== "local" } } @@ -98,7 +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) { - throw new Error(`Failed to checkout branch "${branch}": ${result.stderr.toString().trim()}`) + throw gitError(`Failed to checkout branch "${branch}"`, result.stderr) } } @@ -113,7 +119,7 @@ export async function merge(branch: string, cwd: string): Promise { if (files.length > 0) return files // Not a conflict — some other merge failure - throw new Error(`Failed to merge branch "${branch}": ${result.stderr.toString().trim()}`) + throw gitError(`Failed to merge branch "${branch}"`, result.stderr) } /** Return the staged diff as text. */ @@ -125,20 +131,31 @@ 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) { - throw new Error(`Failed to commit: ${result.stderr.toString().trim()}`) + throw gitError("Failed to commit", result.stderr) + } +} + +/** Accept "theirs" version of a conflicted file. */ +export async function checkoutTheirs(file: string, cwd: string): Promise { + const result = await $`git checkout --theirs -- ${file}`.cwd(cwd).nothrow().quiet() + if (result.exitCode !== 0) { + throw gitError(`Failed to checkout theirs for ${file}`, result.stderr) } } /** Stage a file. */ export async function stageFile(file: string, cwd: string): Promise { - await $`git add ${file}`.cwd(cwd).nothrow().quiet() + const result = await $`git add ${file}`.cwd(cwd).nothrow().quiet() + if (result.exitCode !== 0) { + throw gitError(`Failed to stage ${file}`, result.stderr) + } } /** Finalize a merge commit after resolving conflicts. */ export async function commitMerge(cwd: string): Promise { const result = await $`git commit --no-edit`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - throw new Error(`Failed to commit merge: ${result.stderr.toString().trim()}`) + throw gitError("Failed to commit merge", result.stderr) } } @@ -194,7 +211,7 @@ export async function rebaseContinue(cwd: string): Promise { const files = unmerged.trim().split("\n").filter(Boolean) if (files.length > 0) return files - throw new Error(`Rebase --continue failed: ${result.stderr.toString().trim()}`) + throw gitError("Rebase --continue failed", result.stderr) } /** Abort an in-progress rebase. */