feat(planning): combined diff of a unit is base..head, no integration branch checkout

This commit is contained in:
mika kuns
2026-10-01 11:25:48 +02:00
parent 1f373a5a67
commit 44d94caabc
@@ -2,6 +2,7 @@ using ClaudeDo.Data;
using ClaudeDo.Data.Git; using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Planning; namespace ClaudeDo.Worker.Planning;
@@ -44,32 +45,47 @@ public sealed class PlanningAggregator
{ {
using var ctx = _dbFactory.CreateDbContext(); using var ctx = _dbFactory.CreateDbContext();
var parent = await ctx.Tasks var parent = await ctx.Tasks
.Include(t => t.List)
.Include(t => t.Worktree) .Include(t => t.Worktree)
.Include(t => t.Children).ThenInclude(c => c.Worktree) .Include(t => t.Children).ThenInclude(c => c.Worktree)
.SingleOrDefaultAsync(t => t.Id == planningTaskId, ct); .SingleOrDefaultAsync(t => t.Id == planningTaskId, ct);
if (parent is null) return new List<SubtaskDiff>(); if (parent is null) return new List<SubtaskDiff>();
// Once a child was absorbed, the parent's branch also carries the children's commits; its own
// share ends where the first child forked off.
var children = parent.Children.OrderBy(c => c.SortOrder).ToList();
var parentOwnHead = children.Any(c => c.Worktree is { State: WorktreeState.Merged, MergeCommit: null })
? children.Select(c => c.Worktree?.BaseCommit).FirstOrDefault(b => b is not null)
: null;
var nodes = new List<TaskEntity>(); var nodes = new List<TaskEntity>();
// An improvement parent carries its own code branch — fold it in first so the // An improvement parent carries its own code branch — fold it in first so the
// combined diff matches what the tree-merge will produce. // combined diff matches what the tree-merge will produce.
if (parent.PlanningPhase == PlanningPhase.None && parent.Worktree is { State: WorktreeState.Active }) if (parent.PlanningPhase == PlanningPhase.None && parent.Worktree is { State: WorktreeState.Active })
nodes.Add(parent); nodes.Add(parent);
nodes.AddRange(parent.Children.OrderBy(c => c.SortOrder)); nodes.AddRange(children);
var result = new List<SubtaskDiff>(); var result = new List<SubtaskDiff>();
foreach (var node in nodes) foreach (var node in nodes)
{ {
if (node.Worktree is null) continue; if (node.Worktree is null) continue;
var wt = node.Worktree; var wt = node.Worktree;
var head = wt.HeadCommit ?? await _git.RevParseHeadAsync(wt.Path, ct); var isOwnShare = node == parent && parentOwnHead is not null;
// An absorbed child's worktree is gone; its commits live on in the unit branch.
var isGone = !Directory.Exists(wt.Path) && wt.HeadCommit is not null && parent.List.WorkingDir is not null;
string head;
string unified; string unified;
try try
{ {
unified = await _git.GetBranchDiffAsync(wt.Path, wt.BaseCommit, ct: ct); head = isOwnShare ? parentOwnHead! : wt.HeadCommit ?? await _git.RevParseHeadAsync(wt.Path, ct);
unified = isOwnShare || isGone
? await _git.GetCommitRangeDiffAsync(parent.List.WorkingDir ?? wt.Path, wt.BaseCommit, head, ct: ct)
: await _git.GetBranchDiffAsync(wt.Path, wt.BaseCommit, ct: ct);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "diff failed for node {Id}", node.Id); _logger.LogWarning(ex, "diff failed for node {Id}", node.Id);
head = wt.HeadCommit ?? wt.BaseCommit;
unified = ""; unified = "";
} }
result.Add(new SubtaskDiff( result.Add(new SubtaskDiff(
@@ -83,6 +99,15 @@ public sealed class PlanningAggregator
{ {
var (planning, repoDir, childSubtasks) = await LoadPlanningContextAsync(planningTaskId, ct); var (planning, repoDir, childSubtasks) = await LoadPlanningContextAsync(planningTaskId, ct);
// A parent that owns the unit branch already is the integration: unitBase..unitHead, no
// checkout in the main checkout. Legacy units (a Done child never absorbed) keep the old path.
if (planning.Worktree is { State: WorktreeState.Active } unitWt
&& !childSubtasks.Any(c => c.Status == TaskStatus.Done && c.Worktree is { State: WorktreeState.Active }))
{
var unitDiff = await _git.GetCommitRangeDiffAsync(repoDir, unitWt.BaseCommit, unitWt.BranchName, ct: ct);
return new CombinedDiffResult.Ok(new CombinedDiffSuccess(unitWt.BranchName, unitDiff));
}
var integrationBranch = BuildIntegrationBranchName(planning); var integrationBranch = BuildIntegrationBranchName(planning);
// Reset: checkout target first (so we're never ON the integration branch when deleting it), // Reset: checkout target first (so we're never ON the integration branch when deleting it),