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
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,12 +319,14 @@ Usage:
stack branch remove [options]

Options:
--working-dir The path to the directory containing the git repository. Defaults to the current directory.
--verbose Show verbose output.
-s, --stack The name of the stack.
-b, --branch The name of the branch.
-y, --yes Confirm the command without prompting.
-?, -h, --help Show help and usage information
--working-dir The path to the directory containing the git repository. Defaults to the current directory.
--verbose Show verbose output.
-s, --stack The name of the stack.
-b, --branch The name of the branch.
-y, --yes Confirm the command without prompting.
--remove-children Remove children branches.
--move-children-to-parent Move children branches to the parent branch.
-?, -h, --help Show help and usage information
```

### Remote commands <!-- omit from toc -->
Expand Down
88 changes: 88 additions & 0 deletions src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -413,4 +413,92 @@ public async Task WhenSchemaIsV1_RemovesBranchAndMovesChildrenToParent()
new("Stack1", repo.RemoteUri, sourceBranch, [new Config.Branch(childBranch, [])])
});
}

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

var stackConfig = new TestStackConfigBuilder()
.WithSchemaVersion(SchemaVersion.V2)
.WithStack(stack => stack
.WithName("Stack1")
.WithRemoteUri(repo.RemoteUri)
.WithSourceBranch(sourceBranch)
.WithBranch(b => b.WithName(branchToRemove).WithChildBranch(b => b.WithName(childBranch))))
.Build();
var inputProvider = Substitute.For<IInputProvider>();
var logger = new TestLogger(testOutputHelper);
var gitClient = new GitClient(logger, repo.GitClientSettings);
var handler = new RemoveBranchCommandHandler(inputProvider, logger, gitClient, stackConfig);

inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>()).Returns("Stack1");
inputProvider.Select(Questions.SelectBranch, Arg.Any<string[]>()).Returns(branchToRemove);
inputProvider.Select(Questions.RemoveBranchChildAction, Arg.Any<RemoveBranchChildAction[]>(), Arg.Any<Func<RemoveBranchChildAction, string>>())
.Returns(RemoveBranchChildAction.RemoveChildren);
inputProvider.Confirm(Questions.ConfirmRemoveBranch).Returns(true);

// Act
await handler.Handle(new RemoveBranchCommandInputs(null, null, false, RemoveBranchChildAction.RemoveChildren));

// Assert
stackConfig.Stacks.Should().BeEquivalentTo(new List<Config.Stack>
{
new("Stack1", repo.RemoteUri, sourceBranch, [])
});

inputProvider.DidNotReceive().Select(Questions.RemoveBranchChildAction, Arg.Any<RemoveBranchChildAction[]>(), Arg.Any<Func<RemoveBranchChildAction, string>>());
}

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

var stackConfig = new TestStackConfigBuilder()
.WithSchemaVersion(SchemaVersion.V2)
.WithStack(stack => stack
.WithName("Stack1")
.WithRemoteUri(repo.RemoteUri)
.WithSourceBranch(sourceBranch)
.WithBranch(b => b.WithName(branchToRemove).WithChildBranch(b => b.WithName(childBranch))))
.Build();
var inputProvider = Substitute.For<IInputProvider>();
var logger = new TestLogger(testOutputHelper);
var gitClient = new GitClient(logger, repo.GitClientSettings);
var handler = new RemoveBranchCommandHandler(inputProvider, logger, gitClient, stackConfig);

inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>()).Returns("Stack1");
inputProvider.Select(Questions.SelectBranch, Arg.Any<string[]>()).Returns(branchToRemove);
inputProvider.Select(Questions.RemoveBranchChildAction, Arg.Any<RemoveBranchChildAction[]>(), Arg.Any<Func<RemoveBranchChildAction, string>>())
.Returns(RemoveBranchChildAction.MoveChildrenToParent);
inputProvider.Confirm(Questions.ConfirmRemoveBranch).Returns(true);

// Act
await handler.Handle(new RemoveBranchCommandInputs(null, null, false, RemoveBranchChildAction.MoveChildrenToParent));

// Assert
stackConfig.Stacks.Should().BeEquivalentTo(new List<Config.Stack>
{
new("Stack1", repo.RemoteUri, sourceBranch, [new Config.Branch(childBranch, [])])
});

inputProvider.DidNotReceive().Select(Questions.RemoveBranchChildAction, Arg.Any<RemoveBranchChildAction[]>(), Arg.Any<Func<RemoveBranchChildAction, string>>());
}
}
37 changes: 30 additions & 7 deletions src/Stack/Commands/Branch/RemoveBranchCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,23 @@ namespace Stack.Commands;

public class RemoveBranchCommand : Command
{
static readonly Option<bool> RemoveChildren = new("--remove-children")
{
Description = "Remove children branches."
};

static readonly Option<bool> MoveChildrenToParent = new("--move-children-to-parent")
{
Description = "Move children branches to the parent branch."
};

public RemoveBranchCommand() : base("remove", "Remove a branch from a stack.")
{
Add(CommonOptions.Stack);
Add(CommonOptions.Branch);
Add(CommonOptions.Confirm);
Add(RemoveChildren);
Add(MoveChildrenToParent);
}

protected override async Task Execute(ParseResult parseResult, CancellationToken cancellationToken)
Expand All @@ -24,16 +36,25 @@ protected override async Task Execute(ParseResult parseResult, CancellationToken
new GitClient(StdErrLogger, new GitClientSettings(Verbose, WorkingDirectory)),
new FileStackConfig());

var removeChildren = parseResult.GetValue(RemoveChildren);
var moveChildrenToParent = parseResult.GetValue(MoveChildrenToParent);

if (removeChildren && moveChildrenToParent)
{
throw new InvalidOperationException("Cannot specify both --remove-children and --move-children-to-parent options.");
}

await handler.Handle(new RemoveBranchCommandInputs(
parseResult.GetValue(CommonOptions.Stack),
parseResult.GetValue(CommonOptions.Branch),
parseResult.GetValue(CommonOptions.Confirm)));
parseResult.GetValue(CommonOptions.Confirm),
removeChildren ? RemoveBranchChildAction.RemoveChildren : moveChildrenToParent ? RemoveBranchChildAction.MoveChildrenToParent : null));
}
Comment on lines +51 to 52
Copy link

Copilot AI Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This nested ternary operator is difficult to read and maintain. Consider using an if-else statement or extracting this logic into a separate method to improve readability.

Suggested change
removeChildren ? RemoveBranchChildAction.RemoveChildren : moveChildrenToParent ? RemoveBranchChildAction.MoveChildrenToParent : null));
}
DetermineRemoveBranchChildAction(removeChildren, moveChildrenToParent)));
}
private static RemoveBranchChildAction? DetermineRemoveBranchChildAction(bool removeChildren, bool moveChildrenToParent)
{
if (removeChildren)
{
return RemoveBranchChildAction.RemoveChildren;
}
else if (moveChildrenToParent)
{
return RemoveBranchChildAction.MoveChildrenToParent;
}
else
{
return null;
}
}

Copilot uses AI. Check for mistakes.
}

public record RemoveBranchCommandInputs(string? StackName, string? BranchName, bool Confirm)
public record RemoveBranchCommandInputs(string? StackName, string? BranchName, bool Confirm, RemoveBranchChildAction? RemoveChildrenAction = null)
{
public static RemoveBranchCommandInputs Empty => new(null, null, false);
public static RemoveBranchCommandInputs Empty => new(null, null, false, null);
}

public class RemoveBranchCommandHandler(
Expand Down Expand Up @@ -70,10 +91,12 @@ public override async Task Handle(RemoveBranchCommandInputs inputs)

if (stackData.SchemaVersion == SchemaVersion.V2)
{
action = inputProvider.Select(
Questions.RemoveBranchChildAction,
[RemoveBranchChildAction.MoveChildrenToParent, RemoveBranchChildAction.RemoveChildren],
(action) => action.Humanize());
action =
inputs.RemoveChildrenAction ??
inputProvider.Select(
Questions.RemoveBranchChildAction,
[RemoveBranchChildAction.MoveChildrenToParent, RemoveBranchChildAction.RemoveChildren],
(action) => action.Humanize());
}

if (inputs.Confirm || inputProvider.Confirm(Questions.ConfirmRemoveBranch))
Expand Down