feat(merge): Stash-Option fuer uncommittete Aenderungen im Ziel-Tree
Ein dirty Ziel-Arbeitsverzeichnis gibt jetzt den eigenen Status dirty_target zurueck statt blocked. Die UI bietet daraufhin einen Retry an, der per git merge --autostash stasht, merged und die Aenderungen danach wiederherstellt - opt-in pro Merge: - Approve & Merge: Bestaetigungsdialog - Merge-Modal: Button "Aenderungen stashen und mergen" - MCP (merge_task/review_task) wirft weiterhin, kein autostash dort
This commit is contained in:
+3
-1
@@ -708,7 +708,9 @@ public sealed class ExternalMcpService
|
||||
else
|
||||
{
|
||||
var r = await _merge.ApproveAndMergeAsync(taskId, targetBranch ?? "", leaveConflictsInTree, cancellationToken, progress, skipVerify);
|
||||
if (r.Status == TaskMergeService.StatusBlocked)
|
||||
// dirty_target is a refusal like blocked -- only the UI turns it into a stash
|
||||
// retry; an MCP session must not read it as an approve that went through.
|
||||
if (r.Status is TaskMergeService.StatusBlocked or TaskMergeService.StatusDirtyTarget)
|
||||
throw new InvalidOperationException(r.ErrorMessage ?? "approve failed");
|
||||
mergeStatus = r.Status;
|
||||
mergeConflicts = r.ConflictFiles;
|
||||
|
||||
@@ -478,7 +478,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
}
|
||||
|
||||
public Task<MergeResultDto> MergeTask(
|
||||
string taskId, string targetBranch, bool removeWorktree, string commitMessage)
|
||||
string taskId, string targetBranch, bool removeWorktree, string commitMessage, bool autostash = false)
|
||||
=> HubGuard(async () =>
|
||||
{
|
||||
// A blank message is handed through deliberately — TaskMergeService builds the
|
||||
@@ -488,7 +488,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
targetBranch ?? "",
|
||||
removeWorktree,
|
||||
commitMessage,
|
||||
CancellationToken.None);
|
||||
leaveConflictsInTree: false,
|
||||
CancellationToken.None,
|
||||
autostash: autostash);
|
||||
return new MergeResultDto(r.Status, r.ConflictFiles, r.ErrorMessage);
|
||||
});
|
||||
|
||||
@@ -683,7 +685,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
if (!result.Ok) throw new HubException(result.Reason ?? "dequeue failed");
|
||||
}
|
||||
|
||||
public Task<MergeResultDto> ApproveReview(string taskId, string targetBranch)
|
||||
public Task<MergeResultDto> ApproveReview(string taskId, string targetBranch, bool autostash = false)
|
||||
=> HubGuard(async () =>
|
||||
{
|
||||
bool hasChildren;
|
||||
@@ -698,7 +700,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
return new MergeResultDto(unitResult.Status, Array.Empty<string>(), unitResult.Reason);
|
||||
}
|
||||
|
||||
var r = await _mergeService.ApproveAndMergeAsync(taskId, targetBranch ?? "", CancellationToken.None);
|
||||
var r = await _mergeService.ApproveAndMergeAsync(taskId, targetBranch ?? "", CancellationToken.None, autostash);
|
||||
if (r.Status == TaskMergeService.StatusBlocked)
|
||||
throw new HubException(r.ErrorMessage ?? "approve failed");
|
||||
return new MergeResultDto(r.Status, r.ConflictFiles, r.ErrorMessage);
|
||||
|
||||
@@ -70,6 +70,10 @@ public sealed class TaskMergeService
|
||||
public const string StatusMergedVerifyPending = "merged_verify_pending";
|
||||
public const string StatusVerifyPassed = "verify_passed";
|
||||
public const string StatusUntrackedCollision = "untracked_collision";
|
||||
// Nothing landed -- the target working dir has uncommitted changes. Its own status rather
|
||||
// than a Blocked reason string so callers can offer the autostash retry (MergeAsync's
|
||||
// autostash: true) without matching on prose.
|
||||
public const string StatusDirtyTarget = "dirty_target";
|
||||
|
||||
public const string StatusReverted = "reverted";
|
||||
public const string StatusConflictAborted = "conflict_aborted";
|
||||
@@ -375,7 +379,8 @@ public sealed class TaskMergeService
|
||||
bool leaveConflictsInTree,
|
||||
CancellationToken ct,
|
||||
IProgress<ProgressNotificationValue>? progress = null,
|
||||
bool skipVerify = false)
|
||||
bool skipVerify = false,
|
||||
bool autostash = false)
|
||||
{
|
||||
var (task, list, wt, verifyCommand) = await LoadMergeContextAsync(taskId, ct);
|
||||
|
||||
@@ -400,8 +405,10 @@ public sealed class TaskMergeService
|
||||
return Blocked("working directory is not a git repository");
|
||||
if (await _git.IsMidMergeAsync(list.WorkingDir, ct))
|
||||
return Blocked("target working directory is mid-merge");
|
||||
if (await _git.HasChangesAsync(list.WorkingDir, includeUntracked: false, ct))
|
||||
return Blocked("target working tree has uncommitted changes");
|
||||
// autostash hands the dirty tree to git itself (merge --autostash) instead of refusing.
|
||||
if (!autostash && await _git.HasChangesAsync(list.WorkingDir, includeUntracked: false, ct))
|
||||
return new MergeResult(StatusDirtyTarget, Array.Empty<string>(),
|
||||
"target working tree has uncommitted changes");
|
||||
|
||||
var currentBranch = await _git.GetCurrentBranchAsync(list.WorkingDir, ct);
|
||||
if (!string.Equals(currentBranch, targetBranch, StringComparison.Ordinal))
|
||||
@@ -417,7 +424,7 @@ public sealed class TaskMergeService
|
||||
var message = string.IsNullOrWhiteSpace(commitMessage)
|
||||
? DefaultMergeMessage(task, list)
|
||||
: commitMessage;
|
||||
var (exitCode, stderr) = await _git.MergeNoFfAsync(list.WorkingDir, wt.BranchName, message, ct);
|
||||
var (exitCode, stderr) = await _git.MergeNoFfAsync(list.WorkingDir, wt.BranchName, message, ct, autostash);
|
||||
if (exitCode != 0)
|
||||
{
|
||||
List<string> files;
|
||||
@@ -973,13 +980,15 @@ public sealed class TaskMergeService
|
||||
}
|
||||
}
|
||||
|
||||
public Task<MergeResult> ApproveAndMergeAsync(string taskId, string targetBranch, CancellationToken ct)
|
||||
=> ApproveAndMergeAsync(taskId, targetBranch, leaveConflictsInTree: false, ct);
|
||||
public Task<MergeResult> ApproveAndMergeAsync(
|
||||
string taskId, string targetBranch, CancellationToken ct, bool autostash = false)
|
||||
=> ApproveAndMergeAsync(taskId, targetBranch, leaveConflictsInTree: false, ct, autostash: autostash);
|
||||
|
||||
public async Task<MergeResult> ApproveAndMergeAsync(
|
||||
string taskId, string targetBranch, bool leaveConflictsInTree, CancellationToken ct,
|
||||
IProgress<ProgressNotificationValue>? progress = null,
|
||||
bool skipVerify = false)
|
||||
bool skipVerify = false,
|
||||
bool autostash = false)
|
||||
{
|
||||
var (task, list, wt, verifyCommand) = await LoadMergeContextAsync(taskId, ct);
|
||||
|
||||
@@ -1070,7 +1079,7 @@ public sealed class TaskMergeService
|
||||
// MergeAsync transitions the task WaitingForReview -> Done on a successful merge.
|
||||
// Remove the worktree on approve (matching the unit-merge path) so merged
|
||||
// worktrees don't pile up; the merge commit on the target branch is the record.
|
||||
return await MergeAsync(taskId, target, removeWorktree: true, commitMessage: "", leaveConflictsInTree, ct, progress, skipVerify);
|
||||
return await MergeAsync(taskId, target, removeWorktree: true, commitMessage: "", leaveConflictsInTree, ct, progress, skipVerify, autostash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user