improve conflict complexity analysis and merge error messaging
This commit is contained in:
parent
376f918a66
commit
db9f40dac9
|
|
@ -7,16 +7,40 @@ import { die } from "../fmt.ts"
|
||||||
import { action as closeAction } from "./close.ts"
|
import { action as closeAction } from "./close.ts"
|
||||||
|
|
||||||
const MAX_DIFF_LINES = 300
|
const MAX_DIFF_LINES = 300
|
||||||
|
const MAX_LOG_LINES = 50
|
||||||
|
const MAX_STAT_LINES = 100
|
||||||
|
const MAX_CONFLICT_REGIONS = 15
|
||||||
|
const MAX_CONFLICTED_LINES = 200
|
||||||
|
|
||||||
function lineCount(text: string): number {
|
interface ConflictAnalysis {
|
||||||
if (!text) return 0
|
regions: number
|
||||||
return text.split("\n").length
|
conflictedLines: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyzeConflicts(content: string): ConflictAnalysis {
|
||||||
|
let regions = 0
|
||||||
|
let conflictedLines = 0
|
||||||
|
let inConflict = false
|
||||||
|
|
||||||
|
for (const line of content.split("\n")) {
|
||||||
|
if (line.startsWith("<<<<<<<")) {
|
||||||
|
regions++
|
||||||
|
inConflict = true
|
||||||
|
} else if (line.startsWith(">>>>>>>")) {
|
||||||
|
inConflict = false
|
||||||
|
} else if (inConflict) {
|
||||||
|
conflictedLines++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { regions, conflictedLines }
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncate(text: string, maxLines = MAX_DIFF_LINES): string {
|
function truncate(text: string, maxLines = MAX_DIFF_LINES): string {
|
||||||
const count = lineCount(text)
|
if (!text) return text
|
||||||
if (count <= maxLines) return text
|
const lines = text.split("\n")
|
||||||
return text.split("\n").slice(0, maxLines).join("\n") + `\n... (truncated, ${count - maxLines} more lines)`
|
if (lines.length <= maxLines) return text
|
||||||
|
return lines.slice(0, maxLines).join("\n") + `\n... (truncated, ${lines.length - maxLines} more lines)`
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function action(branch: string) {
|
export async function action(branch: string) {
|
||||||
|
|
@ -49,22 +73,23 @@ export async function action(branch: string) {
|
||||||
const branchLog = await git.commitLog(`${base}..${branch}`, root)
|
const branchLog = await git.commitLog(`${base}..${branch}`, root)
|
||||||
const branchDiffStat = await git.diffStat(`${base}..${branch}`, root)
|
const branchDiffStat = await git.diffStat(`${base}..${branch}`, root)
|
||||||
|
|
||||||
// Fetch diffs for all conflicted files and partition by complexity
|
// Read conflicted files, fetch diffs, and partition by complexity
|
||||||
spin.text = "Checking conflict complexity"
|
spin.text = "Checking conflict complexity"
|
||||||
const fileDiffs = await Promise.all(
|
const fileEntries = await Promise.all(
|
||||||
conflicts.map(async (file) => {
|
conflicts.map(async (file) => {
|
||||||
const [oursDiff, theirsDiff] = await Promise.all([
|
const [content, oursDiff, theirsDiff] = await Promise.all([
|
||||||
|
Bun.file(join(root, file)).text(),
|
||||||
git.fileDiff(base, "HEAD", file, root),
|
git.fileDiff(base, "HEAD", file, root),
|
||||||
git.fileDiff(base, branch, file, root),
|
git.fileDiff(base, branch, file, root),
|
||||||
])
|
])
|
||||||
return { file, oursDiff, theirsDiff }
|
return { file, content, oursDiff, theirsDiff, ...analyzeConflicts(content) }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const resolvable: typeof fileDiffs = []
|
const resolvable: typeof fileEntries = []
|
||||||
const skipped: string[] = []
|
const skipped: string[] = []
|
||||||
for (const entry of fileDiffs) {
|
for (const entry of fileEntries) {
|
||||||
if (lineCount(entry.oursDiff) > MAX_DIFF_LINES || lineCount(entry.theirsDiff) > MAX_DIFF_LINES) {
|
if (entry.regions > MAX_CONFLICT_REGIONS || entry.conflictedLines > MAX_CONFLICTED_LINES) {
|
||||||
skipped.push(entry.file)
|
skipped.push(entry.file)
|
||||||
} else {
|
} else {
|
||||||
resolvable.push(entry)
|
resolvable.push(entry)
|
||||||
|
|
@ -75,7 +100,9 @@ export async function action(branch: string) {
|
||||||
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}`)
|
||||||
console.log("\nResolve them manually, then run: git add <files> && git commit --no-edit")
|
console.log("\nYour repo is in a MERGING state. To finish:")
|
||||||
|
console.log(" Resolve the files, then run: git add <files> && git commit --no-edit")
|
||||||
|
console.log(" Or abort with: git merge --abort")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,17 +111,15 @@ export async function action(branch: string) {
|
||||||
`Merging branch \`${branch}\` into \`${current}\``,
|
`Merging branch \`${branch}\` into \`${current}\``,
|
||||||
``,
|
``,
|
||||||
`### Commits on \`${branch}\`:`,
|
`### Commits on \`${branch}\`:`,
|
||||||
(branchLog && truncate(branchLog, 50)) || "(no commits)",
|
(branchLog && truncate(branchLog, MAX_LOG_LINES)) || "(no commits)",
|
||||||
``,
|
``,
|
||||||
`### Overall changes on \`${branch}\`:`,
|
`### Overall changes on \`${branch}\`:`,
|
||||||
(branchDiffStat && truncate(branchDiffStat, 100)) || "(no changes)",
|
(branchDiffStat && truncate(branchDiffStat, MAX_STAT_LINES)) || "(no changes)",
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|
||||||
for (const { file, oursDiff, theirsDiff } of resolvable) {
|
for (const { file, content, oursDiff, theirsDiff } of resolvable) {
|
||||||
spin.text = `Resolving ${file}`
|
spin.text = `Resolving ${file}`
|
||||||
|
|
||||||
const content = await Bun.file(join(root, file)).text()
|
|
||||||
|
|
||||||
const context = [
|
const context = [
|
||||||
preamble,
|
preamble,
|
||||||
``,
|
``,
|
||||||
|
|
@ -128,7 +153,9 @@ 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 add <files> && git commit --no-edit")
|
console.log("\nYour repo is in a MERGING state. To finish:")
|
||||||
|
console.log(" Resolve the remaining files, then run: git add <files> && git commit --no-edit")
|
||||||
|
console.log(" Or abort with: git merge --abort")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,12 +149,10 @@ export async function diffStat(range: string, cwd: string): Promise<string> {
|
||||||
return result.text().trim()
|
return result.text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the diff for a specific file between two refs. */
|
/** Get the diff for a specific file between two refs. Returns empty string on failure. */
|
||||||
export async function fileDiff(ref1: string, ref2: string, file: string, cwd: string): Promise<string> {
|
export async function fileDiff(ref1: string, ref2: string, file: string, cwd: string): Promise<string> {
|
||||||
const result = await $`git diff ${ref1} ${ref2} -- ${file}`.cwd(cwd).nothrow().quiet()
|
const result = await $`git diff ${ref1} ${ref2} -- ${file}`.cwd(cwd).nothrow().quiet()
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) return ""
|
||||||
throw new Error(`Failed to get diff for "${file}" between "${ref1}" and "${ref2}"`)
|
|
||||||
}
|
|
||||||
return result.text().trim()
|
return result.text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user