feat(worker): auto-rebase overlapping WaitingForReview branches after a merge

After a merge lands on the target branch, TaskMergeService now rebases every
other WaitingForReview task's active worktree onto the new tip -- but only
when that branch actually touches a file the merge just changed, computed via
GitService.GetChangedFileNamesAsync (both the merge's landed files and each
candidate's own diff). A branch merely behind is left alone. On conflict or
any other failure the rebase is aborted and the branch is left exactly as it
was, with a WorkerLog warning naming the task; a successful rebase updates the
worktree's recorded BaseCommit/HeadCommit and broadcasts WorktreeUpdated.
This commit is contained in:
mika kuns
2026-08-10 14:36:55 +02:00
parent 6a2a19cc9e
commit 2662e7bd98
4 changed files with 314 additions and 0 deletions
+36
View File
@@ -389,6 +389,42 @@ public sealed class GitService
.Count(s => s.Length > 0);
}
/// <summary>Files that differ between two exact refs (2-dot, no merge-base resolution) -- used to see
/// what a target branch itself picked up since a task's fork point, as opposed to
/// <see cref="CountChangedFilesAsync"/>'s 3-dot count of a branch's own changes.</summary>
public async Task<IReadOnlyList<string>> GetChangedFileNamesAsync(
string repoDir, string fromRef, string toRef, CancellationToken ct = default)
{
var (exitCode, stdout, _) = await RunGitAsync(repoDir,
["diff", "--name-only", $"{fromRef}..{toRef}"], ct);
if (exitCode != 0) return Array.Empty<string>();
return stdout
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(s => s.Length > 0)
.ToList();
}
/// <summary>
/// Rebases the branch checked out at <paramref name="worktreePath"/> onto <paramref name="ontoRef"/>.
/// On conflict or any other failure the rebase is aborted before returning, so the worktree is left
/// exactly as it was rather than stranded mid-rebase; <c>ConflictFiles</c> is best-effort and only
/// populated when the failure was an actual conflict.
/// </summary>
public async Task<(int ExitCode, string Stderr, IReadOnlyList<string> ConflictFiles)> RebaseAsync(
string worktreePath, string ontoRef, CancellationToken ct = default)
{
var (exitCode, _, stderr) = await RunGitAsync(worktreePath, ["rebase", ontoRef], ct);
if (exitCode == 0)
return (0, stderr, Array.Empty<string>());
List<string> conflictFiles;
try { conflictFiles = await ListConflictedFilesAsync(worktreePath, ct); }
catch { conflictFiles = new(); }
await RunGitAsync(worktreePath, ["rebase", "--abort"], ct);
return (exitCode, stderr, conflictFiles);
}
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunGitAsync(
string workDir, IEnumerable<string> args, CancellationToken ct, string? stdinData = null, bool trimOutput = true)
{