From f3a4f4bb02504cbc6cad7eac53c614de0bbe768c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:40:34 +0000 Subject: [PATCH 1/3] Add git replay update strategy Agent-Logs-Url: https://github.com/geofflamrock/stack/sessions/e1f9279d-3940-4541-90c3-8d55a41ec675 Co-authored-by: geofflamrock <2915931+geofflamrock@users.noreply.github.com> --- .../Commands/Helpers/StackActionsTests.cs | 162 ++++++++++++++++++ .../Remote/SyncStackCommandHandlerTests.cs | 34 ++-- .../Stack/UpdateStackCommandHandlerTests.cs | 95 ++++++++-- src/Stack/Commands/Helpers/StackActions.cs | 131 ++++++++++++++ src/Stack/Commands/Helpers/StackHelpers.cs | 5 +- src/Stack/Commands/Remote/SyncStackCommand.cs | 10 +- .../Commands/Stack/UpdateStackCommand.cs | 11 +- src/Stack/Git/GitClient.cs | 52 ++++++ src/Stack/Infrastructure/CommonOptions.cs | 6 + 9 files changed, 468 insertions(+), 38 deletions(-) diff --git a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs index 8fae200c..af9e031e 100644 --- a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs +++ b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs @@ -1017,4 +1017,166 @@ public async Task UpdateStack_WhenCheckingPullRequests_AndGitHubClientIsNotAvail await stackActions.Invoking(async a => await a.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None, true)) .Should().ThrowAsync(); } + + [Fact] + public async Task UpdateStack_UsingReplay_WhenConflictDetected_ThrowsConflictException() + { + // Arrange + var sourceBranch = Some.BranchName(); + var feature = Some.BranchName(); + + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); + var conflictResolutionDetector = Substitute.For(); + var stack = new Model.Stack("Stack1", sourceBranch, new List { new(feature, []) }); + + var sourceTip = Some.Sha(); + gitClient.GetBranchStatuses(Arg.Any()).Returns(new Dictionary + { + { sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, false, 0, 0, new Commit(sourceTip, Some.Name())) }, + { feature, new GitBranchStatus(feature, $"origin/{feature}", true, false, 0, 0, new Commit(Some.Sha(), Some.Name())) } + }); + + gitClient.When(g => g.ReplayFromSourceBranch(feature, sourceBranch, sourceTip)).Throws(new ConflictException()); + + var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; + var factory = Substitute.For(); + factory.Create(Arg.Any()).Returns(gitClient); + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + + // Act + var act = async () => await actions.UpdateStack(stack, UpdateStrategy.Replay, CancellationToken.None); + + // Assert + await act.Should().ThrowAsync().WithMessage("*Conflicts detected*"); + } + + [Fact] + public async Task UpdateStack_UsingReplay_WhenNoConflicts_CallsReplayForEachBranch() + { + // Arrange + var sourceBranch = Some.BranchName(); + var feature1 = Some.BranchName(); + var feature2 = Some.BranchName(); + + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); + var conflictResolutionDetector = Substitute.For(); + var stack = new Model.Stack("Stack1", sourceBranch, new List + { + new(feature1, [new(feature2, [])]) + }); + + var sourceTip = Some.Sha(); + var feature1Tip = Some.Sha(); + gitClient.GetBranchStatuses(Arg.Any()).Returns(new Dictionary + { + { sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, false, 0, 0, new Commit(sourceTip, Some.Name())) }, + { feature1, new GitBranchStatus(feature1, $"origin/{feature1}", true, false, 0, 0, new Commit(feature1Tip, Some.Name())) }, + { feature2, new GitBranchStatus(feature2, $"origin/{feature2}", true, false, 0, 0, new Commit(Some.Sha(), Some.Name())) } + }); + + var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; + var factory = Substitute.For(); + factory.Create(Arg.Any()).Returns(gitClient); + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + + // Act + await actions.UpdateStack(stack, UpdateStrategy.Replay, CancellationToken.None); + + // Assert + gitClient.Received(1).ReplayFromSourceBranch(feature1, sourceBranch, sourceTip); + gitClient.Received(1).ReplayFromSourceBranch(feature2, feature1, feature1Tip); + gitClient.DidNotReceive().ChangeBranch(Arg.Any()); + } + + [Fact] + public async Task UpdateStack_UsingReplay_WhenBranchHasMergedPullRequest_SkipsBranch() + { + // Arrange + var sourceBranch = Some.BranchName(); + var inactiveBranch = Some.BranchName(); + + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var gitHubClient = new TestGitHubRepositoryBuilder() + .WithPullRequest(inactiveBranch, pr => pr.Merged()) + .Build(); + var conflictResolutionDetector = Substitute.For(); + + var branchStatuses = new Dictionary + { + { sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, true, 0, 0, new Commit(Some.Sha(), Some.Name())) }, + { inactiveBranch, new GitBranchStatus(inactiveBranch, $"origin/{inactiveBranch}", true, false, 0, 0, new Commit(Some.Sha(), Some.Name())) } + }; + + gitClient.GetBranchStatuses(Arg.Any()).Returns(branchStatuses); + + var stack = new Model.Stack( + "Stack1", + sourceBranch, + new List { new(inactiveBranch, []) }); + + var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; + var factory = Substitute.For(); + factory.Create(executionContext.WorkingDirectory).Returns(gitClient); + factory.Create(Arg.Any()).Returns(gitClient); + + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + + // Act + await actions.UpdateStack(stack, UpdateStrategy.Replay, CancellationToken.None, true); + + // Assert + gitClient.DidNotReceive().ChangeBranch(inactiveBranch); + gitClient.DidNotReceive().ReplayFromSourceBranch(Arg.Any(), Arg.Any(), Arg.Any()); + gitClient.DidNotReceive().ReplayOntoNewParent(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task UpdateStack_UsingReplay_WhenBranchHasNoRemoteTrackingBranch_IsUpdated() + { + // Arrange + var sourceBranch = Some.BranchName(); + var localOnlyBranch = Some.BranchName(); + + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); + var conflictResolutionDetector = Substitute.For(); + + var sourceTip = Some.Sha(); + var branchStatuses = new Dictionary + { + { sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, true, 0, 0, new Commit(sourceTip, Some.Name())) }, + { localOnlyBranch, new GitBranchStatus(localOnlyBranch, null, false, false, 0, 0, new Commit(Some.Sha(), Some.Name())) } + }; + + gitClient.GetBranchStatuses(Arg.Any()).Returns(branchStatuses); + + var stack = new Model.Stack( + "Stack1", + sourceBranch, + new List { new(localOnlyBranch, []) }); + + var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; + var factory = Substitute.For(); + factory.Create(executionContext.WorkingDirectory).Returns(gitClient); + factory.Create(Arg.Any()).Returns(gitClient); + + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + + // Act + await actions.UpdateStack(stack, UpdateStrategy.Replay, CancellationToken.None); + + // Assert + gitClient.DidNotReceive().ChangeBranch(Arg.Any()); + gitClient.Received(1).ReplayFromSourceBranch(localOnlyBranch, sourceBranch, sourceTip); + } } \ No newline at end of file diff --git a/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs index b6258ee9..725b8ac4 100644 --- a/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs @@ -55,7 +55,7 @@ public async Task WhenNameIsProvided_DoesNotAskForName_SyncsCorrectStack() inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any(), Arg.Any(), Arg.Any>()).Returns(Task.FromResult(UpdateStrategy.Merge)); // Act - await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -98,7 +98,7 @@ public async Task WhenNameIsProvided_ButStackDoesNotExist_Throws() // Act and assert var invalidStackName = Some.Name(); - await handler.Invoking(async h => await h.Handle(new SyncStackCommandInputs(invalidStackName, 5, false, false, false, false, false), CancellationToken.None)) + await handler.Invoking(async h => await h.Handle(new SyncStackCommandInputs(invalidStackName, 5, false, false, null, false, false, false), CancellationToken.None)) .Should().ThrowAsync() .WithMessage($"Stack '{invalidStackName}' not found."); } @@ -149,7 +149,7 @@ public async Task WhenOnASpecificBranchInTheStack_TheSameBranchIsSetAsCurrentAft inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any(), Arg.Any(), Arg.Any>()).Returns(Task.FromResult(UpdateStrategy.Merge)); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -197,7 +197,7 @@ public async Task WhenOnlyASingleStackExists_DoesNotAskForStackName_SyncsStack() inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any(), Arg.Any(), Arg.Any>()).Returns(Task.FromResult(UpdateStrategy.Merge)); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -248,7 +248,7 @@ public async Task WhenRebaseIsProvided_SyncsStackUsingRebase() inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, true, false, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, true, false, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -298,7 +298,7 @@ public async Task WhenMergeIsProvided_SyncsStackUsingMerge() inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, true, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, true, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -348,7 +348,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndUpdateSettingIsRebase_SyncsS inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -398,7 +398,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndUpdateSettingIsMerge_SyncsSt inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -448,7 +448,7 @@ public async Task WhenGitConfigValueIsSetToMerge_ButRebaseIsSpecified_SyncsStack inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, true, null, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, true, null, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -498,7 +498,7 @@ public async Task WhenGitConfigValueIsSetToRebase_ButMergeIsSpecified_SyncsStack inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, true, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, true, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -549,7 +549,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndNoUpdateSettingExists_AndMer inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -600,7 +600,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndNoUpdateSettingsExists_AndRe inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -629,7 +629,7 @@ public async Task WhenBothRebaseAndMergeAreSpecified_AnErrorIsThrown() // Act and assert await handler - .Invoking(h => h.Handle(new SyncStackCommandInputs(null, 5, true, true, false, false, false), CancellationToken.None)) + .Invoking(h => h.Handle(new SyncStackCommandInputs(null, 5, true, true, null, false, false, false), CancellationToken.None)) .Should().ThrowAsync() .WithMessage("Cannot specify both rebase and merge."); } @@ -675,7 +675,7 @@ public async Task WhenConfirmOptionIsProvided_DoesNotAskForConfirmation() inputProvider.Select(Questions.SelectStack, Arg.Any(), Arg.Any()).Returns("Stack1"); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, true, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, null, true, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -727,7 +727,7 @@ public async Task WhenNoPushOptionIsProvided_DoesNotPushChangesToRemote() inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, true, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, null, false, true, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -775,7 +775,7 @@ public async Task WhenCheckPullRequestsIsTrue_UpdatesStackWithCheckPullRequestsE inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any(), Arg.Any(), Arg.Any>()).Returns(Task.FromResult(UpdateStrategy.Merge)); // Act - await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, false, false, true), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, null, false, false, true), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -823,7 +823,7 @@ public async Task WhenCheckPullRequestsIsFalse_UpdatesStackWithCheckPullRequests inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any(), Arg.Any(), Arg.Any>()).Returns(Task.FromResult(UpdateStrategy.Merge)); // Act - await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); diff --git a/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs index 1affd49f..f33440d4 100644 --- a/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs @@ -43,7 +43,7 @@ public async Task WhenNameIsProvided_DoesNotAskForName_UpdatesCorrectStack() gitClient.GetCurrentBranch().Returns(branch1); // Act - await handler.Handle(new UpdateStackCommandInputs("Stack1", false, true, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs("Stack1", false, true, null, false), CancellationToken.None); // Assert await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any(), Arg.Any()); @@ -81,7 +81,7 @@ public async Task WhenNameIsProvided_ButStackDoesNotExist_Throws() // Act and assert var invalidStackName = Some.Name(); - await handler.Invoking(async h => await h.Handle(new UpdateStackCommandInputs(invalidStackName, false, false, false), CancellationToken.None)) + await handler.Invoking(async h => await h.Handle(new UpdateStackCommandInputs(invalidStackName, false, false, null, false), CancellationToken.None)) .Should().ThrowAsync() .WithMessage($"Stack '{invalidStackName}' not found."); } @@ -119,7 +119,7 @@ public async Task WhenOnASpecificBranchInTheStack_TheSameBranchIsSetAsCurrentAft inputProvider.Select(Questions.SelectStack, Arg.Any(), Arg.Any()).Returns("Stack1"); // Act - await handler.Handle(new UpdateStackCommandInputs(null, false, true, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, false, true, null, false), CancellationToken.None); // Assert current branch preserved gitClient.Received().ChangeBranch(branch1); @@ -152,7 +152,7 @@ public async Task WhenOnlyASingleStackExists_DoesNotAskForStackName_UpdatesStack gitClient.GetCurrentBranch().Returns(branch1); // Act - await handler.Handle(new UpdateStackCommandInputs(null, false, true, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, false, true, null, false), CancellationToken.None); // Assert await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any(), Arg.Any()); @@ -191,7 +191,7 @@ public async Task WhenRebaseIsSpecified_StackIsUpdatedUsingRebase() gitClient.GetCurrentBranch().Returns(branch1); // Act - await handler.Handle(new UpdateStackCommandInputs(null, true, false, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, true, false, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Rebase, Arg.Any()); @@ -230,7 +230,7 @@ public async Task WhenGitConfigValueIsSetToRebase_StackIsUpdatedUsingRebase() gitClient.GetConfigValue("stack.update.strategy").Returns(UpdateStrategy.Rebase.ToString().ToLower()); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, null, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, null, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Rebase, Arg.Any()); @@ -268,7 +268,7 @@ public async Task WhenGitConfigValueIsSetToRebase_ButMergeIsSpecified_StackIsUpd gitClient.GetConfigValue("stack.update.strategy").Returns(UpdateStrategy.Rebase.ToString().ToLower()); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, true, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, true, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any()); @@ -307,7 +307,7 @@ public async Task WhenGitConfigValueIsSetToMerge_StackIsUpdatedUsingMerge() gitClient.GetConfigValue("stack.update.strategy").Returns(UpdateStrategy.Merge.ToString().ToLower()); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, null, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, null, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any()); @@ -346,7 +346,7 @@ public async Task WhenGitConfigValueIsSetToMerge_ButRebaseIsSpecified_StackIsUpd gitClient.GetConfigValue("stack.update.strategy").Returns(UpdateStrategy.Merge.ToString().ToLower()); // Act (rebase specified overrides config) - await handler.Handle(new UpdateStackCommandInputs(null, true, null, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, true, null, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Rebase, Arg.Any()); @@ -386,7 +386,7 @@ public async Task WhenGitConfigValueDoesNotExist_AndRebaseIsSelected_StackIsUpda gitClient.GetConfigValue("stack.update.strategy").Returns((string?)null); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, null, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, null, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Rebase, Arg.Any()); @@ -426,7 +426,7 @@ public async Task WhenGitConfigValueDoesNotExist_AndMergeIsSelected_StackIsUpdat gitClient.GetConfigValue("stack.update.strategy").Returns((string?)null); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, null, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, null, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any()); @@ -462,7 +462,7 @@ public async Task WhenCheckPullRequestsIsTrue_StackIsUpdatedWithCheckPullRequest gitClient.GetConfigValue("stack.update.strategy").Returns((string?)null); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, null, true), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, null, null, true), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any(), true); @@ -498,7 +498,7 @@ public async Task WhenCheckPullRequestsIsFalse_StackIsUpdatedWithCheckPullReques gitClient.GetConfigValue("stack.update.strategy").Returns((string?)null); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, null, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, null, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any(), false); @@ -529,8 +529,75 @@ public async Task WhenBothRebaseAndMergeAreSpecified_AnErrorIsThrown() gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); // Act and assert await handler - .Invoking(h => h.Handle(new UpdateStackCommandInputs(null, true, true, false), CancellationToken.None)) + .Invoking(h => h.Handle(new UpdateStackCommandInputs(null, true, true, null, false), CancellationToken.None)) .Should().ThrowAsync() .WithMessage("Cannot specify both rebase and merge."); } + + [Fact] + public async Task WhenReplayIsSpecified_StackIsUpdatedUsingReplay() + { + // Arrange + var sourceBranch = Some.BranchName(); + var branch1 = Some.BranchName(); + var branch2 = Some.BranchName(); + + var stackRepository = new TestStackRepositoryBuilder() + .WithStack(stack => stack + .WithName("Stack1") + .WithSourceBranch(sourceBranch) + .WithBranch(b1 => b1.WithName(branch1).WithChildBranch(b2 => b2.WithName(branch2)))) + .WithStack(stack => stack + .WithName("Stack2") + .WithSourceBranch(sourceBranch)) + .Build(); + var inputProvider = Substitute.For(); + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var stackActions = Substitute.For(); + var gitClientFactory = Substitute.For(); + var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" }; + var handler = new UpdateStackCommandHandler(inputProvider, logger, displayProvider, gitClientFactory, executionContext, stackRepository, stackActions); + + gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); + + inputProvider.Select(Questions.SelectStack, Arg.Any(), Arg.Any()).Returns("Stack1"); + gitClient.GetCurrentBranch().Returns(branch1); + + // Act + await handler.Handle(new UpdateStackCommandInputs(null, false, false, true, false), CancellationToken.None); + + // Assert + await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Replay, Arg.Any()); + } + + [Fact] + public async Task WhenReplayAndRebaseAreSpecified_Throws() + { + // Arrange + var sourceBranch = Some.BranchName(); + + var stackRepository = new TestStackRepositoryBuilder() + .WithStack(stack => stack + .WithName("Stack1") + .WithSourceBranch(sourceBranch)) + .Build(); + var inputProvider = Substitute.For(); + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var stackActions = Substitute.For(); + var gitClientFactory = Substitute.For(); + var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" }; + var handler = new UpdateStackCommandHandler(inputProvider, logger, displayProvider, gitClientFactory, executionContext, stackRepository, stackActions); + + gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); + + // Act and assert + await handler + .Invoking(h => h.Handle(new UpdateStackCommandInputs(null, true, false, true, false), CancellationToken.None)) + .Should().ThrowAsync() + .WithMessage("Cannot specify more than one of rebase, merge, or replay."); + } } diff --git a/src/Stack/Commands/Helpers/StackActions.cs b/src/Stack/Commands/Helpers/StackActions.cs index b70478c6..10ab5ea3 100644 --- a/src/Stack/Commands/Helpers/StackActions.cs +++ b/src/Stack/Commands/Helpers/StackActions.cs @@ -174,6 +174,10 @@ await displayProvider.DisplayStatus("Checking status of pull requests...", async { await UpdateStackUsingRebase(stack, branchStatuses, pullRequests, cancellationToken); } + else if (strategy == UpdateStrategy.Replay) + { + await UpdateStackUsingReplay(stack, branchStatuses, pullRequests, cancellationToken); + } else { await UpdateStackUsingMerge(stack, branchStatuses, pullRequests, cancellationToken); @@ -500,6 +504,127 @@ private async Task RebaseOntoNewParent( } }, cancellationToken); } + + private async Task UpdateStackUsingReplay( + Model.Stack stack, + Dictionary branchStatuses, + Dictionary pullRequests, + CancellationToken cancellationToken) + { + logger.UpdatingStackUsingReplay(stack.Name); + + foreach (var branchLine in stack.GetAllBranchLines()) + { + await UpdateBranchLineUsingReplay(stack.Name, stack.SourceBranch, branchLine, branchStatuses, pullRequests, cancellationToken); + } + } + + private async Task UpdateBranchLineUsingReplay( + string stackName, + string sourceBranchName, + List branchLine, + Dictionary branchStatuses, + Dictionary pullRequests, + CancellationToken cancellationToken) + { + logger.ReplayingStackForBranchLine(stackName, sourceBranchName, string.Join(" -> ", branchLine.Select(b => b.Name))); + List allBranchesInLine = [GetBranchState(sourceBranchName, branchStatuses, pullRequests), .. branchLine.Select(b => GetBranchState(b.Name, branchStatuses, pullRequests))]; + + foreach (var branch in branchLine) + { + var branchState = allBranchesInLine.First(b => b.Name == branch.Name); + + if (!branchState.IsActive) + { + logger.TraceSkippingInactiveBranch(branch.Name); + continue; + } + + string? lowestInactiveBranchToReParentFrom = null; + var branchesToReplayOnto = new List(); + + foreach (var branchToReplayOnto in allBranchesInLine) + { + if (branchToReplayOnto.Name == branch.Name) + { + break; + } + + if (branchToReplayOnto.IsActive) + { + branchesToReplayOnto.Add(branchToReplayOnto); + } + else if (lowestInactiveBranchToReParentFrom is null) + { + lowestInactiveBranchToReParentFrom = branchToReplayOnto.Name; + } + } + + foreach (var branchToReplayOnto in branchesToReplayOnto) + { + BranchState? lowestInactiveBranchToReParentFromDetail = lowestInactiveBranchToReParentFrom is not null + ? allBranchesInLine.First(b => b.Name == lowestInactiveBranchToReParentFrom) + : null; + var couldReplayOntoParent = lowestInactiveBranchToReParentFromDetail is { Exists: true }; + var parentCommitToReplayFrom = couldReplayOntoParent ? GetCommitShaToReParentFrom(branch.Name, lowestInactiveBranchToReParentFrom!, branchToReplayOnto.Name) : null; + + if (parentCommitToReplayFrom is not null) + { + await ReplayOntoNewParent(branch.Name, branchToReplayOnto.Name, parentCommitToReplayFrom, cancellationToken); + } + else + { + await ReplayFromSourceBranch(branch.Name, branchToReplayOnto.Name, branchStatuses, cancellationToken); + } + } + } + } + + private async Task ReplayFromSourceBranch(string branch, string sourceBranchName, Dictionary branchStatuses, CancellationToken cancellationToken) + { + await displayProvider.DisplayStatusWithSuccess($"Replaying {branch} onto {sourceBranchName}", async ct => + { + var gitClient = GetDefaultGitClient(); + + if (!branchStatuses.TryGetValue(sourceBranchName, out var sourceBranchStatus)) + { + throw new InvalidOperationException($"Could not find branch status for '{sourceBranchName}' when replaying '{branch}'."); + } + + var upstreamSha = sourceBranchStatus.Tip.Sha; + + try + { + gitClient.ReplayFromSourceBranch(branch, sourceBranchName, upstreamSha); + } + catch (ConflictException) + { + throw new Exception($"Conflicts detected when replaying '{branch}' onto '{sourceBranchName}'. Since git replay does not modify the working directory, conflicts cannot be resolved interactively. Please resolve the conflict manually and try again using a different update strategy."); + } + }, cancellationToken); + } + + private async Task ReplayOntoNewParent( + string branch, + string newParentBranchName, + string oldParentCommitSha, + CancellationToken cancellationToken) + { + await displayProvider.DisplayStatusWithSuccess($"Replaying {branch} onto new parent {newParentBranchName}", async ct => + { + var gitClient = GetDefaultGitClient(); + + try + { + gitClient.ReplayOntoNewParent(branch, newParentBranchName, oldParentCommitSha); + } + catch (ConflictException) + { + throw new Exception($"Conflicts detected when replaying '{branch}' onto new parent '{newParentBranchName}'. Since git replay does not modify the working directory, conflicts cannot be resolved interactively. Please resolve the conflict manually and try again using a different update strategy."); + } + }, cancellationToken); + } + private readonly record struct BranchState(string Name, GitBranchStatus? BranchStatus, GitHubPullRequest? PullRequest) { public bool Exists => BranchStatus is not null; @@ -585,4 +710,10 @@ internal static partial class LoggerExtensionMethods [LoggerMessage(Level = LogLevel.Debug, Message = "Commit {CommitSha} exists in branch {BranchToRebaseOnto}, no need to re-parent")] public static partial void CommitExistsInNewParent(this ILogger logger, string commitSha, string branchToRebaseOnto); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Updating stack \"{Stack}\" using replay...")] + public static partial void UpdatingStackUsingReplay(this ILogger logger, string stack); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Replaying stack \"{Stack}\" for branch line: {SourceBranch} --> {BranchLine}")] + public static partial void ReplayingStackForBranchLine(this ILogger logger, string stack, string sourceBranch, string branchLine); } diff --git a/src/Stack/Commands/Helpers/StackHelpers.cs b/src/Stack/Commands/Helpers/StackHelpers.cs index 4f352360..cd9512ff 100644 --- a/src/Stack/Commands/Helpers/StackHelpers.cs +++ b/src/Stack/Commands/Helpers/StackHelpers.cs @@ -553,7 +553,7 @@ public static async Task GetUpdateStrategy( var strategy = await inputProvider.Select( Questions.SelectUpdateStrategy, - [UpdateStrategy.Merge, UpdateStrategy.Rebase], + [UpdateStrategy.Merge, UpdateStrategy.Rebase, UpdateStrategy.Replay], cancellationToken); logger.Answer(Questions.SelectUpdateStrategy, strategy); @@ -655,7 +655,8 @@ public enum MergeConflictAction public enum UpdateStrategy { Merge, - Rebase + Rebase, + Replay } internal static partial class LoggerExtensionMethods diff --git a/src/Stack/Commands/Remote/SyncStackCommand.cs b/src/Stack/Commands/Remote/SyncStackCommand.cs index 14538967..eee91d6d 100644 --- a/src/Stack/Commands/Remote/SyncStackCommand.cs +++ b/src/Stack/Commands/Remote/SyncStackCommand.cs @@ -30,6 +30,7 @@ public SyncStackCommand( Add(CommonOptions.MaxBatchSize); Add(CommonOptions.Rebase); Add(CommonOptions.Merge); + Add(CommonOptions.Replay); Add(CommonOptions.Confirm); Add(CommonOptions.CheckPullRequests); Add(NoPush); @@ -43,6 +44,7 @@ await handler.Handle( parseResult.GetValue(CommonOptions.MaxBatchSize), parseResult.GetValue(CommonOptions.Rebase), parseResult.GetValue(CommonOptions.Merge), + parseResult.GetValue(CommonOptions.Replay), parseResult.GetValue(CommonOptions.Confirm), parseResult.GetValue(NoPush), parseResult.GetValue(CommonOptions.CheckPullRequests)), @@ -55,11 +57,12 @@ public record SyncStackCommandInputs( int MaxBatchSize, bool? Rebase, bool? Merge, + bool? Replay, bool Confirm, bool NoPush, bool CheckPullRequests) { - public static SyncStackCommandInputs Empty => new(null, 5, null, null, false, false, false); + public static SyncStackCommandInputs Empty => new(null, 5, null, null, null, false, false, false); } public class SyncStackCommandHandler( @@ -81,6 +84,9 @@ public override async Task Handle(SyncStackCommandInputs inputs, CancellationTok if (inputs.Rebase == true && inputs.Merge == true) throw new InvalidOperationException("Cannot specify both rebase and merge."); + if ((inputs.Rebase == true ? 1 : 0) + (inputs.Merge == true ? 1 : 0) + (inputs.Replay == true ? 1 : 0) > 1) + throw new InvalidOperationException("Cannot specify more than one of rebase, merge, or replay."); + var gitClient = gitClientFactory.Create(executionContext.WorkingDirectory); var stacksForRemote = repository.GetStacks(); @@ -131,7 +137,7 @@ await displayProvider.DisplayStatusWithSuccess("Pulling changes from remote repo }, cancellationToken); var updateStrategy = await StackHelpers.GetUpdateStrategy( - inputs.Merge == true ? UpdateStrategy.Merge : inputs.Rebase == true ? UpdateStrategy.Rebase : null, + inputs.Replay == true ? UpdateStrategy.Replay : inputs.Merge == true ? UpdateStrategy.Merge : inputs.Rebase == true ? UpdateStrategy.Rebase : null, gitClient, inputProvider, logger, cancellationToken); await displayProvider.DisplayStatus("Updating stack...", async (ct) => diff --git a/src/Stack/Commands/Stack/UpdateStackCommand.cs b/src/Stack/Commands/Stack/UpdateStackCommand.cs index 299d2036..cfae34e1 100644 --- a/src/Stack/Commands/Stack/UpdateStackCommand.cs +++ b/src/Stack/Commands/Stack/UpdateStackCommand.cs @@ -24,6 +24,7 @@ public UpdateStackCommand( Add(CommonOptions.Stack); Add(CommonOptions.Rebase); Add(CommonOptions.Merge); + Add(CommonOptions.Replay); Add(CommonOptions.CheckPullRequests); } @@ -34,14 +35,15 @@ await handler.Handle( parseResult.GetValue(CommonOptions.Stack), parseResult.GetValue(CommonOptions.Rebase), parseResult.GetValue(CommonOptions.Merge), + parseResult.GetValue(CommonOptions.Replay), parseResult.GetValue(CommonOptions.CheckPullRequests)), cancellationToken); } } -public record UpdateStackCommandInputs(string? Stack, bool? Rebase, bool? Merge, bool CheckPullRequests) +public record UpdateStackCommandInputs(string? Stack, bool? Rebase, bool? Merge, bool? Replay, bool CheckPullRequests) { - public static UpdateStackCommandInputs Empty => new(null, null, null, false); + public static UpdateStackCommandInputs Empty => new(null, null, null, null, false); } public record UpdateStackCommandResponse(); @@ -63,6 +65,9 @@ public override async Task Handle(UpdateStackCommandInputs inputs, CancellationT if (inputs.Rebase == true && inputs.Merge == true) throw new InvalidOperationException("Cannot specify both rebase and merge."); + if ((inputs.Rebase == true ? 1 : 0) + (inputs.Merge == true ? 1 : 0) + (inputs.Replay == true ? 1 : 0) > 1) + throw new InvalidOperationException("Cannot specify more than one of rebase, merge, or replay."); + var gitClient = gitClientFactory.Create(executionContext.WorkingDirectory); var stacksForRemote = repository.GetStacks(); @@ -80,7 +85,7 @@ public override async Task Handle(UpdateStackCommandInputs inputs, CancellationT throw new InvalidOperationException($"Stack '{inputs.Stack}' not found."); var updateStrategy = await StackHelpers.GetUpdateStrategy( - inputs.Merge == true ? UpdateStrategy.Merge : inputs.Rebase == true ? UpdateStrategy.Rebase : null, + inputs.Replay == true ? UpdateStrategy.Replay : inputs.Merge == true ? UpdateStrategy.Merge : inputs.Rebase == true ? UpdateStrategy.Rebase : null, gitClient, inputProvider, logger, cancellationToken); await displayProvider.DisplayStatus("Updating stack...", async (ct) => diff --git a/src/Stack/Git/GitClient.cs b/src/Stack/Git/GitClient.cs index 66c7ad54..17490d08 100644 --- a/src/Stack/Git/GitClient.cs +++ b/src/Stack/Git/GitClient.cs @@ -50,6 +50,8 @@ public interface IGitClient void MergeFromLocalSourceBranch(string sourceBranchName); void RebaseFromLocalSourceBranch(string sourceBranchName); void RebaseOntoNewParent(string newParentBranchName, string oldParentBranchName); + void ReplayFromSourceBranch(string branchName, string sourceBranchName, string upstreamSha); + void ReplayOntoNewParent(string branchName, string newParentBranchName, string oldParentCommitSha); void AbortMerge(); void AbortRebase(); void ContinueRebase(); @@ -286,6 +288,56 @@ public void RebaseOntoNewParent(string newParentBranchName, string oldParentBran }); } + public void ReplayFromSourceBranch(string branchName, string sourceBranchName, string upstreamSha) + { + var output = ExecuteGitCommandAndReturnOutput($"replay --onto {sourceBranchName} {upstreamSha}..{branchName}", true, (info, result) => + { + if (result.StandardOutput.Contains("CONFLICT", StringComparison.OrdinalIgnoreCase) || + result.StandardError.Contains("CONFLICT", StringComparison.OrdinalIgnoreCase)) + { + throw new ConflictException(); + } + + throw new ConflictException(); + }); + + UpdateRefsFromReplayOutput(branchName, output); + } + + public void ReplayOntoNewParent(string branchName, string newParentBranchName, string oldParentCommitSha) + { + var output = ExecuteGitCommandAndReturnOutput($"replay --onto {newParentBranchName} {oldParentCommitSha}..{branchName}", true, (info, result) => + { + if (result.StandardOutput.Contains("CONFLICT", StringComparison.OrdinalIgnoreCase) || + result.StandardError.Contains("CONFLICT", StringComparison.OrdinalIgnoreCase)) + { + throw new ConflictException(); + } + + throw new ConflictException(); + }); + + UpdateRefsFromReplayOutput(branchName, output); + } + + private void UpdateRefsFromReplayOutput(string branchName, string replayOutput) + { + var lines = replayOutput.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var parts = line.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 3 && parts[0] == "update") + { + var refName = parts[1]; + var newSha = parts[2]; + ExecuteGitCommand($"update-ref {refName} {newSha}"); + return; + } + } + + throw new Exception($"Could not parse ref update from git replay output for branch '{branchName}'. Output: {replayOutput}"); + } + public void AbortMerge() { ExecuteGitCommand("merge --abort"); diff --git a/src/Stack/Infrastructure/CommonOptions.cs b/src/Stack/Infrastructure/CommonOptions.cs index 2be312ee..e3815d71 100644 --- a/src/Stack/Infrastructure/CommonOptions.cs +++ b/src/Stack/Infrastructure/CommonOptions.cs @@ -50,6 +50,12 @@ public static class CommonOptions Required = false }; + public static Option Replay { get; } = new Option("--replay") + { + Description = "Use replay when updating the stack. Overrides any setting in Git configuration.", + Required = false + }; + public static Option Confirm { get; } = new Option("--yes", "-y") { Description = "Confirm the command without prompting.", From 3036989f64d1d5e9df221aeada95e7ef2ce2eee1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:42:52 +0000 Subject: [PATCH 2/3] Fix unconditional ConflictException in ReplayFromSourceBranch and ReplayOntoNewParent Agent-Logs-Url: https://github.com/geofflamrock/stack/sessions/e1f9279d-3940-4541-90c3-8d55a41ec675 Co-authored-by: geofflamrock <2915931+geofflamrock@users.noreply.github.com> --- src/Stack/Git/GitClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Stack/Git/GitClient.cs b/src/Stack/Git/GitClient.cs index 17490d08..f0f15e26 100644 --- a/src/Stack/Git/GitClient.cs +++ b/src/Stack/Git/GitClient.cs @@ -298,7 +298,7 @@ public void ReplayFromSourceBranch(string branchName, string sourceBranchName, s throw new ConflictException(); } - throw new ConflictException(); + throw new ProcessException(result.StandardError, info.FileName, info.Arguments, result.ExitCode); }); UpdateRefsFromReplayOutput(branchName, output); @@ -314,7 +314,7 @@ public void ReplayOntoNewParent(string branchName, string newParentBranchName, s throw new ConflictException(); } - throw new ConflictException(); + throw new ProcessException(result.StandardError, info.FileName, info.Arguments, result.ExitCode); }); UpdateRefsFromReplayOutput(branchName, output); From b58c4d67513b0186d67c93c5bb2f7b01982d9243 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Thu, 7 May 2026 11:59:45 +1000 Subject: [PATCH 3/3] Remove direct ref updates during replay --- src/Stack/Git/GitClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Stack/Git/GitClient.cs b/src/Stack/Git/GitClient.cs index f0f15e26..afa43f04 100644 --- a/src/Stack/Git/GitClient.cs +++ b/src/Stack/Git/GitClient.cs @@ -301,7 +301,7 @@ public void ReplayFromSourceBranch(string branchName, string sourceBranchName, s throw new ProcessException(result.StandardError, info.FileName, info.Arguments, result.ExitCode); }); - UpdateRefsFromReplayOutput(branchName, output); + // UpdateRefsFromReplayOutput(branchName, output); } public void ReplayOntoNewParent(string branchName, string newParentBranchName, string oldParentCommitSha) @@ -317,7 +317,7 @@ public void ReplayOntoNewParent(string branchName, string newParentBranchName, s throw new ProcessException(result.StandardError, info.FileName, info.Arguments, result.ExitCode); }); - UpdateRefsFromReplayOutput(branchName, output); + // UpdateRefsFromReplayOutput(branchName, output); } private void UpdateRefsFromReplayOutput(string branchName, string replayOutput)