refactor merge: extract lineCount helper, hoist preamble, and gracefully handle git errors

This commit is contained in:
Chris Wanstrath 2026-02-21 08:36:44 -08:00
parent c6b6f52b1f
commit 376f918a66
2 changed files with 29 additions and 26 deletions

View File

@ -8,10 +8,15 @@ import { action as closeAction } from "./close.ts"
const MAX_DIFF_LINES = 300 const MAX_DIFF_LINES = 300
function truncate(text: string, maxLines = 200): string { function lineCount(text: string): number {
const lines = text.split("\n") if (!text) return 0
if (lines.length <= maxLines) return text return text.split("\n").length
return lines.slice(0, maxLines).join("\n") + `\n... (truncated, ${lines.length - maxLines} more lines)` }
function truncate(text: string, maxLines = MAX_DIFF_LINES): string {
const count = lineCount(text)
if (count <= maxLines) return text
return text.split("\n").slice(0, maxLines).join("\n") + `\n... (truncated, ${count - maxLines} more lines)`
} }
export async function action(branch: string) { export async function action(branch: string) {
@ -59,9 +64,7 @@ export async function action(branch: string) {
const resolvable: typeof fileDiffs = [] const resolvable: typeof fileDiffs = []
const skipped: string[] = [] const skipped: string[] = []
for (const entry of fileDiffs) { for (const entry of fileDiffs) {
const oursLines = entry.oursDiff ? entry.oursDiff.split("\n").length : 0 if (lineCount(entry.oursDiff) > MAX_DIFF_LINES || lineCount(entry.theirsDiff) > MAX_DIFF_LINES) {
const theirsLines = entry.theirsDiff ? entry.theirsDiff.split("\n").length : 0
if (oursLines > MAX_DIFF_LINES || theirsLines > MAX_DIFF_LINES) {
skipped.push(entry.file) skipped.push(entry.file)
} else { } else {
resolvable.push(entry) resolvable.push(entry)
@ -69,27 +72,31 @@ export async function action(branch: string) {
} }
if (resolvable.length === 0) { if (resolvable.length === 0) {
await git.abortMerge(root)
spin.fail("All conflicts are too complex for auto-resolution") spin.fail("All conflicts are too complex for auto-resolution")
console.log("\nThe following files need manual resolution:") console.log("\nThe following files need manual resolution:")
for (const file of skipped) console.log(` - ${file}`) for (const file of skipped) console.log(` - ${file}`)
process.exit(1) console.log("\nResolve them manually, then run: git add <files> && git commit --no-edit")
return
} }
const preamble = [
`## Merge Context`,
`Merging branch \`${branch}\` into \`${current}\``,
``,
`### Commits on \`${branch}\`:`,
(branchLog && truncate(branchLog, 50)) || "(no commits)",
``,
`### Overall changes on \`${branch}\`:`,
(branchDiffStat && truncate(branchDiffStat, 100)) || "(no changes)",
].join("\n")
for (const { file, oursDiff, theirsDiff } of resolvable) { for (const { file, oursDiff, theirsDiff } of resolvable) {
spin.text = `Resolving ${file}` spin.text = `Resolving ${file}`
const content = await Bun.file(join(root, file)).text() const content = await Bun.file(join(root, file)).text()
const context = [ const context = [
`## Merge Context`, preamble,
`Merging branch \`${branch}\` into \`${current}\``,
``,
`### Commits on \`${branch}\`:`,
(branchLog && truncate(branchLog, 50)) || "(no commits)",
``,
`### Overall changes on \`${branch}\`:`,
(branchDiffStat && truncate(branchDiffStat, 100)) || "(no changes)",
``, ``,
`---`, `---`,
`## Conflicted File: ${file}`, `## Conflicted File: ${file}`,
@ -121,7 +128,7 @@ export async function action(branch: string) {
spin.succeed(`Resolved ${resolvable.length} of ${conflicts.length} conflict(s)`) spin.succeed(`Resolved ${resolvable.length} of ${conflicts.length} conflict(s)`)
console.log("\nThe following files are too complex for auto-resolution:") console.log("\nThe following files are too complex for auto-resolution:")
for (const file of skipped) console.log(` - ${file}`) for (const file of skipped) console.log(` - ${file}`)
console.log("\nResolve them manually, then run: git commit --no-edit") console.log("\nResolve them manually, then run: git add <files> && git commit --no-edit")
return return
} }

View File

@ -135,21 +135,17 @@ export async function mergeBase(ref1: string, ref2: string, cwd: string): Promis
return result.text().trim() return result.text().trim()
} }
/** Get a one-line-per-commit log for a revision range. */ /** Get a one-line-per-commit log for a revision range. Returns empty string on failure. */
export async function commitLog(range: string, cwd: string): Promise<string> { export async function commitLog(range: string, cwd: string): Promise<string> {
const result = await $`git log --oneline ${range}`.cwd(cwd).nothrow().quiet() const result = await $`git log --oneline ${range}`.cwd(cwd).nothrow().quiet()
if (result.exitCode !== 0) { if (result.exitCode !== 0) return ""
throw new Error(`Failed to get log for range "${range}"`)
}
return result.text().trim() return result.text().trim()
} }
/** Get a diff stat summary for a revision range. */ /** Get a diff stat summary for a revision range. Returns empty string on failure. */
export async function diffStat(range: string, cwd: string): Promise<string> { export async function diffStat(range: string, cwd: string): Promise<string> {
const result = await $`git diff --stat ${range}`.cwd(cwd).nothrow().quiet() const result = await $`git diff --stat ${range}`.cwd(cwd).nothrow().quiet()
if (result.exitCode !== 0) { if (result.exitCode !== 0) return ""
throw new Error(`Failed to get diff stat for range "${range}"`)
}
return result.text().trim() return result.text().trim()
} }