Skip to content

Commit

Permalink
Changes stack update command to operate on local branches (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
geofflamrock authored Dec 30, 2024
1 parent bb81640 commit 36b6848
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 212 deletions.
64 changes: 7 additions & 57 deletions src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,9 @@ public async Task WhenMultipleBranchesExistInAStack_UpdatesAndMergesEachBranchIn
stackConfig.Load().Returns(stacks);

inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>()).Returns("Stack1");
inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true);

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

// Assert
repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch);
Expand Down Expand Up @@ -83,10 +82,9 @@ public async Task WhenABranchInTheStackNoLongerExistsOnTheRemote_SkipsOverUpdati
stackConfig.Load().Returns(stacks);

inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>()).Returns("Stack1");
inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true);

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

// Assert
repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfSourceBranch);
Expand Down Expand Up @@ -121,12 +119,11 @@ public async Task WhenABranchInTheStackExistsOnTheRemote_ButThePullRequestIsMerg
stackConfig.Load().Returns(stacks);

inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>()).Returns("Stack1");
inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true);

gitHubOperations.GetPullRequest(branch1).Returns(new GitHubPullRequest(1, Some.Name(), Some.Name(), GitHubPullRequestStates.Merged, Some.HttpsUri(), false));

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

// Assert
repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfSourceBranch);
Expand Down Expand Up @@ -162,10 +159,8 @@ public async Task WhenNameIsProvided_DoesNotAskForName_UpdatesCorrectStack()
var stacks = new List<Config.Stack>([stack1, stack2]);
stackConfig.Load().Returns(stacks);

inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true);

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

// Assert
repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch);
Expand All @@ -174,48 +169,6 @@ public async Task WhenNameIsProvided_DoesNotAskForName_UpdatesCorrectStack()
inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any<string[]>());
}

[Fact]
public async Task WhenForceIsProvided_DoesNotAskForConfirmation()
{
// Arrange
var sourceBranch = Some.BranchName();
var branch1 = Some.BranchName();
var branch2 = Some.BranchName();
using var repo = new TestGitRepositoryBuilder()
.WithBranch(builder => builder.WithName(sourceBranch).PushToRemote())
.WithBranch(builder => builder.WithName(branch1).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(10).PushToRemote())
.WithBranch(builder => builder.WithName(branch2).FromSourceBranch(branch1).WithNumberOfEmptyCommits(1).PushToRemote())
.WithNumberOfEmptyCommits(b => b.OnBranch(sourceBranch).PushToRemote(), 5)
.WithNumberOfEmptyCommits(b => b.OnBranch(branch1).PushToRemote(), 3)
.Build();

var tipOfSourceBranch = repo.GetTipOfBranch(sourceBranch);
var tipOfBranch1 = repo.GetTipOfBranch(branch1);

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

var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]);
var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []);
var stacks = new List<Config.Stack>([stack1, stack2]);
stackConfig.Load().Returns(stacks);

inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>()).Returns("Stack1");

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

// Assert
repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch);
repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfSourceBranch);
repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfBranch1);
inputProvider.DidNotReceive().Confirm(Questions.ConfirmUpdateStack);
}

[Fact]
public async Task WhenNameIsProvided_ButStackDoesNotExist_Throws()
{
Expand Down Expand Up @@ -248,7 +201,7 @@ public async Task WhenNameIsProvided_ButStackDoesNotExist_Throws()

// Act and assert
var invalidStackName = Some.Name();
await handler.Invoking(async h => await h.Handle(new UpdateStackCommandInputs(invalidStackName, false)))
await handler.Invoking(async h => await h.Handle(new UpdateStackCommandInputs(invalidStackName)))
.Should().ThrowAsync<InvalidOperationException>()
.WithMessage($"Stack '{invalidStackName}' not found.");
}
Expand Down Expand Up @@ -287,10 +240,9 @@ public async Task WhenOnASpecificBranchInTheStack_TheSameBranchIsSetAsCurrentAft
stackConfig.Load().Returns(stacks);

inputProvider.Select(Questions.SelectStack, Arg.Any<string[]>()).Returns("Stack1");
inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true);

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

// Assert
repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch);
Expand Down Expand Up @@ -328,10 +280,8 @@ public async Task WhenOnlyASingleStackExists_DoesNotAskForStackName_UpdatesStack
var stacks = new List<Config.Stack>([stack1]);
stackConfig.Load().Returns(stacks);

inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true);

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

// Assert
repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch);
Expand Down
1 change: 0 additions & 1 deletion src/Stack/Commands/Helpers/Questions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ public static class Questions
public const string SelectBranch = "Select branch:";
public const string BranchName = "Branch name:";
public const string SelectSourceBranch = "Select a branch to start your stack from:";
public const string ConfirmUpdateStack = "Are you sure you want to update this stack?";
public const string ConfirmSyncStack = "Are you sure you want to sync this stack with the remote repository?";
public const string ConfirmDeleteStack = "Are you sure you want to delete this stack?";
public const string ConfirmDeleteBranches = "Are you sure you want to delete these local branches?";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Text;
using Microsoft.VisualBasic;
using Spectre.Console;
using Stack.Config;
using Stack.Git;
Expand All @@ -22,7 +21,7 @@ public record StackStatus(Dictionary<string, BranchDetail> Branches)
public string[] GetActiveBranches() => Branches.Where(b => b.Value.IsActive).Select(b => b.Key).ToArray();
}

public static class StackStatusHelpers
public static class StackHelpers
{
public static Dictionary<Config.Stack, StackStatus> GetStackStatus(
List<Config.Stack> stacks,
Expand Down Expand Up @@ -278,4 +277,80 @@ public static void OutputBranchAndStackCleanup(
outputProvider.Information($"Run {$"stack update --name \"{stack.Name}\"".Example()} to update the stack.");
}
}

public static void UpdateStack(
Config.Stack stack,
StackStatus status,
IGitOperations gitOperations,
IOutputProvider outputProvider)
{
void MergeFromSourceBranch(string branch, string sourceBranchName)
{
outputProvider.Information($"Merging {sourceBranchName.Branch()} into {branch.Branch()}");
gitOperations.ChangeBranch(branch);
gitOperations.MergeFromLocalSourceBranch(sourceBranchName);
}

var sourceBranch = stack.SourceBranch;

foreach (var branch in stack.Branches)
{
var branchDetail = status.Branches[branch];

if (branchDetail.IsActive)
{
MergeFromSourceBranch(branch, sourceBranch);
sourceBranch = branch;
}
else
{
outputProvider.Debug($"Branch '{branch}' no longer exists on the remote repository or the associated pull request is no longer open. Skipping...");
}
}
}

public static void PullChanges(Config.Stack stack, IGitOperations gitOperations, IOutputProvider outputProvider)
{
List<string> allBranchesInStacks = [stack.SourceBranch, .. stack.Branches];
var branchStatus = gitOperations.GetBranchStatuses([.. allBranchesInStacks]);

foreach (var branch in allBranchesInStacks.Where(b => branchStatus[b].RemoteBranchExists))
{
outputProvider.Information($"Pulling changes for {branch.Branch()} from remote");
gitOperations.ChangeBranch(branch);
gitOperations.PullBranch(branch);
}
}

public static void PushChanges(
Config.Stack stack,
int maxBatchSize,
IGitOperations gitOperations,
IOutputProvider outputProvider)
{
var branchStatus = gitOperations.GetBranchStatuses([.. stack.Branches]);

var branchesThatHaveNotBeenPushedToRemote = branchStatus.Where(b => b.Value.RemoteTrackingBranchName is null).Select(b => b.Value.BranchName).ToList();

foreach (var branch in branchesThatHaveNotBeenPushedToRemote)
{
outputProvider.Information($"Pushing new branch {branch.Branch()} to remote");
gitOperations.PushNewBranch(branch);
}

var branchesInStackWithRemote = branchStatus.Where(b => b.Value.RemoteBranchExists).Select(b => b.Value.BranchName).ToList();

var branchGroupsToPush = branchesInStackWithRemote
.Select((b, i) => new { Index = i, Value = b })
.GroupBy(b => b.Index / maxBatchSize)
.Select(g => g.Select(b => b.Value).ToList())
.ToList();

foreach (var branches in branchGroupsToPush)
{
outputProvider.Information($"Pushing changes for {string.Join(", ", branches.Select(b => b.Branch()))} to remote");

gitOperations.PushBranches([.. branches]);
}
}
}
8 changes: 4 additions & 4 deletions src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public async Task<CreatePullRequestsCommandResponse> Handle(CreatePullRequestsCo
throw new InvalidOperationException($"Stack '{inputs.StackName}' not found.");
}

var status = StackStatusHelpers.GetStackStatus(
var status = StackHelpers.GetStackStatus(
stack,
currentBranch,
outputProvider,
Expand All @@ -100,7 +100,7 @@ public async Task<CreatePullRequestsCommandResponse> Handle(CreatePullRequestsCo
}
}

StackStatusHelpers.OutputStackStatus(stack, status, outputProvider);
StackHelpers.OutputStackStatus(stack, status, outputProvider);

outputProvider.NewLine();

Expand Down Expand Up @@ -239,12 +239,12 @@ private static void OutputUpdatedStackStatus(IOutputProvider outputProvider, Con
var branchDetail = status.Branches[branch];
if (branchDetail.PullRequest is not null && branchDetail.PullRequest.State != GitHubPullRequestStates.Closed)
{
branchDisplayItems.Add(StackStatusHelpers.GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail));
branchDisplayItems.Add(StackHelpers.GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail));
}
else
{
var action = pullRequestCreateActions.FirstOrDefault(a => a.HeadBranch == branch);
branchDisplayItems.Add($"{StackStatusHelpers.GetBranchStatusOutput(branch, parentBranch, branchDetail)} *NEW* {action?.Title}{(action?.Draft == true ? " (draft)".Muted() : string.Empty)}");
branchDisplayItems.Add($"{StackHelpers.GetBranchStatusOutput(branch, parentBranch, branchDetail)} *NEW* {action?.Title}{(action?.Draft == true ? " (draft)".Muted() : string.Empty)}");
}
parentBranch = branch;
}
Expand Down
9 changes: 1 addition & 8 deletions src/Stack/Commands/Remote/PullStackCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,7 @@ public async Task Handle(PullStackCommandInputs inputs)
if (stack is null)
throw new InvalidOperationException($"Stack '{inputs.Name}' not found.");

var branchStatus = gitOperations.GetBranchStatuses([stack.SourceBranch, .. stack.Branches]);

foreach (var branch in branchStatus.Where(b => b.Value.RemoteBranchExists))
{
outputProvider.Information($"Pulling changes for {branch.Value.BranchName.Branch()} from remote");
gitOperations.ChangeBranch(branch.Value.BranchName);
gitOperations.PullBranch(branch.Value.BranchName);
}
StackHelpers.PullChanges(stack, gitOperations, outputProvider);

gitOperations.ChangeBranch(currentBranch);
}
Expand Down
25 changes: 1 addition & 24 deletions src/Stack/Commands/Remote/PushStackCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,29 +71,6 @@ public async Task Handle(PushStackCommandInputs inputs)
if (stack is null)
throw new InvalidOperationException($"Stack '{inputs.Name}' not found.");

var branchStatus = gitOperations.GetBranchStatuses([.. stack.Branches]);

var branchesThatHaveNotBeenPushedToRemote = branchStatus.Where(b => b.Value.RemoteTrackingBranchName is null).Select(b => b.Value.BranchName).ToList();

foreach (var branch in branchesThatHaveNotBeenPushedToRemote)
{
outputProvider.Information($"Pushing new branch {branch.Branch()} to remote");
gitOperations.PushNewBranch(branch);
}

var branchesInStackWithRemote = branchStatus.Where(b => b.Value.RemoteBranchExists).Select(b => b.Value.BranchName).ToList();

var branchGroupsToPush = branchesInStackWithRemote
.Select((b, i) => new { Index = i, Value = b })
.GroupBy(b => b.Index / inputs.MaxBatchSize)
.Select(g => g.Select(b => b.Value).ToList())
.ToList();

foreach (var branches in branchGroupsToPush)
{
outputProvider.Information($"Pushing changes for {string.Join(", ", branches.Select(b => b.Branch()))} to remote");

gitOperations.PushBranches([.. branches]);
}
StackHelpers.PushChanges(stack, inputs.MaxBatchSize, gitOperations, outputProvider);
}
}
Loading

0 comments on commit 36b6848

Please sign in to comment.