feat(worker): add WorktreeMaintenanceService for idle-worktree cleanup
This commit is contained in:
@@ -39,8 +39,19 @@ public sealed class WorktreeManager
|
||||
var branchName = $"claudedo/{idForBranch}";
|
||||
var slug = CommitMessageBuilder.ToSlug(list.Name);
|
||||
|
||||
var worktreePath = _cfg.WorktreeRootStrategy.Equals("central", StringComparison.OrdinalIgnoreCase)
|
||||
? Path.Combine(_cfg.CentralWorktreeRoot, slug, task.Id)
|
||||
string strategy;
|
||||
string? centralRoot;
|
||||
using (var settingsCtx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var settings = await new AppSettingsRepository(settingsCtx).GetAsync(ct);
|
||||
strategy = settings.WorktreeStrategy;
|
||||
centralRoot = !string.IsNullOrWhiteSpace(settings.CentralWorktreeRoot)
|
||||
? settings.CentralWorktreeRoot
|
||||
: _cfg.CentralWorktreeRoot;
|
||||
}
|
||||
|
||||
var worktreePath = strategy.Equals("central", StringComparison.OrdinalIgnoreCase)
|
||||
? Path.Combine(centralRoot ?? _cfg.CentralWorktreeRoot, slug, task.Id)
|
||||
: Path.Combine(Path.GetDirectoryName(workingDir)!, ".claudedo-worktrees", slug, task.Id);
|
||||
|
||||
worktreePath = Path.GetFullPath(worktreePath);
|
||||
@@ -48,12 +59,43 @@ public sealed class WorktreeManager
|
||||
// Ensure parent directory exists.
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(worktreePath)!);
|
||||
|
||||
// Create the worktree (this also creates the directory).
|
||||
await _git.WorktreeAddAsync(workingDir, branchName, worktreePath, baseCommit, ct);
|
||||
// Create the worktree. If a stale branch from a previous run remains
|
||||
// (e.g. after force-remove), delete it and retry once.
|
||||
try
|
||||
{
|
||||
await _git.WorktreeAddAsync(workingDir, branchName, worktreePath, baseCommit, ct);
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.Message.Contains("already exists", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogWarning("Branch {Branch} already exists; cleaning phantom worktrees and retrying", branchName);
|
||||
|
||||
// Find and forcefully remove any existing worktree registered against this branch.
|
||||
List<string> stalePaths;
|
||||
try { stalePaths = await _git.ListWorktreePathsForBranchAsync(workingDir, branchName, ct); }
|
||||
catch (Exception listEx)
|
||||
{
|
||||
_logger.LogWarning(listEx, "git worktree list failed during self-heal");
|
||||
stalePaths = new();
|
||||
}
|
||||
foreach (var stalePath in stalePaths)
|
||||
{
|
||||
try { await _git.WorktreeRemoveAsync(workingDir, stalePath, force: true, ct); }
|
||||
catch (Exception wrEx) { _logger.LogWarning(wrEx, "Failed to remove stale worktree at {Path}", stalePath); }
|
||||
}
|
||||
|
||||
try { await _git.WorktreePruneAsync(workingDir, ct); }
|
||||
catch (Exception pruneEx) { _logger.LogWarning(pruneEx, "git worktree prune failed during self-heal"); }
|
||||
try { await _git.BranchDeleteAsync(workingDir, branchName, force: true, ct); }
|
||||
catch (Exception delEx) { _logger.LogWarning(delEx, "git branch -D failed during self-heal"); }
|
||||
|
||||
await _git.WorktreeAddAsync(workingDir, branchName, worktreePath, baseCommit, ct);
|
||||
}
|
||||
|
||||
// Insert worktrees row AFTER git succeeds — if git throws, no row is created.
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var wtRepo = new WorktreeRepository(context);
|
||||
// Drop any stale row from a prior run (force-remove may have left the DB side behind).
|
||||
await wtRepo.DeleteAsync(task.Id, ct);
|
||||
await wtRepo.AddAsync(new WorktreeEntity
|
||||
{
|
||||
TaskId = task.Id,
|
||||
|
||||
Reference in New Issue
Block a user