feat(worker): add AbortMergeAsync to cancel a conflicted merge
This commit is contained in:
@@ -22,6 +22,7 @@ public sealed class TaskMergeService
|
|||||||
public const string StatusMerged = "merged";
|
public const string StatusMerged = "merged";
|
||||||
public const string StatusConflict = "conflict";
|
public const string StatusConflict = "conflict";
|
||||||
public const string StatusBlocked = "blocked";
|
public const string StatusBlocked = "blocked";
|
||||||
|
public const string StatusAborted = "aborted";
|
||||||
|
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
private readonly GitService _git;
|
private readonly GitService _git;
|
||||||
@@ -195,6 +196,32 @@ public sealed class TaskMergeService
|
|||||||
return new MergeResult(StatusMerged, Array.Empty<string>(), null);
|
return new MergeResult(StatusMerged, Array.Empty<string>(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<MergeResult> AbortMergeAsync(string taskId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
ListEntity list;
|
||||||
|
WorktreeEntity? wt;
|
||||||
|
|
||||||
|
using (var ctx = _dbFactory.CreateDbContext())
|
||||||
|
{
|
||||||
|
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, ct)
|
||||||
|
?? throw new KeyNotFoundException($"Task '{taskId}' not found.");
|
||||||
|
list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct)
|
||||||
|
?? throw new InvalidOperationException("List not found.");
|
||||||
|
wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wt is null) return Blocked("task has no worktree");
|
||||||
|
if (wt.State != WorktreeState.Active) return Blocked($"worktree state is {wt.State}");
|
||||||
|
if (string.IsNullOrWhiteSpace(list.WorkingDir)) return Blocked("list has no working directory");
|
||||||
|
if (!await _git.IsMidMergeAsync(list.WorkingDir, ct))
|
||||||
|
return Blocked("repo is not mid-merge");
|
||||||
|
|
||||||
|
await _git.MergeAbortAsync(list.WorkingDir, ct);
|
||||||
|
_logger.LogInformation("Aborted merge of task {TaskId}", taskId);
|
||||||
|
|
||||||
|
return new MergeResult(StatusAborted, Array.Empty<string>(), null);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<MergeTargets> GetTargetsAsync(string taskId, CancellationToken ct)
|
public async Task<MergeTargets> GetTargetsAsync(string taskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TaskEntity task;
|
TaskEntity task;
|
||||||
|
|||||||
@@ -408,6 +408,40 @@ public class TaskMergeServiceTests : IDisposable
|
|||||||
Assert.Equal(WorktreeState.Merged, wt.State);
|
Assert.Equal(WorktreeState.Merged, wt.State);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AbortMergeAsync_AfterConflict_RestoresCleanStateAndLeavesWorktreeActive()
|
||||||
|
{
|
||||||
|
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/t3", 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/t3", repo.BaseCommit);
|
||||||
|
|
||||||
|
var (svc, _) = BuildService(db);
|
||||||
|
await svc.MergeAsync(task.Id, "main", false, "msg", leaveConflictsInTree: true, CancellationToken.None);
|
||||||
|
|
||||||
|
var result = await svc.AbortMergeAsync(task.Id, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal("aborted", result.Status);
|
||||||
|
Assert.False(File.Exists(Path.Combine(repo.RepoDir, ".git", "MERGE_HEAD")));
|
||||||
|
|
||||||
|
using var ctx = db.CreateContext();
|
||||||
|
var wt = ctx.Worktrees.Single(w => w.TaskId == task.Id);
|
||||||
|
Assert.Equal(WorktreeState.Active, wt.State);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task MergeAsync_LeaveConflicts_DoesNotAbortAndReturnsConflictFiles()
|
public async Task MergeAsync_LeaveConflicts_DoesNotAbortAndReturnsConflictFiles()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user