feat(worker): add PlanningAggregator.BuildIntegrationBranchAsync
This commit is contained in:
@@ -70,4 +70,90 @@ public sealed class PlanningAggregator
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<CombinedDiffResult> BuildIntegrationBranchAsync(
|
||||
string planningTaskId, string targetBranch, CancellationToken ct)
|
||||
{
|
||||
var (planning, repoDir, childSubtasks) = await LoadPlanningContextAsync(planningTaskId, ct);
|
||||
|
||||
var integrationBranch = BuildIntegrationBranchName(planning);
|
||||
|
||||
// Reset: delete if exists, then recreate off the target branch.
|
||||
try { await _git.BranchDeleteAsync(repoDir, integrationBranch, force: true, ct); }
|
||||
catch { /* didn't exist */ }
|
||||
|
||||
await _git.CheckoutBranchAsync(repoDir, targetBranch, ct);
|
||||
await GitRawAsync(repoDir, ct, "checkout", "-b", integrationBranch);
|
||||
|
||||
foreach (var child in childSubtasks)
|
||||
{
|
||||
if (child.Worktree is null) continue;
|
||||
var (code, _) = await _git.MergeNoFfAsync(
|
||||
repoDir, child.Worktree.BranchName,
|
||||
$"Integrate subtask: {child.Title}", ct);
|
||||
if (code != 0)
|
||||
{
|
||||
List<string> files;
|
||||
try { files = await _git.ListConflictedFilesAsync(repoDir, ct); }
|
||||
catch { files = new(); }
|
||||
|
||||
try { await _git.MergeAbortAsync(repoDir, ct); } catch { }
|
||||
try { await _git.CheckoutBranchAsync(repoDir, targetBranch, ct); } catch { }
|
||||
try { await _git.BranchDeleteAsync(repoDir, integrationBranch, force: true, ct); } catch { }
|
||||
|
||||
return new CombinedDiffResult.Failed(
|
||||
new CombinedDiffFailure(child.Id, files));
|
||||
}
|
||||
}
|
||||
|
||||
var unifiedDiff = await GitRawAsync(repoDir, ct, "diff", $"{targetBranch}..{integrationBranch}");
|
||||
return new CombinedDiffResult.Ok(new CombinedDiffSuccess(integrationBranch, unifiedDiff));
|
||||
}
|
||||
|
||||
private async Task<(TaskEntity planning, string repoDir, IReadOnlyList<TaskEntity> children)>
|
||||
LoadPlanningContextAsync(string planningTaskId, CancellationToken ct)
|
||||
{
|
||||
using var ctx = _dbFactory.CreateDbContext();
|
||||
var planning = await ctx.Tasks
|
||||
.Include(t => t.List)
|
||||
.Include(t => t.Children).ThenInclude(c => c.Worktree)
|
||||
.SingleOrDefaultAsync(t => t.Id == planningTaskId, ct)
|
||||
?? throw new KeyNotFoundException($"Planning task '{planningTaskId}' not found.");
|
||||
var repoDir = planning.List.WorkingDir
|
||||
?? throw new InvalidOperationException("List has no working directory.");
|
||||
var children = planning.Children.OrderBy(c => c.SortOrder).ToList();
|
||||
return (planning, repoDir, children);
|
||||
}
|
||||
|
||||
internal static string BuildIntegrationBranchName(TaskEntity planning)
|
||||
{
|
||||
var slug = new string(planning.Title
|
||||
.ToLowerInvariant()
|
||||
.Select(c => char.IsLetterOrDigit(c) ? c : '-')
|
||||
.ToArray())
|
||||
.Trim('-');
|
||||
if (string.IsNullOrEmpty(slug)) slug = planning.Id[..8];
|
||||
if (slug.Length > 40) slug = slug[..40].TrimEnd('-');
|
||||
return $"planning/{slug}-integration";
|
||||
}
|
||||
|
||||
private static async Task<string> GitRawAsync(string cwd, CancellationToken ct, params string[] args)
|
||||
{
|
||||
var psi = new System.Diagnostics.ProcessStartInfo("git")
|
||||
{
|
||||
WorkingDirectory = cwd,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
foreach (var a in args) psi.ArgumentList.Add(a);
|
||||
using var p = System.Diagnostics.Process.Start(psi)!;
|
||||
var stdoutTask = p.StandardOutput.ReadToEndAsync();
|
||||
var stderrTask = p.StandardError.ReadToEndAsync();
|
||||
await p.WaitForExitAsync(ct);
|
||||
var stdout = await stdoutTask;
|
||||
var stderr = await stderrTask;
|
||||
if (p.ExitCode != 0) throw new InvalidOperationException($"git {string.Join(' ', args)} failed: {stderr}");
|
||||
return stdout;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user