From 706e36efe0b3fbcb07a10c0b6513707dacba89d7 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 4 Jul 2025 08:15:34 +1000 Subject: [PATCH] Add options for supplying child branch action when removing branch --- README.md | 14 +-- .../Branch/RemoveBranchCommandHandlerTests.cs | 88 +++++++++++++++++++ .../Commands/Branch/RemoveBranchCommand.cs | 37 ++++++-- 3 files changed, 126 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a2c0cca8..993a5410 100644 --- a/README.md +++ b/README.md @@ -319,12 +319,14 @@ Usage: stack branch remove [options] Options: - --working-dir The path to the directory containing the git repository. Defaults to the current directory. - --verbose Show verbose output. - -s, --stack The name of the stack. - -b, --branch The name of the branch. - -y, --yes Confirm the command without prompting. - -?, -h, --help Show help and usage information + --working-dir The path to the directory containing the git repository. Defaults to the current directory. + --verbose Show verbose output. + -s, --stack The name of the stack. + -b, --branch The name of the branch. + -y, --yes Confirm the command without prompting. + --remove-children Remove children branches. + --move-children-to-parent Move children branches to the parent branch. + -?, -h, --help Show help and usage information ``` ### Remote commands diff --git a/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs b/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs index 121672c8..ab667311 100644 --- a/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs @@ -413,4 +413,92 @@ public async Task WhenSchemaIsV1_RemovesBranchAndMovesChildrenToParent() new("Stack1", repo.RemoteUri, sourceBranch, [new Config.Branch(childBranch, [])]) }); } + + [Fact] + public async Task WhenSchemaIsV2_AndRemoveChildrenIsProvided_RemovesBranchAndDeletesChildren() + { + // Arrange + var sourceBranch = Some.BranchName(); + var branchToRemove = Some.BranchName(); + var childBranch = Some.BranchName(); + using var repo = new TestGitRepositoryBuilder() + .WithBranch(sourceBranch) + .WithBranch(branchToRemove) + .WithBranch(childBranch) + .Build(); + + var stackConfig = new TestStackConfigBuilder() + .WithSchemaVersion(SchemaVersion.V2) + .WithStack(stack => stack + .WithName("Stack1") + .WithRemoteUri(repo.RemoteUri) + .WithSourceBranch(sourceBranch) + .WithBranch(b => b.WithName(branchToRemove).WithChildBranch(b => b.WithName(childBranch)))) + .Build(); + var inputProvider = Substitute.For(); + var logger = new TestLogger(testOutputHelper); + var gitClient = new GitClient(logger, repo.GitClientSettings); + var handler = new RemoveBranchCommandHandler(inputProvider, logger, gitClient, stackConfig); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + inputProvider.Select(Questions.SelectBranch, Arg.Any()).Returns(branchToRemove); + inputProvider.Select(Questions.RemoveBranchChildAction, Arg.Any(), Arg.Any>()) + .Returns(RemoveBranchChildAction.RemoveChildren); + inputProvider.Confirm(Questions.ConfirmRemoveBranch).Returns(true); + + // Act + await handler.Handle(new RemoveBranchCommandInputs(null, null, false, RemoveBranchChildAction.RemoveChildren)); + + // Assert + stackConfig.Stacks.Should().BeEquivalentTo(new List + { + new("Stack1", repo.RemoteUri, sourceBranch, []) + }); + + inputProvider.DidNotReceive().Select(Questions.RemoveBranchChildAction, Arg.Any(), Arg.Any>()); + } + + [Fact] + public async Task WhenSchemaIsV2_AndMoveChildrenToParentIsProvided_RemovesBranchAndMovesChildrenToParent() + { + // Arrange + var sourceBranch = Some.BranchName(); + var branchToRemove = Some.BranchName(); + var childBranch = Some.BranchName(); + using var repo = new TestGitRepositoryBuilder() + .WithBranch(sourceBranch) + .WithBranch(branchToRemove) + .WithBranch(childBranch) + .Build(); + + var stackConfig = new TestStackConfigBuilder() + .WithSchemaVersion(SchemaVersion.V2) + .WithStack(stack => stack + .WithName("Stack1") + .WithRemoteUri(repo.RemoteUri) + .WithSourceBranch(sourceBranch) + .WithBranch(b => b.WithName(branchToRemove).WithChildBranch(b => b.WithName(childBranch)))) + .Build(); + var inputProvider = Substitute.For(); + var logger = new TestLogger(testOutputHelper); + var gitClient = new GitClient(logger, repo.GitClientSettings); + var handler = new RemoveBranchCommandHandler(inputProvider, logger, gitClient, stackConfig); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + inputProvider.Select(Questions.SelectBranch, Arg.Any()).Returns(branchToRemove); + inputProvider.Select(Questions.RemoveBranchChildAction, Arg.Any(), Arg.Any>()) + .Returns(RemoveBranchChildAction.MoveChildrenToParent); + inputProvider.Confirm(Questions.ConfirmRemoveBranch).Returns(true); + + // Act + await handler.Handle(new RemoveBranchCommandInputs(null, null, false, RemoveBranchChildAction.MoveChildrenToParent)); + + // Assert + stackConfig.Stacks.Should().BeEquivalentTo(new List + { + new("Stack1", repo.RemoteUri, sourceBranch, [new Config.Branch(childBranch, [])]) + }); + + inputProvider.DidNotReceive().Select(Questions.RemoveBranchChildAction, Arg.Any(), Arg.Any>()); + } } diff --git a/src/Stack/Commands/Branch/RemoveBranchCommand.cs b/src/Stack/Commands/Branch/RemoveBranchCommand.cs index 7cdae3f7..3430e24d 100644 --- a/src/Stack/Commands/Branch/RemoveBranchCommand.cs +++ b/src/Stack/Commands/Branch/RemoveBranchCommand.cs @@ -9,11 +9,23 @@ namespace Stack.Commands; public class RemoveBranchCommand : Command { + static readonly Option RemoveChildren = new("--remove-children") + { + Description = "Remove children branches." + }; + + static readonly Option MoveChildrenToParent = new("--move-children-to-parent") + { + Description = "Move children branches to the parent branch." + }; + public RemoveBranchCommand() : base("remove", "Remove a branch from a stack.") { Add(CommonOptions.Stack); Add(CommonOptions.Branch); Add(CommonOptions.Confirm); + Add(RemoveChildren); + Add(MoveChildrenToParent); } protected override async Task Execute(ParseResult parseResult, CancellationToken cancellationToken) @@ -24,16 +36,25 @@ protected override async Task Execute(ParseResult parseResult, CancellationToken new GitClient(StdErrLogger, new GitClientSettings(Verbose, WorkingDirectory)), new FileStackConfig()); + var removeChildren = parseResult.GetValue(RemoveChildren); + var moveChildrenToParent = parseResult.GetValue(MoveChildrenToParent); + + if (removeChildren && moveChildrenToParent) + { + throw new InvalidOperationException("Cannot specify both --remove-children and --move-children-to-parent options."); + } + await handler.Handle(new RemoveBranchCommandInputs( parseResult.GetValue(CommonOptions.Stack), parseResult.GetValue(CommonOptions.Branch), - parseResult.GetValue(CommonOptions.Confirm))); + parseResult.GetValue(CommonOptions.Confirm), + removeChildren ? RemoveBranchChildAction.RemoveChildren : moveChildrenToParent ? RemoveBranchChildAction.MoveChildrenToParent : null)); } } -public record RemoveBranchCommandInputs(string? StackName, string? BranchName, bool Confirm) +public record RemoveBranchCommandInputs(string? StackName, string? BranchName, bool Confirm, RemoveBranchChildAction? RemoveChildrenAction = null) { - public static RemoveBranchCommandInputs Empty => new(null, null, false); + public static RemoveBranchCommandInputs Empty => new(null, null, false, null); } public class RemoveBranchCommandHandler( @@ -70,10 +91,12 @@ public override async Task Handle(RemoveBranchCommandInputs inputs) if (stackData.SchemaVersion == SchemaVersion.V2) { - action = inputProvider.Select( - Questions.RemoveBranchChildAction, - [RemoveBranchChildAction.MoveChildrenToParent, RemoveBranchChildAction.RemoveChildren], - (action) => action.Humanize()); + action = + inputs.RemoveChildrenAction ?? + inputProvider.Select( + Questions.RemoveBranchChildAction, + [RemoveBranchChildAction.MoveChildrenToParent, RemoveBranchChildAction.RemoveChildren], + (action) => action.Humanize()); } if (inputs.Confirm || inputProvider.Confirm(Questions.ConfirmRemoveBranch))