fix(planning): return to the list base branch after integration cleanup, not a guessed task branch
This commit is contained in:
@@ -133,15 +133,29 @@ public sealed class PlanningAggregator
|
|||||||
var current = await _git.GetCurrentBranchAsync(repoDir, ct);
|
var current = await _git.GetCurrentBranchAsync(repoDir, ct);
|
||||||
if (string.Equals(current, branch, StringComparison.Ordinal))
|
if (string.Equals(current, branch, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var branches = await _git.ListLocalBranchesAsync(repoDir, ct);
|
await _git.CheckoutBranchAsync(repoDir, await ResolveReturnBranchAsync(planning, repoDir, ct), ct);
|
||||||
var target = branches.FirstOrDefault(b => b != branch && !b.StartsWith("claudedo/", StringComparison.Ordinal)) ?? "main";
|
|
||||||
await _git.CheckoutBranchAsync(repoDir, target, ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try { await _git.BranchDeleteAsync(repoDir, branch, force: true, ct); }
|
try { await _git.BranchDeleteAsync(repoDir, branch, force: true, ct); }
|
||||||
catch { /* already gone — idempotent */ }
|
catch { /* already gone — idempotent */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rücksprung vom Integration-Branch: <c>list_config.base_branch</c>, sonst <c>main</c> (bzw. <c>master</c>).
|
||||||
|
/// Nie aus der Branchliste raten — die enthält auch Task-Branches, die in Worktrees ausgecheckt sind.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<string> ResolveReturnBranchAsync(TaskEntity planning, string repoDir, CancellationToken ct)
|
||||||
|
{
|
||||||
|
using var ctx = _dbFactory.CreateDbContext();
|
||||||
|
var baseBranch = await ctx.ListConfigs.AsNoTracking()
|
||||||
|
.Where(c => c.ListId == planning.ListId)
|
||||||
|
.Select(c => c.BaseBranch)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
if (!string.IsNullOrWhiteSpace(baseBranch)) return baseBranch;
|
||||||
|
var branches = await _git.ListLocalBranchesAsync(repoDir, ct);
|
||||||
|
return !branches.Contains("main") && branches.Contains("master") ? "master" : "main";
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<(TaskEntity planning, string repoDir, IReadOnlyList<TaskEntity> children)>
|
private async Task<(TaskEntity planning, string repoDir, IReadOnlyList<TaskEntity> children)>
|
||||||
LoadPlanningContextAsync(string planningTaskId, CancellationToken ct)
|
LoadPlanningContextAsync(string planningTaskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -243,9 +243,6 @@ public sealed class WorktreeManager
|
|||||||
_logger.LogInformation("Discarded worktree for task {TaskId} (branch {Branch})", wt.TaskId, wt.BranchName);
|
_logger.LogInformation("Discarded worktree for task {TaskId} (branch {Branch})", wt.TaskId, wt.BranchName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Improvement children (parent is a non-planning task with its own worktree) branch
|
|
||||||
// from the parent's recorded HEAD so they build on the parent's not-yet-merged work.
|
|
||||||
// Planning children and standalone tasks base off the list's current HEAD.
|
|
||||||
/// <summary>Ticket-Id des Tasks, sonst die seines Parents (Children erben das Ticket ihrer Unit).</summary>
|
/// <summary>Ticket-Id des Tasks, sonst die seines Parents (Children erben das Ticket ihrer Unit).</summary>
|
||||||
private async Task<int?> ResolveTicketIdAsync(TaskEntity task, CancellationToken ct)
|
private async Task<int?> ResolveTicketIdAsync(TaskEntity task, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -259,6 +256,9 @@ public sealed class WorktreeManager
|
|||||||
return TicketStatusMap.ParseBandelTicketId(parentRef);
|
return TicketStatusMap.ParseBandelTicketId(parentRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Improvement children (parent is a non-planning task with its own worktree) branch
|
||||||
|
// from the parent's recorded HEAD so they build on the parent's not-yet-merged work.
|
||||||
|
// Planning children and standalone tasks base off list_config.base_branch, else the current HEAD.
|
||||||
private async Task<string> ResolveBaseCommitAsync(TaskEntity task, string workingDir, CancellationToken ct)
|
private async Task<string> ResolveBaseCommitAsync(TaskEntity task, string workingDir, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (task.ParentTaskId is not null)
|
if (task.ParentTaskId is not null)
|
||||||
|
|||||||
@@ -214,6 +214,39 @@ public class PlanningAggregatorTests : IDisposable
|
|||||||
Assert.DoesNotContain(branches, b => b == ok.Value.IntegrationBranch);
|
Assert.DoesNotContain(branches, b => b == ok.Value.IntegrationBranch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, "main")]
|
||||||
|
[InlineData("dev", "dev")]
|
||||||
|
public async Task CleanupIntegrationBranchAsync_ReturnsToBaseBranchOrMain_NotATaskBranch(string? baseBranch, string expected)
|
||||||
|
{
|
||||||
|
var db = NewDb();
|
||||||
|
var repo = NewRepo();
|
||||||
|
GitRepoFixture.RunGit(repo.RepoDir, "branch", "-m", "main");
|
||||||
|
GitRepoFixture.RunGit(repo.RepoDir, "branch", "dev");
|
||||||
|
var (parentId, _, _) = await SeedPlanningWithTwoChildrenAsync(db, repo);
|
||||||
|
|
||||||
|
// Sorts before main and is checked out in a worktree — the old guess picked it and the checkout threw.
|
||||||
|
var taskWt = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
|
||||||
|
_wtCleanups.Add((repo.RepoDir, taskWt));
|
||||||
|
GitRepoFixture.RunGit(repo.RepoDir, "worktree", "add", "-b", "chore/1-task", taskWt, repo.BaseCommit);
|
||||||
|
|
||||||
|
if (baseBranch is not null)
|
||||||
|
{
|
||||||
|
using var ctx = db.CreateContext();
|
||||||
|
var listId = (await new TaskRepository(ctx).GetByIdAsync(parentId))!.ListId;
|
||||||
|
await new ListRepository(ctx).SetConfigAsync(new ListConfigEntity { ListId = listId, BaseBranch = baseBranch });
|
||||||
|
}
|
||||||
|
|
||||||
|
var git = new GitService();
|
||||||
|
var svc = new PlanningAggregator(db.CreateFactory(), git, NullLogger<PlanningAggregator>.Instance);
|
||||||
|
var built = Assert.IsType<CombinedDiffResult.Ok>(await svc.BuildIntegrationBranchAsync(parentId, "main", CancellationToken.None));
|
||||||
|
Assert.Equal(built.Value.IntegrationBranch, await git.GetCurrentBranchAsync(repo.RepoDir));
|
||||||
|
|
||||||
|
await svc.CleanupIntegrationBranchAsync(parentId, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(expected, await git.GetCurrentBranchAsync(repo.RepoDir));
|
||||||
|
}
|
||||||
|
|
||||||
private void SeedWorktreeWithFile(ClaudeDoDbContext ctx, GitRepoFixture repo, string taskId, string filename, string content)
|
private void SeedWorktreeWithFile(ClaudeDoDbContext ctx, GitRepoFixture repo, string taskId, string filename, string content)
|
||||||
{
|
{
|
||||||
var wtPath = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
|
var wtPath = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
|
||||||
|
|||||||
Reference in New Issue
Block a user