feat(worker): add WorktreeMaintenanceService for idle-worktree cleanup
This commit is contained in:
@@ -73,6 +73,21 @@ public sealed class GitService
|
||||
return stdout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full diff between <paramref name="baseRef"/> and the current working tree
|
||||
/// (committed-on-branch changes + uncommitted work). Used for viewing a Claude
|
||||
/// task's total impact relative to where the branch started.
|
||||
/// </summary>
|
||||
public async Task<string> GetBranchDiffAsync(string worktreePath, string baseRef, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, _) = await RunGitAsync(worktreePath,
|
||||
["diff", baseRef], ct);
|
||||
if (exitCode == 0 && !string.IsNullOrWhiteSpace(stdout))
|
||||
return stdout;
|
||||
// Fallback: whatever the worktree has vs HEAD (uncommitted only).
|
||||
return await GetDiffAsync(worktreePath, ct);
|
||||
}
|
||||
|
||||
public async Task<string> DiffStatAsync(string worktreePath, string baseCommit, string headCommit, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath,
|
||||
@@ -93,6 +108,41 @@ public sealed class GitService
|
||||
throw new InvalidOperationException($"git worktree remove failed (exit {exitCode}): {stderr}");
|
||||
}
|
||||
|
||||
public async Task<List<string>> ListWorktreePathsForBranchAsync(string repoDir, string branchName, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["worktree", "list", "--porcelain"], ct);
|
||||
if (exitCode != 0) return new();
|
||||
|
||||
var target = $"refs/heads/{branchName}";
|
||||
var paths = new List<string>();
|
||||
string? currentPath = null;
|
||||
foreach (var raw in stdout.Split('\n'))
|
||||
{
|
||||
var line = raw.TrimEnd('\r');
|
||||
if (line.StartsWith("worktree ", StringComparison.Ordinal))
|
||||
{
|
||||
currentPath = line["worktree ".Length..].Trim();
|
||||
}
|
||||
else if (line.StartsWith("branch ", StringComparison.Ordinal))
|
||||
{
|
||||
var b = line["branch ".Length..].Trim();
|
||||
if (b == target && currentPath is not null) paths.Add(currentPath);
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
currentPath = null;
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
public async Task WorktreePruneAsync(string repoDir, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["worktree", "prune"], ct);
|
||||
if (exitCode != 0)
|
||||
throw new InvalidOperationException($"git worktree prune failed (exit {exitCode}): {stderr}");
|
||||
}
|
||||
|
||||
public async Task BranchDeleteAsync(string repoDir, string branchName, bool force = false, CancellationToken ct = default)
|
||||
{
|
||||
var flag = force ? "-D" : "-d";
|
||||
|
||||
@@ -40,4 +40,17 @@ public sealed class WorktreeRepository
|
||||
{
|
||||
await _context.Worktrees.Where(w => w.TaskId == taskId).ExecuteDeleteAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<List<WorktreeEntity>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Worktrees.AsNoTracking().ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<List<WorktreeEntity>> GetByStatesAsync(
|
||||
IReadOnlyCollection<WorktreeState> states, CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Worktrees.AsNoTracking()
|
||||
.Where(w => states.Contains(w.State))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user