From 215b85970d1f472dd5158f13af4e9a71c639249d Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 20 Dec 2024 12:39:13 +1100 Subject: [PATCH 01/26] Show local status by default in status command --- .../Stack/StackStatusCommandHandlerTests.cs | 32 +++---- .../Commands/Helpers/StackStatusHelpers.cs | 89 ++++++++++--------- .../Commands/Stack/StackStatusCommand.cs | 11 ++- src/Stack/Git/GitOperations.cs | 16 ++++ 4 files changed, 89 insertions(+), 59 deletions(-) diff --git a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs index d55a43db..ff755157 100644 --- a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs @@ -54,8 +54,8 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneHasAPullRequests_Retur // Assert var expectedBranchDetails = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5), PullRequest = pr } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0) } } + { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5, 0, 0), PullRequest = pr } }, + { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0) } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { @@ -108,8 +108,8 @@ public async Task WhenStackNameIsProvided_DoesNotAskForStack_ReturnsStatus() // Assert var expectedBranchDetails = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5), PullRequest = pr } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0) } } + { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5, 0, 0), PullRequest = pr } }, + { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0) } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { @@ -165,12 +165,12 @@ public async Task WhenAllStacksAreRequested_ReturnsStatusOfEachStack() // Assert var expectedBranchDetailsForStack1 = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5), PullRequest = pr } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0) } } + { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5, 0, 0), PullRequest = pr } }, + { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0) } } }; var expectedBranchDetailsForStack2 = new Dictionary { - { aThirdBranch, new BranchDetail { Status = new BranchStatus(true, true, 3, 5) } } + { aThirdBranch, new BranchDetail { Status = new BranchStatus(true, true, 3, 5, 0, 0) } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { @@ -228,12 +228,12 @@ public async Task WhenAllStacksAreRequested_WithStacksInMultipleRepositories_Ret // Assert var expectedBranchDetailsForStack1 = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5), PullRequest = pr } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0) } } + { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5, 0, 0), PullRequest = pr } }, + { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0) } } }; var expectedBranchDetailsForStack2 = new Dictionary { - { aThirdBranch, new BranchDetail { Status = new BranchStatus(true, true, 3, 5) } } + { aThirdBranch, new BranchDetail { Status = new BranchStatus(true, true, 3, 5, 0, 0) } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { @@ -325,8 +325,8 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem // Assert var expectedBranchDetails = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, false, 0, 0) } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 11, 0), PullRequest = pr } } // The 11 commits are the 10 commits from the parent branch and one from this branch + { aBranch, new BranchDetail { Status = new BranchStatus(true, false, 0, 0, 0, 0) } }, + { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 11, 0, 0, 0), PullRequest = pr } } // The 11 commits are the 10 commits from the parent branch and one from this branch }; response.Statuses.Should().BeEquivalentTo(new Dictionary { @@ -378,8 +378,8 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem // Assert var expectedBranchDetails = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(false, false, 0, 0) } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0), PullRequest = pr } } + { aBranch, new BranchDetail { Status = new BranchStatus(false, false, 0, 0, 0, 0) } }, + { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0), PullRequest = pr } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { @@ -430,8 +430,8 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_ReturnsStatus() // Assert var expectedBranchDetails = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5), PullRequest = pr } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0) } } + { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5, 0, 0), PullRequest = pr } }, + { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0) } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { diff --git a/src/Stack/Commands/Helpers/StackStatusHelpers.cs b/src/Stack/Commands/Helpers/StackStatusHelpers.cs index 232477fb..4c9be275 100644 --- a/src/Stack/Commands/Helpers/StackStatusHelpers.cs +++ b/src/Stack/Commands/Helpers/StackStatusHelpers.cs @@ -8,14 +8,14 @@ namespace Stack.Commands.Helpers; public class BranchDetail { - public BranchStatus Status { get; set; } = new(false, false, 0, 0); + public BranchStatus Status { get; set; } = new(false, false, 0, 0, 0, 0); public GitHubPullRequest? PullRequest { get; set; } public bool IsActive => Status.ExistsLocally && Status.ExistsInRemote && (PullRequest is null || PullRequest.State != GitHubPullRequestStates.Merged); public bool CouldBeCleanedUp => Status.ExistsLocally && (!Status.ExistsInRemote || PullRequest is not null && PullRequest.State == GitHubPullRequestStates.Merged); public bool HasPullRequest => PullRequest is not null && PullRequest.State != GitHubPullRequestStates.Closed; } -public record BranchStatus(bool ExistsLocally, bool ExistsInRemote, int Ahead, int Behind); +public record BranchStatus(bool ExistsLocally, bool ExistsInRemote, int AheadOfParent, int BehindParent, int AheadOfRemote, int BehindRemote); public record StackStatus(Dictionary Branches) { public string[] GetActiveBranches() => Branches.Where(b => b.Value.IsActive).Select(b => b.Key).ToArray(); @@ -28,7 +28,8 @@ public static class StackStatusHelpers string currentBranch, IOutputProvider outputProvider, IGitOperations gitOperations, - IGitHubOperations gitHubOperations) + IGitHubOperations gitHubOperations, + bool includePullRequestStatus = true) { var stacksToCheckStatusFor = new Dictionary(); @@ -37,65 +38,72 @@ public static class StackStatusHelpers .ToList() .ForEach(stack => stacksToCheckStatusFor.Add(stack, new StackStatus([]))); - outputProvider.Status("Checking status of remote branches...", () => + var allBranchesInStacks = stacks.SelectMany(s => new List([s.SourceBranch]).Concat(s.Branches)).Distinct().ToArray(); + + outputProvider.Status("Checking status of branches...", () => { + var branchesThatExistInRemote = gitOperations.GetBranchesThatExistInRemote(allBranchesInStacks); + var branchesThatExistLocally = gitOperations.GetBranchesThatExistLocally(allBranchesInStacks); + foreach (var (stack, status) in stacksToCheckStatusFor) { - var allBranchesInStack = new List([stack.SourceBranch]).Concat(stack.Branches).Distinct().ToArray(); - var branchesThatExistInRemote = gitOperations.GetBranchesThatExistInRemote(allBranchesInStack); - var branchesThatExistLocally = gitOperations.GetBranchesThatExistLocally(allBranchesInStack); - - gitOperations.FetchBranches(branchesThatExistInRemote); - - void CheckRemoteBranch(string branch, string sourceBranch) + void CheckBranchStatus(string branch, string sourceBranch) { var branchExistsLocally = branchesThatExistLocally.Contains(branch); - var (ahead, behind) = gitOperations.GetStatusOfRemoteBranch(branch, sourceBranch); - var branchStatus = new BranchStatus(branchExistsLocally, true, ahead, behind); + var (ahead, behind) = gitOperations.CompareBranches(branch, sourceBranch); + var (aheadRemote, behindRemote) = gitOperations.GetComparisonToRemoteTrackingBranch(branch); + var branchStatus = new BranchStatus(branchExistsLocally, true, ahead, behind, aheadRemote, behindRemote); status.Branches[branch].Status = branchStatus; } var parentBranch = stack.SourceBranch; + status.Branches.Add(stack.SourceBranch, new BranchDetail()); + var sourceBranchRemoteStatus = gitOperations.GetComparisonToRemoteTrackingBranch(stack.SourceBranch); + status.Branches[stack.SourceBranch].Status = new BranchStatus(branchesThatExistLocally.Contains(stack.SourceBranch), true, 0, 0, sourceBranchRemoteStatus.Ahead, sourceBranchRemoteStatus.Behind); + foreach (var branch in stack.Branches) { status.Branches.Add(branch, new BranchDetail()); if (branchesThatExistInRemote.Contains(branch)) { - CheckRemoteBranch(branch, parentBranch); + CheckBranchStatus(branch, parentBranch); parentBranch = branch; } else { - status.Branches[branch].Status = new BranchStatus(branchesThatExistLocally.Contains(branch), false, 0, 0); + status.Branches[branch].Status = new BranchStatus(branchesThatExistLocally.Contains(branch), false, 0, 0, 0, 0); } } } }); - outputProvider.Status("Checking status of GitHub pull requests...", () => + if (includePullRequestStatus) { - foreach (var (stack, status) in stacksToCheckStatusFor) + outputProvider.Status("Checking status of GitHub pull requests...", () => { - try + foreach (var (stack, status) in stacksToCheckStatusFor) { - foreach (var branch in stack.Branches) + try { - var pr = gitHubOperations.GetPullRequest(branch); - - if (pr is not null) + foreach (var branch in stack.Branches) { - status.Branches[branch].PullRequest = pr; + var pr = gitHubOperations.GetPullRequest(branch); + + if (pr is not null) + { + status.Branches[branch].PullRequest = pr; + } } } + catch (Exception ex) + { + outputProvider.Warning($"Error checking GitHub pull requests: {ex.Message}"); + } } - catch (Exception ex) - { - outputProvider.Warning($"Error checking GitHub pull requests: {ex.Message}"); - } - } - }); + }); + } return stacksToCheckStatusFor; } @@ -174,10 +182,6 @@ public static string GetBranchStatusOutput( { var branchNameBuilder = new StringBuilder(); var currentBranch = gitOperations.GetCurrentBranch(); - var branchIsMerged = - branchDetail.Status.ExistsInRemote == false || - branchDetail.Status.ExistsLocally == false || - branchDetail.PullRequest is not null && branchDetail.PullRequest.State == GitHubPullRequestStates.Merged; var color = !branchDetail.IsActive ? "grey" : branch.Equals(currentBranch, StringComparison.OrdinalIgnoreCase) ? "blue" : null; Decoration? decoration = !branchDetail.IsActive ? Decoration.Strikethrough : null; @@ -201,17 +205,22 @@ public static string GetBranchStatusOutput( if (branchDetail.IsActive) { - if (branchDetail.Status.Ahead > 0 && branchDetail.Status.Behind > 0) + if (branchDetail.Status.AheadOfRemote > 0 || branchDetail.Status.BehindRemote > 0) + { + branchNameBuilder.Append($" {branchDetail.Status.BehindRemote}{Emoji.Known.DownArrow}{branchDetail.Status.AheadOfRemote}{Emoji.Known.UpArrow}".Muted()); + } + + if (branchDetail.Status.AheadOfParent > 0 && branchDetail.Status.BehindParent > 0) { - branchNameBuilder.Append($" [grey]({branchDetail.Status.Ahead} ahead, {branchDetail.Status.Behind} behind {parentBranch})[/]"); + branchNameBuilder.Append($" [grey]({branchDetail.Status.AheadOfParent} ahead, {branchDetail.Status.BehindParent} behind {parentBranch})[/]"); } - else if (branchDetail.Status.Ahead > 0) + else if (branchDetail.Status.AheadOfParent > 0) { - branchNameBuilder.Append($" [grey]({branchDetail.Status.Ahead} ahead of {parentBranch})[/]"); + branchNameBuilder.Append($" [grey]({branchDetail.Status.AheadOfParent} ahead of {parentBranch})[/]"); } - else if (branchDetail.Status.Behind > 0) + else if (branchDetail.Status.BehindParent > 0) { - branchNameBuilder.Append($" [grey]({branchDetail.Status.Behind} behind {parentBranch})[/]"); + branchNameBuilder.Append($" [grey]({branchDetail.Status.BehindParent} behind {parentBranch})[/]"); } } @@ -245,7 +254,7 @@ public static void OutputBranchAndStackCleanup( outputProvider.Information($"Run {$"stack delete --name \"{stack.Name}\"".Example()} to delete the stack."); } - if (status.Branches.Values.Any(branch => branch.Status.ExistsInRemote && branch.Status.ExistsLocally && branch.Status.Behind > 0)) + if (status.Branches.Values.Any(branch => branch.Status.ExistsInRemote && branch.Status.ExistsLocally && branch.Status.BehindParent > 0)) { outputProvider.NewLine(); outputProvider.Information("There are changes in source branches that have not been applied to the stack."); diff --git a/src/Stack/Commands/Stack/StackStatusCommand.cs b/src/Stack/Commands/Stack/StackStatusCommand.cs index 8c2b66a1..cdddb0e2 100644 --- a/src/Stack/Commands/Stack/StackStatusCommand.cs +++ b/src/Stack/Commands/Stack/StackStatusCommand.cs @@ -17,6 +17,10 @@ public class StackStatusCommandSettings : CommandSettingsBase [Description("Show status of all stacks.")] [CommandOption("--all")] public bool All { get; init; } + + [Description("Show full status including pull requests.")] + [CommandOption("--full")] + public bool Full { get; init; } } public class StackStatusCommand : AsyncCommand @@ -33,13 +37,13 @@ public override async Task ExecuteAsync(CommandContext context, StackStatus new GitHubOperations(outputProvider, settings.GetGitHubOperationSettings()), new StackConfig()); - await handler.Handle(new StackStatusCommandInputs(settings.Name, settings.All)); + await handler.Handle(new StackStatusCommandInputs(settings.Name, settings.All, settings.Full)); return 0; } } -public record StackStatusCommandInputs(string? Name, bool All); +public record StackStatusCommandInputs(string? Name, bool All, bool Full); public record StackStatusCommandResponse(Dictionary Statuses); public class StackStatusCommandHandler( @@ -81,7 +85,8 @@ public async Task Handle(StackStatusCommandInputs in currentBranch, outputProvider, gitOperations, - gitHubOperations); + gitHubOperations, + inputs.Full); StackStatusHelpers.OutputStackStatus(stackStatusResults, gitOperations, outputProvider); diff --git a/src/Stack/Git/GitOperations.cs b/src/Stack/Git/GitOperations.cs index 60c9b5af..e29d8d80 100644 --- a/src/Stack/Git/GitOperations.cs +++ b/src/Stack/Git/GitOperations.cs @@ -31,6 +31,8 @@ public interface IGitOperations bool IsRemoteBranchFullyMerged(string branchName, string sourceBranchName); string[] GetBranchesThatHaveBeenMerged(string[] branches, string sourceBranchName); (int Ahead, int Behind) GetStatusOfRemoteBranch(string branchName, string sourceBranchName); + (int Ahead, int Behind) CompareBranches(string branchName, string sourceBranchName); + (int Ahead, int Behind) GetComparisonToRemoteTrackingBranch(string branchName); string GetRemoteUri(); string[] GetLocalBranchesOrderedByMostRecentCommitterDate(); string GetRootOfRepository(); @@ -139,6 +141,20 @@ public string[] GetBranchesThatHaveBeenMerged(string[] branches, string sourceBr return (int.Parse(parts[0]), int.Parse(parts[1])); } + public (int Ahead, int Behind) CompareBranches(string branchName, string sourceBranchName) + { + var status = ExecuteGitCommandAndReturnOutput($"rev-list --left-right --count {branchName}...{sourceBranchName}").Trim(); + var parts = status.Split('\t'); + return (int.Parse(parts[0]), int.Parse(parts[1])); + } + + public (int Ahead, int Behind) GetComparisonToRemoteTrackingBranch(string branchName) + { + var status = ExecuteGitCommandAndReturnOutput($"rev-list --left-right --count {branchName}...origin/{branchName}").Trim(); + var parts = status.Split('\t'); + return (int.Parse(parts[0]), int.Parse(parts[1])); + } + public string GetRemoteUri() { return ExecuteGitCommandAndReturnOutput("remote get-url origin").Trim(); From 80843923563f399a486a29521284104daa17063e Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 20 Dec 2024 15:51:11 +1100 Subject: [PATCH 02/26] Make showing status much better and quicker --- .../Stack/StackStatusCommandHandlerTests.cs | 199 +++++++++++------- .../Helpers/TestGitRepositoryBuilder.cs | 8 +- .../Commands/Helpers/StackStatusHelpers.cs | 112 +++++----- .../PullRequests/CreatePullRequestsCommand.cs | 10 +- .../Commands/Stack/StackStatusCommand.cs | 11 +- src/Stack/Git/GitOperations.cs | 45 +++- .../Infrastructure/ConsoleOutputProvider.cs | 9 +- 7 files changed, 245 insertions(+), 149 deletions(-) diff --git a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs index ff755157..3f897179 100644 --- a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs @@ -16,15 +16,19 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneHasAPullRequests_Retur { // Arrange var sourceBranch = Some.BranchName(); - var aBranch = Some.BranchName(); - var aSecondBranch = 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(aBranch).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(10).PushToRemote()) - .WithBranch(builder => builder.WithName(aSecondBranch).FromSourceBranch(aBranch).WithNumberOfEmptyCommits(1).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) .Build(); + var tipOfSourceBranch = repo.GetTipOfBranch(sourceBranch); + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); var outputProvider = Substitute.For(); @@ -32,7 +36,7 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneHasAPullRequests_Retur var gitHubOperations = Substitute.For(); var handler = new StackStatusCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); - var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [aBranch, aSecondBranch]); + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); var stacks = new List([stack1, stack2]); stackConfig.Load().Returns(stacks); @@ -45,24 +49,26 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneHasAPullRequests_Retur var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .GetPullRequest(aBranch) + .GetPullRequest(branch1) .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, false)); + var response = await handler.Handle(new StackStatusCommandInputs(null, false, false, true)); // Assert var expectedBranchDetails = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5, 0, 0), PullRequest = pr } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0) } } + { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, + { branch1, new BranchDetail { Status = new BranchStatus(true, true, false, 10, 5, 0, 0, new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim())), PullRequest = pr } }, + { branch2, new BranchDetail { Status = new BranchStatus(true, true, false, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())) } } }; - response.Statuses.Should().BeEquivalentTo(new Dictionary - { + response.Statuses.Should().BeEquivalentTo( + new Dictionary { - stack1, new(expectedBranchDetails) - } - }); + { + stack1, new(expectedBranchDetails) + } + }); } [Fact] @@ -70,15 +76,19 @@ public async Task WhenStackNameIsProvided_DoesNotAskForStack_ReturnsStatus() { // Arrange var sourceBranch = Some.BranchName(); - var aBranch = Some.BranchName(); - var aSecondBranch = 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(aBranch).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(10).PushToRemote()) - .WithBranch(builder => builder.WithName(aSecondBranch).FromSourceBranch(aBranch).WithNumberOfEmptyCommits(1).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) .Build(); + var tipOfSourceBranch = repo.GetTipOfBranch(sourceBranch); + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); var outputProvider = Substitute.For(); @@ -86,7 +96,7 @@ public async Task WhenStackNameIsProvided_DoesNotAskForStack_ReturnsStatus() var gitHubOperations = Substitute.For(); var handler = new StackStatusCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); - var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [aBranch, aSecondBranch]); + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); var stacks = new List([stack1, stack2]); stackConfig.Load().Returns(stacks); @@ -99,17 +109,18 @@ public async Task WhenStackNameIsProvided_DoesNotAskForStack_ReturnsStatus() var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .GetPullRequest(aBranch) + .GetPullRequest(branch1) .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs("Stack1", false)); + var response = await handler.Handle(new StackStatusCommandInputs("Stack1", false, false, true)); // Assert var expectedBranchDetails = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5, 0, 0), PullRequest = pr } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0) } } + { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, + { branch1, new BranchDetail { Status = new BranchStatus(true, true, false, 10, 5, 0, 0, new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim())), PullRequest = pr } }, + { branch2, new BranchDetail { Status = new BranchStatus(true, true, false, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())) } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { @@ -126,17 +137,22 @@ public async Task WhenAllStacksAreRequested_ReturnsStatusOfEachStack() { // Arrange var sourceBranch = Some.BranchName(); - var aBranch = Some.BranchName(); - var aSecondBranch = Some.BranchName(); - var aThirdBranch = Some.BranchName(); + var branch1 = Some.BranchName(); + var branch2 = Some.BranchName(); + var branch3 = Some.BranchName(); using var repo = new TestGitRepositoryBuilder() .WithBranch(builder => builder.WithName(sourceBranch).PushToRemote()) - .WithBranch(builder => builder.WithName(aBranch).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(10).PushToRemote()) - .WithBranch(builder => builder.WithName(aSecondBranch).FromSourceBranch(aBranch).WithNumberOfEmptyCommits(1).PushToRemote()) - .WithBranch(builder => builder.WithName(aThirdBranch).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(3).PushToRemote()) + .WithBranch(builder => builder.WithName(branch1).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(10).PushToRemote()) + .WithBranch(builder => builder.WithName(branch2).FromSourceBranch(branch1).WithNumberOfEmptyCommits(1).PushToRemote()) + .WithBranch(builder => builder.WithName(branch3).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(3).PushToRemote()) .WithNumberOfEmptyCommits(b => b.OnBranch(sourceBranch).PushToRemote(), 5) .Build(); + var tipOfSourceBranch = repo.GetTipOfBranch(sourceBranch); + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + var tipOfBranch3 = repo.GetTipOfBranch(branch3); + var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); var outputProvider = Substitute.For(); @@ -144,8 +160,8 @@ public async Task WhenAllStacksAreRequested_ReturnsStatusOfEachStack() var gitHubOperations = Substitute.For(); var handler = new StackStatusCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); - var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [aBranch, aSecondBranch]); - var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, [aThirdBranch]); + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, [branch3]); var stacks = new List([stack1, stack2]); stackConfig.Load().Returns(stacks); @@ -156,21 +172,23 @@ public async Task WhenAllStacksAreRequested_ReturnsStatusOfEachStack() var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .GetPullRequest(aBranch) + .GetPullRequest(branch1) .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, true)); + var response = await handler.Handle(new StackStatusCommandInputs(null, true, false, true)); // Assert var expectedBranchDetailsForStack1 = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5, 0, 0), PullRequest = pr } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0) } } + { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, + { branch1, new BranchDetail { Status = new BranchStatus(true, true, false, 10, 5, 0, 0, new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim())), PullRequest = pr } }, + { branch2, new BranchDetail { Status = new BranchStatus(true, true, false, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())) } } }; var expectedBranchDetailsForStack2 = new Dictionary { - { aThirdBranch, new BranchDetail { Status = new BranchStatus(true, true, 3, 5, 0, 0) } } + { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, + { branch3, new BranchDetail { Status = new BranchStatus(true, true, false, 3, 5, 0, 0, new Commit(tipOfBranch3.Sha[..7], tipOfBranch3.Message.Trim())) } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { @@ -188,17 +206,22 @@ public async Task WhenAllStacksAreRequested_WithStacksInMultipleRepositories_Ret { // Arrange var sourceBranch = Some.BranchName(); - var aBranch = Some.BranchName(); - var aSecondBranch = Some.BranchName(); - var aThirdBranch = Some.BranchName(); + var branch1 = Some.BranchName(); + var branch2 = Some.BranchName(); + var branch3 = Some.BranchName(); using var repo = new TestGitRepositoryBuilder() .WithBranch(builder => builder.WithName(sourceBranch).PushToRemote()) - .WithBranch(builder => builder.WithName(aBranch).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(10).PushToRemote()) - .WithBranch(builder => builder.WithName(aSecondBranch).FromSourceBranch(aBranch).WithNumberOfEmptyCommits(1).PushToRemote()) - .WithBranch(builder => builder.WithName(aThirdBranch).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(3).PushToRemote()) + .WithBranch(builder => builder.WithName(branch1).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(10).PushToRemote()) + .WithBranch(builder => builder.WithName(branch2).FromSourceBranch(branch1).WithNumberOfEmptyCommits(1).PushToRemote()) + .WithBranch(builder => builder.WithName(branch3).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(3).PushToRemote()) .WithNumberOfEmptyCommits(b => b.OnBranch(sourceBranch).PushToRemote(), 5) .Build(); + var tipOfSourceBranch = repo.GetTipOfBranch(sourceBranch); + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + var tipOfBranch3 = repo.GetTipOfBranch(branch3); + var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); var outputProvider = Substitute.For(); @@ -206,8 +229,8 @@ public async Task WhenAllStacksAreRequested_WithStacksInMultipleRepositories_Ret var gitHubOperations = Substitute.For(); var handler = new StackStatusCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); - var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [aBranch, aSecondBranch]); - var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, [aThirdBranch]); + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, [branch3]); var stack3 = new Config.Stack("Stack2", Some.HttpsUri().ToString(), Some.BranchName(), [Some.BranchName()]); var stacks = new List([stack1, stack2]); stackConfig.Load().Returns(stacks); @@ -219,21 +242,23 @@ public async Task WhenAllStacksAreRequested_WithStacksInMultipleRepositories_Ret var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .GetPullRequest(aBranch) + .GetPullRequest(branch1) .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, true)); + var response = await handler.Handle(new StackStatusCommandInputs(null, true, false, true)); // Assert var expectedBranchDetailsForStack1 = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5, 0, 0), PullRequest = pr } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0) } } + { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, + { branch1, new BranchDetail { Status = new BranchStatus(true, true, false, 10, 5, 0, 0, new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim())), PullRequest = pr } }, + { branch2, new BranchDetail { Status = new BranchStatus(true, true, false, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())) } } }; var expectedBranchDetailsForStack2 = new Dictionary { - { aThirdBranch, new BranchDetail { Status = new BranchStatus(true, true, 3, 5, 0, 0) } } + { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, + { branch3, new BranchDetail { Status = new BranchStatus(true, true, false, 3, 5, 0, 0, new Commit(tipOfBranch3.Sha[..7], tipOfBranch3.Message.Trim())) } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { @@ -277,7 +302,7 @@ public async Task WhenStackNameIsProvided_ButStackDoesNotExist_Throws() // Act and assert var incorrectStackName = Some.Name(); await handler - .Invoking(async h => await h.Handle(new StackStatusCommandInputs(incorrectStackName, false))) + .Invoking(async h => await h.Handle(new StackStatusCommandInputs(incorrectStackName, false, false, false))) .Should().ThrowAsync() .WithMessage($"Stack '{incorrectStackName}' not found."); } @@ -287,15 +312,19 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem { // Arrange var sourceBranch = Some.BranchName(); - var aBranch = Some.BranchName(); - var aSecondBranch = Some.BranchName(); - var aThirdBranch = Some.BranchName(); + var branch1 = "branch-1"; + var branch2 = "branch-2"; + var branch3 = "branch-3"; using var repo = new TestGitRepositoryBuilder() .WithBranch(builder => builder.WithName(sourceBranch).WithNumberOfEmptyCommits(5).PushToRemote()) - .WithBranch(builder => builder.WithName(aBranch).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(10)) - .WithBranch(builder => builder.WithName(aSecondBranch).FromSourceBranch(aBranch).WithNumberOfEmptyCommits(1).PushToRemote()) + .WithBranch(builder => builder.WithName(branch1).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(10)) + .WithBranch(builder => builder.WithName(branch2).FromSourceBranch(branch1).WithNumberOfEmptyCommits(1).PushToRemote()) .Build(); + var tipOfSourceBranch = repo.GetTipOfBranch(sourceBranch); + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); var outputProvider = Substitute.For(); @@ -303,8 +332,8 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem var gitHubOperations = Substitute.For(); var handler = new StackStatusCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); - var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [aBranch, aSecondBranch]); - var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, [aThirdBranch]); + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, [branch3]); var stacks = new List([stack1, stack2]); stackConfig.Load().Returns(stacks); @@ -316,17 +345,18 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .GetPullRequest(aSecondBranch) + .GetPullRequest(branch2) .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, false)); + var response = await handler.Handle(new StackStatusCommandInputs(null, false, false, true)); // Assert var expectedBranchDetails = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, false, 0, 0, 0, 0) } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 11, 0, 0, 0), PullRequest = pr } } // The 11 commits are the 10 commits from the parent branch and one from this branch + { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, false, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, + { branch1, new BranchDetail { Status = new BranchStatus(true, false, false, 0, 0, 0, 0, new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim())) } }, + { branch2, new BranchDetail { Status = new BranchStatus(true, true, true, 11, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())), PullRequest = pr } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { @@ -341,14 +371,17 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem { // Arrange var sourceBranch = Some.BranchName(); - var aBranch = Some.BranchName(); - var aSecondBranch = Some.BranchName(); - var aThirdBranch = Some.BranchName(); + var branch1 = Some.BranchName(); + var branch2 = Some.BranchName(); + var branch3 = Some.BranchName(); using var repo = new TestGitRepositoryBuilder() .WithBranch(builder => builder.WithName(sourceBranch).WithNumberOfEmptyCommits(5).PushToRemote()) - .WithBranch(builder => builder.WithName(aSecondBranch).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(1).PushToRemote()) + .WithBranch(builder => builder.WithName(branch2).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(1).PushToRemote()) .Build(); + var tipOfSourceBranch = repo.GetTipOfBranch(sourceBranch); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); var outputProvider = Substitute.For(); @@ -356,8 +389,8 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem var gitHubOperations = Substitute.For(); var handler = new StackStatusCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); - var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [aBranch, aSecondBranch]); - var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, [aThirdBranch]); + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, [branch3]); var stacks = new List([stack1, stack2]); stackConfig.Load().Returns(stacks); @@ -369,17 +402,18 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .GetPullRequest(aSecondBranch) + .GetPullRequest(branch2) .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, false)); + var response = await handler.Handle(new StackStatusCommandInputs(null, false, false, true)); // Assert var expectedBranchDetails = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(false, false, 0, 0, 0, 0) } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0), PullRequest = pr } } + { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, false, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, + { branch1, new BranchDetail { Status = new BranchStatus(false, false, false, 0, 0, 0, 0, null) } }, + { branch2, new BranchDetail { Status = new BranchStatus(true, true, true, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())), PullRequest = pr } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { @@ -394,15 +428,19 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_ReturnsStatus() { // Arrange var sourceBranch = Some.BranchName(); - var aBranch = Some.BranchName(); - var aSecondBranch = 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(aBranch).FromSourceBranch(sourceBranch).WithNumberOfEmptyCommits(10).PushToRemote()) - .WithBranch(builder => builder.WithName(aSecondBranch).FromSourceBranch(aBranch).WithNumberOfEmptyCommits(1).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) .Build(); + var tipOfSourceBranch = repo.GetTipOfBranch(sourceBranch); + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); var outputProvider = Substitute.For(); @@ -410,7 +448,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_ReturnsStatus() var gitHubOperations = Substitute.For(); var handler = new StackStatusCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); - var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [aBranch, aSecondBranch]); + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); var stacks = new List([stack1]); stackConfig.Load().Returns(stacks); @@ -421,17 +459,18 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_ReturnsStatus() var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .GetPullRequest(aBranch) + .GetPullRequest(branch1) .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, false)); + var response = await handler.Handle(new StackStatusCommandInputs(null, false, false, true)); // Assert var expectedBranchDetails = new Dictionary { - { aBranch, new BranchDetail { Status = new BranchStatus(true, true, 10, 5, 0, 0), PullRequest = pr } }, - { aSecondBranch, new BranchDetail { Status = new BranchStatus(true, true, 1, 0, 0, 0) } } + { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, + { branch1, new BranchDetail { Status = new BranchStatus(true, true, false, 10, 5, 0, 0, new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim())), PullRequest = pr } }, + { branch2, new BranchDetail { Status = new BranchStatus(true, true, false, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())) } } }; response.Statuses.Should().BeEquivalentTo(new Dictionary { diff --git a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs index 14dfcf8a..96a4944f 100644 --- a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs +++ b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs @@ -56,7 +56,7 @@ public void Build(Repository repository, string defaultBranchName) } } - private static Commit CreateEmptyCommit(Repository repository, Branch branch, string message) + private static LibGit2Sharp.Commit CreateEmptyCommit(Repository repository, Branch branch, string message) { repository.Refs.UpdateTarget("HEAD", branch.CanonicalName); var signature = new Signature(Some.Name(), Some.Name(), DateTimeOffset.Now); @@ -238,7 +238,7 @@ public TestGitRepository Build() return new TestGitRepository(localDirectory, remoteDirectory, localRepo); } - private static Commit CreateInitialCommit(Repository repository) + private static LibGit2Sharp.Commit CreateInitialCommit(Repository repository) { var message = $"Initial commit"; var signature = new Signature(Some.Name(), Some.Name(), DateTimeOffset.Now); @@ -252,12 +252,12 @@ public class TestGitRepository(TemporaryDirectory LocalDirectory, TemporaryDirec public string RemoteUri => RemoteDirectory.DirectoryPath; public GitOperationSettings GitOperationSettings => new GitOperationSettings(false, false, LocalDirectory.DirectoryPath); - public Commit GetTipOfBranch(string branchName) + public LibGit2Sharp.Commit GetTipOfBranch(string branchName) { return LocalRepository.Branches[branchName].Tip; } - public List GetCommitsReachableFromBranch(string branchName) + public List GetCommitsReachableFromBranch(string branchName) { return [.. LocalRepository.Branches[branchName].Commits]; } diff --git a/src/Stack/Commands/Helpers/StackStatusHelpers.cs b/src/Stack/Commands/Helpers/StackStatusHelpers.cs index 4c9be275..6474291f 100644 --- a/src/Stack/Commands/Helpers/StackStatusHelpers.cs +++ b/src/Stack/Commands/Helpers/StackStatusHelpers.cs @@ -1,4 +1,5 @@ using System.Text; +using Microsoft.VisualBasic; using Spectre.Console; using Stack.Config; using Stack.Git; @@ -8,14 +9,14 @@ namespace Stack.Commands.Helpers; public class BranchDetail { - public BranchStatus Status { get; set; } = new(false, false, 0, 0, 0, 0); + public BranchStatus Status { get; set; } = new(false, false, false, 0, 0, 0, 0, null); public GitHubPullRequest? PullRequest { get; set; } public bool IsActive => Status.ExistsLocally && Status.ExistsInRemote && (PullRequest is null || PullRequest.State != GitHubPullRequestStates.Merged); public bool CouldBeCleanedUp => Status.ExistsLocally && (!Status.ExistsInRemote || PullRequest is not null && PullRequest.State == GitHubPullRequestStates.Merged); public bool HasPullRequest => PullRequest is not null && PullRequest.State != GitHubPullRequestStates.Closed; } -public record BranchStatus(bool ExistsLocally, bool ExistsInRemote, int AheadOfParent, int BehindParent, int AheadOfRemote, int BehindRemote); +public record BranchStatus(bool ExistsLocally, bool ExistsInRemote, bool IsCurrentBranch, int AheadOfParent, int BehindParent, int AheadOfRemote, int BehindRemote, Commit? Tip); public record StackStatus(Dictionary Branches) { public string[] GetActiveBranches() => Branches.Where(b => b.Value.IsActive).Select(b => b.Key).ToArray(); @@ -29,6 +30,7 @@ public static class StackStatusHelpers IOutputProvider outputProvider, IGitOperations gitOperations, IGitHubOperations gitHubOperations, + bool includeParentBranchStatus = true, bool includePullRequestStatus = true) { var stacksToCheckStatusFor = new Dictionary(); @@ -40,44 +42,37 @@ public static class StackStatusHelpers var allBranchesInStacks = stacks.SelectMany(s => new List([s.SourceBranch]).Concat(s.Branches)).Distinct().ToArray(); - outputProvider.Status("Checking status of branches...", () => + var branchStatuses = gitOperations.GetBranchStatuses(allBranchesInStacks); + + foreach (var (stack, status) in stacksToCheckStatusFor) { - var branchesThatExistInRemote = gitOperations.GetBranchesThatExistInRemote(allBranchesInStacks); - var branchesThatExistLocally = gitOperations.GetBranchesThatExistLocally(allBranchesInStacks); + var parentBranch = stack.SourceBranch; - foreach (var (stack, status) in stacksToCheckStatusFor) + status.Branches.Add(stack.SourceBranch, new BranchDetail()); + branchStatuses.TryGetValue(stack.SourceBranch, out var sourceBranchStatus); + if (sourceBranchStatus is not null) { - void CheckBranchStatus(string branch, string sourceBranch) - { - var branchExistsLocally = branchesThatExistLocally.Contains(branch); - var (ahead, behind) = gitOperations.CompareBranches(branch, sourceBranch); - var (aheadRemote, behindRemote) = gitOperations.GetComparisonToRemoteTrackingBranch(branch); - var branchStatus = new BranchStatus(branchExistsLocally, true, ahead, behind, aheadRemote, behindRemote); - status.Branches[branch].Status = branchStatus; - } - - var parentBranch = stack.SourceBranch; + status.Branches[stack.SourceBranch].Status = new BranchStatus(true, sourceBranchStatus.RemoteBranchExists, sourceBranchStatus.IsCurrentBranch, 0, 0, sourceBranchStatus.Ahead, sourceBranchStatus.Behind, sourceBranchStatus.Tip); + } - status.Branches.Add(stack.SourceBranch, new BranchDetail()); - var sourceBranchRemoteStatus = gitOperations.GetComparisonToRemoteTrackingBranch(stack.SourceBranch); - status.Branches[stack.SourceBranch].Status = new BranchStatus(branchesThatExistLocally.Contains(stack.SourceBranch), true, 0, 0, sourceBranchRemoteStatus.Ahead, sourceBranchRemoteStatus.Behind); + foreach (var branch in stack.Branches) + { + status.Branches.Add(branch, new BranchDetail()); + branchStatuses.TryGetValue(branch, out var branchStatus); - foreach (var branch in stack.Branches) + if (branchStatus is not null) { - status.Branches.Add(branch, new BranchDetail()); + var (aheadOfParent, behindParent) = includeParentBranchStatus && branchStatus.RemoteBranchExists ? gitOperations.CompareBranches(branch, parentBranch) : (0, 0); - if (branchesThatExistInRemote.Contains(branch)) + status.Branches[branch].Status = new BranchStatus(true, branchStatus.RemoteBranchExists, branchStatus.IsCurrentBranch, aheadOfParent, behindParent, branchStatus.Ahead, branchStatus.Behind, branchStatus.Tip); + + if (branchStatus.RemoteBranchExists) { - CheckBranchStatus(branch, parentBranch); parentBranch = branch; } - else - { - status.Branches[branch].Status = new BranchStatus(branchesThatExistLocally.Contains(branch), false, 0, 0, 0, 0); - } } } - }); + } if (includePullRequestStatus) { @@ -122,22 +117,25 @@ public static StackStatus GetStackStatus( public static void OutputStackStatus( Dictionary stackStatuses, - IGitOperations gitOperations, IOutputProvider outputProvider) { foreach (var (stack, status) in stackStatuses) { - OutputStackStatus(stack, status, gitOperations, outputProvider); + OutputStackStatus(stack, status, outputProvider); + outputProvider.NewLine(); } } public static void OutputStackStatus( Config.Stack stack, StackStatus status, - IGitOperations gitOperations, IOutputProvider outputProvider) { - var header = $"{stack.Name.Stack()}: {stack.SourceBranch.Muted()}"; + var header = stack.SourceBranch.Branch(); + if (status.Branches.TryGetValue(stack.SourceBranch, out var sourceBranchStatus)) + { + header = GetBranchStatusOutput(stack.SourceBranch, null, sourceBranchStatus); + } var items = new List(); string parentBranch = stack.SourceBranch; @@ -146,7 +144,7 @@ public static void OutputStackStatus( { if (status.Branches.TryGetValue(branch, out var branchDetail)) { - items.Add(GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail, gitOperations)); + items.Add(GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail)); if (branchDetail.IsActive) { @@ -154,17 +152,17 @@ public static void OutputStackStatus( } } } + outputProvider.Information(stack.Name.Stack()); outputProvider.Tree(header, [.. items]); } public static string GetBranchAndPullRequestStatusOutput( string branch, - string parentBranch, - BranchDetail branchDetail, - IGitOperations gitOperations) + string? parentBranch, + BranchDetail branchDetail) { var branchNameBuilder = new StringBuilder(); - branchNameBuilder.Append(GetBranchStatusOutput(branch, parentBranch, branchDetail, gitOperations)); + branchNameBuilder.Append(GetBranchStatusOutput(branch, parentBranch, branchDetail)); if (branchDetail.PullRequest is not null) { @@ -176,53 +174,65 @@ public static string GetBranchAndPullRequestStatusOutput( public static string GetBranchStatusOutput( string branch, - string parentBranch, - BranchDetail branchDetail, - IGitOperations gitOperations) + string? parentBranch, + BranchDetail branchDetail) { var branchNameBuilder = new StringBuilder(); - var currentBranch = gitOperations.GetCurrentBranch(); - var color = !branchDetail.IsActive ? "grey" : branch.Equals(currentBranch, StringComparison.OrdinalIgnoreCase) ? "blue" : null; - Decoration? decoration = !branchDetail.IsActive ? Decoration.Strikethrough : null; + var branchName = branchDetail.Status.IsCurrentBranch ? $"* [{Color.Green}]{branch}[/]" : branch; + Color? color = branchDetail.Status.ExistsLocally ? null : Color.Grey; + Decoration? decoration = branchDetail.Status.ExistsLocally ? null : Decoration.Strikethrough; if (color is not null && decoration is not null) { - branchNameBuilder.Append($"[{decoration} {color}]{branch}[/]"); + branchNameBuilder.Append($"[{decoration} {color}]{branchName}[/]"); } else if (color is not null) { - branchNameBuilder.Append($"[{color}]{branch}[/]"); + branchNameBuilder.Append($"[{color}]{branchName}[/]"); } else if (decoration is not null) { - branchNameBuilder.Append($"[{decoration}]{branch}[/]"); + branchNameBuilder.Append($"[{decoration}]{branchName}[/]"); } else { - branchNameBuilder.Append(branch); + branchNameBuilder.Append(branchName); } if (branchDetail.IsActive) { if (branchDetail.Status.AheadOfRemote > 0 || branchDetail.Status.BehindRemote > 0) { - branchNameBuilder.Append($" {branchDetail.Status.BehindRemote}{Emoji.Known.DownArrow}{branchDetail.Status.AheadOfRemote}{Emoji.Known.UpArrow}".Muted()); + branchNameBuilder.Append($" {branchDetail.Status.BehindRemote}{Emoji.Known.DownArrow}{branchDetail.Status.AheadOfRemote}{Emoji.Known.UpArrow}"); } if (branchDetail.Status.AheadOfParent > 0 && branchDetail.Status.BehindParent > 0) { - branchNameBuilder.Append($" [grey]({branchDetail.Status.AheadOfParent} ahead, {branchDetail.Status.BehindParent} behind {parentBranch})[/]"); + branchNameBuilder.Append($" ({branchDetail.Status.AheadOfParent} ahead, {branchDetail.Status.BehindParent} behind {parentBranch})".Muted()); } else if (branchDetail.Status.AheadOfParent > 0) { - branchNameBuilder.Append($" [grey]({branchDetail.Status.AheadOfParent} ahead of {parentBranch})[/]"); + branchNameBuilder.Append($" ({branchDetail.Status.AheadOfParent} ahead of {parentBranch})".Muted()); } else if (branchDetail.Status.BehindParent > 0) { - branchNameBuilder.Append($" [grey]({branchDetail.Status.BehindParent} behind {parentBranch})[/]"); + branchNameBuilder.Append($" ({branchDetail.Status.BehindParent} behind {parentBranch})".Muted()); } } + else if (branchDetail.Status.ExistsLocally && !branchDetail.Status.ExistsInRemote) + { + branchNameBuilder.Append(" (remote branch deleted)".Muted()); + } + else if (branchDetail.PullRequest is not null && branchDetail.PullRequest.State == GitHubPullRequestStates.Merged) + { + branchNameBuilder.Append(" (pull request merged)".Muted()); + } + + if (branchDetail.Status.Tip is not null) + { + branchNameBuilder.Append($" {branchDetail.Status.Tip.Sha[..7].Commit()} {branchDetail.Status.Tip.Message}"); + } return branchNameBuilder.ToString(); } diff --git a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs index 9f28f509..46c8168d 100644 --- a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs +++ b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs @@ -100,7 +100,7 @@ public async Task Handle(CreatePullRequestsCo } } - StackStatusHelpers.OutputStackStatus(stack, status, gitOperations, outputProvider); + StackStatusHelpers.OutputStackStatus(stack, status, outputProvider); outputProvider.NewLine(); @@ -112,7 +112,7 @@ public async Task Handle(CreatePullRequestsCo outputProvider.NewLine(); - OutputUpdatedStackStatus(outputProvider, gitOperations, stack, status, pullRequestCreateActions); + OutputUpdatedStackStatus(outputProvider, stack, status, pullRequestCreateActions); outputProvider.NewLine(); @@ -229,7 +229,7 @@ private static List CreatePullRequests( return pullRequests; } - private static void OutputUpdatedStackStatus(IOutputProvider outputProvider, IGitOperations gitOperations, Config.Stack stack, StackStatus status, List pullRequestCreateActions) + private static void OutputUpdatedStackStatus(IOutputProvider outputProvider, Config.Stack stack, StackStatus status, List pullRequestCreateActions) { var branchDisplayItems = new List(); var parentBranch = stack.SourceBranch; @@ -239,12 +239,12 @@ private static void OutputUpdatedStackStatus(IOutputProvider outputProvider, IGi var branchDetail = status.Branches[branch]; if (branchDetail.PullRequest is not null && branchDetail.PullRequest.State != GitHubPullRequestStates.Closed) { - branchDisplayItems.Add(StackStatusHelpers.GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail, gitOperations)); + branchDisplayItems.Add(StackStatusHelpers.GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail)); } else { var action = pullRequestCreateActions.FirstOrDefault(a => a.HeadBranch == branch); - branchDisplayItems.Add($"{StackStatusHelpers.GetBranchStatusOutput(branch, parentBranch, branchDetail, gitOperations)} *NEW* {action?.Title}{(action?.Draft == true ? " (draft)".Muted() : string.Empty)}"); + branchDisplayItems.Add($"{StackStatusHelpers.GetBranchStatusOutput(branch, parentBranch, branchDetail)} *NEW* {action?.Title}{(action?.Draft == true ? " (draft)".Muted() : string.Empty)}"); } parentBranch = branch; } diff --git a/src/Stack/Commands/Stack/StackStatusCommand.cs b/src/Stack/Commands/Stack/StackStatusCommand.cs index cdddb0e2..15d7bde5 100644 --- a/src/Stack/Commands/Stack/StackStatusCommand.cs +++ b/src/Stack/Commands/Stack/StackStatusCommand.cs @@ -18,6 +18,10 @@ public class StackStatusCommandSettings : CommandSettingsBase [CommandOption("--all")] public bool All { get; init; } + [Description("Show minimal status.")] + [CommandOption("--minimal")] + public bool Minimal { get; init; } + [Description("Show full status including pull requests.")] [CommandOption("--full")] public bool Full { get; init; } @@ -37,13 +41,13 @@ public override async Task ExecuteAsync(CommandContext context, StackStatus new GitHubOperations(outputProvider, settings.GetGitHubOperationSettings()), new StackConfig()); - await handler.Handle(new StackStatusCommandInputs(settings.Name, settings.All, settings.Full)); + await handler.Handle(new StackStatusCommandInputs(settings.Name, settings.All, settings.Minimal, settings.Full)); return 0; } } -public record StackStatusCommandInputs(string? Name, bool All, bool Full); +public record StackStatusCommandInputs(string? Name, bool All, bool Minimal, bool Full); public record StackStatusCommandResponse(Dictionary Statuses); public class StackStatusCommandHandler( @@ -86,9 +90,10 @@ public async Task Handle(StackStatusCommandInputs in outputProvider, gitOperations, gitHubOperations, + !inputs.Minimal, inputs.Full); - StackStatusHelpers.OutputStackStatus(stackStatusResults, gitOperations, outputProvider); + StackStatusHelpers.OutputStackStatus(stackStatusResults, outputProvider); if (stacksToCheckStatusFor.Count == 1) { diff --git a/src/Stack/Git/GitOperations.cs b/src/Stack/Git/GitOperations.cs index e29d8d80..794153d0 100644 --- a/src/Stack/Git/GitOperations.cs +++ b/src/Stack/Git/GitOperations.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Text; +using System.Text.RegularExpressions; using Octopus.Shellfish; using Stack.Infrastructure; @@ -10,7 +11,9 @@ public record GitOperationSettings(bool DryRun, bool Verbose, string? WorkingDir public static GitOperationSettings Default => new(false, false, null); } +public record Commit(string Sha, string Message); +public record GitBranchStatus(string? RemoteTrackingBranchName, bool RemoteBranchExists, bool IsCurrentBranch, int Ahead, int Behind, Commit Tip); public interface IGitOperations { @@ -33,6 +36,8 @@ public interface IGitOperations (int Ahead, int Behind) GetStatusOfRemoteBranch(string branchName, string sourceBranchName); (int Ahead, int Behind) CompareBranches(string branchName, string sourceBranchName); (int Ahead, int Behind) GetComparisonToRemoteTrackingBranch(string branchName); + Commit GetTipOfBranch(string branchName); + Dictionary GetBranchStatuses(string[] branches); string GetRemoteUri(); string[] GetLocalBranchesOrderedByMostRecentCommitterDate(); string GetRootOfRepository(); @@ -117,9 +122,9 @@ public string[] GetBranchesThatExistLocally(string[] branches) public string[] GetBranchesThatExistInRemote(string[] branches) { - var remoteBranches = ExecuteGitCommandAndReturnOutput($"ls-remote --heads origin {string.Join(" ", branches)}").Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + var remoteBranches = ExecuteGitCommandAndReturnOutput("branch --remote --format=%(refname:short)").Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); - return branches.Where(b => remoteBranches.Any(rb => rb.EndsWith(b))).ToArray(); + return branches.Where(b => remoteBranches.Any(lb => lb.Equals($"origin/{b}", StringComparison.OrdinalIgnoreCase))).ToArray(); } public bool IsRemoteBranchFullyMerged(string branchName, string sourceBranchName) @@ -155,6 +160,42 @@ public string[] GetBranchesThatHaveBeenMerged(string[] branches, string sourceBr return (int.Parse(parts[0]), int.Parse(parts[1])); } + public Commit GetTipOfBranch(string branchName) + { + var commit = ExecuteGitCommandAndReturnOutput($"rev-list -n 1 {branchName}").Trim(); + var message = ExecuteGitCommandAndReturnOutput($"log -1 --pretty=%B {commit}").Trim(); + return new Commit(commit, message); + } + + public Dictionary GetBranchStatuses(string[] branches) + { + var statuses = new Dictionary(); + + var gitBranchVerbose = ExecuteGitCommandAndReturnOutput("branch -vv").Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + var regex = new Regex(@"^(?\*)?\s*(?\S+)\s+(?\S+)\s*(\[(?[^:]+)?(?::\s*(?(ahead\s+(?\d+),\s*behind\s+(?\d+))|(ahead\s+(?\d+))|(behind\s+(?\d+))|(gone)))?\])?\s+(?.+)$"); + + foreach (var branchStatus in gitBranchVerbose) + { + var match = regex.Match(branchStatus); + + if (match.Success && branches.Contains(match.Groups["branchName"].Value)) + { + var branchName = match.Groups["branchName"].Value; + var isCurrentBranch = match.Groups["isCurrentBranch"].Success; + var remoteTrackingBranchName = match.Groups["remoteTrackingBranchName"].Value; + var ahead = match.Groups["ahead"].Success ? int.Parse(match.Groups["ahead"].Value) : (match.Groups["aheadOnly"].Success ? int.Parse(match.Groups["aheadOnly"].Value) : 0); + var behind = match.Groups["behind"].Success ? int.Parse(match.Groups["behind"].Value) : (match.Groups["behindOnly"].Success ? int.Parse(match.Groups["behindOnly"].Value) : 0); + var remoteBranchExists = !string.IsNullOrEmpty(remoteTrackingBranchName) && !match.Groups["status"].Value.Contains("gone"); + var sha = match.Groups["sha"].Value; + var message = match.Groups["message"].Value; + + statuses.Add(branchName, new GitBranchStatus(remoteTrackingBranchName, remoteBranchExists, isCurrentBranch, ahead, behind, new Commit(sha, message))); + } + } + + return statuses; + } + public string GetRemoteUri() { return ExecuteGitCommandAndReturnOutput("remote get-url origin").Trim(); diff --git a/src/Stack/Infrastructure/ConsoleOutputProvider.cs b/src/Stack/Infrastructure/ConsoleOutputProvider.cs index 9daf30b9..78fe7d5a 100644 --- a/src/Stack/Infrastructure/ConsoleOutputProvider.cs +++ b/src/Stack/Infrastructure/ConsoleOutputProvider.cs @@ -51,8 +51,9 @@ public void Rule(string message) public static class OutputStyleExtensionMethods { - public static string Stack(this string name) => $"[yellow]{name}[/]"; - public static string Branch(this string name) => $"[blue]{name}[/]"; - public static string Muted(this string name) => $"[grey]{name}[/]"; - public static string Example(this string name) => $"[aqua]{name}[/]"; + public static string Stack(this string name) => $"[{Color.Yellow}]{name}[/]"; + public static string Branch(this string name) => $"[{Color.Blue}]{name}[/]"; + public static string Muted(this string name) => $"[{Color.Grey}]{name}[/]"; + public static string Example(this string name) => $"[{Color.Aqua}]{name}[/]"; + public static string Commit(this string name) => $"[{Color.Orange1}]{name}[/]"; } \ No newline at end of file From b30c30363bf8d3d298d5cacea5a8e9cf683682fe Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 20 Dec 2024 15:59:38 +1100 Subject: [PATCH 03/26] Remove `--minimal` option for now --- .../Stack/StackStatusCommandHandlerTests.cs | 16 ++++++++-------- src/Stack/Commands/Helpers/StackStatusHelpers.cs | 3 +-- src/Stack/Commands/Stack/StackStatusCommand.cs | 9 ++------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs index 3f897179..4ba41e45 100644 --- a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs @@ -53,7 +53,7 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneHasAPullRequests_Retur .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, false, false, true)); + var response = await handler.Handle(new StackStatusCommandInputs(null, false, true)); // Assert var expectedBranchDetails = new Dictionary @@ -113,7 +113,7 @@ public async Task WhenStackNameIsProvided_DoesNotAskForStack_ReturnsStatus() .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs("Stack1", false, false, true)); + var response = await handler.Handle(new StackStatusCommandInputs("Stack1", false, true)); // Assert var expectedBranchDetails = new Dictionary @@ -176,7 +176,7 @@ public async Task WhenAllStacksAreRequested_ReturnsStatusOfEachStack() .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, true, false, true)); + var response = await handler.Handle(new StackStatusCommandInputs(null, true, true)); // Assert var expectedBranchDetailsForStack1 = new Dictionary @@ -246,7 +246,7 @@ public async Task WhenAllStacksAreRequested_WithStacksInMultipleRepositories_Ret .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, true, false, true)); + var response = await handler.Handle(new StackStatusCommandInputs(null, true, true)); // Assert var expectedBranchDetailsForStack1 = new Dictionary @@ -302,7 +302,7 @@ public async Task WhenStackNameIsProvided_ButStackDoesNotExist_Throws() // Act and assert var incorrectStackName = Some.Name(); await handler - .Invoking(async h => await h.Handle(new StackStatusCommandInputs(incorrectStackName, false, false, false))) + .Invoking(async h => await h.Handle(new StackStatusCommandInputs(incorrectStackName, false, false))) .Should().ThrowAsync() .WithMessage($"Stack '{incorrectStackName}' not found."); } @@ -349,7 +349,7 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, false, false, true)); + var response = await handler.Handle(new StackStatusCommandInputs(null, false, true)); // Assert var expectedBranchDetails = new Dictionary @@ -406,7 +406,7 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, false, false, true)); + var response = await handler.Handle(new StackStatusCommandInputs(null, false, true)); // Assert var expectedBranchDetails = new Dictionary @@ -463,7 +463,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_ReturnsStatus() .Returns(pr); // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, false, false, true)); + var response = await handler.Handle(new StackStatusCommandInputs(null, false, true)); // Assert var expectedBranchDetails = new Dictionary diff --git a/src/Stack/Commands/Helpers/StackStatusHelpers.cs b/src/Stack/Commands/Helpers/StackStatusHelpers.cs index 6474291f..2b5fdb69 100644 --- a/src/Stack/Commands/Helpers/StackStatusHelpers.cs +++ b/src/Stack/Commands/Helpers/StackStatusHelpers.cs @@ -30,7 +30,6 @@ public static class StackStatusHelpers IOutputProvider outputProvider, IGitOperations gitOperations, IGitHubOperations gitHubOperations, - bool includeParentBranchStatus = true, bool includePullRequestStatus = true) { var stacksToCheckStatusFor = new Dictionary(); @@ -62,7 +61,7 @@ public static class StackStatusHelpers if (branchStatus is not null) { - var (aheadOfParent, behindParent) = includeParentBranchStatus && branchStatus.RemoteBranchExists ? gitOperations.CompareBranches(branch, parentBranch) : (0, 0); + var (aheadOfParent, behindParent) = branchStatus.RemoteBranchExists ? gitOperations.CompareBranches(branch, parentBranch) : (0, 0); status.Branches[branch].Status = new BranchStatus(true, branchStatus.RemoteBranchExists, branchStatus.IsCurrentBranch, aheadOfParent, behindParent, branchStatus.Ahead, branchStatus.Behind, branchStatus.Tip); diff --git a/src/Stack/Commands/Stack/StackStatusCommand.cs b/src/Stack/Commands/Stack/StackStatusCommand.cs index 15d7bde5..8db34b25 100644 --- a/src/Stack/Commands/Stack/StackStatusCommand.cs +++ b/src/Stack/Commands/Stack/StackStatusCommand.cs @@ -18,10 +18,6 @@ public class StackStatusCommandSettings : CommandSettingsBase [CommandOption("--all")] public bool All { get; init; } - [Description("Show minimal status.")] - [CommandOption("--minimal")] - public bool Minimal { get; init; } - [Description("Show full status including pull requests.")] [CommandOption("--full")] public bool Full { get; init; } @@ -41,13 +37,13 @@ public override async Task ExecuteAsync(CommandContext context, StackStatus new GitHubOperations(outputProvider, settings.GetGitHubOperationSettings()), new StackConfig()); - await handler.Handle(new StackStatusCommandInputs(settings.Name, settings.All, settings.Minimal, settings.Full)); + await handler.Handle(new StackStatusCommandInputs(settings.Name, settings.All, settings.Full)); return 0; } } -public record StackStatusCommandInputs(string? Name, bool All, bool Minimal, bool Full); +public record StackStatusCommandInputs(string? Name, bool All, bool Full); public record StackStatusCommandResponse(Dictionary Statuses); public class StackStatusCommandHandler( @@ -90,7 +86,6 @@ public async Task Handle(StackStatusCommandInputs in outputProvider, gitOperations, gitHubOperations, - !inputs.Minimal, inputs.Full); StackStatusHelpers.OutputStackStatus(stackStatusResults, outputProvider); From 1b318b6867c76ac902a8fc2db69407c8d645ecfc Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 20 Dec 2024 16:19:14 +1100 Subject: [PATCH 04/26] Separate git branch status parsing --- .../Git/GitBranchStatusParserTests.cs | 85 +++++++++++++++++++ src/Stack/Git/GitBranchStatusParser.cs | 31 +++++++ src/Stack/Git/GitOperations.cs | 17 +--- 3 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 src/Stack.Tests/Git/GitBranchStatusParserTests.cs create mode 100644 src/Stack/Git/GitBranchStatusParser.cs diff --git a/src/Stack.Tests/Git/GitBranchStatusParserTests.cs b/src/Stack.Tests/Git/GitBranchStatusParserTests.cs new file mode 100644 index 00000000..248988c1 --- /dev/null +++ b/src/Stack.Tests/Git/GitBranchStatusParserTests.cs @@ -0,0 +1,85 @@ +using FluentAssertions; +using Stack.Git; + +namespace Stack.Tests.Git; + +public class GitBranchStatusParserTests +{ + [Fact] + public void WhenBranchIsCurrentBranch_ReturnsCorrectStatus() + { + // Arrange + var branchStatus = "* main 1234567 [origin/main] Some message"; + + // Act + var result = GitBranchStatusParser.Parse(branchStatus); + + // Assert + result.Should().Be(new GitBranchStatus("main", "origin/main", true, true, 0, 0, new Commit("1234567", "Some message"))); + } + + [Fact] + public void WhenBranchIsAheadAndBehindItsRemoteTrackingBranch_ReturnsCorrectStatus() + { + // Arrange + var branchStatus = " main 1234567 [origin/main: ahead 1, behind 2] Some message"; + + // Act + var result = GitBranchStatusParser.Parse(branchStatus); + + // Assert + result.Should().Be(new GitBranchStatus("main", "origin/main", true, false, 1, 2, new Commit("1234567", "Some message"))); + } + + [Fact] + public void WhenBranchIsAheadOfItsRemoteTrackingBranch_ReturnsCorrectStatus() + { + // Arrange + var branchStatus = " main 1234567 [origin/main: ahead 1] Some message"; + + // Act + var result = GitBranchStatusParser.Parse(branchStatus); + + // Assert + result.Should().Be(new GitBranchStatus("main", "origin/main", true, false, 1, 0, new Commit("1234567", "Some message"))); + } + + [Fact] + public void WhenBranchIsBehindItsRemoteTrackingBranch_ReturnsCorrectStatus() + { + // Arrange + var branchStatus = " main 1234567 [origin/main: behind 2] Some message"; + + // Act + var result = GitBranchStatusParser.Parse(branchStatus); + + // Assert + result.Should().Be(new GitBranchStatus("main", "origin/main", true, false, 0, 2, new Commit("1234567", "Some message"))); + } + + [Fact] + public void WhenBranchIsNotTracked_ReturnsCorrectStatus() + { + // Arrange + var branchStatus = " main 1234567 Some message"; + + // Act + var result = GitBranchStatusParser.Parse(branchStatus); + + // Assert + result.Should().Be(new GitBranchStatus("main", null, false, false, 0, 0, new Commit("1234567", "Some message"))); + } + + [Fact] + public void WhenRemoteTrackingBranchIsGone_ReturnsCorrectStatus() + { + // Arrange + var branchStatus = " main 1234567 [origin/main: gone] Some message"; + + // Act + var result = GitBranchStatusParser.Parse(branchStatus); + + // Assert + result.Should().Be(new GitBranchStatus("main", "origin/main", false, false, 0, 0, new Commit("1234567", "Some message"))); + } +} diff --git a/src/Stack/Git/GitBranchStatusParser.cs b/src/Stack/Git/GitBranchStatusParser.cs new file mode 100644 index 00000000..238388ba --- /dev/null +++ b/src/Stack/Git/GitBranchStatusParser.cs @@ -0,0 +1,31 @@ +using System.Text.RegularExpressions; + +namespace Stack.Git; + +public static class GitBranchStatusParser +{ + static Regex regex = new( + @"^(?\*)?\s*(?\S+)\s+(?\S+)\s*(\[(?[^:]+)?(?::\s*(?(ahead\s+(?\d+),\s*behind\s+(?\d+))|(ahead\s+(?\d+))|(behind\s+(?\d+))|(gone)))?\])?\s+(?.+)$", + RegexOptions.Compiled); + + public static GitBranchStatus? Parse(string branchStatus) + { + var match = regex.Match(branchStatus); + + if (match.Success) + { + var branchName = match.Groups["branchName"].Value; + var isCurrentBranch = match.Groups["isCurrentBranch"].Success; + var remoteTrackingBranchName = string.IsNullOrEmpty(match.Groups["remoteTrackingBranchName"].Value) ? null : match.Groups["remoteTrackingBranchName"].Value; + var ahead = match.Groups["ahead"].Success ? int.Parse(match.Groups["ahead"].Value) : (match.Groups["aheadOnly"].Success ? int.Parse(match.Groups["aheadOnly"].Value) : 0); + var behind = match.Groups["behind"].Success ? int.Parse(match.Groups["behind"].Value) : (match.Groups["behindOnly"].Success ? int.Parse(match.Groups["behindOnly"].Value) : 0); + var remoteBranchExists = remoteTrackingBranchName is not null && !match.Groups["status"].Value.Contains("gone"); + var sha = match.Groups["sha"].Value; + var message = match.Groups["message"].Value; + + return new GitBranchStatus(branchName, remoteTrackingBranchName, remoteBranchExists, isCurrentBranch, ahead, behind, new Commit(sha, message)); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Stack/Git/GitOperations.cs b/src/Stack/Git/GitOperations.cs index 794153d0..65e9ab1d 100644 --- a/src/Stack/Git/GitOperations.cs +++ b/src/Stack/Git/GitOperations.cs @@ -13,7 +13,7 @@ public record GitOperationSettings(bool DryRun, bool Verbose, string? WorkingDir public record Commit(string Sha, string Message); -public record GitBranchStatus(string? RemoteTrackingBranchName, bool RemoteBranchExists, bool IsCurrentBranch, int Ahead, int Behind, Commit Tip); +public record GitBranchStatus(string BranchName, string? RemoteTrackingBranchName, bool RemoteBranchExists, bool IsCurrentBranch, int Ahead, int Behind, Commit Tip); public interface IGitOperations { @@ -176,20 +176,11 @@ public Dictionary GetBranchStatuses(string[] branches) foreach (var branchStatus in gitBranchVerbose) { - var match = regex.Match(branchStatus); + var status = GitBranchStatusParser.Parse(branchStatus); - if (match.Success && branches.Contains(match.Groups["branchName"].Value)) + if (status is not null && branches.Contains(status.BranchName)) { - var branchName = match.Groups["branchName"].Value; - var isCurrentBranch = match.Groups["isCurrentBranch"].Success; - var remoteTrackingBranchName = match.Groups["remoteTrackingBranchName"].Value; - var ahead = match.Groups["ahead"].Success ? int.Parse(match.Groups["ahead"].Value) : (match.Groups["aheadOnly"].Success ? int.Parse(match.Groups["aheadOnly"].Value) : 0); - var behind = match.Groups["behind"].Success ? int.Parse(match.Groups["behind"].Value) : (match.Groups["behindOnly"].Success ? int.Parse(match.Groups["behindOnly"].Value) : 0); - var remoteBranchExists = !string.IsNullOrEmpty(remoteTrackingBranchName) && !match.Groups["status"].Value.Contains("gone"); - var sha = match.Groups["sha"].Value; - var message = match.Groups["message"].Value; - - statuses.Add(branchName, new GitBranchStatus(remoteTrackingBranchName, remoteBranchExists, isCurrentBranch, ahead, behind, new Commit(sha, message))); + statuses.Add(status.BranchName, status); } } From 24e37be6c6c268fdc1db1629e4ff7de564f36513 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 20 Dec 2024 16:21:47 +1100 Subject: [PATCH 05/26] Revert change to detecting remote branches for other commands --- src/Stack/Git/GitOperations.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Stack/Git/GitOperations.cs b/src/Stack/Git/GitOperations.cs index 65e9ab1d..0fe8ed80 100644 --- a/src/Stack/Git/GitOperations.cs +++ b/src/Stack/Git/GitOperations.cs @@ -122,9 +122,9 @@ public string[] GetBranchesThatExistLocally(string[] branches) public string[] GetBranchesThatExistInRemote(string[] branches) { - var remoteBranches = ExecuteGitCommandAndReturnOutput("branch --remote --format=%(refname:short)").Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + var remoteBranches = ExecuteGitCommandAndReturnOutput($"ls-remote --heads origin {string.Join(" ", branches)}").Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); - return branches.Where(b => remoteBranches.Any(lb => lb.Equals($"origin/{b}", StringComparison.OrdinalIgnoreCase))).ToArray(); + return branches.Where(b => remoteBranches.Any(rb => rb.EndsWith(b))).ToArray(); } public bool IsRemoteBranchFullyMerged(string branchName, string sourceBranchName) From 4410d0bf9377152184656d28e82540ae7994c4f9 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 20 Dec 2024 16:24:36 +1100 Subject: [PATCH 06/26] Cleanup --- src/Stack/Git/GitOperations.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Stack/Git/GitOperations.cs b/src/Stack/Git/GitOperations.cs index 0fe8ed80..9f60b0a6 100644 --- a/src/Stack/Git/GitOperations.cs +++ b/src/Stack/Git/GitOperations.cs @@ -35,8 +35,6 @@ public interface IGitOperations string[] GetBranchesThatHaveBeenMerged(string[] branches, string sourceBranchName); (int Ahead, int Behind) GetStatusOfRemoteBranch(string branchName, string sourceBranchName); (int Ahead, int Behind) CompareBranches(string branchName, string sourceBranchName); - (int Ahead, int Behind) GetComparisonToRemoteTrackingBranch(string branchName); - Commit GetTipOfBranch(string branchName); Dictionary GetBranchStatuses(string[] branches); string GetRemoteUri(); string[] GetLocalBranchesOrderedByMostRecentCommitterDate(); @@ -153,20 +151,6 @@ public string[] GetBranchesThatHaveBeenMerged(string[] branches, string sourceBr return (int.Parse(parts[0]), int.Parse(parts[1])); } - public (int Ahead, int Behind) GetComparisonToRemoteTrackingBranch(string branchName) - { - var status = ExecuteGitCommandAndReturnOutput($"rev-list --left-right --count {branchName}...origin/{branchName}").Trim(); - var parts = status.Split('\t'); - return (int.Parse(parts[0]), int.Parse(parts[1])); - } - - public Commit GetTipOfBranch(string branchName) - { - var commit = ExecuteGitCommandAndReturnOutput($"rev-list -n 1 {branchName}").Trim(); - var message = ExecuteGitCommandAndReturnOutput($"log -1 --pretty=%B {commit}").Trim(); - return new Commit(commit, message); - } - public Dictionary GetBranchStatuses(string[] branches) { var statuses = new Dictionary(); From 369aba480bfce444b56f79241e4995eb902af4b4 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 20 Dec 2024 16:35:05 +1100 Subject: [PATCH 07/26] Adds pull command --- src/Stack/Commands/Stack/PullStackCommand.cs | 76 ++++++++++++++++++++ src/Stack/Help/CommandGroups.cs | 1 + src/Stack/Help/CommandNames.cs | 1 + src/Stack/Help/StackHelpProvider.cs | 1 + src/Stack/Program.cs | 3 + 5 files changed, 82 insertions(+) create mode 100644 src/Stack/Commands/Stack/PullStackCommand.cs diff --git a/src/Stack/Commands/Stack/PullStackCommand.cs b/src/Stack/Commands/Stack/PullStackCommand.cs new file mode 100644 index 00000000..6207183b --- /dev/null +++ b/src/Stack/Commands/Stack/PullStackCommand.cs @@ -0,0 +1,76 @@ +using System.ComponentModel; +using Spectre.Console; +using Spectre.Console.Cli; +using Stack.Commands.Helpers; +using Stack.Config; +using Stack.Git; +using Stack.Infrastructure; + +namespace Stack.Commands; + +public class PullStackCommandSettings : DryRunCommandSettingsBase +{ + [Description("The name of the stack to pull changes from the remote for.")] + [CommandOption("-n|--name")] + public string? Name { get; init; } +} + +public class PullStackCommand : AsyncCommand +{ + public override async Task ExecuteAsync(CommandContext context, PullStackCommandSettings settings) + { + var console = AnsiConsole.Console; + var outputProvider = new ConsoleOutputProvider(console); + + var handler = new PullStackCommandHandler( + new ConsoleInputProvider(console), + outputProvider, + new GitOperations(outputProvider, settings.GetGitOperationSettings()), + new StackConfig()); + + await handler.Handle(new PullStackCommandInputs(settings.Name)); + + return 0; + } +} + +public record PullStackCommandInputs(string? Name); +public class PullStackCommandHandler( + IInputProvider inputProvider, + IOutputProvider outputProvider, + IGitOperations gitOperations, + IStackConfig stackConfig) +{ + public async Task Handle(PullStackCommandInputs inputs) + { + await Task.CompletedTask; + var stacks = stackConfig.Load(); + + var remoteUri = gitOperations.GetRemoteUri(); + var stacksForRemote = stacks.Where(s => s.RemoteUri.Equals(remoteUri, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (stacksForRemote.Count == 0) + { + outputProvider.Information("No stacks found for current repository."); + return; + } + + var currentBranch = gitOperations.GetCurrentBranch(); + + var stack = inputProvider.SelectStack(outputProvider, inputs.Name, stacksForRemote, currentBranch); + + 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); + } + + gitOperations.ChangeBranch(currentBranch); + } +} diff --git a/src/Stack/Help/CommandGroups.cs b/src/Stack/Help/CommandGroups.cs index 7c1e98c4..c3028c78 100644 --- a/src/Stack/Help/CommandGroups.cs +++ b/src/Stack/Help/CommandGroups.cs @@ -4,6 +4,7 @@ public static class CommandGroups { public const string Stack = "Stack"; public const string Branch = "Branch"; + public const string Remote = "Remote"; public const string GitHub = "GitHub"; public const string Advanced = "Advanced"; } diff --git a/src/Stack/Help/CommandNames.cs b/src/Stack/Help/CommandNames.cs index 351adbfe..3966ed2d 100644 --- a/src/Stack/Help/CommandNames.cs +++ b/src/Stack/Help/CommandNames.cs @@ -16,4 +16,5 @@ public static class CommandNames public const string Create = "create"; public const string Open = "open"; public const string Remove = "remove"; + public const string Pull = "pull"; } diff --git a/src/Stack/Help/StackHelpProvider.cs b/src/Stack/Help/StackHelpProvider.cs index 84cf6296..1a5b3316 100644 --- a/src/Stack/Help/StackHelpProvider.cs +++ b/src/Stack/Help/StackHelpProvider.cs @@ -11,6 +11,7 @@ public class StackHelpProvider(ICommandAppSettings settings) : HelpProvider(sett { { CommandGroups.Stack, [CommandNames.New, CommandNames.List, CommandNames.List, CommandNames.Delete, CommandNames.Status] }, { CommandGroups.Branch, [CommandNames.Switch, CommandNames.Update, CommandNames.Cleanup, CommandNames.Branch] }, + { CommandGroups.Remote, [CommandNames.Pull] }, { CommandGroups.GitHub, [CommandNames.Pr] }, { CommandGroups.Advanced, [CommandNames.Config] }, }; diff --git a/src/Stack/Program.cs b/src/Stack/Program.cs index 34f82027..2f6bf127 100644 --- a/src/Stack/Program.cs +++ b/src/Stack/Program.cs @@ -27,6 +27,9 @@ branch.AddCommand(CommandNames.Remove).WithDescription("Removes a branch from a stack."); }); + // Remote commands + configure.AddCommand(CommandNames.Pull).WithDescription("Pull changes from the remote server for a stack."); + // GitHub commands configure.AddBranch(CommandNames.Pr, pr => { From bba932fb7ccaabd7171d1e4760a1a3e750cc11f3 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 20 Dec 2024 16:37:53 +1100 Subject: [PATCH 08/26] Tiny improvement to output when getting status of single stack --- src/Stack/Commands/Stack/StackStatusCommand.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Stack/Commands/Stack/StackStatusCommand.cs b/src/Stack/Commands/Stack/StackStatusCommand.cs index 8db34b25..2cc771d6 100644 --- a/src/Stack/Commands/Stack/StackStatusCommand.cs +++ b/src/Stack/Commands/Stack/StackStatusCommand.cs @@ -88,6 +88,11 @@ public async Task Handle(StackStatusCommandInputs in gitHubOperations, inputs.Full); + if (stackStatusResults.Count == 1) + { + outputProvider.NewLine(); + } + StackStatusHelpers.OutputStackStatus(stackStatusResults, outputProvider); if (stacksToCheckStatusFor.Count == 1) From 6b573fb028125d1fe1a31d4767f730e482906fd5 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 20 Dec 2024 16:54:12 +1100 Subject: [PATCH 09/26] wip pull tests --- .../Stack/PullStackCommandHandlerTests.cs | 226 ++++++++++++++++++ .../Helpers/TestGitRepositoryBuilder.cs | 5 + 2 files changed, 231 insertions(+) create mode 100644 src/Stack.Tests/Commands/Stack/PullStackCommandHandlerTests.cs diff --git a/src/Stack.Tests/Commands/Stack/PullStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/PullStackCommandHandlerTests.cs new file mode 100644 index 00000000..77a18ccf --- /dev/null +++ b/src/Stack.Tests/Commands/Stack/PullStackCommandHandlerTests.cs @@ -0,0 +1,226 @@ +using FluentAssertions; +using NSubstitute; +using Stack.Commands; +using Stack.Config; +using Stack.Git; +using Stack.Tests.Helpers; +using Stack.Infrastructure; +using Stack.Commands.Helpers; +using Xunit.Abstractions; + +namespace Stack.Tests.Commands.Stack; + +public class PullStackCommandHandlerTests(ITestOutputHelper testOutputHelper) +{ + [Fact] + public async Task WhenChangesExistOnTheRemote_TheyArePulledDownToTheLocalBranch() + { + // 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($"origin/{sourceBranch}"), 5) + .WithNumberOfEmptyCommits(b => b.OnBranch($"origin/{branch1}"), 3) + .Build(); + + var tipOfRemoteSourceBranch = repo.GetTipOfRemoteBranch(sourceBranch); + var tipOfRemoteBranch1 = repo.GetTipOfRemoteBranch(sourceBranch); + + repo.GetCommitsReachableFromBranch(sourceBranch).Should().NotContain(tipOfRemoteSourceBranch); + repo.GetCommitsReachableFromBranch(branch1).Should().NotContain(tipOfRemoteBranch1); + + var stackConfig = Substitute.For(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var handler = new PullStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + gitOperations.ChangeBranch(branch1); + + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); + var stacks = new List([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true); + + // Act + await handler.Handle(new PullStackCommandInputs(null)); + + // Assert + repo.GetCommitsReachableFromBranch(sourceBranch).Should().Contain(tipOfRemoteSourceBranch); + repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfRemoteBranch1); + } + + [Fact] + public async Task WhenNameIsProvided_DoesNotAskForName_UpdatesCorrectStack() + { + // 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(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var gitHubOperations = Substitute.For(); + 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([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true); + + // Act + await handler.Handle(new UpdateStackCommandInputs("Stack1", false)); + + // Assert + repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch); + repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfSourceBranch); + repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfBranch1); + inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any()); + } + + [Fact] + public async Task WhenNameIsProvided_ButStackDoesNotExist_Throws() + { + // 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(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var gitHubOperations = Substitute.For(); + 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([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + // Act and assert + var invalidStackName = Some.Name(); + await handler.Invoking(async h => await h.Handle(new UpdateStackCommandInputs(invalidStackName, false))) + .Should().ThrowAsync() + .WithMessage($"Stack '{invalidStackName}' not found."); + } + + [Fact] + public async Task WhenOnASpecificBranchInTheStack_TheSameBranchIsSetAsCurrentAfterTheUpdate() + { + // 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(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var gitHubOperations = Substitute.For(); + var handler = new UpdateStackCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); + + // We are on a specific branch in the stack + gitOperations.ChangeBranch(branch1); + + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); + var stacks = new List([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true); + + // Act + await handler.Handle(new UpdateStackCommandInputs(null, false)); + + // Assert + repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch); + repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfSourceBranch); + repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfBranch1); + gitOperations.GetCurrentBranch().Should().Be(branch1); + } + + [Fact] + public async Task WhenOnlyASingleStackExists_DoesNotAskForStackName_UpdatesStack() + { + // 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(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var gitHubOperations = Substitute.For(); + var handler = new UpdateStackCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); + + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stacks = new List([stack1]); + stackConfig.Load().Returns(stacks); + + inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true); + + // Act + await handler.Handle(new UpdateStackCommandInputs(null, false)); + + // Assert + repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch); + repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfSourceBranch); + repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfBranch1); + + inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any()); + } +} diff --git a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs index 96a4944f..689a0e64 100644 --- a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs +++ b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs @@ -262,6 +262,11 @@ public LibGit2Sharp.Commit GetTipOfBranch(string branchName) return [.. LocalRepository.Branches[branchName].Commits]; } + public LibGit2Sharp.Commit GetTipOfRemoteBranch(string branchName) + { + return LocalRepository.Branches[$"origin/{branchName}"].Tip; + } + public void Dispose() { GC.SuppressFinalize(this); From 192560266f3ecdab04d9be8019e67d2f27ba1c0c Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Tue, 24 Dec 2024 15:55:44 +1100 Subject: [PATCH 10/26] Add more tests --- .../Stack/PullStackCommandHandlerTests.cs | 122 ++++++------------ .../Helpers/TestGitRepositoryBuilder.cs | 27 +++- 2 files changed, 64 insertions(+), 85 deletions(-) diff --git a/src/Stack.Tests/Commands/Stack/PullStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/PullStackCommandHandlerTests.cs index 77a18ccf..3151675b 100644 --- a/src/Stack.Tests/Commands/Stack/PullStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/PullStackCommandHandlerTests.cs @@ -23,12 +23,12 @@ public async Task WhenChangesExistOnTheRemote_TheyArePulledDownToTheLocalBranch( .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($"origin/{sourceBranch}"), 5) - .WithNumberOfEmptyCommits(b => b.OnBranch($"origin/{branch1}"), 3) + .WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf(sourceBranch, 5, b => b.PushToRemote()) + .WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf(branch1, 3, b => b.PushToRemote()) .Build(); var tipOfRemoteSourceBranch = repo.GetTipOfRemoteBranch(sourceBranch); - var tipOfRemoteBranch1 = repo.GetTipOfRemoteBranch(sourceBranch); + var tipOfRemoteBranch1 = repo.GetTipOfRemoteBranch(branch1); repo.GetCommitsReachableFromBranch(sourceBranch).Should().NotContain(tipOfRemoteSourceBranch); repo.GetCommitsReachableFromBranch(branch1).Should().NotContain(tipOfRemoteBranch1); @@ -47,7 +47,6 @@ public async Task WhenChangesExistOnTheRemote_TheyArePulledDownToTheLocalBranch( stackConfig.Load().Returns(stacks); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); - inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true); // Act await handler.Handle(new PullStackCommandInputs(null)); @@ -58,7 +57,7 @@ public async Task WhenChangesExistOnTheRemote_TheyArePulledDownToTheLocalBranch( } [Fact] - public async Task WhenNameIsProvided_DoesNotAskForName_UpdatesCorrectStack() + public async Task WhenNameIsProvided_DoesNotAskForName_PullsChangesFromRemoteForBranchesInStack() { // Arrange var sourceBranch = Some.BranchName(); @@ -68,34 +67,35 @@ public async Task WhenNameIsProvided_DoesNotAskForName_UpdatesCorrectStack() .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) + .WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf(sourceBranch, 5, b => b.PushToRemote()) + .WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf(branch1, 3, b => b.PushToRemote()) .Build(); - var tipOfSourceBranch = repo.GetTipOfBranch(sourceBranch); - var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfRemoteSourceBranch = repo.GetTipOfRemoteBranch(sourceBranch); + var tipOfRemoteBranch1 = repo.GetTipOfRemoteBranch(branch1); + + repo.GetCommitsReachableFromBranch(sourceBranch).Should().NotContain(tipOfRemoteSourceBranch); + repo.GetCommitsReachableFromBranch(branch1).Should().NotContain(tipOfRemoteBranch1); var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); var outputProvider = new TestOutputProvider(testOutputHelper); var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); - var gitHubOperations = Substitute.For(); - var handler = new UpdateStackCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); + var handler = new PullStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + gitOperations.ChangeBranch(branch1); var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); var stacks = new List([stack1, stack2]); stackConfig.Load().Returns(stacks); - inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true); - // Act - await handler.Handle(new UpdateStackCommandInputs("Stack1", false)); + await handler.Handle(new PullStackCommandInputs("Stack1")); // Assert - repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch); - repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfSourceBranch); - repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfBranch1); + repo.GetCommitsReachableFromBranch(sourceBranch).Should().Contain(tipOfRemoteSourceBranch); + repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfRemoteBranch1); inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any()); } @@ -110,34 +110,40 @@ public async Task WhenNameIsProvided_ButStackDoesNotExist_Throws() .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) + .WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf(sourceBranch, 5, b => b.PushToRemote()) + .WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf(branch1, 3, b => b.PushToRemote()) .Build(); - var tipOfSourceBranch = repo.GetTipOfBranch(sourceBranch); - var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfRemoteSourceBranch = repo.GetTipOfRemoteBranch(sourceBranch); + var tipOfRemoteBranch1 = repo.GetTipOfRemoteBranch(branch1); + + repo.GetCommitsReachableFromBranch(sourceBranch).Should().NotContain(tipOfRemoteSourceBranch); + repo.GetCommitsReachableFromBranch(branch1).Should().NotContain(tipOfRemoteBranch1); var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); var outputProvider = new TestOutputProvider(testOutputHelper); var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); - var gitHubOperations = Substitute.For(); - var handler = new UpdateStackCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); + var handler = new PullStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + gitOperations.ChangeBranch(branch1); var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); var stacks = new List([stack1, stack2]); stackConfig.Load().Returns(stacks); + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + // 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 PullStackCommandInputs(invalidStackName))) .Should().ThrowAsync() .WithMessage($"Stack '{invalidStackName}' not found."); } [Fact] - public async Task WhenOnASpecificBranchInTheStack_TheSameBranchIsSetAsCurrentAfterTheUpdate() + public async Task WhenChangesExistOnTheRemote_ForABranchThatIsNotInTheStack_TheyAreNotPulledDownToTheLocalBranch() { // Arrange var sourceBranch = Some.BranchName(); @@ -147,80 +153,32 @@ public async Task WhenOnASpecificBranchInTheStack_TheSameBranchIsSetAsCurrentAft .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) + .WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf(sourceBranch, 5, b => b.PushToRemote()) + .WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf(branch1, 3, b => b.PushToRemote()) + .WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf(branch2, 3, b => b.PushToRemote()) .Build(); - var tipOfSourceBranch = repo.GetTipOfBranch(sourceBranch); - var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfRemoteBranch2 = repo.GetTipOfRemoteBranch(branch2); var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); var outputProvider = new TestOutputProvider(testOutputHelper); var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); - var gitHubOperations = Substitute.For(); - var handler = new UpdateStackCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); + var handler = new PullStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); - // We are on a specific branch in the stack gitOperations.ChangeBranch(branch1); - var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); - var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, [branch2]); var stacks = new List([stack1, stack2]); stackConfig.Load().Returns(stacks); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); - inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true); - - // Act - await handler.Handle(new UpdateStackCommandInputs(null, false)); - - // Assert - repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch); - repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfSourceBranch); - repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfBranch1); - gitOperations.GetCurrentBranch().Should().Be(branch1); - } - - [Fact] - public async Task WhenOnlyASingleStackExists_DoesNotAskForStackName_UpdatesStack() - { - // 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(); - var inputProvider = Substitute.For(); - var outputProvider = new TestOutputProvider(testOutputHelper); - var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); - var gitHubOperations = Substitute.For(); - var handler = new UpdateStackCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); - - var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); - var stacks = new List([stack1]); - stackConfig.Load().Returns(stacks); - - inputProvider.Confirm(Questions.ConfirmUpdateStack).Returns(true); // Act - await handler.Handle(new UpdateStackCommandInputs(null, false)); + await handler.Handle(new PullStackCommandInputs(null)); // Assert - repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch); - repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfSourceBranch); - repo.GetCommitsReachableFromBranch(branch2).Should().Contain(tipOfBranch1); - - inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any()); + repo.GetCommitsReachableFromBranch(branch2).Should().NotContain(tipOfRemoteBranch2); } } diff --git a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs index 689a0e64..7575b5e3 100644 --- a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs +++ b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs @@ -67,7 +67,7 @@ private static LibGit2Sharp.Commit CreateEmptyCommit(Repository repository, Bran public class CommitBuilder { - string? branchName; + Func? getBranchName; string? message; string? authorName; string? authorEmail; @@ -78,7 +78,13 @@ public class CommitBuilder public CommitBuilder OnBranch(string branch) { - this.branchName = branch; + getBranchName = (_) => branch; + return this; + } + + public CommitBuilder OnBranch(Func getBranchName) + { + this.getBranchName = getBranchName; return this; } @@ -118,8 +124,9 @@ public void Build(Repository repository) { Branch? branch = null; - if (branchName is not null) + if (getBranchName is not null) { + var branchName = getBranchName(repository); branch = repository.Branches[branchName]; } @@ -201,6 +208,20 @@ public TestGitRepositoryBuilder WithNumberOfEmptyCommits(Action c return this; } + public TestGitRepositoryBuilder WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf(string branch, int number, Action commitBuilder) + { + for (var i = 0; i < number; i++) + { + commitBuilders.Add(b => + { + commitBuilder(b); + b.OnBranch(r => r.Branches[branch].TrackedBranch.CanonicalName); + b.AllowEmptyCommit(); + }); + } + return this; + } + public TestGitRepository Build() { var remote = Path.Join(Path.GetTempPath(), Guid.NewGuid().ToString("N"), ".git"); From 4df13a85697f247d208511d473bf02becad30864 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 Dec 2024 11:45:06 +1100 Subject: [PATCH 11/26] Improve getting tip of remote branch --- src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs index 7575b5e3..eab413fa 100644 --- a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs +++ b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs @@ -285,7 +285,9 @@ public LibGit2Sharp.Commit GetTipOfBranch(string branchName) public LibGit2Sharp.Commit GetTipOfRemoteBranch(string branchName) { - return LocalRepository.Branches[$"origin/{branchName}"].Tip; + var branch = LocalRepository.Branches[branchName]; + var remoteBranchName = branch.TrackedBranch.CanonicalName; + return LocalRepository.Branches[remoteBranchName].Tip; } public void Dispose() From e95e5cbedc2cdec33227fcb400a5b38d3135897b Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 Dec 2024 11:54:14 +1100 Subject: [PATCH 12/26] Adds push command --- src/Stack/Commands/Stack/PushStackCommand.cs | 94 ++++++++++++++++++++ src/Stack/Git/GitOperations.cs | 14 +++ src/Stack/Help/CommandNames.cs | 1 + src/Stack/Help/StackHelpProvider.cs | 2 +- src/Stack/Program.cs | 1 + 5 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src/Stack/Commands/Stack/PushStackCommand.cs diff --git a/src/Stack/Commands/Stack/PushStackCommand.cs b/src/Stack/Commands/Stack/PushStackCommand.cs new file mode 100644 index 00000000..a81719ef --- /dev/null +++ b/src/Stack/Commands/Stack/PushStackCommand.cs @@ -0,0 +1,94 @@ +using System.ComponentModel; +using Spectre.Console; +using Spectre.Console.Cli; +using Stack.Commands.Helpers; +using Stack.Config; +using Stack.Git; +using Stack.Infrastructure; + +namespace Stack.Commands; + +public class PushStackCommandSettings : DryRunCommandSettingsBase +{ + [Description("The name of the stack to push changes from the remote for.")] + [CommandOption("-n|--name")] + public string? Name { get; init; } + + [Description("Force the push of the stack.")] + [CommandOption("-f|--force")] + public bool Force { get; init; } + + [Description("Force the push of the stack with lease.")] + [CommandOption("--force-with-lease")] + public bool ForceWithLease { get; init; } + + [Description("The maximum number of branches to push changes for at once.")] + [CommandOption("--max-batch-size")] + [DefaultValue(5)] + public int MaxBatchSize { get; init; } = 5; +} + +public class PushStackCommand : AsyncCommand +{ + public override async Task ExecuteAsync(CommandContext context, PushStackCommandSettings settings) + { + var console = AnsiConsole.Console; + var outputProvider = new ConsoleOutputProvider(console); + + var handler = new PushStackCommandHandler( + new ConsoleInputProvider(console), + outputProvider, + new GitOperations(outputProvider, settings.GetGitOperationSettings()), + new StackConfig()); + + await handler.Handle(new PushStackCommandInputs(settings.Name, settings.Force, settings.ForceWithLease, settings.MaxBatchSize)); + + return 0; + } +} + +public record PushStackCommandInputs(string? Name, bool Force, bool ForceWithLease, int MaxBatchSize); +public class PushStackCommandHandler( + IInputProvider inputProvider, + IOutputProvider outputProvider, + IGitOperations gitOperations, + IStackConfig stackConfig) +{ + public async Task Handle(PushStackCommandInputs inputs) + { + await Task.CompletedTask; + var stacks = stackConfig.Load(); + + var remoteUri = gitOperations.GetRemoteUri(); + var stacksForRemote = stacks.Where(s => s.RemoteUri.Equals(remoteUri, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (stacksForRemote.Count == 0) + { + outputProvider.Information("No stacks found for current repository."); + return; + } + + var currentBranch = gitOperations.GetCurrentBranch(); + + var stack = inputProvider.SelectStack(outputProvider, inputs.Name, stacksForRemote, currentBranch); + + if (stack is null) + throw new InvalidOperationException($"Stack '{inputs.Name}' not found."); + + var branchStatus = gitOperations.GetBranchStatuses([.. stack.Branches]); + 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)} to remote"); + + gitOperations.PushBranches([.. branches], inputs.Force, inputs.ForceWithLease); + } + } +} diff --git a/src/Stack/Git/GitOperations.cs b/src/Stack/Git/GitOperations.cs index 9f60b0a6..ef4cdcba 100644 --- a/src/Stack/Git/GitOperations.cs +++ b/src/Stack/Git/GitOperations.cs @@ -23,6 +23,7 @@ public interface IGitOperations void ChangeBranch(string branchName); void FetchBranches(string[] branches); void PullBranch(string branchName); + void PushBranches(string[] branches, bool force, bool forceWithLease); void UpdateBranch(string branchName); void DeleteLocalBranch(string branchName); void MergeFromLocalSourceBranch(string sourceBranchName); @@ -74,6 +75,19 @@ public void PullBranch(string branchName) ExecuteGitCommand($"pull origin {branchName}"); } + public void PushBranches(string[] branches, bool force, bool forceWithLease) + { + var command = $"push origin {string.Join(" ", branches)}"; + + if (force) + command += " --force"; + + if (forceWithLease) + command += " --force-with-lease"; + + ExecuteGitCommand(command); + } + public void UpdateBranch(string branchName) { var currentBranch = GetCurrentBranch(); diff --git a/src/Stack/Help/CommandNames.cs b/src/Stack/Help/CommandNames.cs index 3966ed2d..07e6a9c8 100644 --- a/src/Stack/Help/CommandNames.cs +++ b/src/Stack/Help/CommandNames.cs @@ -17,4 +17,5 @@ public static class CommandNames public const string Open = "open"; public const string Remove = "remove"; public const string Pull = "pull"; + public const string Push = "push"; } diff --git a/src/Stack/Help/StackHelpProvider.cs b/src/Stack/Help/StackHelpProvider.cs index 1a5b3316..f4226259 100644 --- a/src/Stack/Help/StackHelpProvider.cs +++ b/src/Stack/Help/StackHelpProvider.cs @@ -11,7 +11,7 @@ public class StackHelpProvider(ICommandAppSettings settings) : HelpProvider(sett { { CommandGroups.Stack, [CommandNames.New, CommandNames.List, CommandNames.List, CommandNames.Delete, CommandNames.Status] }, { CommandGroups.Branch, [CommandNames.Switch, CommandNames.Update, CommandNames.Cleanup, CommandNames.Branch] }, - { CommandGroups.Remote, [CommandNames.Pull] }, + { CommandGroups.Remote, [CommandNames.Pull, CommandNames.Push] }, { CommandGroups.GitHub, [CommandNames.Pr] }, { CommandGroups.Advanced, [CommandNames.Config] }, }; diff --git a/src/Stack/Program.cs b/src/Stack/Program.cs index 2f6bf127..093be80b 100644 --- a/src/Stack/Program.cs +++ b/src/Stack/Program.cs @@ -29,6 +29,7 @@ // Remote commands configure.AddCommand(CommandNames.Pull).WithDescription("Pull changes from the remote server for a stack."); + configure.AddCommand(CommandNames.Push).WithDescription("Push changes to the remote server for a stack."); // GitHub commands configure.AddBranch(CommandNames.Pr, pr => From 734fae3628b88b33381d483ab1137b0c1448a638 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 Dec 2024 11:55:58 +1100 Subject: [PATCH 13/26] Add formatting --- src/Stack/Commands/Stack/PushStackCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stack/Commands/Stack/PushStackCommand.cs b/src/Stack/Commands/Stack/PushStackCommand.cs index a81719ef..4fc75901 100644 --- a/src/Stack/Commands/Stack/PushStackCommand.cs +++ b/src/Stack/Commands/Stack/PushStackCommand.cs @@ -86,7 +86,7 @@ public async Task Handle(PushStackCommandInputs inputs) foreach (var branches in branchGroupsToPush) { - outputProvider.Information($"Pushing changes for {string.Join(", ", branches)} to remote"); + outputProvider.Information($"Pushing changes for {string.Join(", ", branches.Select(b => b.Branch()))} to remote"); gitOperations.PushBranches([.. branches], inputs.Force, inputs.ForceWithLease); } From d167733c5c0c15e236086a861fbb67cffcef84e5 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 Dec 2024 12:01:02 +1100 Subject: [PATCH 14/26] Testing out capturing std err --- src/Stack/Git/GitOperations.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Stack/Git/GitOperations.cs b/src/Stack/Git/GitOperations.cs index ef4cdcba..8dffd4a5 100644 --- a/src/Stack/Git/GitOperations.cs +++ b/src/Stack/Git/GitOperations.cs @@ -85,7 +85,7 @@ public void PushBranches(string[] branches, bool force, bool forceWithLease) if (forceWithLease) command += " --force-with-lease"; - ExecuteGitCommand(command); + ExecuteGitCommand(command, true); } public void UpdateBranch(string branchName) @@ -232,7 +232,7 @@ public void OpenFileInEditorAndWaitForClose(string path) process.WaitForExit(); } - private string ExecuteGitCommandAndReturnOutput(string command) + private string ExecuteGitCommandAndReturnOutput(string command, bool captureStandardError = true) { if (settings.Verbose) outputProvider.Debug($"git {command}"); @@ -259,10 +259,17 @@ private string ExecuteGitCommandAndReturnOutput(string command) outputProvider.Debug($"{infoBuilder}"); } - return infoBuilder.ToString(); + var output = infoBuilder.ToString(); + + if (captureStandardError) + { + output += $"{Environment.NewLine}{errorBuilder}"; + } + + return output; } - private void ExecuteGitCommand(string command) + private void ExecuteGitCommand(string command, bool captureStandardError = true) { if (settings.DryRun) { @@ -271,7 +278,7 @@ private void ExecuteGitCommand(string command) } else { - var output = ExecuteGitCommandAndReturnOutput(command); + var output = ExecuteGitCommandAndReturnOutput(command, captureStandardError); if (!settings.Verbose && output.Length > 0) { From 3254a3ae2825aceb3bcb893dd5396fb588554297 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 Dec 2024 12:02:51 +1100 Subject: [PATCH 15/26] Make std err optional --- src/Stack/Git/GitOperations.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Stack/Git/GitOperations.cs b/src/Stack/Git/GitOperations.cs index 8dffd4a5..321b9a8a 100644 --- a/src/Stack/Git/GitOperations.cs +++ b/src/Stack/Git/GitOperations.cs @@ -232,7 +232,7 @@ public void OpenFileInEditorAndWaitForClose(string path) process.WaitForExit(); } - private string ExecuteGitCommandAndReturnOutput(string command, bool captureStandardError = true) + private string ExecuteGitCommandAndReturnOutput(string command, bool captureStandardError = false) { if (settings.Verbose) outputProvider.Debug($"git {command}"); @@ -269,7 +269,7 @@ private string ExecuteGitCommandAndReturnOutput(string command, bool captureStan return output; } - private void ExecuteGitCommand(string command, bool captureStandardError = true) + private void ExecuteGitCommand(string command, bool captureStandardError = false) { if (settings.DryRun) { From e5f81ec3cc50ae197dd2536ae213d321a7ae7d8d Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 Dec 2024 12:12:06 +1100 Subject: [PATCH 16/26] Make formatting less noisy --- src/Stack/Commands/Helpers/StackStatusHelpers.cs | 6 +++--- src/Stack/Infrastructure/ConsoleOutputProvider.cs | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Stack/Commands/Helpers/StackStatusHelpers.cs b/src/Stack/Commands/Helpers/StackStatusHelpers.cs index 2b5fdb69..e53c465d 100644 --- a/src/Stack/Commands/Helpers/StackStatusHelpers.cs +++ b/src/Stack/Commands/Helpers/StackStatusHelpers.cs @@ -165,7 +165,7 @@ public static string GetBranchAndPullRequestStatusOutput( if (branchDetail.PullRequest is not null) { - branchNameBuilder.Append($" {branchDetail.PullRequest.GetPullRequestDisplay()}"); + branchNameBuilder.Append($" {branchDetail.PullRequest.GetPullRequestDisplay()}"); } return branchNameBuilder.ToString(); @@ -178,7 +178,7 @@ public static string GetBranchStatusOutput( { var branchNameBuilder = new StringBuilder(); - var branchName = branchDetail.Status.IsCurrentBranch ? $"* [{Color.Green}]{branch}[/]" : branch; + var branchName = branchDetail.Status.IsCurrentBranch ? $"* {branch.Branch()}" : branch; Color? color = branchDetail.Status.ExistsLocally ? null : Color.Grey; Decoration? decoration = branchDetail.Status.ExistsLocally ? null : Decoration.Strikethrough; @@ -230,7 +230,7 @@ public static string GetBranchStatusOutput( if (branchDetail.Status.Tip is not null) { - branchNameBuilder.Append($" {branchDetail.Status.Tip.Sha[..7].Commit()} {branchDetail.Status.Tip.Message}"); + branchNameBuilder.Append($" {branchDetail.Status.Tip.Sha[..7]} {branchDetail.Status.Tip.Message}"); } return branchNameBuilder.ToString(); diff --git a/src/Stack/Infrastructure/ConsoleOutputProvider.cs b/src/Stack/Infrastructure/ConsoleOutputProvider.cs index 78fe7d5a..aca97ee2 100644 --- a/src/Stack/Infrastructure/ConsoleOutputProvider.cs +++ b/src/Stack/Infrastructure/ConsoleOutputProvider.cs @@ -55,5 +55,4 @@ public static class OutputStyleExtensionMethods public static string Branch(this string name) => $"[{Color.Blue}]{name}[/]"; public static string Muted(this string name) => $"[{Color.Grey}]{name}[/]"; public static string Example(this string name) => $"[{Color.Aqua}]{name}[/]"; - public static string Commit(this string name) => $"[{Color.Orange1}]{name}[/]"; } \ No newline at end of file From 9a1c158568f3a73f509923beb8ce9ac3a975f189 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 Dec 2024 12:30:34 +1100 Subject: [PATCH 17/26] Add tests --- .../Stack/PushStackCommandHandlerTests.cs | 317 ++++++++++++++++++ .../Helpers/TestGitRepositoryBuilder.cs | 17 +- src/Stack/Commands/Stack/PushStackCommand.cs | 6 +- 3 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 src/Stack.Tests/Commands/Stack/PushStackCommandHandlerTests.cs diff --git a/src/Stack.Tests/Commands/Stack/PushStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/PushStackCommandHandlerTests.cs new file mode 100644 index 00000000..b901079a --- /dev/null +++ b/src/Stack.Tests/Commands/Stack/PushStackCommandHandlerTests.cs @@ -0,0 +1,317 @@ +using FluentAssertions; +using NSubstitute; +using Stack.Commands; +using Stack.Config; +using Stack.Git; +using Stack.Tests.Helpers; +using Stack.Infrastructure; +using Stack.Commands.Helpers; +using Xunit.Abstractions; + +namespace Stack.Tests.Commands.Stack; + +public class PushStackCommandHandlerTests(ITestOutputHelper testOutputHelper) +{ + [Fact] + public async Task WhenChangesExistLocally_TheyArePushedToTheRemote() + { + // 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(branch1, 3, false) + .WithNumberOfEmptyCommits(branch2, 2, false) + .Build(); + + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().NotContain(tipOfBranch1); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().NotContain(tipOfBranch2); + + var stackConfig = Substitute.For(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var handler = new PushStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + gitOperations.ChangeBranch(branch1); + + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); + var stacks = new List([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + + // Act + await handler.Handle(PushStackCommandInputs.Default); + + // Assert + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfBranch1); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().Contain(tipOfBranch2); + } + + [Fact] + public async Task WhenNameIsProvided_DoesNotAskForName_PushesChangesToRemoteForBranchesInStack() + { + // 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(branch1, 3, false) + .WithNumberOfEmptyCommits(branch2, 2, false) + .Build(); + + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().NotContain(tipOfBranch1); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().NotContain(tipOfBranch2); + + var stackConfig = Substitute.For(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var handler = new PushStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + gitOperations.ChangeBranch(branch1); + + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); + var stacks = new List([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + + // Act + await handler.Handle(new PushStackCommandInputs("Stack1", false, false, 5)); + + // Assert + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfBranch1); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().Contain(tipOfBranch2); + inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any()); + } + + [Fact] + public async Task WhenNameIsProvided_ButStackDoesNotExist_Throws() + { + // 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(branch1, 3, false) + .WithNumberOfEmptyCommits(branch2, 2, false) + .Build(); + + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().NotContain(tipOfBranch1); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().NotContain(tipOfBranch2); + + var stackConfig = Substitute.For(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var handler = new PushStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + gitOperations.ChangeBranch(branch1); + + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); + var stacks = new List([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + + // Act and assert + var invalidStackName = Some.Name(); + await handler.Invoking(async h => await h.Handle(new PushStackCommandInputs(invalidStackName, false, false, 5))) + .Should().ThrowAsync() + .WithMessage($"Stack '{invalidStackName}' not found."); + } + + [Fact] + public async Task WhenChangesExistLocally_ForABranchThatIsNotInTheStack_TheyAreNotPushedToTheRemote() + { + // 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(branch1, 3, false) + .WithNumberOfEmptyCommits(branch2, 2, false) + .Build(); + + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().NotContain(tipOfBranch1); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().NotContain(tipOfBranch2); + + var stackConfig = Substitute.For(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var handler = new PushStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + gitOperations.ChangeBranch(branch1); + + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, [branch2]); + var stacks = new List([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + + // Act + await handler.Handle(PushStackCommandInputs.Default); + + // Assert + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().NotContain(tipOfBranch2); + } + + [Fact] + public async Task WhenForceIsProvided_ChangesAreForcePushedToTheRemote() + { + // 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()) + .Build(); + + repo.RebaseCommits(branch1, sourceBranch); + repo.RebaseCommits(branch2, branch1); + + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + + var stackConfig = Substitute.For(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var handler = new PushStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + gitOperations.ChangeBranch(branch1); + + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); + var stacks = new List([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + + // Act + await handler.Handle(new PushStackCommandInputs(null, true, false, 5)); + + // Assert + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfBranch1); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().Contain(tipOfBranch2); + } + + [Fact] + public async Task WhenForceWithLeaseIsProvided_ChangesAreForcePushedToTheRemoteWithLease() + { + // 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()) + .Build(); + + repo.RebaseCommits(branch1, sourceBranch); + repo.RebaseCommits(branch2, branch1); + + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + + var stackConfig = Substitute.For(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var handler = new PushStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + gitOperations.ChangeBranch(branch1); + + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); + var stacks = new List([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + + // Act + await handler.Handle(new PushStackCommandInputs(null, false, true, 5)); + + // Assert + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfBranch1); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().Contain(tipOfBranch2); + } + + [Fact] + public async Task WhenNumberOfBranchesIsGreaterThanMaxBatchSize_ChangesAreSuccessfullyPushedToTheRemoteInBatches() + { + // 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(branch1, 3, false) + .WithNumberOfEmptyCommits(branch2, 2, false) + .Build(); + + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().NotContain(tipOfBranch1); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().NotContain(tipOfBranch2); + + var stackConfig = Substitute.For(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var handler = new PushStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + gitOperations.ChangeBranch(branch1); + + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); + var stacks = new List([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + + // Act + await handler.Handle(new PushStackCommandInputs(null, false, false, 1)); + + // Assert + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfBranch1); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().Contain(tipOfBranch2); + } +} diff --git a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs index eab413fa..75c829bf 100644 --- a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs +++ b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs @@ -271,7 +271,7 @@ private static LibGit2Sharp.Commit CreateInitialCommit(Repository repository) public class TestGitRepository(TemporaryDirectory LocalDirectory, TemporaryDirectory RemoteDirectory, Repository LocalRepository) : IDisposable { public string RemoteUri => RemoteDirectory.DirectoryPath; - public GitOperationSettings GitOperationSettings => new GitOperationSettings(false, false, LocalDirectory.DirectoryPath); + public GitOperationSettings GitOperationSettings => new GitOperationSettings(false, true, LocalDirectory.DirectoryPath); public LibGit2Sharp.Commit GetTipOfBranch(string branchName) { @@ -290,6 +290,21 @@ public LibGit2Sharp.Commit GetTipOfRemoteBranch(string branchName) return LocalRepository.Branches[remoteBranchName].Tip; } + public List GetCommitsReachableFromRemoteBranch(string branchName) + { + var branch = LocalRepository.Branches[branchName]; + var remoteBranchName = branch.TrackedBranch.CanonicalName; + return [.. LocalRepository.Branches[remoteBranchName].Commits]; + } + + public void RebaseCommits(string branchName, string sourceBranchName) + { + var branch = LocalRepository.Branches[branchName]; + var sourceBranch = LocalRepository.Branches[sourceBranchName]; + var remoteBranchName = branch.TrackedBranch.CanonicalName; + LocalRepository.Rebase.Start(branch, LocalRepository.Branches[remoteBranchName], sourceBranch, new Identity(Some.Name(), Some.Email()), new RebaseOptions()); + } + public void Dispose() { GC.SuppressFinalize(this); diff --git a/src/Stack/Commands/Stack/PushStackCommand.cs b/src/Stack/Commands/Stack/PushStackCommand.cs index 4fc75901..a02e8aaf 100644 --- a/src/Stack/Commands/Stack/PushStackCommand.cs +++ b/src/Stack/Commands/Stack/PushStackCommand.cs @@ -47,7 +47,11 @@ public override async Task ExecuteAsync(CommandContext context, PushStackCo } } -public record PushStackCommandInputs(string? Name, bool Force, bool ForceWithLease, int MaxBatchSize); +public record PushStackCommandInputs(string? Name, bool Force, bool ForceWithLease, int MaxBatchSize) +{ + public static PushStackCommandInputs Default => new(null, false, false, 5); +} + public class PushStackCommandHandler( IInputProvider inputProvider, IOutputProvider outputProvider, From 2f543b0e49b4448ae7b035bf9e835f69f437d148 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 Dec 2024 15:36:05 +1100 Subject: [PATCH 18/26] Push new branch if required --- .../Stack/PushStackCommandHandlerTests.cs | 43 +++++++++++++++++++ src/Stack/Commands/Stack/PushStackCommand.cs | 9 ++++ 2 files changed, 52 insertions(+) diff --git a/src/Stack.Tests/Commands/Stack/PushStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/PushStackCommandHandlerTests.cs index b901079a..6ac51d3d 100644 --- a/src/Stack.Tests/Commands/Stack/PushStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/PushStackCommandHandlerTests.cs @@ -314,4 +314,47 @@ public async Task WhenNumberOfBranchesIsGreaterThanMaxBatchSize_ChangesAreSucces repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfBranch1); repo.GetCommitsReachableFromRemoteBranch(branch2).Should().Contain(tipOfBranch2); } + + [Fact] + public async Task WhenBranchDoesNotExistOnRemote_ItIsPushedToTheRemote() + { + // 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)) + .WithNumberOfEmptyCommits(branch1, 3, false) + .WithNumberOfEmptyCommits(branch2, 2, false) + .Build(); + + var tipOfBranch1 = repo.GetTipOfBranch(branch1); + var tipOfBranch2 = repo.GetTipOfBranch(branch2); + + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().NotContain(tipOfBranch1); + + var stackConfig = Substitute.For(); + var inputProvider = Substitute.For(); + var outputProvider = new TestOutputProvider(testOutputHelper); + var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); + var handler = new PushStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); + + gitOperations.ChangeBranch(branch1); + + var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); + var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); + var stacks = new List([stack1, stack2]); + stackConfig.Load().Returns(stacks); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + + // Act + await handler.Handle(PushStackCommandInputs.Default); + + // Assert + repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfBranch1); + repo.GetCommitsReachableFromRemoteBranch(branch2).Should().Contain(tipOfBranch2); + } } diff --git a/src/Stack/Commands/Stack/PushStackCommand.cs b/src/Stack/Commands/Stack/PushStackCommand.cs index a02e8aaf..edc60766 100644 --- a/src/Stack/Commands/Stack/PushStackCommand.cs +++ b/src/Stack/Commands/Stack/PushStackCommand.cs @@ -80,6 +80,15 @@ public async Task Handle(PushStackCommandInputs inputs) 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 From b1eac2e258e4c229d3706e0ac59ca9078e135221 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 30 Dec 2024 12:07:52 +1100 Subject: [PATCH 19/26] Update help --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dbd3b426..52658a2b 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ OPTIONS: ### `stack status` -Shows the status of a stack, including commits compared to other branches and the status of any associated pull requests. +Shows the status of a stack, including commits compared to other branches and optionally the status of any associated pull requests. ```shell USAGE: @@ -154,6 +154,7 @@ OPTIONS: --working-dir The path to the directory containing the git repository. Defaults to the current directory -n, --name The name of the stack to show the status of --all Show status of all stacks + --full Show full status including pull requests ``` ### `stack delete` From a0a8d7d569ae75455b23e82e3cebd8e4089c8025 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 30 Dec 2024 12:10:34 +1100 Subject: [PATCH 20/26] Update help and readme --- README.md | 19 +++++++++++++++++++ src/Stack/Program.cs | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dbd3b426..f5d377ae 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,25 @@ OPTIONS: -f, --force Force removing the branch without prompting ``` +## Remote commands + +### `stack pull` + +```shell +Pulls changes from the remote repository for a stack. + +USAGE: + stack pull [OPTIONS] + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + --verbose Show verbose output + --working-dir The path to the directory containing the git repository. Defaults to the current directory + --dry-run Show what would happen without making any changes + -n, --name The name of the stack to pull changes from the remote for +``` + ## GitHub commands ### `stack pr create` diff --git a/src/Stack/Program.cs b/src/Stack/Program.cs index 2f6bf127..b1898fdd 100644 --- a/src/Stack/Program.cs +++ b/src/Stack/Program.cs @@ -28,7 +28,7 @@ }); // Remote commands - configure.AddCommand(CommandNames.Pull).WithDescription("Pull changes from the remote server for a stack."); + configure.AddCommand(CommandNames.Pull).WithDescription("Pulls changes from the remote repository for a stack."); // GitHub commands configure.AddBranch(CommandNames.Pr, pr => From 1ce819695db7de0f22725ed684296a315d879872 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 30 Dec 2024 12:11:05 +1100 Subject: [PATCH 21/26] Update help --- src/Stack/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stack/Program.cs b/src/Stack/Program.cs index 093be80b..0ed3694f 100644 --- a/src/Stack/Program.cs +++ b/src/Stack/Program.cs @@ -29,7 +29,7 @@ // Remote commands configure.AddCommand(CommandNames.Pull).WithDescription("Pull changes from the remote server for a stack."); - configure.AddCommand(CommandNames.Push).WithDescription("Push changes to the remote server for a stack."); + configure.AddCommand(CommandNames.Push).WithDescription("Pushes changes to the remote repository for a stack."); // GitHub commands configure.AddBranch(CommandNames.Pr, pr => From 4d5fcc5d476442f6ebe43dc70b7a10e12e89767c Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 30 Dec 2024 12:13:14 +1100 Subject: [PATCH 22/26] Update readme --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index d50cca2b..a1a88eac 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,26 @@ OPTIONS: -n, --name The name of the stack to pull changes from the remote for ``` +### `stack push` + +```shell +Pushes changes to the remote repository for a stack. + +USAGE: + stack push [OPTIONS] + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + --verbose Show verbose output + --working-dir The path to the directory containing the git repository. Defaults to the current directory + --dry-run Show what would happen without making any changes + -n, --name The name of the stack to push changes from the remote for + -f, --force Force the push of the stack + --force-with-lease Force the push of the stack with lease + --max-batch-size The maximum number of branches to push changes for at once (default: 5) +``` + ## GitHub commands ### `stack pr create` From 009017dab1d7f249760de19d2e79a1d26c2feec7 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 30 Dec 2024 21:31:55 +1100 Subject: [PATCH 23/26] Cleanup --- .../Commands/{Stack => Remote}/PullStackCommandHandlerTests.cs | 2 +- src/Stack/Commands/{Stack => Remote}/PullStackCommand.cs | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/Stack.Tests/Commands/{Stack => Remote}/PullStackCommandHandlerTests.cs (99%) rename src/Stack/Commands/{Stack => Remote}/PullStackCommand.cs (100%) diff --git a/src/Stack.Tests/Commands/Stack/PullStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Remote/PullStackCommandHandlerTests.cs similarity index 99% rename from src/Stack.Tests/Commands/Stack/PullStackCommandHandlerTests.cs rename to src/Stack.Tests/Commands/Remote/PullStackCommandHandlerTests.cs index 3151675b..a16ca6a1 100644 --- a/src/Stack.Tests/Commands/Stack/PullStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Remote/PullStackCommandHandlerTests.cs @@ -8,7 +8,7 @@ using Stack.Commands.Helpers; using Xunit.Abstractions; -namespace Stack.Tests.Commands.Stack; +namespace Stack.Tests.Commands.Remote; public class PullStackCommandHandlerTests(ITestOutputHelper testOutputHelper) { diff --git a/src/Stack/Commands/Stack/PullStackCommand.cs b/src/Stack/Commands/Remote/PullStackCommand.cs similarity index 100% rename from src/Stack/Commands/Stack/PullStackCommand.cs rename to src/Stack/Commands/Remote/PullStackCommand.cs From 2079a485547c87884d703ea84037b78441b5bc27 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 30 Dec 2024 21:33:49 +1100 Subject: [PATCH 24/26] Cleanup --- .../Commands/{Stack => Remote}/PushStackCommandHandlerTests.cs | 2 +- src/Stack/Commands/{Stack => Remote}/PushStackCommand.cs | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/Stack.Tests/Commands/{Stack => Remote}/PushStackCommandHandlerTests.cs (99%) rename src/Stack/Commands/{Stack => Remote}/PushStackCommand.cs (100%) diff --git a/src/Stack.Tests/Commands/Stack/PushStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Remote/PushStackCommandHandlerTests.cs similarity index 99% rename from src/Stack.Tests/Commands/Stack/PushStackCommandHandlerTests.cs rename to src/Stack.Tests/Commands/Remote/PushStackCommandHandlerTests.cs index 6ac51d3d..c3d6a0e9 100644 --- a/src/Stack.Tests/Commands/Stack/PushStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Remote/PushStackCommandHandlerTests.cs @@ -8,7 +8,7 @@ using Stack.Commands.Helpers; using Xunit.Abstractions; -namespace Stack.Tests.Commands.Stack; +namespace Stack.Tests.Commands.Remote; public class PushStackCommandHandlerTests(ITestOutputHelper testOutputHelper) { diff --git a/src/Stack/Commands/Stack/PushStackCommand.cs b/src/Stack/Commands/Remote/PushStackCommand.cs similarity index 100% rename from src/Stack/Commands/Stack/PushStackCommand.cs rename to src/Stack/Commands/Remote/PushStackCommand.cs From a71d84e71dbd9e91a7be14209aca59ed4d8ee976 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 30 Dec 2024 22:06:15 +1100 Subject: [PATCH 25/26] Remove force options --- README.md | 2 - .../Remote/PushStackCommandHandlerTests.cs | 90 +------------------ .../Helpers/TestGitRepositoryBuilder.cs | 8 -- src/Stack/Commands/Remote/PushStackCommand.cs | 16 +--- src/Stack/Git/GitOperations.cs | 10 +-- 5 files changed, 9 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index a1a88eac..02b7c814 100644 --- a/README.md +++ b/README.md @@ -314,8 +314,6 @@ OPTIONS: --working-dir The path to the directory containing the git repository. Defaults to the current directory --dry-run Show what would happen without making any changes -n, --name The name of the stack to push changes from the remote for - -f, --force Force the push of the stack - --force-with-lease Force the push of the stack with lease --max-batch-size The maximum number of branches to push changes for at once (default: 5) ``` diff --git a/src/Stack.Tests/Commands/Remote/PushStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Remote/PushStackCommandHandlerTests.cs index c3d6a0e9..6e3a455d 100644 --- a/src/Stack.Tests/Commands/Remote/PushStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Remote/PushStackCommandHandlerTests.cs @@ -93,7 +93,7 @@ public async Task WhenNameIsProvided_DoesNotAskForName_PushesChangesToRemoteForB inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); // Act - await handler.Handle(new PushStackCommandInputs("Stack1", false, false, 5)); + await handler.Handle(new PushStackCommandInputs("Stack1", 5)); // Assert repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfBranch1); @@ -139,7 +139,7 @@ public async Task WhenNameIsProvided_ButStackDoesNotExist_Throws() // Act and assert var invalidStackName = Some.Name(); - await handler.Invoking(async h => await h.Handle(new PushStackCommandInputs(invalidStackName, false, false, 5))) + await handler.Invoking(async h => await h.Handle(new PushStackCommandInputs(invalidStackName, 5))) .Should().ThrowAsync() .WithMessage($"Stack '{invalidStackName}' not found."); } @@ -187,90 +187,6 @@ public async Task WhenChangesExistLocally_ForABranchThatIsNotInTheStack_TheyAreN repo.GetCommitsReachableFromRemoteBranch(branch2).Should().NotContain(tipOfBranch2); } - [Fact] - public async Task WhenForceIsProvided_ChangesAreForcePushedToTheRemote() - { - // 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()) - .Build(); - - repo.RebaseCommits(branch1, sourceBranch); - repo.RebaseCommits(branch2, branch1); - - var tipOfBranch1 = repo.GetTipOfBranch(branch1); - var tipOfBranch2 = repo.GetTipOfBranch(branch2); - - var stackConfig = Substitute.For(); - var inputProvider = Substitute.For(); - var outputProvider = new TestOutputProvider(testOutputHelper); - var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); - var handler = new PushStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); - - gitOperations.ChangeBranch(branch1); - - var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); - var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); - var stacks = new List([stack1, stack2]); - stackConfig.Load().Returns(stacks); - - inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); - - // Act - await handler.Handle(new PushStackCommandInputs(null, true, false, 5)); - - // Assert - repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfBranch1); - repo.GetCommitsReachableFromRemoteBranch(branch2).Should().Contain(tipOfBranch2); - } - - [Fact] - public async Task WhenForceWithLeaseIsProvided_ChangesAreForcePushedToTheRemoteWithLease() - { - // 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()) - .Build(); - - repo.RebaseCommits(branch1, sourceBranch); - repo.RebaseCommits(branch2, branch1); - - var tipOfBranch1 = repo.GetTipOfBranch(branch1); - var tipOfBranch2 = repo.GetTipOfBranch(branch2); - - var stackConfig = Substitute.For(); - var inputProvider = Substitute.For(); - var outputProvider = new TestOutputProvider(testOutputHelper); - var gitOperations = new GitOperations(outputProvider, repo.GitOperationSettings); - var handler = new PushStackCommandHandler(inputProvider, outputProvider, gitOperations, stackConfig); - - gitOperations.ChangeBranch(branch1); - - var stack1 = new Config.Stack("Stack1", repo.RemoteUri, sourceBranch, [branch1, branch2]); - var stack2 = new Config.Stack("Stack2", repo.RemoteUri, sourceBranch, []); - var stacks = new List([stack1, stack2]); - stackConfig.Load().Returns(stacks); - - inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); - - // Act - await handler.Handle(new PushStackCommandInputs(null, false, true, 5)); - - // Assert - repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfBranch1); - repo.GetCommitsReachableFromRemoteBranch(branch2).Should().Contain(tipOfBranch2); - } - [Fact] public async Task WhenNumberOfBranchesIsGreaterThanMaxBatchSize_ChangesAreSuccessfullyPushedToTheRemoteInBatches() { @@ -308,7 +224,7 @@ public async Task WhenNumberOfBranchesIsGreaterThanMaxBatchSize_ChangesAreSucces inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); // Act - await handler.Handle(new PushStackCommandInputs(null, false, false, 1)); + await handler.Handle(new PushStackCommandInputs(null, 1)); // Assert repo.GetCommitsReachableFromRemoteBranch(branch1).Should().Contain(tipOfBranch1); diff --git a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs index 75c829bf..a14de348 100644 --- a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs +++ b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs @@ -297,14 +297,6 @@ public LibGit2Sharp.Commit GetTipOfRemoteBranch(string branchName) return [.. LocalRepository.Branches[remoteBranchName].Commits]; } - public void RebaseCommits(string branchName, string sourceBranchName) - { - var branch = LocalRepository.Branches[branchName]; - var sourceBranch = LocalRepository.Branches[sourceBranchName]; - var remoteBranchName = branch.TrackedBranch.CanonicalName; - LocalRepository.Rebase.Start(branch, LocalRepository.Branches[remoteBranchName], sourceBranch, new Identity(Some.Name(), Some.Email()), new RebaseOptions()); - } - public void Dispose() { GC.SuppressFinalize(this); diff --git a/src/Stack/Commands/Remote/PushStackCommand.cs b/src/Stack/Commands/Remote/PushStackCommand.cs index edc60766..0851530b 100644 --- a/src/Stack/Commands/Remote/PushStackCommand.cs +++ b/src/Stack/Commands/Remote/PushStackCommand.cs @@ -14,14 +14,6 @@ public class PushStackCommandSettings : DryRunCommandSettingsBase [CommandOption("-n|--name")] public string? Name { get; init; } - [Description("Force the push of the stack.")] - [CommandOption("-f|--force")] - public bool Force { get; init; } - - [Description("Force the push of the stack with lease.")] - [CommandOption("--force-with-lease")] - public bool ForceWithLease { get; init; } - [Description("The maximum number of branches to push changes for at once.")] [CommandOption("--max-batch-size")] [DefaultValue(5)] @@ -41,15 +33,15 @@ public override async Task ExecuteAsync(CommandContext context, PushStackCo new GitOperations(outputProvider, settings.GetGitOperationSettings()), new StackConfig()); - await handler.Handle(new PushStackCommandInputs(settings.Name, settings.Force, settings.ForceWithLease, settings.MaxBatchSize)); + await handler.Handle(new PushStackCommandInputs(settings.Name, settings.MaxBatchSize)); return 0; } } -public record PushStackCommandInputs(string? Name, bool Force, bool ForceWithLease, int MaxBatchSize) +public record PushStackCommandInputs(string? Name, int MaxBatchSize) { - public static PushStackCommandInputs Default => new(null, false, false, 5); + public static PushStackCommandInputs Default => new(null, 5); } public class PushStackCommandHandler( @@ -101,7 +93,7 @@ public async Task Handle(PushStackCommandInputs inputs) { outputProvider.Information($"Pushing changes for {string.Join(", ", branches.Select(b => b.Branch()))} to remote"); - gitOperations.PushBranches([.. branches], inputs.Force, inputs.ForceWithLease); + gitOperations.PushBranches([.. branches]); } } } diff --git a/src/Stack/Git/GitOperations.cs b/src/Stack/Git/GitOperations.cs index 321b9a8a..ef2173a9 100644 --- a/src/Stack/Git/GitOperations.cs +++ b/src/Stack/Git/GitOperations.cs @@ -23,7 +23,7 @@ public interface IGitOperations void ChangeBranch(string branchName); void FetchBranches(string[] branches); void PullBranch(string branchName); - void PushBranches(string[] branches, bool force, bool forceWithLease); + void PushBranches(string[] branches); void UpdateBranch(string branchName); void DeleteLocalBranch(string branchName); void MergeFromLocalSourceBranch(string sourceBranchName); @@ -75,16 +75,10 @@ public void PullBranch(string branchName) ExecuteGitCommand($"pull origin {branchName}"); } - public void PushBranches(string[] branches, bool force, bool forceWithLease) + public void PushBranches(string[] branches) { var command = $"push origin {string.Join(" ", branches)}"; - if (force) - command += " --force"; - - if (forceWithLease) - command += " --force-with-lease"; - ExecuteGitCommand(command, true); } From 6b8b7dcb21bcf8a2ee42f3d43365f639bb6bd02d Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 30 Dec 2024 22:14:34 +1100 Subject: [PATCH 26/26] Cleanup --- src/Stack/Git/GitOperations.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Stack/Git/GitOperations.cs b/src/Stack/Git/GitOperations.cs index 9f60b0a6..d2a7f459 100644 --- a/src/Stack/Git/GitOperations.cs +++ b/src/Stack/Git/GitOperations.cs @@ -156,7 +156,6 @@ public Dictionary GetBranchStatuses(string[] branches) var statuses = new Dictionary(); var gitBranchVerbose = ExecuteGitCommandAndReturnOutput("branch -vv").Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); - var regex = new Regex(@"^(?\*)?\s*(?\S+)\s+(?\S+)\s*(\[(?[^:]+)?(?::\s*(?(ahead\s+(?\d+),\s*behind\s+(?\d+))|(ahead\s+(?\d+))|(behind\s+(?\d+))|(gone)))?\])?\s+(?.+)$"); foreach (var branchStatus in gitBranchVerbose) {