diff --git a/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs b/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs index dc8f8d91..00e24810 100644 --- a/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs @@ -81,7 +81,7 @@ public async Task WhenStackNameProvided_DoesNotAskForStackName_AddsBranchFromSta inputProvider.Select(Questions.SelectBranch, Arg.Any()).Returns(branchToAdd); // Act - await handler.Handle(new AddBranchCommandInputs("Stack1", null)); + await handler.Handle(new AddBranchCommandInputs("Stack1", null, null)); // Assert inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any()); @@ -120,7 +120,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_AddsBranchFromSt inputProvider.Select(Questions.SelectBranch, Arg.Any()).Returns(branchToAdd); // Act - await handler.Handle(new AddBranchCommandInputs(null, null)); + await handler.Handle(new AddBranchCommandInputs(null, null, null)); // Assert inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any()); @@ -158,7 +158,7 @@ public async Task WhenStackNameProvided_ButStackDoesNotExist_Throws() // Act and assert var invalidStackName = Some.Name(); - await handler.Invoking(async h => await h.Handle(new AddBranchCommandInputs(invalidStackName, null))) + await handler.Invoking(async h => await h.Handle(new AddBranchCommandInputs(invalidStackName, null, null))) .Should() .ThrowAsync() .WithMessage($"Stack '{invalidStackName}' not found."); @@ -193,7 +193,7 @@ public async Task WhenBranchNameProvided_DoesNotAskForBranchName_AddsBranchFromS inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); // Act - await handler.Handle(new AddBranchCommandInputs(null, branchToAdd)); + await handler.Handle(new AddBranchCommandInputs(null, branchToAdd, null)); // Assert stackConfig.Stacks.Should().BeEquivalentTo(new List @@ -234,7 +234,7 @@ public async Task WhenBranchNameProvided_ButBranchDoesNotExistLocally_Throws() // Act and assert var invalidBranchName = Some.BranchName(); - await handler.Invoking(async h => await h.Handle(new AddBranchCommandInputs(null, invalidBranchName))) + await handler.Invoking(async h => await h.Handle(new AddBranchCommandInputs(null, invalidBranchName, null))) .Should() .ThrowAsync() .WithMessage($"Branch '{invalidBranchName}' does not exist locally."); @@ -271,7 +271,7 @@ public async Task WhenBranchNameProvided_ButBranchAlreadyExistsInStack_Throws() inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); // Act and assert - await handler.Invoking(async h => await h.Handle(new AddBranchCommandInputs(null, branchToAdd))) + await handler.Invoking(async h => await h.Handle(new AddBranchCommandInputs(null, branchToAdd, null))) .Should() .ThrowAsync() .WithMessage($"Branch '{branchToAdd}' already exists in stack 'Stack1'."); @@ -304,7 +304,7 @@ public async Task WhenAllInputsProvided_DoesNotAskForAnything_AddsBranchFromStac var handler = new AddBranchCommandHandler(inputProvider, logger, gitClient, stackConfig); // Act - await handler.Handle(new AddBranchCommandInputs("Stack1", branchToAdd)); + await handler.Handle(new AddBranchCommandInputs("Stack1", branchToAdd, null)); // Assert stackConfig.Stacks.Should().BeEquivalentTo(new List @@ -314,4 +314,140 @@ public async Task WhenAllInputsProvided_DoesNotAskForAnything_AddsBranchFromStac }); inputProvider.ReceivedCalls().Should().BeEmpty(); } + + [Fact] + public async Task WhenV2Schema_AndParentBranchNotProvided_AsksForParentBranch_CreatesNewBranchUnderneathParent() + { + // Arrange + var sourceBranch = Some.BranchName(); + var firstBranch = Some.BranchName(); + var childBranch = Some.BranchName(); + var branchToAdd = Some.BranchName(); + using var repo = new TestGitRepositoryBuilder() + .WithBranch(sourceBranch) + .WithBranch(firstBranch) + .WithBranch(childBranch) + .WithBranch(b => b.WithName(branchToAdd).FromSourceBranch(firstBranch)) + .Build(); + + var inputProvider = Substitute.For(); + var logger = new TestLogger(testOutputHelper); + var gitClient = new GitClient(logger, repo.GitClientSettings); + var stackConfig = new TestStackConfigBuilder() + .WithSchemaVersion(SchemaVersion.V2) + .WithStack(stack => stack + .WithName("Stack1") + .WithRemoteUri(repo.RemoteUri) + .WithSourceBranch(sourceBranch) + .WithBranch(branch => branch.WithName(firstBranch).WithChildBranch(child => child.WithName(childBranch)))) + .WithStack(stack => stack + .WithName("Stack2") + .WithRemoteUri(repo.RemoteUri) + .WithSourceBranch(sourceBranch)) + .Build(); + var handler = new AddBranchCommandHandler(inputProvider, logger, gitClient, stackConfig); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + inputProvider.Select(Questions.SelectBranch, Arg.Any()).Returns(branchToAdd); + inputProvider.Select(Questions.SelectParentBranch, Arg.Any()).Returns(firstBranch); + + // Act + await handler.Handle(new AddBranchCommandInputs(null, null, null)); + + // Assert + stackConfig.Stacks.Should().BeEquivalentTo(new List + { + new("Stack1", repo.RemoteUri, sourceBranch, [new Config.Branch(firstBranch, [new Config.Branch(childBranch, []), new Config.Branch(branchToAdd, [])])]), + new("Stack2", repo.RemoteUri, sourceBranch, []) + }); + } + + [Fact] + public async Task WhenV2Schema_AndParentBranchProvided_DoesNotAskForParentBranch_CreatesNewBranchUnderneathParent() + { + // Arrange + var sourceBranch = Some.BranchName(); + var firstBranch = Some.BranchName(); + var childBranch = Some.BranchName(); + var branchToAdd = Some.BranchName(); + using var repo = new TestGitRepositoryBuilder() + .WithBranch(sourceBranch) + .WithBranch(firstBranch) + .WithBranch(childBranch) + .WithBranch(b => b.WithName(branchToAdd).FromSourceBranch(firstBranch)) + .Build(); + + var inputProvider = Substitute.For(); + var logger = new TestLogger(testOutputHelper); + var gitClient = new GitClient(logger, repo.GitClientSettings); + var stackConfig = new TestStackConfigBuilder() + .WithSchemaVersion(SchemaVersion.V2) + .WithStack(stack => stack + .WithName("Stack1") + .WithRemoteUri(repo.RemoteUri) + .WithSourceBranch(sourceBranch) + .WithBranch(branch => branch.WithName(firstBranch).WithChildBranch(child => child.WithName(childBranch)))) + .WithStack(stack => stack + .WithName("Stack2") + .WithRemoteUri(repo.RemoteUri) + .WithSourceBranch(sourceBranch)) + .Build(); + var handler = new AddBranchCommandHandler(inputProvider, logger, gitClient, stackConfig); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + inputProvider.Select(Questions.SelectBranch, Arg.Any()).Returns(branchToAdd); + + // Act + await handler.Handle(new AddBranchCommandInputs(null, null, firstBranch)); + + // Assert + stackConfig.Stacks.Should().BeEquivalentTo(new List + { + new("Stack1", repo.RemoteUri, sourceBranch, [new Config.Branch(firstBranch, [new Config.Branch(childBranch, []), new Config.Branch(branchToAdd, [])])]), + new("Stack2", repo.RemoteUri, sourceBranch, []) + }); + + inputProvider.DidNotReceive().Select(Questions.SelectParentBranch, Arg.Any()); + } + + [Fact] + public async Task WhenV1Schema_AndParentBranchProvided_ThrowsException() + { + // Arrange + var sourceBranch = Some.BranchName(); + var firstBranch = Some.BranchName(); + var childBranch = Some.BranchName(); + var branchToAdd = Some.BranchName(); + using var repo = new TestGitRepositoryBuilder() + .WithBranch(sourceBranch) + .WithBranch(firstBranch) + .WithBranch(childBranch) + .WithBranch(b => b.WithName(branchToAdd).FromSourceBranch(firstBranch)) + .Build(); + + var inputProvider = Substitute.For(); + var logger = new TestLogger(testOutputHelper); + var gitClient = new GitClient(logger, repo.GitClientSettings); + var stackConfig = new TestStackConfigBuilder() + .WithSchemaVersion(SchemaVersion.V1) + .WithStack(stack => stack + .WithName("Stack1") + .WithRemoteUri(repo.RemoteUri) + .WithSourceBranch(sourceBranch) + .WithBranch(branch => branch.WithName(firstBranch).WithChildBranch(child => child.WithName(childBranch)))) + .WithStack(stack => stack + .WithName("Stack2") + .WithRemoteUri(repo.RemoteUri) + .WithSourceBranch(sourceBranch)) + .Build(); + var handler = new AddBranchCommandHandler(inputProvider, logger, gitClient, stackConfig); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + inputProvider.Select(Questions.SelectBranch, Arg.Any()).Returns(branchToAdd); + + // Act and assert + await handler.Invoking(h => h.Handle(new AddBranchCommandInputs(null, null, firstBranch))) + .Should().ThrowAsync() + .WithMessage("Parent branches are not supported in stacks with schema version v1. Please migrate the stack to v2 format."); + } } diff --git a/src/Stack/Commands/Branch/AddBranchCommand.cs b/src/Stack/Commands/Branch/AddBranchCommand.cs index f58b5dab..e66c8fb5 100644 --- a/src/Stack/Commands/Branch/AddBranchCommand.cs +++ b/src/Stack/Commands/Branch/AddBranchCommand.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using Spectre.Console; using Spectre.Console.Cli; using Stack.Commands.Helpers; using Stack.Config; @@ -17,6 +16,10 @@ public class AddBranchCommandSettings : CommandSettingsBase [Description("The name of the branch to add.")] [CommandOption("-n|--name")] public string? Name { get; init; } + + [Description("The name of the parent branch to add branch as a child of.")] + [CommandOption("-p|--parent")] + public string? Parent { get; init; } } public class AddBranchCommand : Command @@ -29,13 +32,13 @@ protected override async Task Execute(AddBranchCommandSettings settings) new GitClient(StdErrLogger, settings.GetGitClientSettings()), new FileStackConfig()); - await handler.Handle(new AddBranchCommandInputs(settings.Stack, settings.Name)); + await handler.Handle(new AddBranchCommandInputs(settings.Stack, settings.Name, settings.Parent)); } } -public record AddBranchCommandInputs(string? StackName, string? BranchName) +public record AddBranchCommandInputs(string? StackName, string? BranchName, string? ParentBranchName) { - public static AddBranchCommandInputs Empty => new(null, null); + public static AddBranchCommandInputs Empty => new(null, null, null); } public class AddBranchCommandHandler( @@ -63,6 +66,11 @@ public override async Task Handle(AddBranchCommandInputs inputs) return; } + if (stackData.SchemaVersion == SchemaVersion.V1 && inputs.ParentBranchName is not null) + { + throw new InvalidOperationException("Parent branches are not supported in stacks with schema version v1. Please migrate the stack to v2 format."); + } + var stack = inputProvider.SelectStack(logger, inputs.StackName, stacksForRemote, currentBranch); if (stack is null) @@ -70,8 +78,6 @@ public override async Task Handle(AddBranchCommandInputs inputs) throw new InvalidOperationException($"Stack '{inputs.StackName}' not found."); } - var deepestChildBranchFromFirstTree = stack.GetDeepestChildBranchFromFirstTree(); - var sourceBranch = deepestChildBranchFromFirstTree?.Name ?? stack.SourceBranch; var branchName = inputProvider.SelectBranch(logger, inputs.BranchName, branches); if (stack.AllBranchNames.Contains(branchName)) @@ -84,12 +90,32 @@ public override async Task Handle(AddBranchCommandInputs inputs) throw new InvalidOperationException($"Branch '{branchName}' does not exist locally."); } + Branch? sourceBranch = null; + + if (stackData.SchemaVersion == SchemaVersion.V1) + { + // In V1 schema there is only a single set of branches, we always add to the end. + sourceBranch = stack.GetAllBranches().LastOrDefault(); + } + if (stackData.SchemaVersion == SchemaVersion.V2) + { + var parentBranchName = inputProvider.SelectParentBranch(logger, inputs.ParentBranchName, stack); + + if (parentBranchName != stack.SourceBranch) + { + sourceBranch = stack.GetAllBranches().FirstOrDefault(b => b.Name.Equals(parentBranchName, StringComparison.OrdinalIgnoreCase)); + if (sourceBranch is null) + { + throw new InvalidOperationException($"Branch '{parentBranchName}' not found in stack '{stack.Name}'."); + } + } + } + logger.Information($"Adding branch {branchName.Branch()} to stack {stack.Name.Stack()}"); - if (deepestChildBranchFromFirstTree is not null) + if (sourceBranch is not null) { - // If the stack has branches, we add the new branch to the first branch's children - deepestChildBranchFromFirstTree.Children.Add(new Branch(branchName, [])); + sourceBranch.Children.Add(new Branch(branchName, [])); } else { diff --git a/src/Stack/Commands/Helpers/InputProviderExtensionMethods.cs b/src/Stack/Commands/Helpers/InputProviderExtensionMethods.cs index ef2971e2..7ec716d2 100644 --- a/src/Stack/Commands/Helpers/InputProviderExtensionMethods.cs +++ b/src/Stack/Commands/Helpers/InputProviderExtensionMethods.cs @@ -79,10 +79,9 @@ public static string SelectBranch( this IInputProvider inputProvider, ILogger logger, string? name, - string[] branches, - string question = Questions.SelectBranch) + string[] branches) { - return inputProvider.Select(logger, question, name, branches); + return inputProvider.Select(logger, Questions.SelectBranch, name, branches); } public static string SelectParentBranch(