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