Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 143 additions & 7 deletions src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public async Task WhenStackNameProvided_DoesNotAskForStackName_AddsBranchFromSta
inputProvider.Select(Questions.SelectBranch, Arg.Any<string[]>()).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<string[]>());
Expand Down Expand Up @@ -120,7 +120,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_AddsBranchFromSt
inputProvider.Select(Questions.SelectBranch, Arg.Any<string[]>()).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<string[]>());
Expand Down Expand Up @@ -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<InvalidOperationException>()
.WithMessage($"Stack '{invalidStackName}' not found.");
Expand Down Expand Up @@ -193,7 +193,7 @@ public async Task WhenBranchNameProvided_DoesNotAskForBranchName_AddsBranchFromS
inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>()).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<Config.Stack>
Expand Down Expand Up @@ -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<InvalidOperationException>()
.WithMessage($"Branch '{invalidBranchName}' does not exist locally.");
Expand Down Expand Up @@ -271,7 +271,7 @@ public async Task WhenBranchNameProvided_ButBranchAlreadyExistsInStack_Throws()
inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>()).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<InvalidOperationException>()
.WithMessage($"Branch '{branchToAdd}' already exists in stack 'Stack1'.");
Expand Down Expand Up @@ -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<Config.Stack>
Expand All @@ -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<IInputProvider>();
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<string[]>()).Returns("Stack1");
inputProvider.Select(Questions.SelectBranch, Arg.Any<string[]>()).Returns(branchToAdd);
inputProvider.Select(Questions.SelectParentBranch, Arg.Any<string[]>()).Returns(firstBranch);

// Act
await handler.Handle(new AddBranchCommandInputs(null, null, null));

// Assert
stackConfig.Stacks.Should().BeEquivalentTo(new List<Config.Stack>
{
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<IInputProvider>();
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<string[]>()).Returns("Stack1");
inputProvider.Select(Questions.SelectBranch, Arg.Any<string[]>()).Returns(branchToAdd);

// Act
await handler.Handle(new AddBranchCommandInputs(null, null, firstBranch));

// Assert
stackConfig.Stacks.Should().BeEquivalentTo(new List<Config.Stack>
{
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<string[]>());
}

[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<IInputProvider>();
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<string[]>()).Returns("Stack1");
inputProvider.Select(Questions.SelectBranch, Arg.Any<string[]>()).Returns(branchToAdd);

// Act and assert
await handler.Invoking(h => h.Handle(new AddBranchCommandInputs(null, null, firstBranch)))
.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("Parent branches are not supported in stacks with schema version v1. Please migrate the stack to v2 format.");
}
}
44 changes: 35 additions & 9 deletions src/Stack/Commands/Branch/AddBranchCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.ComponentModel;
using Spectre.Console;
using Spectre.Console.Cli;
using Stack.Commands.Helpers;
using Stack.Config;
Expand All @@ -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<AddBranchCommandSettings>
Expand All @@ -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(
Expand Down Expand Up @@ -63,15 +66,18 @@ 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)
{
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))
Expand All @@ -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
{
Expand Down
5 changes: 2 additions & 3 deletions src/Stack/Commands/Helpers/InputProviderExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down