refactor conflict resolution into shared helper and add rebase round limit
This commit is contained in:
parent
0c5e44bb5d
commit
603c92b595
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { join } from "path"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import * as git from "../git.ts"
|
import * as git from "../git.ts"
|
||||||
import * as vm from "../vm.ts"
|
import * as vm from "../vm.ts"
|
||||||
|
|
@ -16,6 +17,30 @@ export async function requireSession(branch: string): Promise<{ root: string; se
|
||||||
return { root, session }
|
return { root, session }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolve conflict markers in files using Claude, then stage them. */
|
||||||
|
export async function resolveConflicts(
|
||||||
|
files: string[],
|
||||||
|
cwd: string,
|
||||||
|
onFile: (file: string) => void,
|
||||||
|
): Promise<void> {
|
||||||
|
for (const file of files) {
|
||||||
|
onFile(file)
|
||||||
|
const content = await Bun.file(join(cwd, file)).text()
|
||||||
|
|
||||||
|
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}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Bun.write(join(cwd, file), resolved.stdout.trimEnd() + "\n")
|
||||||
|
await git.stageFile(file, cwd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Stage all changes, generate a commit message, and commit. Returns true on success. */
|
/** Stage all changes, generate a commit message, and commit. Returns true on success. */
|
||||||
export async function saveChanges(worktree: string, branch: string, message?: string): Promise<boolean> {
|
export async function saveChanges(worktree: string, branch: string, message?: string): Promise<boolean> {
|
||||||
const spin = spinner("Staging changes", branch)
|
const spin = spinner("Staging changes", branch)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { join } from "path"
|
|
||||||
import * as git from "../git.ts"
|
import * as git from "../git.ts"
|
||||||
import * as vm from "../vm.ts"
|
import * as vm from "../vm.ts"
|
||||||
import * as state from "../state.ts"
|
import * as state from "../state.ts"
|
||||||
import { spinner } from "../spinner.ts"
|
import { spinner } from "../spinner.ts"
|
||||||
import { die } from "../fmt.ts"
|
import { die } from "../fmt.ts"
|
||||||
import { action as closeAction } from "./close.ts"
|
import { action as closeAction } from "./close.ts"
|
||||||
|
import { resolveConflicts } from "./helpers.ts"
|
||||||
|
|
||||||
export async function action(branch: string) {
|
export async function action(branch: string) {
|
||||||
const root = await git.repoRoot()
|
const root = await git.repoRoot()
|
||||||
|
|
@ -28,23 +28,7 @@ export async function action(branch: string) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await vm.ensure((msg) => { spin.text = msg })
|
await vm.ensure((msg) => { spin.text = msg })
|
||||||
|
await resolveConflicts(conflicts, root, (file) => { spin.text = `Resolving ${file}` })
|
||||||
for (const file of conflicts) {
|
|
||||||
spin.text = `Resolving ${file}`
|
|
||||||
const content = await Bun.file(join(root, file)).text()
|
|
||||||
|
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
await Bun.write(join(root, file), resolved.stdout + "\n")
|
|
||||||
await git.stageFile(file, root)
|
|
||||||
}
|
|
||||||
|
|
||||||
await git.commitMerge(root)
|
await git.commitMerge(root)
|
||||||
spin.succeed(`Resolved ${conflicts.length} conflict(s) and merged ${branch}`)
|
spin.succeed(`Resolved ${conflicts.length} conflict(s) and merged ${branch}`)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { join } from "path"
|
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import * as git from "../git.ts"
|
import * as git from "../git.ts"
|
||||||
import * as vm from "../vm.ts"
|
import * as vm from "../vm.ts"
|
||||||
import { spinner } from "../spinner.ts"
|
import { spinner } from "../spinner.ts"
|
||||||
import { die } from "../fmt.ts"
|
import { die } from "../fmt.ts"
|
||||||
import { requireSession } from "./helpers.ts"
|
import { requireSession, resolveConflicts } from "./helpers.ts"
|
||||||
|
|
||||||
|
const MAX_REBASE_ROUNDS = 10
|
||||||
|
|
||||||
export async function action(branch: string) {
|
export async function action(branch: string) {
|
||||||
const { root, session } = await requireSession(branch)
|
const { root, session } = await requireSession(branch)
|
||||||
|
|
@ -17,7 +18,6 @@ export async function action(branch: string) {
|
||||||
const main = await git.mainBranch(root)
|
const main = await git.mainBranch(root)
|
||||||
const spin = spinner("Fetching origin", branch)
|
const spin = spinner("Fetching origin", branch)
|
||||||
|
|
||||||
try {
|
|
||||||
await $`git -C ${root} fetch origin ${main}`.nothrow().quiet()
|
await $`git -C ${root} fetch origin ${main}`.nothrow().quiet()
|
||||||
spin.text = `Rebasing onto origin/${main}`
|
spin.text = `Rebasing onto origin/${main}`
|
||||||
|
|
||||||
|
|
@ -27,36 +27,30 @@ export async function action(branch: string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve conflicts with Claude, looping for each rebased commit
|
spin.stop()
|
||||||
console.log(`◆ Rebase conflicts in ${conflicts.length} file(s). Resolving with Claude...`)
|
console.log(`◆ Rebase conflicts in ${conflicts.length} file(s). Resolving with Claude...`)
|
||||||
await vm.ensure((msg) => { spin.text = msg })
|
const spin2 = spinner("Starting container", branch)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await vm.ensure((msg) => { spin2.text = msg })
|
||||||
|
|
||||||
let round = 1
|
let round = 1
|
||||||
while (conflicts.length > 0) {
|
while (conflicts.length > 0) {
|
||||||
for (const file of conflicts) {
|
if (round > MAX_REBASE_ROUNDS) {
|
||||||
spin.text = `Resolving ${file} (round ${round})`
|
throw new Error(`Exceeded ${MAX_REBASE_ROUNDS} conflict resolution rounds — aborting rebase`)
|
||||||
const content = await Bun.file(join(worktree, file)).text()
|
|
||||||
|
|
||||||
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}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Bun.write(join(worktree, file), resolved.stdout + "\n")
|
await resolveConflicts(conflicts, worktree, (file) => {
|
||||||
await git.stageFile(file, worktree)
|
spin2.text = `Resolving ${file} (round ${round})`
|
||||||
}
|
})
|
||||||
|
|
||||||
conflicts = await git.rebaseContinue(worktree)
|
conflicts = await git.rebaseContinue(worktree)
|
||||||
round++
|
round++
|
||||||
}
|
}
|
||||||
|
|
||||||
spin.succeed(`Rebased ${branch} onto ${main} (resolved ${round - 1} conflict round(s))`)
|
spin2.succeed(`Rebased ${branch} onto ${main} (resolved ${round - 1} conflict round(s))`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
spin.fail(String((err as Error).message ?? err))
|
spin2.fail(String((err as Error).message ?? err))
|
||||||
await git.rebaseAbort(worktree)
|
await git.rebaseAbort(worktree)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user