From 3331c248985554f579539ee3afba4aadccfff1a3 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Wed, 22 Apr 2026 09:37:35 +0200 Subject: [PATCH] feat(worker): implement TaskMergeService happy path --- .../Services/TaskMergeService.cs | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/ClaudeDo.Worker/Services/TaskMergeService.cs b/src/ClaudeDo.Worker/Services/TaskMergeService.cs index 21d65c9..bdddac3 100644 --- a/src/ClaudeDo.Worker/Services/TaskMergeService.cs +++ b/src/ClaudeDo.Worker/Services/TaskMergeService.cs @@ -75,8 +75,55 @@ public sealed class TaskMergeService if (await _git.HasChangesAsync(list.WorkingDir, ct)) return Blocked("target working tree has uncommitted changes"); - // Body added in later tasks. - throw new NotImplementedException(); + var (exitCode, stderr) = await _git.MergeNoFfAsync(list.WorkingDir, wt.BranchName, commitMessage, ct); + if (exitCode != 0) + { + List files; + try { files = await _git.ListConflictedFilesAsync(list.WorkingDir, ct); } + catch { files = new(); } + try { await _git.MergeAbortAsync(list.WorkingDir, ct); } + catch (Exception ex) { _logger.LogWarning(ex, "git merge --abort failed after conflict"); } + + if (files.Count == 0) + { + // Non-conflict failure (e.g. unrelated histories). + return new MergeResult(StatusBlocked, Array.Empty(), $"merge failed: {stderr}"); + } + + return new MergeResult(StatusConflict, files, null); + } + + string? cleanupWarning = null; + if (removeWorktree) + { + try + { + await _git.WorktreeRemoveAsync(list.WorkingDir, wt.Path, force: false, ct); + try { await _git.BranchDeleteAsync(list.WorkingDir, wt.BranchName, force: false, ct); } + catch (Exception ex) + { + _logger.LogWarning(ex, "branch delete failed for {Branch}", wt.BranchName); + cleanupWarning = $"worktree removed, branch delete failed: {ex.Message}"; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "worktree remove failed for {Path}", wt.Path); + cleanupWarning = $"worktree remove failed: {ex.Message}"; + } + } + + using (var ctx = _dbFactory.CreateDbContext()) + { + await new WorktreeRepository(ctx).SetStateAsync(taskId, WorktreeState.Merged, ct); + } + await _broadcaster.WorktreeUpdated(taskId); + + _logger.LogInformation( + "Merged task {TaskId} branch {Branch} into {Target} (remove worktree: {Remove})", + taskId, wt.BranchName, targetBranch, removeWorktree); + + return new MergeResult(StatusMerged, Array.Empty(), cleanupWarning); } public async Task GetTargetsAsync(string taskId, CancellationToken ct)