feat(worker): scaffold TaskMergeService with pre-flight checks
This commit is contained in:
104
src/ClaudeDo.Worker/Services/TaskMergeService.cs
Normal file
104
src/ClaudeDo.Worker/Services/TaskMergeService.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Worker.Services;
|
||||
|
||||
public sealed record MergeResult(
|
||||
string Status,
|
||||
IReadOnlyList<string> ConflictFiles,
|
||||
string? ErrorMessage);
|
||||
|
||||
public sealed record MergeTargets(
|
||||
string DefaultBranch,
|
||||
IReadOnlyList<string> LocalBranches);
|
||||
|
||||
public sealed class TaskMergeService
|
||||
{
|
||||
public const string StatusMerged = "merged";
|
||||
public const string StatusConflict = "conflict";
|
||||
public const string StatusBlocked = "blocked";
|
||||
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly GitService _git;
|
||||
private readonly HubBroadcaster _broadcaster;
|
||||
private readonly ILogger<TaskMergeService> _logger;
|
||||
|
||||
public TaskMergeService(
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
GitService git,
|
||||
HubBroadcaster broadcaster,
|
||||
ILogger<TaskMergeService> logger)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_git = git;
|
||||
_broadcaster = broadcaster;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<MergeResult> MergeAsync(
|
||||
string taskId,
|
||||
string targetBranch,
|
||||
bool removeWorktree,
|
||||
string commitMessage,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TaskEntity task;
|
||||
ListEntity list;
|
||||
WorktreeEntity? wt;
|
||||
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
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 (task.Status == TaskStatus.Running)
|
||||
return Blocked("task is running");
|
||||
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.IsGitRepoAsync(list.WorkingDir, ct))
|
||||
return Blocked("working directory is not a git repository");
|
||||
if (await _git.IsMidMergeAsync(list.WorkingDir, ct))
|
||||
return Blocked("target working directory is mid-merge");
|
||||
if (await _git.HasChangesAsync(list.WorkingDir, ct))
|
||||
return Blocked("target working tree has uncommitted changes");
|
||||
|
||||
// Body added in later tasks.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<MergeTargets> GetTargetsAsync(string taskId, CancellationToken ct)
|
||||
{
|
||||
TaskEntity task;
|
||||
ListEntity list;
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(list.WorkingDir))
|
||||
return new MergeTargets("", Array.Empty<string>());
|
||||
|
||||
var current = await _git.GetCurrentBranchAsync(list.WorkingDir, ct);
|
||||
var branches = await _git.ListLocalBranchesAsync(list.WorkingDir, ct);
|
||||
return new MergeTargets(current, branches);
|
||||
}
|
||||
|
||||
private static MergeResult Blocked(string reason) =>
|
||||
new(StatusBlocked, Array.Empty<string>(), reason);
|
||||
}
|
||||
Reference in New Issue
Block a user