fix(claude-do): merge Modify/Delete- und Rename-Konflikte im Merge sichtbar machen
ClaudeDo-Task: 059bcf98-34d5-4ca0-822d-317980fadf8d
This commit is contained in:
@@ -1109,6 +1109,63 @@ public class TaskMergeServiceTests : IDisposable
|
||||
Assert.Contains(file.Segments, s => !s.IsConflict && s.Text.Contains("line1"));
|
||||
}
|
||||
|
||||
// git never writes <<<<<<< markers for a modify/delete conflict -- it just leaves the
|
||||
// modifying side's content sitting in the working tree, which used to read as an
|
||||
// already-resolved stable file (no conflict at all) to both GetConflictDocumentsAsync and
|
||||
// ContinueMergeAsync's marker scan, so `continue_merge` would silently keep the modification.
|
||||
[Fact]
|
||||
public async Task ModifyDeleteConflict_IsSynthesizedAndCannotBeSilentlyStaged()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
|
||||
var db = NewDb();
|
||||
var repo = NewRepo();
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", "-m", "main");
|
||||
|
||||
var wtPath = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
|
||||
_wtCleanups.Add((repo.RepoDir, wtPath));
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "worktree", "add", "-b", "claudedo/modify-delete", wtPath, repo.BaseCommit);
|
||||
File.WriteAllText(Path.Combine(wtPath, "README.md"), "# modified by branch\n");
|
||||
GitRepoFixture.RunGit(wtPath, "commit", "-am", "branch modifies README");
|
||||
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "rm", "README.md");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "main deletes README");
|
||||
|
||||
var (_, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);
|
||||
await SeedWorktree(db, task.Id, wtPath, "claudedo/modify-delete", repo.BaseCommit);
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
var merge = await svc.MergeAsync(task.Id, "main", removeWorktree: false, "msg",
|
||||
leaveConflictsInTree: true, CancellationToken.None);
|
||||
Assert.Equal(TaskMergeService.StatusConflict, merge.Status);
|
||||
|
||||
// No <<<<<<< markers anywhere -- git left the branch's modification as-is.
|
||||
Assert.DoesNotContain("<<<<<<<", File.ReadAllText(Path.Combine(repo.RepoDir, "README.md")));
|
||||
|
||||
var docs = await svc.GetConflictDocumentsAsync(task.Id, CancellationToken.None);
|
||||
var file = Assert.Single(docs.Files);
|
||||
Assert.EndsWith("README.md", file.Path.Replace('\\', '/'));
|
||||
Assert.False(file.IsBinary);
|
||||
var conflict = Assert.Single(file.Segments.Where(s => s.IsConflict).ToList());
|
||||
Assert.Equal("", conflict.Ours); // ours (main) deleted it
|
||||
Assert.Contains("modified by branch", conflict.Theirs); // theirs (branch) modified it
|
||||
|
||||
// Nobody resolved it — continue must refuse rather than silently keeping the modification.
|
||||
var headBefore = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();
|
||||
var refused = await svc.ContinueMergeAsync(task.Id, CancellationToken.None);
|
||||
Assert.Equal(TaskMergeService.StatusConflict, refused.Status);
|
||||
Assert.Contains("README.md", refused.ConflictFiles);
|
||||
Assert.Equal(headBefore, GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim());
|
||||
Assert.True(await new GitService().IsMidMergeAsync(repo.RepoDir));
|
||||
|
||||
// Explicitly resolving to the deleted (ours) side must `git rm`, not stage an empty file.
|
||||
await svc.WriteResolutionAsync(task.Id, "README.md", "", CancellationToken.None);
|
||||
var result = await svc.ContinueMergeAsync(task.Id, CancellationToken.None);
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.Status);
|
||||
Assert.False(File.Exists(Path.Combine(repo.RepoDir, "README.md")));
|
||||
Assert.False(await new GitService().IsMidMergeAsync(repo.RepoDir));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApproveAndMergeAsync_NoWorktree_MarksDone()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user