-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5326fc9
commit be24c3a
Showing
4 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
208 changes: 208 additions & 0 deletions
208
src/Stack.Tests/Commands/Stack/CleanupStackCommandHandlerTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
using FluentAssertions; | ||
using NSubstitute; | ||
using Stack.Commands; | ||
using Stack.Config; | ||
using Stack.Git; | ||
using Stack.Infrastructure; | ||
using Stack.Tests.Helpers; | ||
|
||
namespace Stack.Tests.Commands.Stack; | ||
|
||
public class CleanupStackCommandHandlerTests | ||
{ | ||
[Fact] | ||
public async Task WhenBranchExistsLocally_ButNotInRemote_BranchIsDeletedLocally() | ||
{ | ||
// Arrange | ||
var gitOperations = Substitute.For<IGitOperations>(); | ||
var stackConfig = Substitute.For<IStackConfig>(); | ||
var inputProvider = Substitute.For<ICleanupStackCommandInputProvider>(); | ||
var outputProvider = Substitute.For<IOutputProvider>(); | ||
var handler = new CleanupStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); | ||
|
||
var remoteUri = Some.HttpsUri().ToString(); | ||
|
||
gitOperations.GetRemoteUri().Returns(remoteUri); | ||
gitOperations.GetCurrentBranch().Returns("branch-1"); | ||
gitOperations.GetBranchesThatExistLocally(Arg.Any<string[]>()).Returns(["branch-1", "branch-2"]); | ||
gitOperations.GetBranchesThatExistInRemote(Arg.Any<string[]>()).Returns(["branch-1"]); | ||
|
||
var stacks = new List<Config.Stack>( | ||
[ | ||
new("Stack1", remoteUri, "branch-1", ["branch-2"]) | ||
]); | ||
stackConfig.Load().Returns(stacks); | ||
|
||
inputProvider.SelectStack(Arg.Any<List<Config.Stack>>(), Arg.Any<string>()).Returns("Stack1"); | ||
inputProvider.ConfirmCleanup().Returns(true); | ||
|
||
// Act | ||
await handler.Handle(CleanupStackCommandInputs.Empty); | ||
|
||
// Assert | ||
gitOperations.Received().DeleteLocalBranch("branch-2"); | ||
} | ||
|
||
[Fact] | ||
public async Task WhenBranchExistsLocally_AndInRemote_BranchIsNotDeletedLocally() | ||
{ | ||
// Arrange | ||
var gitOperations = Substitute.For<IGitOperations>(); | ||
var stackConfig = Substitute.For<IStackConfig>(); | ||
var inputProvider = Substitute.For<ICleanupStackCommandInputProvider>(); | ||
var outputProvider = Substitute.For<IOutputProvider>(); | ||
var handler = new CleanupStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); | ||
|
||
var remoteUri = Some.HttpsUri().ToString(); | ||
|
||
gitOperations.GetRemoteUri().Returns(remoteUri); | ||
gitOperations.GetCurrentBranch().Returns("branch-1"); | ||
gitOperations.GetBranchesThatExistLocally(Arg.Any<string[]>()).Returns(["branch-1", "branch-2"]); | ||
gitOperations.GetBranchesThatExistInRemote(Arg.Any<string[]>()).Returns(["branch-1", "branch-2"]); | ||
|
||
var stacks = new List<Config.Stack>( | ||
[ | ||
new("Stack1", remoteUri, "branch-1", ["branch-2"]) | ||
]); | ||
stackConfig.Load().Returns(stacks); | ||
|
||
inputProvider.SelectStack(Arg.Any<List<Config.Stack>>(), Arg.Any<string>()).Returns("Stack1"); | ||
inputProvider.ConfirmCleanup().Returns(true); | ||
|
||
// Act | ||
await handler.Handle(CleanupStackCommandInputs.Empty); | ||
|
||
// Assert | ||
gitOperations.DidNotReceive().DeleteLocalBranch("branch-2"); | ||
} | ||
|
||
[Fact] | ||
public async Task WhenConfirmationIsFalse_DoesNotDeleteAnyBranches() | ||
{ | ||
// Arrange | ||
var gitOperations = Substitute.For<IGitOperations>(); | ||
var stackConfig = Substitute.For<IStackConfig>(); | ||
var inputProvider = Substitute.For<ICleanupStackCommandInputProvider>(); | ||
var outputProvider = Substitute.For<IOutputProvider>(); | ||
var handler = new CleanupStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); | ||
|
||
var remoteUri = Some.HttpsUri().ToString(); | ||
|
||
gitOperations.GetRemoteUri().Returns(remoteUri); | ||
gitOperations.GetCurrentBranch().Returns("branch-1"); | ||
gitOperations.GetBranchesThatExistLocally(Arg.Any<string[]>()).Returns(["branch-1", "branch-2"]); | ||
gitOperations.GetBranchesThatExistInRemote(Arg.Any<string[]>()).Returns(["branch-1"]); | ||
|
||
var stacks = new List<Config.Stack>( | ||
[ | ||
new("Stack1", remoteUri, "branch-1", ["branch-2"]) | ||
]); | ||
stackConfig.Load().Returns(stacks); | ||
|
||
inputProvider.SelectStack(Arg.Any<List<Config.Stack>>(), Arg.Any<string>()).Returns("Stack1"); | ||
inputProvider.ConfirmCleanup().Returns(false); | ||
|
||
// Act | ||
await handler.Handle(CleanupStackCommandInputs.Empty); | ||
|
||
// Assert | ||
gitOperations.DidNotReceive().DeleteLocalBranch("branch-2"); | ||
} | ||
|
||
[Fact] | ||
public async Task WhenStackNameIsProvided_ItIsNotAskedFor() | ||
{ | ||
// Arrange | ||
var gitOperations = Substitute.For<IGitOperations>(); | ||
var stackConfig = Substitute.For<IStackConfig>(); | ||
var inputProvider = Substitute.For<ICleanupStackCommandInputProvider>(); | ||
var outputProvider = Substitute.For<IOutputProvider>(); | ||
var handler = new CleanupStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); | ||
|
||
var remoteUri = Some.HttpsUri().ToString(); | ||
|
||
gitOperations.GetRemoteUri().Returns(remoteUri); | ||
gitOperations.GetCurrentBranch().Returns("branch-1"); | ||
gitOperations.GetBranchesThatExistLocally(Arg.Any<string[]>()).Returns(["branch-1", "branch-2"]); | ||
gitOperations.GetBranchesThatExistInRemote(Arg.Any<string[]>()).Returns(["branch-1"]); | ||
|
||
var stacks = new List<Config.Stack>( | ||
[ | ||
new("Stack1", remoteUri, "branch-1", ["branch-2"]) | ||
]); | ||
stackConfig.Load().Returns(stacks); | ||
|
||
inputProvider.ConfirmCleanup().Returns(true); | ||
|
||
// Act | ||
await handler.Handle(new CleanupStackCommandInputs("Stack1", false)); | ||
|
||
// Assert | ||
inputProvider.DidNotReceive().SelectStack(Arg.Any<List<Config.Stack>>(), Arg.Any<string>()); | ||
} | ||
|
||
[Fact] | ||
public async Task WhenForceIsProvided_ItIsNotAskedFor() | ||
{ | ||
// Arrange | ||
var gitOperations = Substitute.For<IGitOperations>(); | ||
var stackConfig = Substitute.For<IStackConfig>(); | ||
var inputProvider = Substitute.For<ICleanupStackCommandInputProvider>(); | ||
var outputProvider = Substitute.For<IOutputProvider>(); | ||
var handler = new CleanupStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); | ||
|
||
var remoteUri = Some.HttpsUri().ToString(); | ||
|
||
gitOperations.GetRemoteUri().Returns(remoteUri); | ||
gitOperations.GetCurrentBranch().Returns("branch-1"); | ||
gitOperations.GetBranchesThatExistLocally(Arg.Any<string[]>()).Returns(["branch-1", "branch-2"]); | ||
gitOperations.GetBranchesThatExistInRemote(Arg.Any<string[]>()).Returns(["branch-1"]); | ||
|
||
var stacks = new List<Config.Stack>( | ||
[ | ||
new("Stack1", remoteUri, "branch-1", ["branch-2"]) | ||
]); | ||
stackConfig.Load().Returns(stacks); | ||
|
||
inputProvider.SelectStack(Arg.Any<List<Config.Stack>>(), Arg.Any<string>()).Returns("Stack1"); | ||
|
||
// Act | ||
await handler.Handle(new CleanupStackCommandInputs(null, true)); | ||
|
||
// Assert | ||
inputProvider.DidNotReceive().ConfirmCleanup(); | ||
} | ||
|
||
[Fact] | ||
public async Task WhenStackNameIsProvided_ButStackDoesNotExist_Throws() | ||
{ | ||
// Arrange | ||
var gitOperations = Substitute.For<IGitOperations>(); | ||
var stackConfig = Substitute.For<IStackConfig>(); | ||
var inputProvider = Substitute.For<ICleanupStackCommandInputProvider>(); | ||
var outputProvider = Substitute.For<IOutputProvider>(); | ||
var handler = new CleanupStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); | ||
|
||
var remoteUri = Some.HttpsUri().ToString(); | ||
|
||
gitOperations.GetRemoteUri().Returns(remoteUri); | ||
gitOperations.GetCurrentBranch().Returns("branch-1"); | ||
gitOperations.GetBranchesThatExistLocally(Arg.Any<string[]>()).Returns(["branch-1", "branch-2"]); | ||
gitOperations.GetBranchesThatExistInRemote(Arg.Any<string[]>()).Returns(["branch-1"]); | ||
|
||
var stacks = new List<Config.Stack>( | ||
[ | ||
new("Stack1", remoteUri, "branch-1", ["branch-2"]) | ||
]); | ||
stackConfig.Load().Returns(stacks); | ||
|
||
inputProvider.ConfirmCleanup().Returns(true); | ||
|
||
// Act and assert | ||
var invalidStackName = Some.Name(); | ||
await handler.Invoking(async h => await h.Handle(new CleanupStackCommandInputs(invalidStackName, false))) | ||
.Should() | ||
.ThrowAsync<InvalidOperationException>() | ||
.WithMessage($"Stack '{invalidStackName}' not found."); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
|
||
using System.ComponentModel; | ||
using Spectre.Console; | ||
using Spectre.Console.Cli; | ||
using Stack.Config; | ||
using Stack.Git; | ||
using Stack.Infrastructure; | ||
|
||
namespace Stack.Commands; | ||
|
||
public class CleanupStackCommandSettings : DryRunCommandSettingsBase | ||
{ | ||
[Description("The name of the stack to cleanup.")] | ||
[CommandOption("-n|--name")] | ||
public string? Name { get; init; } | ||
|
||
[Description("Cleanup the stack without prompting.")] | ||
[CommandOption("-f|--force")] | ||
public bool Force { get; init; } | ||
} | ||
|
||
public class CleanupStackCommand : AsyncCommand<CleanupStackCommandSettings> | ||
{ | ||
public override async Task<int> ExecuteAsync(CommandContext context, CleanupStackCommandSettings settings) | ||
{ | ||
await Task.CompletedTask; | ||
|
||
var console = AnsiConsole.Console; | ||
var handler = new CleanupStackCommandHandler( | ||
new CleanupStackCommandInputProvider(new ConsoleInputProvider(console)), | ||
new ConsoleOutputProvider(console), | ||
new GitOperations(console, settings.GetGitOperationSettings()), | ||
new StackConfig()); | ||
|
||
await handler.Handle(new CleanupStackCommandInputs(settings.Name, settings.Force)); | ||
|
||
return 0; | ||
} | ||
} | ||
|
||
public interface ICleanupStackCommandInputProvider | ||
{ | ||
string SelectStack(List<Config.Stack> stacks, string currentBranch); | ||
bool ConfirmCleanup(); | ||
} | ||
|
||
public class CleanupStackCommandInputProvider(IInputProvider inputProvider) : ICleanupStackCommandInputProvider | ||
{ | ||
const string SelectStackPrompt = "Select stack:"; | ||
const string CleanupStackPrompt = "Do you want to continue?"; | ||
|
||
public string SelectStack(List<Config.Stack> stacks, string currentBranch) | ||
{ | ||
return inputProvider.Select(SelectStackPrompt, stacks.OrderByCurrentStackThenByName(currentBranch).Select(s => s.Name).ToArray()); | ||
} | ||
|
||
public bool ConfirmCleanup() | ||
{ | ||
return inputProvider.Confirm(CleanupStackPrompt); | ||
} | ||
} | ||
|
||
public record CleanupStackCommandInputs(string? Name, bool Force) | ||
{ | ||
public static CleanupStackCommandInputs Empty => new(null, false); | ||
} | ||
|
||
public record CleanupStackCommandResponse(string? CleanedUpStackName); | ||
|
||
public class CleanupStackCommandHandler( | ||
ICleanupStackCommandInputProvider inputProvider, | ||
IOutputProvider outputProvider, | ||
IGitOperations gitOperations, | ||
IStackConfig stackConfig) | ||
{ | ||
public async Task Handle(CleanupStackCommandInputs inputs) | ||
{ | ||
await Task.CompletedTask; | ||
var stacks = stackConfig.Load(); | ||
|
||
var remoteUri = gitOperations.GetRemoteUri(); | ||
var currentBranch = gitOperations.GetCurrentBranch(); | ||
|
||
var stacksForRemote = stacks.Where(s => s.RemoteUri.Equals(remoteUri, StringComparison.OrdinalIgnoreCase)).ToList(); | ||
|
||
var stackSelection = inputs.Name ?? inputProvider.SelectStack(stacksForRemote, currentBranch); | ||
var stack = stacksForRemote.FirstOrDefault(s => s.Name.Equals(stackSelection, StringComparison.OrdinalIgnoreCase)); | ||
|
||
if (stack is null) | ||
{ | ||
throw new InvalidOperationException($"Stack '{inputs.Name}' not found."); | ||
} | ||
|
||
var branchesInTheStackThatExistLocally = gitOperations.GetBranchesThatExistLocally([.. stack.Branches]); | ||
var branchesInTheStackThatExistInTheRemote = gitOperations.GetBranchesThatExistInRemote([.. stack.Branches]); | ||
|
||
var branchesToCleanUp = branchesInTheStackThatExistLocally.Except(branchesInTheStackThatExistInTheRemote).ToList(); | ||
|
||
if (branchesToCleanUp.Count == 0) | ||
{ | ||
outputProvider.Information("No branches to clean up"); | ||
return; | ||
} | ||
|
||
if (!inputs.Force) | ||
{ | ||
outputProvider.Information($"The following branches from stack {stack.Name.Stack()} will be deleted:"); | ||
|
||
foreach (var branch in branchesToCleanUp) | ||
{ | ||
outputProvider.Information($" {branch.Branch()}"); | ||
} | ||
} | ||
|
||
if (inputs.Force || inputProvider.ConfirmCleanup()) | ||
{ | ||
foreach (var branch in stack.Branches) | ||
{ | ||
if (!branchesInTheStackThatExistInTheRemote.Contains(branch) && | ||
branchesInTheStackThatExistLocally.Contains(branch)) | ||
{ | ||
outputProvider.Information($"Deleting local branch {branch.Branch()}"); | ||
gitOperations.DeleteLocalBranch(branch); | ||
} | ||
} | ||
|
||
outputProvider.Information($"Stack {stack.Name.Stack()} cleaned up"); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters