Skip to content

Commit bec7fc9

Browse files
Skip child action prompt when removing branches with no children (#382)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: geofflamrock <2915931+geofflamrock@users.noreply.github.com>
1 parent 426b337 commit bec7fc9

File tree

3 files changed

+191
-3
lines changed

3 files changed

+191
-3
lines changed

src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,4 +497,136 @@ public async Task WhenMoveChildrenToParentIsProvided_RemovesBranchAndMovesChildr
497497

498498
await inputProvider.DidNotReceive().Select(Questions.RemoveBranchChildAction, Arg.Any<RemoveBranchChildAction[]>(), Arg.Any<CancellationToken>(), Arg.Any<Func<RemoveBranchChildAction, string>>());
499499
}
500+
501+
[Fact]
502+
public async Task WhenBranchHasNoChildren_DoesNotAskForChildAction()
503+
{
504+
// Arrange
505+
var sourceBranch = Some.BranchName();
506+
var branchToRemove = Some.BranchName();
507+
var remoteUri = Some.HttpsUri().ToString();
508+
509+
var stackConfig = new TestStackConfigBuilder()
510+
.WithStack(stack => stack
511+
.WithName("Stack1")
512+
.WithRemoteUri(remoteUri)
513+
.WithSourceBranch(sourceBranch)
514+
.WithBranch(b => b.WithName(branchToRemove))) // Branch with no children
515+
.Build();
516+
var inputProvider = Substitute.For<IInputProvider>();
517+
var logger = XUnitLogger.CreateLogger<RemoveBranchCommandHandler>(testOutputHelper);
518+
var gitClient = Substitute.For<IGitClient>();
519+
var gitClientFactory = Substitute.For<IGitClientFactory>();
520+
var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" };
521+
var handler = new RemoveBranchCommandHandler(inputProvider, logger, gitClientFactory, executionContext, stackConfig);
522+
523+
gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient);
524+
525+
gitClient.GetRemoteUri().Returns(remoteUri);
526+
gitClient.GetCurrentBranch().Returns(sourceBranch);
527+
528+
inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>(), Arg.Any<CancellationToken>()).Returns("Stack1");
529+
inputProvider.Select(Questions.SelectBranch, Arg.Any<string[]>(), Arg.Any<CancellationToken>()).Returns(branchToRemove);
530+
inputProvider.Confirm(Questions.ConfirmRemoveBranch, Arg.Any<CancellationToken>()).Returns(true);
531+
532+
// Act
533+
await handler.Handle(RemoveBranchCommandInputs.Empty, CancellationToken.None);
534+
535+
// Assert
536+
stackConfig.Stacks.Should().BeEquivalentTo(new List<Config.Stack>
537+
{
538+
new("Stack1", remoteUri, sourceBranch, [])
539+
});
540+
541+
// Should not ask for child action when branch has no children
542+
await inputProvider.DidNotReceive().Select(Questions.RemoveBranchChildAction, Arg.Any<RemoveBranchChildAction[]>(), Arg.Any<CancellationToken>(), Arg.Any<Func<RemoveBranchChildAction, string>>());
543+
}
544+
545+
[Fact]
546+
public async Task WhenBranchHasNoChildrenButRemoveChildrenIsProvided_DoesNotAskForChildAction()
547+
{
548+
// Arrange
549+
var sourceBranch = Some.BranchName();
550+
var branchToRemove = Some.BranchName();
551+
var remoteUri = Some.HttpsUri().ToString();
552+
553+
var stackConfig = new TestStackConfigBuilder()
554+
.WithStack(stack => stack
555+
.WithName("Stack1")
556+
.WithRemoteUri(remoteUri)
557+
.WithSourceBranch(sourceBranch)
558+
.WithBranch(b => b.WithName(branchToRemove))) // Branch with no children
559+
.Build();
560+
var inputProvider = Substitute.For<IInputProvider>();
561+
var logger = XUnitLogger.CreateLogger<RemoveBranchCommandHandler>(testOutputHelper);
562+
var gitClient = Substitute.For<IGitClient>();
563+
var gitClientFactory = Substitute.For<IGitClientFactory>();
564+
var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" };
565+
var handler = new RemoveBranchCommandHandler(inputProvider, logger, gitClientFactory, executionContext, stackConfig);
566+
567+
gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient);
568+
569+
gitClient.GetRemoteUri().Returns(remoteUri);
570+
gitClient.GetCurrentBranch().Returns(sourceBranch);
571+
572+
inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>(), Arg.Any<CancellationToken>()).Returns("Stack1");
573+
inputProvider.Select(Questions.SelectBranch, Arg.Any<string[]>(), Arg.Any<CancellationToken>()).Returns(branchToRemove);
574+
inputProvider.Confirm(Questions.ConfirmRemoveBranch, Arg.Any<CancellationToken>()).Returns(true);
575+
576+
// Act
577+
await handler.Handle(new RemoveBranchCommandInputs(null, null, false, RemoveBranchChildAction.RemoveChildren), CancellationToken.None);
578+
579+
// Assert
580+
stackConfig.Stacks.Should().BeEquivalentTo(new List<Config.Stack>
581+
{
582+
new("Stack1", remoteUri, sourceBranch, [])
583+
});
584+
585+
// Should not ask for child action when explicitly provided, even if branch has no children
586+
await inputProvider.DidNotReceive().Select(Questions.RemoveBranchChildAction, Arg.Any<RemoveBranchChildAction[]>(), Arg.Any<CancellationToken>(), Arg.Any<Func<RemoveBranchChildAction, string>>());
587+
}
588+
589+
[Fact]
590+
public async Task WhenBranchHasNoChildrenButMoveChildrenToParentIsProvided_DoesNotAskForChildAction()
591+
{
592+
// Arrange
593+
var sourceBranch = Some.BranchName();
594+
var branchToRemove = Some.BranchName();
595+
var remoteUri = Some.HttpsUri().ToString();
596+
597+
var stackConfig = new TestStackConfigBuilder()
598+
.WithStack(stack => stack
599+
.WithName("Stack1")
600+
.WithRemoteUri(remoteUri)
601+
.WithSourceBranch(sourceBranch)
602+
.WithBranch(b => b.WithName(branchToRemove))) // Branch with no children
603+
.Build();
604+
var inputProvider = Substitute.For<IInputProvider>();
605+
var logger = XUnitLogger.CreateLogger<RemoveBranchCommandHandler>(testOutputHelper);
606+
var gitClient = Substitute.For<IGitClient>();
607+
var gitClientFactory = Substitute.For<IGitClientFactory>();
608+
var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" };
609+
var handler = new RemoveBranchCommandHandler(inputProvider, logger, gitClientFactory, executionContext, stackConfig);
610+
611+
gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient);
612+
613+
gitClient.GetRemoteUri().Returns(remoteUri);
614+
gitClient.GetCurrentBranch().Returns(sourceBranch);
615+
616+
inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>(), Arg.Any<CancellationToken>()).Returns("Stack1");
617+
inputProvider.Select(Questions.SelectBranch, Arg.Any<string[]>(), Arg.Any<CancellationToken>()).Returns(branchToRemove);
618+
inputProvider.Confirm(Questions.ConfirmRemoveBranch, Arg.Any<CancellationToken>()).Returns(true);
619+
620+
// Act
621+
await handler.Handle(new RemoveBranchCommandInputs(null, null, false, RemoveBranchChildAction.MoveChildrenToParent), CancellationToken.None);
622+
623+
// Assert
624+
stackConfig.Stacks.Should().BeEquivalentTo(new List<Config.Stack>
625+
{
626+
new("Stack1", remoteUri, sourceBranch, [])
627+
});
628+
629+
// Should not ask for child action when explicitly provided, even if branch has no children
630+
await inputProvider.DidNotReceive().Select(Questions.RemoveBranchChildAction, Arg.Any<RemoveBranchChildAction[]>(), Arg.Any<CancellationToken>(), Arg.Any<Func<RemoveBranchChildAction, string>>());
631+
}
500632
}

src/Stack/Commands/Branch/RemoveBranchCommand.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,31 @@ public override async Task Handle(RemoveBranchCommandInputs inputs, Cancellation
9797
throw new InvalidOperationException($"Branch '{branchName}' not found in stack '{stack.Name}'.");
9898
}
9999

100-
var action =
101-
inputs.RemoveChildrenAction ??
102-
await inputProvider.Select(
100+
// Find the branch to check if it has children
101+
var branch = stack.FindBranch(branchName);
102+
var hasChildren = branch?.Children.Count > 0;
103+
104+
RemoveBranchChildAction action;
105+
106+
if (inputs.RemoveChildrenAction.HasValue)
107+
{
108+
// Use the explicitly provided action
109+
action = inputs.RemoveChildrenAction.Value;
110+
}
111+
else if (hasChildren)
112+
{
113+
// Only ask for action if the branch has children
114+
action = await inputProvider.Select(
103115
Questions.RemoveBranchChildAction,
104116
new[] { RemoveBranchChildAction.MoveChildrenToParent, RemoveBranchChildAction.RemoveChildren },
105117
cancellationToken,
106118
(action) => action.Humanize());
119+
}
120+
else
121+
{
122+
// Default action when no children (doesn't matter which one we choose)
123+
action = RemoveBranchChildAction.RemoveChildren;
124+
}
107125

108126
if (inputs.Confirm || await inputProvider.Confirm(Questions.ConfirmRemoveBranch, cancellationToken))
109127
{

src/Stack/Config/Stack.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,44 @@ public Stack ChangeName(string newName)
100100
{
101101
return this with { Name = newName };
102102
}
103+
104+
public Branch? FindBranch(string branchName)
105+
{
106+
foreach (var branch in Branches)
107+
{
108+
if (branch.Name.Equals(branchName, StringComparison.OrdinalIgnoreCase))
109+
{
110+
return branch;
111+
}
112+
113+
var found = FindBranchRecursive(branch, branchName);
114+
if (found != null)
115+
{
116+
return found;
117+
}
118+
}
119+
120+
return null;
121+
}
122+
123+
static Branch? FindBranchRecursive(Branch branch, string branchName)
124+
{
125+
foreach (var child in branch.Children)
126+
{
127+
if (child.Name.Equals(branchName, StringComparison.OrdinalIgnoreCase))
128+
{
129+
return child;
130+
}
131+
132+
var found = FindBranchRecursive(child, branchName);
133+
if (found != null)
134+
{
135+
return found;
136+
}
137+
}
138+
139+
return null;
140+
}
103141
}
104142

105143
public record Branch(string Name, List<Branch> Children)

0 commit comments

Comments
 (0)