fix(worker): validate conflict markers before staging in ContinueMergeAsync

git add -A resolves the index U-stage regardless of file content, so a
conflicted file whose markers were never removed still got staged and
committed as-is. Check previously-conflicted files for leftover
<<<<<<< markers before staging; refuse with a conflict result if any
remain, leaving the repo mid-merge so Abort still cleans up.

Adds a regression test that leaves one conflict unresolved and asserts
ContinueMergeAsync refuses (no commit) and AbortMergeAsync still
restores a clean tree.
This commit is contained in:
mika kuns
2026-07-23 17:16:46 +02:00
parent 85c7e650c9
commit 377409e633
3 changed files with 66 additions and 1 deletions
@@ -204,6 +204,26 @@ public sealed class TaskMergeService
if (!await _git.IsMidMergeAsync(list.WorkingDir, ct))
return Blocked("repo is not mid-merge");
// Validate BEFORE staging: `git add` marks a conflicted path resolved regardless of
// its content, so an unresolved file with markers still in it would otherwise get
// staged (and committed) as-is. Check text content for markers first; binary files
// can't carry markers, so they're left to the post-stage index check below.
var unresolved = await _git.ListConflictedFilesAsync(list.WorkingDir, ct);
var stillConflicted = new List<string>();
foreach (var path in unresolved)
{
var full = Path.Combine(list.WorkingDir, path.Replace('/', Path.DirectorySeparatorChar));
string text;
try { text = await File.ReadAllTextAsync(full, ct); }
catch { continue; }
if (!LooksBinary(text) && ConflictMarkerParser.HasConflicts(text))
stillConflicted.Add(path);
}
if (stillConflicted.Count > 0)
return new MergeResult(StatusConflict, stillConflicted, "conflicts not fully resolved");
await _git.AddAllAsync(list.WorkingDir, ct);
var remaining = await _git.ListConflictedFilesAsync(list.WorkingDir, ct);