Merge branch 'claudedo/340b4a2df0f14235a34fe0d02b2dc06f'
This commit is contained in:
@@ -19,10 +19,17 @@ public sealed record MergeTargets(
|
||||
string DefaultBranch,
|
||||
IReadOnlyList<string> LocalBranches);
|
||||
|
||||
// VerifyExitCode/VerifyDurationMs/VerifyOutputTail are only populated when the caller asked for
|
||||
// a verify run (runVerify=true) AND the list has a verify command configured AND the preview came
|
||||
// back clean — a conflicting or unavailable preview never reaches the verify step. -1 is used for
|
||||
// VerifyExitCode when the command timed out or failed to start (mirrors the post-merge gate).
|
||||
public sealed record MergePreviewResult(
|
||||
string Status,
|
||||
IReadOnlyList<string> ConflictFiles,
|
||||
int ChangedFileCount);
|
||||
int ChangedFileCount,
|
||||
int? VerifyExitCode = null,
|
||||
long? VerifyDurationMs = null,
|
||||
string? VerifyOutputTail = null);
|
||||
|
||||
public sealed record ConflictDocuments(
|
||||
string TaskId,
|
||||
@@ -522,9 +529,21 @@ public sealed class TaskMergeService
|
||||
return new MergeTargets(current, branches);
|
||||
}
|
||||
|
||||
public async Task<MergePreviewResult> PreviewAsync(string taskId, string targetBranch, CancellationToken ct)
|
||||
public Task<MergePreviewResult> PreviewAsync(string taskId, string targetBranch, CancellationToken ct)
|
||||
=> PreviewAsync(taskId, targetBranch, runVerify: false, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Non-destructive merge preview via <see cref="GitService.PreviewMergeAsync"/>. When
|
||||
/// <paramref name="runVerify"/> is true and the list has a verify command configured, a clean
|
||||
/// preview is additionally materialized into a scratch worktree (outside the target repo, always
|
||||
/// cleaned up) and built/tested there — the real working tree is never touched. No verify
|
||||
/// command configured, or runVerify=false, reproduces the pre-existing behavior exactly: no
|
||||
/// delay, no scratch worktree.
|
||||
/// </summary>
|
||||
public async Task<MergePreviewResult> PreviewAsync(
|
||||
string taskId, string targetBranch, bool runVerify, CancellationToken ct)
|
||||
{
|
||||
var (_, list, wt, _) = await LoadMergeContextAsync(taskId, ct);
|
||||
var (_, list, wt, verifyCommand) = await LoadMergeContextAsync(taskId, ct);
|
||||
|
||||
if (wt is null || wt.State != WorktreeState.Active)
|
||||
return new MergePreviewResult(PreviewUnavailable, Array.Empty<string>(), 0);
|
||||
@@ -542,7 +561,60 @@ public sealed class TaskMergeService
|
||||
return new MergePreviewResult(PreviewConflict, preview.ConflictFiles, 0);
|
||||
|
||||
var count = await _git.CountChangedFilesAsync(list.WorkingDir, target, wt.BranchName, ct);
|
||||
return new MergePreviewResult(PreviewClean, Array.Empty<string>(), count);
|
||||
|
||||
if (!runVerify || string.IsNullOrWhiteSpace(verifyCommand) || preview.TreeOid is null)
|
||||
return new MergePreviewResult(PreviewClean, Array.Empty<string>(), count);
|
||||
|
||||
var (exitCode, durationMs, outputTail) = await RunPreviewVerifyAsync(
|
||||
list.WorkingDir, target, preview.TreeOid, verifyCommand, ct);
|
||||
return new MergePreviewResult(PreviewClean, Array.Empty<string>(), count, exitCode, durationMs, outputTail);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the tree a clean merge-tree preview would produce into a scratch, detached-HEAD
|
||||
/// worktree (never a branch, never the real working tree) and runs the verify command there.
|
||||
/// The scratch worktree lives under the OS temp dir and is always removed, even on failure or
|
||||
/// cancellation.
|
||||
/// </summary>
|
||||
private async Task<(int ExitCode, long DurationMs, string OutputTail)> RunPreviewVerifyAsync(
|
||||
string repoDir, string targetBranch, string treeOid, string verifyCommand, CancellationToken ct)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var scratchPath = Path.Combine(Path.GetTempPath(), "claudedo-preview-verify", Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
var parentSha = await _git.RevParseAsync(repoDir, targetBranch, ct);
|
||||
var commitSha = await _git.CommitTreeAsync(repoDir, treeOid, parentSha, "claudedo preview verify", ct);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(scratchPath)!);
|
||||
await _git.WorktreeAddDetachedAsync(repoDir, scratchPath, commitSha, ct);
|
||||
|
||||
VerifyCommandResult result;
|
||||
try
|
||||
{
|
||||
result = await _verify.RunAsync(scratchPath, verifyCommand, VerifyTimeout, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (-1, sw.ElapsedMilliseconds, $"verify command failed to start: {ex.Message}");
|
||||
}
|
||||
|
||||
var exitCode = result.TimedOut ? -1 : result.ExitCode;
|
||||
var tail = exitCode == 0 ? "" : TailOutput(result.Output);
|
||||
return (exitCode, sw.ElapsedMilliseconds, tail);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (-1, sw.ElapsedMilliseconds, $"failed to prepare merge preview for verification: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Best-effort cleanup on the un-cancelled token — a caller-cancelled ct must not
|
||||
// leave the scratch worktree behind.
|
||||
try { await _git.WorktreeRemoveAsync(repoDir, scratchPath, force: true, CancellationToken.None); }
|
||||
catch { /* nothing more we can do */ }
|
||||
try { if (Directory.Exists(scratchPath)) Directory.Delete(scratchPath, recursive: true); }
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
public Task<MergeResult> ApproveAndMergeAsync(string taskId, string targetBranch, CancellationToken ct)
|
||||
|
||||
Reference in New Issue
Block a user