From 7d3488be8584a9be2a59e4f5f7356199fc1adae4 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Tue, 24 Jun 2025 19:30:07 +1000 Subject: [PATCH] Add option to not push changes during sync --- README.md | 1 + .../Remote/SyncStackCommandHandlerTests.cs | 76 +++++++++++++++---- src/Stack/Commands/Remote/SyncStackCommand.cs | 17 ++++- 3 files changed, 77 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 722b659b..69eff135 100644 --- a/README.md +++ b/README.md @@ -389,6 +389,7 @@ OPTIONS: --rebase Use rebase when updating the stack. Overrides any setting in Git configuration --merge Use merge when updating the stack. Overrides any setting in Git configuration --yes Confirm the sync without prompting + --no-push Don't push changes to the remote repository ``` ### GitHub commands diff --git a/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs index c4032365..1330dc55 100644 --- a/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs @@ -53,7 +53,7 @@ public async Task WhenChangesExistOnTheSourceBranchOnTheRemote_PullsChanges_Upda inputProvider.Confirm(Questions.ConfirmSyncStack).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false)); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, false)); // Assert repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfRemoteSourceBranch); @@ -100,7 +100,7 @@ public async Task WhenNameIsProvided_DoesNotAskForName_SyncsCorrectStack() inputProvider.Confirm(Questions.ConfirmSyncStack).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, false)); + await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, false, false)); // Assert repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfRemoteSourceBranch); @@ -147,7 +147,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))) + await handler.Invoking(async h => await h.Handle(new SyncStackCommandInputs(invalidStackName, 5, false, false, false, false))) .Should().ThrowAsync() .WithMessage($"Stack '{invalidStackName}' not found."); } @@ -194,7 +194,7 @@ public async Task WhenOnASpecificBranchInTheStack_TheSameBranchIsSetAsCurrentAft inputProvider.Confirm(Questions.ConfirmSyncStack).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false)); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, false)); // Assert repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfRemoteSourceBranch); @@ -238,7 +238,7 @@ public async Task WhenOnlyASingleStackExists_DoesNotAskForStackName_SyncsStack() inputProvider.Confirm(Questions.ConfirmSyncStack).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false)); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, false)); // Assert repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfRemoteSourceBranch); @@ -290,7 +290,7 @@ public async Task WhenUsingRebase_ChangesExistOnTheSourceBranchOnTheRemote_Pulls inputProvider.Confirm(Questions.ConfirmSyncStack).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, true, false, false)); + await handler.Handle(new SyncStackCommandInputs(null, 5, true, false, false, false)); // Assert var tipOfBranch1 = repo.GetTipOfBranch(branch1); @@ -342,7 +342,7 @@ public async Task WhenUsingMerge_ChangesExistOnTheSourceBranchOnTheRemote_PullsC inputProvider.Confirm(Questions.ConfirmSyncStack).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, true, false)); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, true, false, false)); // Assert var tipOfBranch1 = repo.GetTipOfBranch(branch1); @@ -395,7 +395,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndUpdateSettingIsRebase_DoesSy inputProvider.Confirm(Questions.ConfirmSyncStack).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false)); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false)); // Assert var tipOfBranch1 = repo.GetTipOfBranch(branch1); @@ -448,7 +448,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndUpdateSettingIsMerge_DoesSyn inputProvider.Confirm(Questions.ConfirmSyncStack).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false)); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false)); // Assert var tipOfBranch1 = repo.GetTipOfBranch(branch1); @@ -501,7 +501,7 @@ public async Task WhenGitConfigValueIsSetToMerge_ButRebaseIsSpecified_DoesSyncUs inputProvider.Confirm(Questions.ConfirmSyncStack).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, true, null, false)); + await handler.Handle(new SyncStackCommandInputs(null, 5, true, null, false, false)); // Assert var tipOfBranch1 = repo.GetTipOfBranch(branch1); @@ -554,7 +554,7 @@ public async Task WhenGitConfigValueIsSetToRebase_ButMergeIsSpecified_DoesSyncUs inputProvider.Confirm(Questions.ConfirmSyncStack).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, true, false)); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, true, false, false)); // Assert var tipOfBranch1 = repo.GetTipOfBranch(branch1); @@ -577,7 +577,7 @@ public async Task WhenBothRebaseAndMergeAreSpecified_AnErrorIsThrown() // Act and assert await handler - .Invoking(h => h.Handle(new SyncStackCommandInputs(null, 5, true, true, false))) + .Invoking(h => h.Handle(new SyncStackCommandInputs(null, 5, true, true, false, false))) .Should().ThrowAsync() .WithMessage("Cannot specify both rebase and merge."); } @@ -622,11 +622,61 @@ public async Task WhenConfirmOptionIsProvided_DoesNotAskForConfirmation() inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, true)); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, true, false)); // Assert repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfRemoteSourceBranch); repo.GetCommitsReachableFromRemoteBranch(branch2).Should().Contain(tipOfRemoteSourceBranch); inputProvider.DidNotReceive().Confirm(Questions.ConfirmSyncStack); } + + [Fact] + public async Task WhenNoPushOptionIsProvided_DoesNotPushChangesToRemote() + { + // Arrange + var sourceBranch = Some.BranchName(); + var branch1 = Some.BranchName(); + var branch2 = Some.BranchName(); + using var repo = new TestGitRepositoryBuilder() + .WithBranch(builder => builder.WithName(sourceBranch).PushToRemote()) + .WithBranch(builder => builder.WithName(branch1).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(10).PushToRemote()) + .WithBranch(builder => builder.WithName(branch2).FromSourceBranch(branch1).WithNumberOfEmptyCommits(1).PushToRemote()) + .WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf(sourceBranch, 5, b => b.PushToRemote()) + .Build(); + + var tipOfRemoteSourceBranch = repo.GetTipOfRemoteBranch(sourceBranch); + repo.GetCommitsReachableFromBranch(sourceBranch).Should().NotContain(tipOfRemoteSourceBranch); + + var stackConfig = new TestStackConfigBuilder() + .WithStack(stack => stack + .WithName("Stack1") + .WithRemoteUri(repo.RemoteUri) + .WithSourceBranch(sourceBranch) + .WithBranch(stackBranch => stackBranch.WithName(branch1)) + .WithBranch(stackBranch => stackBranch.WithName(branch2))) + .WithStack(stack => stack + .WithName("Stack2") + .WithRemoteUri(repo.RemoteUri) + .WithSourceBranch(sourceBranch)) + .Build(); + var inputProvider = Substitute.For(); + var logger = new TestLogger(testOutputHelper); + var gitClient = new GitClient(logger, repo.GitClientSettings); + var gitHubClient = Substitute.For(); + var handler = new SyncStackCommandHandler(inputProvider, logger, gitClient, gitHubClient, stackConfig); + + gitClient.ChangeBranch(branch1); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + inputProvider.Confirm(Questions.ConfirmSyncStack).Returns(true); + + // Act + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, true)); + + // Assert + repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfRemoteSourceBranch); + repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfRemoteSourceBranch); + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().NotContain(tipOfRemoteSourceBranch); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().NotContain(tipOfRemoteSourceBranch); + } } diff --git a/src/Stack/Commands/Remote/SyncStackCommand.cs b/src/Stack/Commands/Remote/SyncStackCommand.cs index 6fb7da9f..bf7d5b06 100644 --- a/src/Stack/Commands/Remote/SyncStackCommand.cs +++ b/src/Stack/Commands/Remote/SyncStackCommand.cs @@ -8,6 +8,11 @@ namespace Stack.Commands; public class SyncStackCommand : Command { + readonly Option NoPush = new("--no-push") + { + Description = "Don't push changes to the remote repository" + }; + public SyncStackCommand() : base("sync", "Sync a stack with the remote repository. Shortcut for `git fetch --prune`, `stack pull`, `stack update` and `stack push`.") { Add(CommonOptions.Stack); @@ -15,6 +20,7 @@ public class SyncStackCommand : Command Add(CommonOptions.Rebase); Add(CommonOptions.Merge); Add(CommonOptions.Confirm); + Add(NoPush); } protected override async Task Execute(ParseResult parseResult, CancellationToken cancellationToken) @@ -31,7 +37,8 @@ await handler.Handle(new SyncStackCommandInputs( parseResult.GetValue(CommonOptions.MaxBatchSize), parseResult.GetValue(CommonOptions.Rebase), parseResult.GetValue(CommonOptions.Merge), - parseResult.GetValue(CommonOptions.Confirm))); + parseResult.GetValue(CommonOptions.Confirm), + parseResult.GetValue(NoPush))); } } @@ -40,9 +47,10 @@ public record SyncStackCommandInputs( int MaxBatchSize, bool? Rebase, bool? Merge, - bool Confirm) + bool Confirm, + bool NoPush) { - public static SyncStackCommandInputs Empty => new(null, 5, null, null, false); + public static SyncStackCommandInputs Empty => new(null, 5, null, null, false, false); } public class SyncStackCommandHandler( @@ -108,7 +116,8 @@ public override async Task Handle(SyncStackCommandInputs inputs) var forceWithLease = inputs.Rebase == true || StackHelpers.GetUpdateStrategyConfigValue(gitClient) == UpdateStrategy.Rebase; - StackHelpers.PushChanges(stack, inputs.MaxBatchSize, forceWithLease, gitClient, logger); + if (!inputs.NoPush) + StackHelpers.PushChanges(stack, inputs.MaxBatchSize, forceWithLease, gitClient, logger); if (stack.SourceBranch.Equals(currentBranch, StringComparison.InvariantCultureIgnoreCase) || stack.AllBranchNames.Contains(currentBranch, StringComparer.OrdinalIgnoreCase))