diff --git a/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs b/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs index b41de55b..121672c8 100644 --- a/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs @@ -289,4 +289,128 @@ public async Task WhenConfirmProvided_DoesNotAskForConfirmation_RemovesBranchFro }); inputProvider.DidNotReceive().Confirm(Questions.ConfirmRemoveBranch); } + + [Fact] + public async Task WhenSchemaIsV2_AndChildActionIsMoveChildrenToParent_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(RemoveBranchCommandInputs.Empty); + + // Assert + stackConfig.Stacks.Should().BeEquivalentTo(new List + { + new("Stack1", repo.RemoteUri, sourceBranch, [new Config.Branch(childBranch, [])]) + }); + } + + [Fact] + public async Task WhenSchemaIsV2_AndChildActionIsRemoveChildren_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(RemoveBranchCommandInputs.Empty); + + // Assert + stackConfig.Stacks.Should().BeEquivalentTo(new List + { + new("Stack1", repo.RemoteUri, sourceBranch, []) + }); + } + + [Fact] + public async Task WhenSchemaIsV1_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.V1) + .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.Confirm(Questions.ConfirmRemoveBranch).Returns(true); + + // Act + await handler.Handle(RemoveBranchCommandInputs.Empty); + + // Assert + stackConfig.Stacks.Should().BeEquivalentTo(new List + { + new("Stack1", repo.RemoteUri, sourceBranch, [new Config.Branch(childBranch, [])]) + }); + } } diff --git a/src/Stack.Tests/Helpers/TestStackConfigBuilder.cs b/src/Stack.Tests/Helpers/TestStackConfigBuilder.cs index e5ccacd5..4af17276 100644 --- a/src/Stack.Tests/Helpers/TestStackConfigBuilder.cs +++ b/src/Stack.Tests/Helpers/TestStackConfigBuilder.cs @@ -15,9 +15,9 @@ public TestStackConfigBuilder WithStack(Action stackBuilder) return this; } - public TestStackConfigBuilder WithSchemaVersion(SchemaVersion schemaVersion) + public TestStackConfigBuilder WithSchemaVersion(SchemaVersion version) { - this.schemaVersion = schemaVersion; + schemaVersion = version; return this; } diff --git a/src/Stack/Commands/Branch/RemoveBranchCommand.cs b/src/Stack/Commands/Branch/RemoveBranchCommand.cs index 70d745ac..3948f6c2 100644 --- a/src/Stack/Commands/Branch/RemoveBranchCommand.cs +++ b/src/Stack/Commands/Branch/RemoveBranchCommand.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using Humanizer; using Spectre.Console.Cli; using Stack.Commands.Helpers; using Stack.Config; @@ -71,9 +72,19 @@ public override async Task Handle(RemoveBranchCommandInputs inputs) throw new InvalidOperationException($"Branch '{branchName}' not found in stack '{stack.Name}'."); } + var action = RemoveBranchChildAction.MoveChildrenToParent; + + if (stackData.SchemaVersion == SchemaVersion.V2) + { + action = inputProvider.Select( + Questions.RemoveBranchChildAction, + [RemoveBranchChildAction.MoveChildrenToParent, RemoveBranchChildAction.RemoveChildren], + (action) => action.Humanize()); + } + if (inputs.Confirm || inputProvider.Confirm(Questions.ConfirmRemoveBranch)) { - stack.RemoveBranch(branchName); + stack.RemoveBranch(branchName, action); stackConfig.Save(stackData); logger.Information($"Branch {branchName.Branch()} removed from stack {stack.Name.Stack()}"); @@ -81,3 +92,12 @@ public override async Task Handle(RemoveBranchCommandInputs inputs) } } +public enum RemoveBranchChildAction +{ + [Description("Move children branches to parent branch")] + MoveChildrenToParent, + + [Description("Remove children branches")] + RemoveChildren +} + diff --git a/src/Stack/Commands/Helpers/Questions.cs b/src/Stack/Commands/Helpers/Questions.cs index c7f8ac63..77cc28ee 100644 --- a/src/Stack/Commands/Helpers/Questions.cs +++ b/src/Stack/Commands/Helpers/Questions.cs @@ -15,6 +15,7 @@ public static class Questions public const string ConfirmDeleteStack = "Are you sure you want to delete this stack?"; public const string ConfirmDeleteBranches = "Are you sure you want to delete these local branches?"; public const string ConfirmRemoveBranch = "Are you sure you want to remove this branch from the stack?"; + public const string RemoveBranchChildAction = "What do you want to do with the children of this branch?"; public const string AddOrCreateBranch = "Add or create a branch:"; public const string SelectPullRequestsToCreate = "Select branches to create pull requests for:"; public const string ConfirmCreatePullRequests = "Are you sure you want to create pull requests for branches in this stack?"; diff --git a/src/Stack/Config/Stack.cs b/src/Stack/Config/Stack.cs index 131c94a7..4d418851 100644 --- a/src/Stack/Config/Stack.cs +++ b/src/Stack/Config/Stack.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using Humanizer; +using Stack.Commands; namespace Stack.Config; @@ -62,7 +63,7 @@ public string GetDefaultBranchName() return $"{Name.Kebaberize()}-{fullBranchNames.Count + 1}"; } - public void RemoveBranch(string branchName) + public void RemoveBranch(string branchName, RemoveBranchChildAction action) { if (Branches.Count == 0) { @@ -73,35 +74,40 @@ public void RemoveBranch(string branchName) { if (branch.Name.Equals(branchName, StringComparison.OrdinalIgnoreCase)) { - var childrenToMove = branch.Children; Branches.Remove(branch); - branch.Children.AddRange(childrenToMove); + + if (action == RemoveBranchChildAction.MoveChildrenToParent) + { + Branches.AddRange(branch.Children); + } return; } - if (RemoveBranch(branch, branchName)) + if (RemoveBranch(branch, branchName, action)) { return; } } } - static bool RemoveBranch(Branch branch, string branchName) + static bool RemoveBranch(Branch branch, string branchName, RemoveBranchChildAction action) { var childBranch = branch.Children.FirstOrDefault(c => c.Name.Equals(branchName, StringComparison.OrdinalIgnoreCase)); if (childBranch != null) { - // Get all the children of the branch to be removed, - // and add them to the parent of the branch being removed. - var childrenToMove = childBranch.Children; branch.Children.Remove(childBranch); - branch.Children.AddRange(childrenToMove); + if (action == RemoveBranchChildAction.MoveChildrenToParent) + { + // Get all the children of the branch to be removed, + // and add them to the parent of the branch being removed. + branch.Children.AddRange(childBranch.Children); + } return true; } foreach (var child in branch.Children) { - if (RemoveBranch(child, branchName)) + if (RemoveBranch(child, branchName, action)) { return true; }