From ae6604caad7d9d547db09968c8d511befc475414 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 20 Feb 2026 19:21:16 -0800 Subject: [PATCH] add merge context (branch log, diff stats, per-file diffs) to improve conflict resolution quality --- src/commands/merge.ts | 37 +++++++++++++++++++++++++++++++++++-- src/git.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/commands/merge.ts b/src/commands/merge.ts index d7a24c1..834914e 100644 --- a/src/commands/merge.ts +++ b/src/commands/merge.ts @@ -29,13 +29,46 @@ export async function action(branch: string) { try { await vm.ensure((msg) => { spin.text = msg }) + // Gather merge context + spin.text = "Gathering merge context" + const current = await git.currentBranch(root) + const base = await git.mergeBase("HEAD", branch, root) + const branchLog = await git.log(`${base}..${branch}`, root) + const branchDiffStat = await git.diffStat(`${base}..${branch}`, root) + for (const file of conflicts) { spin.text = `Resolving ${file}` + + const oursDiff = await git.fileDiff(base, "HEAD", file, root) + const theirsDiff = await git.fileDiff(base, branch, file, root) const content = await Bun.file(join(root, file)).text() - const resolved = await vm.claudePipe( + const context = [ + `## Merge Context`, + `Merging branch \`${branch}\` into \`${current}\``, + ``, + `### Commits on \`${branch}\`:`, + branchLog || "(no commits)", + ``, + `### Overall changes on \`${branch}\`:`, + branchDiffStat || "(no changes)", + ``, + `---`, + `## Conflicted File: ${file}`, + ``, + `### Changes made on current branch (\`${current}\`):`, + oursDiff || "(no changes to this file)", + ``, + `### Changes made on incoming branch (\`${branch}\`):`, + theirsDiff || "(no changes to this file)", + ``, + `### File with conflict markers:`, content, - "resolve this merge conflict. output ONLY the resolved file content with no markdown fences, no explanation, no surrounding text.", + ].join("\n") + + const resolved = await vm.claudePipe( + context, + "Resolve the merge conflicts in the file shown at the end. Use the diffs and commit history to understand the intent of each side. Preserve all non-conflicting changes from both sides. Output ONLY the resolved file content — no markdown fences, no explanation, no surrounding text.", ) if (resolved.exitCode !== 0) { diff --git a/src/git.ts b/src/git.ts index f7db35a..babcb4f 100644 --- a/src/git.ts +++ b/src/git.ts @@ -126,6 +126,33 @@ export async function isDirty(worktreePath: string): Promise { return result.text().trim().length > 0 } +/** Find the merge base (common ancestor) between two refs. */ +export async function mergeBase(ref1: string, ref2: string, cwd: string): Promise { + const result = await $`git merge-base ${ref1} ${ref2}`.cwd(cwd).nothrow().quiet() + if (result.exitCode !== 0) { + throw new Error(`Could not find merge base between "${ref1}" and "${ref2}"`) + } + return result.text().trim() +} + +/** Get a one-line-per-commit log for a revision range. */ +export async function log(range: string, cwd: string): Promise { + const result = await $`git log --oneline ${range}`.cwd(cwd).nothrow().quiet() + return result.text().trim() +} + +/** Get a diff stat summary for a revision range. */ +export async function diffStat(range: string, cwd: string): Promise { + const result = await $`git diff --stat ${range}`.cwd(cwd).nothrow().quiet() + return result.text().trim() +} + +/** Get the diff for a specific file between two refs. */ +export async function fileDiff(ref1: string, ref2: string, file: string, cwd: string): Promise { + const result = await $`git diff ${ref1} ${ref2} -- ${file}`.cwd(cwd).nothrow().quiet() + return result.text().trim() +} + /** Check if a branch has commits beyond main. */ export async function hasNewCommits(worktreePath: string): Promise { const main = await mainBranch(worktreePath)