fix(worker): make modify/delete and rename/delete merge conflicts visible instead of silently staging them
ConflictMarkerParser only recognizes text-marker conflicts, so a modify/delete or rename/delete conflict (git never writes markers for those) was invisible to the resolver: GetConflictDocumentsAsync read it as an already-resolved stable file, and ContinueMergeAsync's marker scan let it through, so `git add` on the unmerged path silently kept whichever side wasn't deleted. GetConflictDocumentsAsync now tells a modify/delete-style conflict (one index stage missing) apart from an ordinary already-resolved file (both stages present) and synthesizes a real whole-file conflict block from the index stages so it shows up in the resolver. ContinueMergeAsync compares such a path against git's own default checkout content before treating it as resolved, so an untouched file blocks the merge instead of being silently staged. Resolving to the deleted side (empty content) now runs `git rm` instead of staging a tracked, zero-byte file.
This commit is contained in:
@@ -376,6 +376,32 @@ public sealed class GitService
|
||||
throw new InvalidOperationException($"git add '{path}' failed (exit {exitCode}): {stderr}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stages <paramref name="path"/> as deleted (`git rm -f`), removing it from both the index
|
||||
/// and, if present, the working tree. Used to resolve a merge conflict to "keep the
|
||||
/// deletion" — unlike <see cref="AddPathAsync"/>, which would stage an empty file as a
|
||||
/// tracked, zero-byte file rather than actually removing it.
|
||||
/// </summary>
|
||||
public async Task RemovePathAsync(string repoDir, string path, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["rm", "-f", "--", path], ct);
|
||||
if (exitCode != 0)
|
||||
throw new InvalidOperationException($"git rm '{path}' failed (exit {exitCode}): {stderr}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Content of <paramref name="path"/> at a merge stage (1=base, 2=ours, 3=theirs) via
|
||||
/// `git show :<stage>:<path>`. Null when that stage doesn't exist for the path —
|
||||
/// the conventional way to tell a modify/delete or rename/delete conflict apart from an
|
||||
/// ordinary content conflict (git never writes `<<<<<<<` markers for those,
|
||||
/// it just leaves whichever side wasn't deleted in the working tree).
|
||||
/// </summary>
|
||||
public async Task<string?> ShowConflictStageAsync(string repoDir, int stage, string path, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["show", $":{stage}:{path}"], ct, trimOutput: false);
|
||||
return exitCode == 0 ? stdout : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Non-destructive mergeability probe via `git merge-tree --write-tree`. Writes only
|
||||
/// loose objects — the working tree, index, and refs are left untouched.
|
||||
|
||||
Reference in New Issue
Block a user