refactor(worker): detect untracked worktrees by path instead of branch prefix
This commit is contained in:
@@ -1,36 +1,38 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Worker.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// Startup-only sweep: warns when a list's repo has git-registered <c>claudedo/*</c> worktrees
|
||||
/// that the <c>worktrees</c> table doesn't know about (e.g. left behind by a manual cleanup
|
||||
/// Startup-only sweep: warns when a list's repo has git-registered ClaudeDo task worktrees (under
|
||||
/// <c>.claudedo-worktrees</c> or the central root, planning sessions excluded) that the <c>worktrees</c> table doesn't know about (e.g. left behind by a manual cleanup
|
||||
/// outside the app). Detection only — nothing is removed or pruned.
|
||||
/// </summary>
|
||||
public sealed class UntrackedWorktreeRecovery : IHostedService
|
||||
{
|
||||
public const string Phase = "untracked-worktrees";
|
||||
|
||||
private const string BranchPrefix = "claudedo/";
|
||||
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly GitService _git;
|
||||
private readonly HubBroadcaster _broadcaster;
|
||||
private readonly WorkerConfig _cfg;
|
||||
private readonly ILogger<UntrackedWorktreeRecovery> _logger;
|
||||
|
||||
public UntrackedWorktreeRecovery(
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
GitService git,
|
||||
HubBroadcaster broadcaster,
|
||||
WorkerConfig cfg,
|
||||
ILogger<UntrackedWorktreeRecovery> logger)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_git = git;
|
||||
_broadcaster = broadcaster;
|
||||
_cfg = cfg;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -41,6 +43,10 @@ public sealed class UntrackedWorktreeRecovery : IHostedService
|
||||
var dbPaths = (await new WorktreeRepository(ctx).GetAllAsync(cancellationToken))
|
||||
.Select(w => w.Path)
|
||||
.ToList();
|
||||
var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
||||
var centralRoot = !string.IsNullOrWhiteSpace(settings.CentralWorktreeRoot)
|
||||
? settings.CentralWorktreeRoot
|
||||
: _cfg.CentralWorktreeRoot;
|
||||
|
||||
var foundAny = false;
|
||||
foreach (var list in lists.Where(l => !string.IsNullOrWhiteSpace(l.WorkingDir)))
|
||||
@@ -57,7 +63,7 @@ public sealed class UntrackedWorktreeRecovery : IHostedService
|
||||
}
|
||||
|
||||
var claudedoPaths = registered
|
||||
.Where(w => w.Branch is not null && w.Branch.StartsWith(BranchPrefix, StringComparison.Ordinal))
|
||||
.Where(w => w.Branch is not null && IsTaskWorktreePath(w.Path, centralRoot))
|
||||
.Select(w => w.Path);
|
||||
|
||||
var untracked = FindUntrackedPaths(claudedoPaths, dbPaths);
|
||||
@@ -84,6 +90,26 @@ public sealed class UntrackedWorktreeRecovery : IHostedService
|
||||
return registeredPaths.Where(p => !known.Contains(Normalize(p))).ToList();
|
||||
}
|
||||
|
||||
/// <summary>True für einen Pfad unter <c>.claudedo-worktrees</c> oder dem Central-Root, außer Planning-Session-Worktrees.</summary>
|
||||
public static bool IsTaskWorktreePath(string path, string centralRoot)
|
||||
{
|
||||
var sep = Path.DirectorySeparatorChar;
|
||||
var full = Normalize(path);
|
||||
string? rest = null;
|
||||
|
||||
var central = Normalize(centralRoot);
|
||||
if (full.StartsWith(central + sep, StringComparison.OrdinalIgnoreCase))
|
||||
rest = full[(central.Length + 1)..];
|
||||
else
|
||||
{
|
||||
var marker = $"{sep}.claudedo-worktrees{sep}";
|
||||
var idx = full.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
|
||||
if (idx >= 0) rest = full[(idx + marker.Length)..];
|
||||
}
|
||||
|
||||
return rest is not null && !rest.StartsWith($"planning{sep}", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string Normalize(string path) =>
|
||||
Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,18 @@ public sealed class UntrackedWorktreeRecoveryTests : IDisposable
|
||||
Assert.Equal(registered, untracked);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"C:\src\.claudedo-worktrees\lager-app\412-fix", @"C:\Users\x\.claudeDo\worktrees", true)]
|
||||
[InlineData(@"C:\src\.claudedo-worktrees\planning\abc123", @"C:\Users\x\.claudeDo\worktrees", false)]
|
||||
[InlineData(@"C:\Users\x\.claudeDo\worktrees\lager-app\412-fix", @"C:\Users\x\.claudeDo\worktrees", true)]
|
||||
[InlineData(@"C:\Users\x\.claudeDo\worktrees\planning\abc123", @"C:\Users\x\.claudeDo\worktrees", false)]
|
||||
[InlineData(@"C:\src\my-own-worktree", @"C:\Users\x\.claudeDo\worktrees", false)]
|
||||
public void IsTaskWorktreePath(string path, string centralRoot, bool expected)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
Assert.Equal(expected, UntrackedWorktreeRecovery.IsTaskWorktreePath(path, centralRoot));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_NoLists_DoesNotThrow()
|
||||
{
|
||||
@@ -62,6 +74,7 @@ public sealed class UntrackedWorktreeRecoveryTests : IDisposable
|
||||
_db.CreateFactory(),
|
||||
new ClaudeDo.Data.Git.GitService(),
|
||||
new HubBroadcaster(hubContext),
|
||||
new ClaudeDo.Worker.Config.WorkerConfig(),
|
||||
NullLogger<UntrackedWorktreeRecovery>.Instance);
|
||||
|
||||
await sut.StartAsync(CancellationToken.None);
|
||||
|
||||
Reference in New Issue
Block a user