fix(worktrees): detect external merges, clean up empty dirs, prune in owning repo

- ContentInMain: HEAD ancestor of the default branch or squash-merged
  (merge-tree == target tree), against local origin/<default> without fetch;
  list_worktrees mergedIntoMain and the overview modal ("merged (external)")
  use it, "Cleanup finished" takes clean Active rows that are in main
- cleanup_task_worktree skips the dirty check for a non-git directory
- prune / worktree remove / branch -D run in the repo owning the worktree
  (git-common-dir), not the list's working dir; branch already gone reports
  branchDeleted=true
This commit is contained in:
mika kuns
2026-10-02 09:07:43 +02:00
parent 9c015f34cc
commit 0c0bb7f24c
12 changed files with 333 additions and 22 deletions
@@ -489,4 +489,45 @@ public sealed class WorktreeMcpToolsTests : IDisposable
WorktreeMcpTools.ProgressReportInterval = original;
}
}
[Fact]
public async Task CleanupTaskWorktree_EmptyDirectory_RemovesDbRow()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var (task, list, wt) = await SeedWorktreeAsync(TaskStatus.Done);
// Leave an empty, non-git directory behind where the worktree was.
GitRepoFixture.RunGit(list.WorkingDir!, "worktree", "remove", "--force", wt.WorktreePath);
Directory.CreateDirectory(wt.WorktreePath);
var sut = BuildSut(CreateQueue());
var rows = await sut.ListWorktrees(CancellationToken.None);
Assert.Equal("", rows.Single(r => r.TaskId == task.Id).HeadCommit);
var result = await sut.CleanupTaskWorktree(task.Id, false, CancellationToken.None);
Assert.True(result.Removed);
Assert.False(Directory.Exists(wt.WorktreePath));
using var ctx = _db.CreateContext();
Assert.Null(await new WorktreeRepository(ctx).GetByTaskIdAsync(task.Id));
}
[Fact]
public async Task ListWorktrees_SquashMergedBranch_ReportsMergedIntoMain()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var (task, list, wt) = await SeedWorktreeAsync(TaskStatus.Done);
File.WriteAllText(Path.Combine(wt.WorktreePath, "feature.txt"), "x");
GitRepoFixture.RunGit(wt.WorktreePath, "add", "-A");
GitRepoFixture.RunGit(wt.WorktreePath, "commit", "-m", "feature");
GitRepoFixture.RunGit(list.WorkingDir!, "merge", "--squash", wt.BranchName);
GitRepoFixture.RunGit(list.WorkingDir!, "commit", "-m", "squash");
var sut = BuildSut(CreateQueue());
var row = (await sut.ListWorktrees(CancellationToken.None)).Single(r => r.TaskId == task.Id);
Assert.True(row.ContentInMain);
Assert.True(row.MergedIntoMain);
}
}