feat(worker): add leaveConflictsInTree option to TaskMergeService.MergeAsync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ public sealed class TaskMergeService
|
|||||||
string targetBranch,
|
string targetBranch,
|
||||||
bool removeWorktree,
|
bool removeWorktree,
|
||||||
string commitMessage,
|
string commitMessage,
|
||||||
|
bool leaveConflictsInTree,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
TaskEntity task;
|
TaskEntity task;
|
||||||
@@ -89,6 +90,11 @@ public sealed class TaskMergeService
|
|||||||
try { files = await _git.ListConflictedFilesAsync(list.WorkingDir, ct); }
|
try { files = await _git.ListConflictedFilesAsync(list.WorkingDir, ct); }
|
||||||
catch { files = new(); }
|
catch { files = new(); }
|
||||||
|
|
||||||
|
if (leaveConflictsInTree && files.Count > 0)
|
||||||
|
{
|
||||||
|
return new MergeResult(StatusConflict, files, null);
|
||||||
|
}
|
||||||
|
|
||||||
// If abort fails the repo is left mid-merge; the caller must resolve manually.
|
// If abort fails the repo is left mid-merge; the caller must resolve manually.
|
||||||
// Return Blocked (not conflict) so the UI does not offer a stale conflict list.
|
// Return Blocked (not conflict) so the UI does not offer a stale conflict list.
|
||||||
try { await _git.MergeAbortAsync(list.WorkingDir, ct); }
|
try { await _git.MergeAbortAsync(list.WorkingDir, ct); }
|
||||||
@@ -141,6 +147,14 @@ public sealed class TaskMergeService
|
|||||||
return new MergeResult(StatusMerged, Array.Empty<string>(), cleanupWarning);
|
return new MergeResult(StatusMerged, Array.Empty<string>(), cleanupWarning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<MergeResult> MergeAsync(
|
||||||
|
string taskId,
|
||||||
|
string targetBranch,
|
||||||
|
bool removeWorktree,
|
||||||
|
string commitMessage,
|
||||||
|
CancellationToken ct)
|
||||||
|
=> MergeAsync(taskId, targetBranch, removeWorktree, commitMessage, leaveConflictsInTree: false, ct);
|
||||||
|
|
||||||
public async Task<MergeTargets> GetTargetsAsync(string taskId, CancellationToken ct)
|
public async Task<MergeTargets> GetTargetsAsync(string taskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TaskEntity task;
|
TaskEntity task;
|
||||||
|
|||||||
@@ -51,6 +51,22 @@ public class TaskMergeServiceTests : IDisposable
|
|||||||
NullLogger<WorktreeManager>.Instance);
|
NullLogger<WorktreeManager>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task SeedWorktree(
|
||||||
|
DbFixture db, string taskId, string path, string branchName, string baseCommit)
|
||||||
|
{
|
||||||
|
var wt = new WorktreeEntity
|
||||||
|
{
|
||||||
|
TaskId = taskId,
|
||||||
|
Path = path,
|
||||||
|
BranchName = branchName,
|
||||||
|
BaseCommit = baseCommit,
|
||||||
|
State = WorktreeState.Active,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
using var ctx = db.CreateContext();
|
||||||
|
await new WorktreeRepository(ctx).AddAsync(wt);
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<(ListEntity list, TaskEntity task)> SeedListAndTask(
|
private static async Task<(ListEntity list, TaskEntity task)> SeedListAndTask(
|
||||||
DbFixture db, string workingDir, TaskStatus status)
|
DbFixture db, string workingDir, TaskStatus status)
|
||||||
{
|
{
|
||||||
@@ -351,6 +367,44 @@ public class TaskMergeServiceTests : IDisposable
|
|||||||
Assert.Equal("blocked", result.Status);
|
Assert.Equal("blocked", result.Status);
|
||||||
Assert.Contains("switch target branch", result.ErrorMessage ?? "");
|
Assert.Contains("switch target branch", result.ErrorMessage ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MergeAsync_LeaveConflicts_DoesNotAbortAndReturnsConflictFiles()
|
||||||
|
{
|
||||||
|
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||||
|
|
||||||
|
var db = NewDb();
|
||||||
|
var repo = NewRepo();
|
||||||
|
GitRepoFixture.RunGit(repo.RepoDir, "branch", "-m", "main");
|
||||||
|
|
||||||
|
File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "# main change\n");
|
||||||
|
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-am", "main change");
|
||||||
|
|
||||||
|
var wtPath = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
|
||||||
|
_wtCleanups.Add((repo.RepoDir, wtPath));
|
||||||
|
GitRepoFixture.RunGit(repo.RepoDir, "worktree", "add", "-b", "claudedo/t1", wtPath, repo.BaseCommit);
|
||||||
|
File.WriteAllText(Path.Combine(wtPath, "README.md"), "# branch change\n");
|
||||||
|
GitRepoFixture.RunGit(wtPath, "commit", "-am", "branch change");
|
||||||
|
|
||||||
|
var (_, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);
|
||||||
|
await SeedWorktree(db, task.Id, wtPath, "claudedo/t1", repo.BaseCommit);
|
||||||
|
|
||||||
|
var (svc, _) = BuildService(db);
|
||||||
|
|
||||||
|
var result = await svc.MergeAsync(
|
||||||
|
task.Id, "main", removeWorktree: false, "msg",
|
||||||
|
leaveConflictsInTree: true,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(TaskMergeService.StatusConflict, result.Status);
|
||||||
|
Assert.Contains("README.md", result.ConflictFiles);
|
||||||
|
|
||||||
|
var midMerge = File.Exists(Path.Combine(repo.RepoDir, ".git", "MERGE_HEAD"));
|
||||||
|
Assert.True(midMerge, "repo should be left in mid-merge state");
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
GitRepoFixture.RunGit(repo.RepoDir, "merge", "--abort");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Test doubles
|
#region Test doubles
|
||||||
|
|||||||
Reference in New Issue
Block a user