feat(planning): combined diff of a unit is base..head, no integration branch checkout
This commit is contained in:
@@ -2,6 +2,7 @@ using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Data.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Worker.Planning;
|
||||
|
||||
@@ -44,32 +45,47 @@ public sealed class PlanningAggregator
|
||||
{
|
||||
using var ctx = _dbFactory.CreateDbContext();
|
||||
var parent = await ctx.Tasks
|
||||
.Include(t => t.List)
|
||||
.Include(t => t.Worktree)
|
||||
.Include(t => t.Children).ThenInclude(c => c.Worktree)
|
||||
.SingleOrDefaultAsync(t => t.Id == planningTaskId, ct);
|
||||
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>();
|
||||
// An improvement parent carries its own code branch — fold it in first so the
|
||||
// combined diff matches what the tree-merge will produce.
|
||||
if (parent.PlanningPhase == PlanningPhase.None && parent.Worktree is { State: WorktreeState.Active })
|
||||
nodes.Add(parent);
|
||||
nodes.AddRange(parent.Children.OrderBy(c => c.SortOrder));
|
||||
nodes.AddRange(children);
|
||||
|
||||
var result = new List<SubtaskDiff>();
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.Worktree is null) continue;
|
||||
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;
|
||||
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)
|
||||
{
|
||||
_logger.LogWarning(ex, "diff failed for node {Id}", node.Id);
|
||||
head = wt.HeadCommit ?? wt.BaseCommit;
|
||||
unified = "";
|
||||
}
|
||||
result.Add(new SubtaskDiff(
|
||||
@@ -83,6 +99,15 @@ public sealed class PlanningAggregator
|
||||
{
|
||||
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);
|
||||
|
||||
// Reset: checkout target first (so we're never ON the integration branch when deleting it),
|
||||
|
||||
Reference in New Issue
Block a user