Skip to content

Commit

Permalink
Adds --push option when creating new branches (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
geofflamrock authored Dec 30, 2024
1 parent 41d26ba commit 0f3ed93
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 38 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ OPTIONS:
-n, --name The name of the stack. Must be unique
-s, --source-branch The source branch to use for the new branch. Defaults to the default branch for the repository
-b, --branch The name of the branch to create within the stack
--push Push the new branch to the remote repository
```

### `stack list`
Expand Down Expand Up @@ -243,6 +244,7 @@ OPTIONS:
--dry-run Show what would happen without making any changes
-s, --stack The name of the stack to create the branch in
-n, --name The name of the branch to create
--push Push the new branch to the remote repository
```

### `stack branch add`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
using FluentAssertions;
using NSubstitute;
using Spectre.Console;
using Stack.Commands;
using Stack.Commands.Helpers;
using Stack.Config;
using Stack.Git;
using Stack.Infrastructure;
using Stack.Tests.Helpers;

namespace Stack.Tests.Commands.Stack;
namespace Stack.Tests.Commands.Branch;

public class AddBranchCommandHandlerTests
{
Expand Down
62 changes: 54 additions & 8 deletions src/Stack.Tests/Commands/Branch/NewBranchCommandHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Stack.Tests.Commands.Branch;
public class NewBranchCommandHandlerTests
{
[Fact]
public async Task WhenNoInputsProvided_AsksForStackAndBranchAndConfirms_CreatesAndAddsBranchToStackAndSwitchesToBranch()
public async Task WhenNoInputsProvided_AsksForStackAndBranchAndConfirms_CreatesAndAddsBranchToStack_DoesNotPushToRemote_AndSwitchesToBranch()
{
// Arrange
var sourceBranch = Some.BranchName();
Expand Down Expand Up @@ -53,6 +53,7 @@ public async Task WhenNoInputsProvided_AsksForStackAndBranchAndConfirms_CreatesA
new("Stack2", repo.RemoteUri, sourceBranch, [])
});
gitOperations.GetCurrentBranch().Should().Be(newBranch);
repo.GetBranches().Should().Contain(b => b.FriendlyName == newBranch && !b.IsTracking);
}

[Fact]
Expand Down Expand Up @@ -130,7 +131,7 @@ public async Task WhenStackNameProvided_DoesNotAskForStackName_CreatesAndAddsBra
inputProvider.Text(Questions.BranchName, Arg.Any<string>()).Returns(newBranch);

// Act
await handler.Handle(new NewBranchCommandInputs("Stack1", null, false));
await handler.Handle(new NewBranchCommandInputs("Stack1", null, false, false));

// Assert
inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any<string[]>());
Expand Down Expand Up @@ -171,7 +172,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_CreatesAndAddsBr
inputProvider.Text(Questions.BranchName, Arg.Any<string>()).Returns(newBranch);

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

// Assert
inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any<string[]>());
Expand Down Expand Up @@ -208,7 +209,7 @@ public async Task WhenStackNameProvided_ButStackDoesNotExist_Throws()

// Act and assert
var invalidStackName = Some.Name();
await handler.Invoking(async h => await h.Handle(new NewBranchCommandInputs(invalidStackName, null, false)))
await handler.Invoking(async h => await h.Handle(new NewBranchCommandInputs(invalidStackName, null, false, false)))
.Should()
.ThrowAsync<InvalidOperationException>()
.WithMessage($"Stack '{invalidStackName}' not found.");
Expand Down Expand Up @@ -245,7 +246,7 @@ public async Task WhenBranchNameProvided_DoesNotAskForBranchName_CreatesAndAddsB
inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>()).Returns("Stack1");

// Act
await handler.Handle(new NewBranchCommandInputs(null, newBranch, false));
await handler.Handle(new NewBranchCommandInputs(null, newBranch, false, false));

// Assert
stacks.Should().BeEquivalentTo(new List<Config.Stack>
Expand Down Expand Up @@ -284,7 +285,7 @@ public async Task WhenBranchNameProvided_ButBranchAlreadyExistLocally_Throws()

// Act and assert
var invalidBranchName = Some.Name();
await handler.Invoking(async h => await h.Handle(new NewBranchCommandInputs(null, anotherBranch, false)))
await handler.Invoking(async h => await h.Handle(new NewBranchCommandInputs(null, anotherBranch, false, false)))
.Should()
.ThrowAsync<InvalidOperationException>()
.WithMessage($"Branch '{anotherBranch}' already exists locally.");
Expand Down Expand Up @@ -318,7 +319,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 NewBranchCommandInputs(null, newBranch, false)))
await handler.Invoking(async h => await h.Handle(new NewBranchCommandInputs(null, newBranch, false, false)))
.Should()
.ThrowAsync<InvalidOperationException>()
.WithMessage($"Branch '{newBranch}' already exists in stack 'Stack1'.");
Expand Down Expand Up @@ -353,7 +354,7 @@ public async Task WhenAllInputsProvided_DoesNotAskForAnything_CreatesAndAddsBran
.Do(ci => stacks = ci.ArgAt<List<Config.Stack>>(0));

// Act
await handler.Handle(new NewBranchCommandInputs("Stack1", newBranch, true));
await handler.Handle(new NewBranchCommandInputs("Stack1", newBranch, true, false));

// Assert
stacks.Should().BeEquivalentTo(new List<Config.Stack>
Expand Down Expand Up @@ -407,4 +408,49 @@ public async Task WhenStackHasANameWithMultipleWords_SuggestsAGoodDefaultNewBran
});
gitOperations.GetCurrentBranch().Should().Be(newBranch);
}

[Fact]
public async Task WhenPushingToTheRemote_CreatesAndAddsBranchToStack_AndPushesBranchToTheRemote()
{
// Arrange
var sourceBranch = Some.BranchName();
var anotherBranch = Some.BranchName();
var newBranch = Some.BranchName();
using var repo = new TestGitRepositoryBuilder()
.WithBranch(sourceBranch)
.WithBranch(anotherBranch)
.Build();

var stackConfig = Substitute.For<IStackConfig>();
var inputProvider = Substitute.For<IInputProvider>();
var outputProvider = Substitute.For<IOutputProvider>();
var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings);
var handler = new NewBranchCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig);

var stacks = new List<Config.Stack>(
[
new("Stack1", repo.RemoteUri, sourceBranch, [anotherBranch]),
new("Stack2", repo.RemoteUri, sourceBranch, [])
]);
stackConfig.Load().Returns(stacks);
stackConfig
.WhenForAnyArgs(s => s.Save(Arg.Any<List<Config.Stack>>()))
.Do(ci => stacks = ci.ArgAt<List<Config.Stack>>(0));

inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>()).Returns("Stack1");
inputProvider.Text(Questions.BranchName, Arg.Any<string>()).Returns(newBranch);
inputProvider.Confirm(Questions.ConfirmSwitchToBranch).Returns(false);

// Act
await handler.Handle(new NewBranchCommandInputs(null, null, false, true));

// Assert
stacks.Should().BeEquivalentTo(new List<Config.Stack>
{
new("Stack1", repo.RemoteUri, sourceBranch, [anotherBranch, newBranch]),
new("Stack2", repo.RemoteUri, sourceBranch, [])
});
gitOperations.GetCurrentBranch().Should().NotBe(newBranch);
repo.GetBranches().Should().Contain(b => b.FriendlyName == newBranch && b.IsTracking);
}
}
54 changes: 49 additions & 5 deletions src/Stack.Tests/Commands/Stack/NewStackCommandHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Stack.Tests.Commands.Stack;
public class NewStackCommandHandlerTests
{
[Fact]
public async Task WithANewBranch_AndSwitchingToTheBranch_TheStackIsCreatedAndTheCurrentBranchIsChanged()
public async Task WithANewBranch_AndSwitchingToTheBranch_TheStackIsCreated_DoesNotPushToRemote_AndTheCurrentBranchIsChanged()
{
// Arrange
var sourceBranch = Some.BranchName();
Expand Down Expand Up @@ -52,6 +52,7 @@ public async Task WithANewBranch_AndSwitchingToTheBranch_TheStackIsCreatedAndThe
});

gitOperations.GetCurrentBranch().Should().Be(newBranch);
repo.GetBranches().Should().Contain(b => b.FriendlyName == newBranch && !b.IsTracking);
}

[Fact]
Expand Down Expand Up @@ -259,7 +260,7 @@ public async Task WhenStackNameIsProvidedInInputs_TheProviderIsNotAskedForAName_
inputProvider.Select(Questions.SelectSourceBranch, Arg.Any<string[]>()).Returns(sourceBranch);
inputProvider.Confirm(Questions.ConfirmAddOrCreateBranch).Returns(false);

var inputs = new NewStackCommandInputs("Stack1", null, null);
var inputs = new NewStackCommandInputs("Stack1", null, null, false);

// Act
var response = await handler.Handle(inputs);
Expand Down Expand Up @@ -299,7 +300,7 @@ public async Task WhenSourceBranchIsProvidedInInputs_TheProviderIsNotAskedForThe
inputProvider.Text(Questions.StackName).Returns("Stack1");
inputProvider.Confirm(Questions.ConfirmAddOrCreateBranch).Returns(false);

var inputs = new NewStackCommandInputs(null, sourceBranch, null);
var inputs = new NewStackCommandInputs(null, sourceBranch, null, false);

// Act
var response = await handler.Handle(inputs);
Expand Down Expand Up @@ -341,7 +342,7 @@ public async Task WhenBranchNameIsProvidedInInputs_TheProviderIsNotAskedForTheBr
inputProvider.Select(Questions.SelectSourceBranch, Arg.Any<string[]>()).Returns(sourceBranch);
// Note there shouldn't be any more inputs required at all

var inputs = new NewStackCommandInputs(null, null, newBranch);
var inputs = new NewStackCommandInputs(null, null, newBranch, false);

// Act
var response = await handler.Handle(inputs);
Expand Down Expand Up @@ -382,7 +383,7 @@ public async Task WhenAllInputsAreProvided_TheProviderIsNotAskedForAnything_AndT
.WhenForAnyArgs(s => s.Save(Arg.Any<List<Config.Stack>>()))
.Do(ci => stacks = ci.ArgAt<List<Config.Stack>>(0));

var inputs = new NewStackCommandInputs("Stack1", sourceBranch, newBranch);
var inputs = new NewStackCommandInputs("Stack1", sourceBranch, newBranch, true);

// Act
var response = await handler.Handle(inputs);
Expand Down Expand Up @@ -439,4 +440,47 @@ public async Task WhenAStackHasANameWithMultipleWords_SuggestsAGoodDefaultNewBra

gitOperations.GetCurrentBranch().Should().Be(newBranch);
}

[Fact]
public async Task WithANewBranch_AndPushingTheBranchToTheRemote_TheStackIsCreatedAndTheBranchExistsOnTheRemote()
{
// Arrange
var sourceBranch = Some.BranchName();
var newBranch = Some.BranchName();
using var repo = new TestGitRepositoryBuilder()
.WithBranch(sourceBranch)
.Build();

var stackConfig = Substitute.For<IStackConfig>();
var inputProvider = Substitute.For<IInputProvider>();
var outputProvider = Substitute.For<IOutputProvider>();
var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings);
var gitHubOperations = Substitute.For<IGitHubOperations>();
var handler = new NewStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig);

var stacks = new List<Config.Stack>();
stackConfig.Load().Returns(stacks);
stackConfig
.WhenForAnyArgs(s => s.Save(Arg.Any<List<Config.Stack>>()))
.Do(ci => stacks = ci.ArgAt<List<Config.Stack>>(0));

inputProvider.Text(Questions.StackName).Returns("Stack1");
inputProvider.Select(Questions.SelectSourceBranch, Arg.Any<string[]>()).Returns(sourceBranch);
inputProvider.Confirm(Questions.ConfirmAddOrCreateBranch).Returns(true);
inputProvider.Select(Questions.AddOrCreateBranch, Arg.Any<BranchAction[]>(), Arg.Any<Func<BranchAction, string>>()).Returns(BranchAction.Create);
inputProvider.Text(Questions.BranchName, Arg.Any<string>()).Returns(newBranch);
inputProvider.Confirm(Questions.ConfirmSwitchToBranch).Returns(false);

// Act
var response = await handler.Handle(new NewStackCommandInputs(null, null, null, true));

// Assert
response.Should().BeEquivalentTo(new NewStackCommandResponse("Stack1", sourceBranch, BranchAction.Create, newBranch));
stacks.Should().BeEquivalentTo(new List<Config.Stack>
{
new("Stack1", repo.RemoteUri, sourceBranch, [newBranch])
});

repo.GetBranches().Should().Contain(b => b.FriendlyName == newBranch && b.IsTracking);
}
}
5 changes: 5 additions & 0 deletions src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,11 @@ public LibGit2Sharp.Commit GetTipOfRemoteBranch(string branchName)
return [.. LocalRepository.Branches[remoteBranchName].Commits];
}

public List<LibGit2Sharp.Branch> GetBranches()
{
return [.. LocalRepository.Branches];
}

public void Dispose()
{
GC.SuppressFinalize(this);
Expand Down
23 changes: 18 additions & 5 deletions src/Stack/Commands/Branch/NewBranchCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public class NewBranchCommandSettings : DryRunCommandSettingsBase
[Description("Force creating the branch without prompting.")]
[CommandOption("-f|--force")]
public bool Force { get; init; }

[Description("Push the new branch to the remote repository.")]
[CommandOption("--push")]
public bool Push { get; init; }
}

public class NewBranchCommand : AsyncCommand<NewBranchCommandSettings>
Expand All @@ -38,15 +42,15 @@ public override async Task<int> ExecuteAsync(CommandContext context, NewBranchCo
new GitOperations(outputProvider, settings.GetGitOperationSettings()),
new StackConfig());

await handler.Handle(new NewBranchCommandInputs(settings.Stack, settings.Name, settings.Force));
await handler.Handle(new NewBranchCommandInputs(settings.Stack, settings.Name, settings.Force, settings.Push));

return 0;
}
}

public record NewBranchCommandInputs(string? StackName, string? BranchName, bool Force)
public record NewBranchCommandInputs(string? StackName, string? BranchName, bool Force, bool Push)
{
public static NewBranchCommandInputs Empty => new(null, null, false);
public static NewBranchCommandInputs Empty => new(null, null, false, false);
}

public record NewBranchCommandResponse();
Expand Down Expand Up @@ -99,13 +103,22 @@ public async Task<NewBranchCommandResponse> Handle(NewBranchCommandInputs inputs
outputProvider.Information($"Creating branch {branchName.Branch()} from {sourceBranch.Branch()} in stack {stack.Name.Stack()}");

gitOperations.CreateNewBranch(branchName, sourceBranch);
gitOperations.PushNewBranch(branchName);

stack.Branches.Add(branchName);

stackConfig.Save(stacks);

outputProvider.Information($"Branch created");
if (inputs.Push)
{
gitOperations.PushNewBranch(branchName);
}

outputProvider.Information($"Branch {branchName.Branch()} created.");

if (!inputs.Push)
{
outputProvider.Information($"Use {$"stack push --name \"{stack.Name}\"".Example()} to push the branch to the remote repository.");
}

if (inputs.Force || inputProvider.Confirm(Questions.ConfirmSwitchToBranch))
{
Expand Down
Loading

0 comments on commit 0f3ed93

Please sign in to comment.