feat(merge): Stash-Option fuer uncommittete Aenderungen im Ziel-Tree
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 43s

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:
mika kuns
2026-09-11 11:43:28 +02:00
parent 8e2d37e63e
commit 251b70ce83
20 changed files with 164 additions and 43 deletions
@@ -41,7 +41,7 @@ public class DetailsIslandConflictSeamTests : IDisposable
private sealed class ConflictApproveWorkerClient : StubWorkerClient
{
public override bool IsConnected => true;
public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) =>
public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch, bool autostash = false) =>
Task.FromResult<MergeResultDto?>(new MergeResultDto("conflict", new[] { "a.cs" }, null));
}
@@ -55,7 +55,7 @@ public class DetailsIslandPlanningTests : IDisposable
private sealed class ThrowingReviewWorkerClient : StubWorkerClient
{
public override bool IsConnected => true;
public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) =>
public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch, bool autostash = false) =>
Task.FromException<MergeResultDto?>(new InvalidOperationException("Task is not waiting for review; cannot approve."));
}
@@ -141,7 +141,7 @@ public class DetailsIslandReviewActionsTests : IDisposable
{
public override bool IsConnected => true;
public string ExceptionMessage { get; init; } = "blocked: target working tree has uncommitted changes";
public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) =>
public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch, bool autostash = false) =>
throw new Exception(ExceptionMessage);
}
@@ -167,7 +167,7 @@ public class DetailsIslandReviewActionsTests : IDisposable
{
public override bool IsConnected => true;
public TaskCompletionSource<MergeResultDto?> Gate { get; } = new();
public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Gate.Task;
public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch, bool autostash = false) => Gate.Task;
}
[Fact]
@@ -21,17 +21,22 @@ public class MergeModalViewModelTests
public TaskCompletionSource<MergeResultDto> MergeGate { get; } = new();
public bool BlockMerge { get; set; }
public string? CapturedCommitMessage { get; private set; }
public bool CapturedAutostash { get; private set; }
/// Result for a call with autostash=false; the autostash retry always merges.
public MergeResultDto CleanResult { get; set; } = new("merged", new List<string>(), null);
public override Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId) =>
Task.FromResult(Targets);
public override Task<MergeResultDto> MergeTaskAsync(
string taskId, string targetBranch, bool removeWorktree, string commitMessage)
string taskId, string targetBranch, bool removeWorktree, string commitMessage, bool autostash = false)
{
CapturedCommitMessage = commitMessage;
CapturedAutostash = autostash;
return BlockMerge
? MergeGate.Task
: Task.FromResult(new MergeResultDto("merged", new List<string>(), null));
: Task.FromResult(autostash ? new MergeResultDto("merged", new List<string>(), null) : CleanResult);
}
}
@@ -90,6 +95,26 @@ public class MergeModalViewModelTests
Assert.True(vm.Merged);
}
[Fact]
public async Task Dirty_target_offers_the_stash_retry_and_merges_with_autostash()
{
var (vm, worker) = Build();
await vm.InitializeAsync("task-1", "do the thing");
worker.CleanResult = new MergeResultDto("dirty_target", new List<string>(), "target working tree has uncommitted changes");
await vm.SubmitCommand.ExecuteAsync(null);
Assert.True(vm.ShowStashRetry);
Assert.False(vm.Merged);
Assert.False(worker.CapturedAutostash);
await vm.StashAndMergeCommand.ExecuteAsync(null);
Assert.True(worker.CapturedAutostash);
Assert.True(vm.Merged);
Assert.False(vm.ShowStashRetry);
}
[Fact]
public async Task Progress_for_another_task_is_ignored()
{
@@ -37,7 +37,7 @@ public class TasksIslandApproveReviewTests : IDisposable
private sealed class ThrowingWorkerClient : StubWorkerClient
{
public string ExceptionMessage { get; init; } = "blocked: target working tree has uncommitted changes";
public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) =>
public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch, bool autostash = false) =>
throw new Exception(ExceptionMessage);
}