fix: Parse merge-tree output from stdout instead of catch block

simple-git's .raw() resolves successfully even on exit code 1,
returning stdout content. git merge-tree --write-tree outputs
CONFLICT markers to stdout (not stderr), so the catch block
never fired and conflicts were reported as clean merges.
This commit is contained in:
Lukas May
2026-03-06 11:27:32 +01:00
parent 5b497b84a0
commit cc181ee6ba

View File

@@ -168,20 +168,16 @@ export class SimpleGitBranchManager implements BranchManager {
async checkMergeability(repoPath: string, sourceBranch: string, targetBranch: string): Promise<MergeabilityResult> { async checkMergeability(repoPath: string, sourceBranch: string, targetBranch: string): Promise<MergeabilityResult> {
const git = simpleGit(repoPath); const git = simpleGit(repoPath);
try { // git merge-tree --write-tree outputs everything to stdout.
// git merge-tree --write-tree merges source INTO target virtually. // simple-git's .raw() resolves with stdout even on exit code 1 (conflicts),
// Exit 0 = clean merge, non-zero = conflicts. // so we parse the output text instead of relying on catch.
await git.raw(['merge-tree', '--write-tree', targetBranch, sourceBranch]); const output = await git.raw(['merge-tree', '--write-tree', targetBranch, sourceBranch]);
log.debug({ repoPath, sourceBranch, targetBranch }, 'merge-tree check: clean');
return { mergeable: true };
} catch (err) {
const stderr = err instanceof Error ? err.message : String(err);
// Parse conflict file names from "CONFLICT (content): Merge conflict in <path>" // Parse conflict file names from "CONFLICT (content): Merge conflict in <path>"
const conflictPattern = /CONFLICT \([^)]+\): (?:Merge conflict in|.* -> )(.+)/g; const conflictPattern = /CONFLICT \([^)]+\): (?:Merge conflict in|.* -> )(.+)/g;
const conflicts: string[] = []; const conflicts: string[] = [];
let match: RegExpExecArray | null; let match: RegExpExecArray | null;
while ((match = conflictPattern.exec(stderr)) !== null) { while ((match = conflictPattern.exec(output)) !== null) {
conflicts.push(match[1].trim()); conflicts.push(match[1].trim());
} }
@@ -190,15 +186,13 @@ export class SimpleGitBranchManager implements BranchManager {
return { mergeable: false, conflicts }; return { mergeable: false, conflicts };
} }
// If we couldn't parse conflicts but the command failed, it's still a conflict // Fallback: check for any CONFLICT text we couldn't parse specifically
// (could be add/add, rename conflicts, etc.) if (output.includes('CONFLICT')) {
if (stderr.includes('CONFLICT')) {
log.debug({ repoPath, sourceBranch, targetBranch }, 'merge-tree check: unparsed conflicts'); log.debug({ repoPath, sourceBranch, targetBranch }, 'merge-tree check: unparsed conflicts');
return { mergeable: false, conflicts: ['(unable to parse conflict details)'] }; return { mergeable: false, conflicts: ['(unable to parse conflict details)'] };
} }
// Genuine error (not a conflict) log.debug({ repoPath, sourceBranch, targetBranch }, 'merge-tree check: clean');
throw err; return { mergeable: true };
}
} }
} }