From 0f3ed93dd9b3c7270e9bf284cfa0895dc1563846 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 30 Dec 2024 22:50:41 +1100 Subject: [PATCH] Adds `--push` option when creating new branches (#165) --- README.md | 2 + .../Branch/AddBranchCommandHandlerTests.cs | 3 +- .../Branch/NewBranchCommandHandlerTests.cs | 62 ++++++++++++++++--- .../Stack/NewStackCommandHandlerTests.cs | 54 ++++++++++++++-- .../Helpers/TestGitRepositoryBuilder.cs | 5 ++ src/Stack/Commands/Branch/NewBranchCommand.cs | 23 +++++-- src/Stack/Commands/Stack/NewStackCommand.cs | 49 +++++++++------ 7 files changed, 160 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 02b7c81..0200660 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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` diff --git a/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs b/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs index 3a905fa..005dabc 100644 --- a/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs @@ -1,6 +1,5 @@ using FluentAssertions; using NSubstitute; -using Spectre.Console; using Stack.Commands; using Stack.Commands.Helpers; using Stack.Config; @@ -8,7 +7,7 @@ using Stack.Infrastructure; using Stack.Tests.Helpers; -namespace Stack.Tests.Commands.Stack; +namespace Stack.Tests.Commands.Branch; public class AddBranchCommandHandlerTests { diff --git a/src/Stack.Tests/Commands/Branch/NewBranchCommandHandlerTests.cs b/src/Stack.Tests/Commands/Branch/NewBranchCommandHandlerTests.cs index aec5421..a539978 100644 --- a/src/Stack.Tests/Commands/Branch/NewBranchCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Branch/NewBranchCommandHandlerTests.cs @@ -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(); @@ -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] @@ -130,7 +131,7 @@ public async Task WhenStackNameProvided_DoesNotAskForStackName_CreatesAndAddsBra inputProvider.Text(Questions.BranchName, Arg.Any()).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()); @@ -171,7 +172,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_CreatesAndAddsBr inputProvider.Text(Questions.BranchName, Arg.Any()).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()); @@ -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() .WithMessage($"Stack '{invalidStackName}' not found."); @@ -245,7 +246,7 @@ public async Task WhenBranchNameProvided_DoesNotAskForBranchName_CreatesAndAddsB inputProvider.Select(Questions.SelectStack, Arg.Any()).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 @@ -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() .WithMessage($"Branch '{anotherBranch}' already exists locally."); @@ -318,7 +319,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 NewBranchCommandInputs(null, newBranch, false))) + await handler.Invoking(async h => await h.Handle(new NewBranchCommandInputs(null, newBranch, false, false))) .Should() .ThrowAsync() .WithMessage($"Branch '{newBranch}' already exists in stack 'Stack1'."); @@ -353,7 +354,7 @@ public async Task WhenAllInputsProvided_DoesNotAskForAnything_CreatesAndAddsBran .Do(ci => stacks = ci.ArgAt>(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 @@ -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(); + var inputProvider = Substitute.For(); + var outputProvider = Substitute.For(); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var handler = new NewBranchCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + var stacks = new List( + [ + new("Stack1", repo.RemoteUri, sourceBranch, [anotherBranch]), + new("Stack2", repo.RemoteUri, sourceBranch, []) + ]); + stackConfig.Load().Returns(stacks); + stackConfig + .WhenForAnyArgs(s => s.Save(Arg.Any>())) + .Do(ci => stacks = ci.ArgAt>(0)); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + inputProvider.Text(Questions.BranchName, Arg.Any()).Returns(newBranch); + inputProvider.Confirm(Questions.ConfirmSwitchToBranch).Returns(false); + + // Act + await handler.Handle(new NewBranchCommandInputs(null, null, false, true)); + + // Assert + stacks.Should().BeEquivalentTo(new List + { + 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); + } } diff --git a/src/Stack.Tests/Commands/Stack/NewStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/NewStackCommandHandlerTests.cs index c37a384..1ac92b6 100644 --- a/src/Stack.Tests/Commands/Stack/NewStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/NewStackCommandHandlerTests.cs @@ -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(); @@ -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] @@ -259,7 +260,7 @@ public async Task WhenStackNameIsProvidedInInputs_TheProviderIsNotAskedForAName_ inputProvider.Select(Questions.SelectSourceBranch, Arg.Any()).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); @@ -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); @@ -341,7 +342,7 @@ public async Task WhenBranchNameIsProvidedInInputs_TheProviderIsNotAskedForTheBr inputProvider.Select(Questions.SelectSourceBranch, Arg.Any()).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); @@ -382,7 +383,7 @@ public async Task WhenAllInputsAreProvided_TheProviderIsNotAskedForAnything_AndT .WhenForAnyArgs(s => s.Save(Arg.Any>())) .Do(ci => stacks = ci.ArgAt>(0)); - var inputs = new NewStackCommandInputs("Stack1", sourceBranch, newBranch); + var inputs = new NewStackCommandInputs("Stack1", sourceBranch, newBranch, true); // Act var response = await handler.Handle(inputs); @@ -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(); + var inputProvider = Substitute.For(); + var outputProvider = Substitute.For(); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var gitHubOperations = Substitute.For(); + var handler = new NewStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + var stacks = new List(); + stackConfig.Load().Returns(stacks); + stackConfig + .WhenForAnyArgs(s => s.Save(Arg.Any>())) + .Do(ci => stacks = ci.ArgAt>(0)); + + inputProvider.Text(Questions.StackName).Returns("Stack1"); + inputProvider.Select(Questions.SelectSourceBranch, Arg.Any()).Returns(sourceBranch); + inputProvider.Confirm(Questions.ConfirmAddOrCreateBranch).Returns(true); + inputProvider.Select(Questions.AddOrCreateBranch, Arg.Any(), Arg.Any>()).Returns(BranchAction.Create); + inputProvider.Text(Questions.BranchName, Arg.Any()).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 + { + new("Stack1", repo.RemoteUri, sourceBranch, [newBranch]) + }); + + repo.GetBranches().Should().Contain(b => b.FriendlyName == newBranch && b.IsTracking); + } } diff --git a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs index a14de34..f6c63bc 100644 --- a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs +++ b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs @@ -297,6 +297,11 @@ public LibGit2Sharp.Commit GetTipOfRemoteBranch(string branchName) return [.. LocalRepository.Branches[remoteBranchName].Commits]; } + public List GetBranches() + { + return [.. LocalRepository.Branches]; + } + public void Dispose() { GC.SuppressFinalize(this); diff --git a/src/Stack/Commands/Branch/NewBranchCommand.cs b/src/Stack/Commands/Branch/NewBranchCommand.cs index d9601b6..5fe80d2 100644 --- a/src/Stack/Commands/Branch/NewBranchCommand.cs +++ b/src/Stack/Commands/Branch/NewBranchCommand.cs @@ -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 @@ -38,15 +42,15 @@ public override async Task 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(); @@ -99,13 +103,22 @@ public async Task 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)) { diff --git a/src/Stack/Commands/Stack/NewStackCommand.cs b/src/Stack/Commands/Stack/NewStackCommand.cs index 619b1d8..58257f7 100644 --- a/src/Stack/Commands/Stack/NewStackCommand.cs +++ b/src/Stack/Commands/Stack/NewStackCommand.cs @@ -24,6 +24,10 @@ public class NewStackCommandSettings : CommandSettingsBase [Description("The name of the branch to create within the stack.")] [CommandOption("-b|--branch")] public string? BranchName { get; init; } + + [Description("Push the new branch to the remote repository.")] + [CommandOption("--push")] + public bool Push { get; init; } } public enum BranchAction @@ -48,29 +52,16 @@ public override async Task ExecuteAsync(CommandContext context, NewStackCom new GitOperations(outputProvider, settings.GetGitOperationSettings()), new StackConfig()); - var response = await handler.Handle( - new NewStackCommandInputs(settings.Name, settings.SourceBranch, settings.BranchName)); - - if (response.BranchAction is BranchAction.Create) - { - console.MarkupLine($"Stack [yellow]{response.StackName}[/] created from source branch [blue]{response.SourceBranch}[/] with new branch [blue]{response.BranchName}[/]"); - } - else if (response.BranchAction is BranchAction.Add) - { - console.MarkupLine($"Stack [yellow]{response.StackName}[/] created from source branch [blue]{response.SourceBranch}[/] with existing branch [blue]{response.BranchName}[/]"); - } - else - { - console.MarkupLine($"Stack [yellow]{response.StackName}[/] created from source branch [blue]{response.SourceBranch}[/]"); - } + await handler.Handle( + new NewStackCommandInputs(settings.Name, settings.SourceBranch, settings.BranchName, settings.Push)); return 0; } } -public record NewStackCommandInputs(string? Name, string? SourceBranch, string? BranchName) +public record NewStackCommandInputs(string? Name, string? SourceBranch, string? BranchName, bool Push) { - public static NewStackCommandInputs Empty => new(null, null, null); + public static NewStackCommandInputs Empty => new(null, null, null, false); } public record NewStackCommandResponse(string StackName, string SourceBranch, BranchAction? BranchAction, string? BranchName); @@ -111,7 +102,11 @@ public async Task Handle(NewStackCommandInputs inputs) branchName = inputProvider.Text(outputProvider, Questions.BranchName, inputs.BranchName, stack.GetDefaultBranchName()); gitOperations.CreateNewBranch(branchName, sourceBranch); - gitOperations.PushNewBranch(branchName); + + if (inputs.Push) + { + gitOperations.PushNewBranch(branchName); + } } else { @@ -133,6 +128,24 @@ public async Task Handle(NewStackCommandInputs inputs) gitOperations.ChangeBranch(branchName); } + if (branchAction is BranchAction.Create) + { + outputProvider.Information($"Stack {name.Stack()} created from source branch {sourceBranch.Branch()} with new branch {branchName!.Branch()}"); + + if (!inputs.Push) + { + outputProvider.Information($"Use {$"stack push --name \"{name}\"".Example()} to push the branch to the remote repository."); + } + } + else if (branchAction is BranchAction.Add) + { + outputProvider.Information($"Stack {name.Stack()} created from source branch {sourceBranch.Branch()} with existing branch {branchName!.Branch()}"); + } + else + { + outputProvider.Information($"Stack {name.Stack()} created from source branch {sourceBranch.Branch()}"); + } + return new NewStackCommandResponse(name, sourceBranch, branchAction, branchName); } }