feat(worker): add leaveConflictsInTree option to TaskMergeService.MergeAsync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-24 15:06:33 +02:00
parent 8afbf20613
commit e77ba35b0e
2 changed files with 68 additions and 0 deletions

View File

@@ -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;

View File

@@ -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