From d8f27d7b65bbf34ab0c53a604c915674615f90ad Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 26 May 2025 18:37:22 +1000 Subject: [PATCH 01/10] Refactor stack status --- .../Commands/Helpers/StackHelpersTests.cs | 41 +-- .../Stack/DeleteStackCommandHandlerTests.cs | 8 +- .../Stack/StackStatusCommandHandlerTests.cs | 247 +++++++------ src/Stack.Tests/Helpers/Some.cs | 1 + src/Stack/Commands/Helpers/StackHelpers.cs | 343 +++++++++--------- .../PullRequests/CreatePullRequestsCommand.cs | 37 +- .../SetPullRequestDescriptionCommand.cs | 9 +- src/Stack/Commands/Remote/SyncStackCommand.cs | 4 +- .../Commands/Stack/StackStatusCommand.cs | 89 +---- .../Commands/Stack/UpdateStackCommand.cs | 2 +- 10 files changed, 352 insertions(+), 429 deletions(-) diff --git a/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs b/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs index 97f4adea..77a815f4 100644 --- a/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs +++ b/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs @@ -1,5 +1,6 @@ using FluentAssertions; using NSubstitute; +using Stack.Commands; using Stack.Commands.Helpers; using Stack.Git; using Stack.Infrastructure; @@ -22,20 +23,9 @@ public void UpdateStack_WhenThereAreConflictsMergingBranches_AndUpdateIsContinue var inputProvider = Substitute.For(); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, [branch1, branch2]); - var branchDetail1 = new BranchDetail - { - Status = new BranchStatus(true, true, true, false, 0, 0, 0, 0, null) - }; - var branchDetail2 = new BranchDetail - { - Status = new BranchStatus(true, true, true, false, 0, 0, 0, 0, null) - }; - - var stackStatus = new StackStatus(new Dictionary - { - { branch1, branchDetail1 }, - { branch2, branchDetail2 }, - }); + var branchDetail1 = new BranchDetail(branch1, true, new Commit(Some.Sha(), Some.Name()), new RemoteTrackingBranchStatus($"origin/{branch1}", true, 0, 0), null, null); + var branchDetail2 = new BranchDetail(branch2, true, new Commit(Some.Sha(), Some.Name()), new RemoteTrackingBranchStatus($"origin/{branch2}", true, 0, 0), null, null); + var stackStatus = new StackStatus("Stack1", new Branch(sourceBranch, true, null, null), [branchDetail1, branchDetail2]); inputProvider .Select( @@ -76,20 +66,9 @@ public void UpdateStack_WhenThereAreConflictsMergingBranches_AndUpdateIsAborted_ var logger = new TestLogger(testOutputHelper); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, [branch1, branch2]); - var branchDetail1 = new BranchDetail - { - Status = new BranchStatus(true, true, true, false, 0, 0, 0, 0, null) - }; - var branchDetail2 = new BranchDetail - { - Status = new BranchStatus(true, true, true, false, 0, 0, 0, 0, null) - }; - - var stackStatus = new StackStatus(new Dictionary - { - { branch1, branchDetail1 }, - { branch2, branchDetail2 }, - }); + var branchDetail1 = new BranchDetail(branch1, true, new Commit(Some.Sha(), Some.Name()), new RemoteTrackingBranchStatus($"origin/{branch1}", true, 0, 0), null, null); + var branchDetail2 = new BranchDetail(branch2, true, new Commit(Some.Sha(), Some.Name()), new RemoteTrackingBranchStatus($"origin/{branch2}", true, 0, 0), null, null); + var stackStatus = new StackStatus("Stack1", new Branch(sourceBranch, true, null, null), [branchDetail1, branchDetail2]); gitClient .When(g => g.MergeFromLocalSourceBranch(sourceBranch)) @@ -166,7 +145,7 @@ public void UpdateStackUsingRebase_WhenARemoteBranchIsDeleted_RebasesOntoThePare repo.DeleteRemoteTrackingBranch(branch1); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, [branch1, branch2]); - var stackStatus = StackHelpers.GetStackStatus(stack, branch1, logger, gitClient, gitHubClient, false); + var stackStatus = StackHelpers.GetStackStatusNew(stack, branch1, logger, gitClient, gitHubClient, false); // Act StackHelpers.UpdateStackUsingRebase(stack, stackStatus, gitClient, inputProvider, logger); @@ -237,7 +216,7 @@ public void UpdateStackUsingRebase_WhenARemoteBranchIsDeleted_ButTheTargetBranch repo.Push(sourceBranch); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, [branch1, branch2]); - var stackStatus = StackHelpers.GetStackStatus(stack, branch1, logger, gitClient, gitHubClient, false); + var stackStatus = StackHelpers.GetStackStatusNew(stack, branch1, logger, gitClient, gitHubClient, false); // Act: Even though the parent branch (branch1) has been deleted on the remote, // we should not explicitly re-parent the target branch (branch2) onto the source branch @@ -286,7 +265,7 @@ public void UpdateStackUsingRebase_WhenARemoteBranchIsDeleted_AndLocalBranchIsDe repo.Commit(); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, [branch1, branch2]); - var stackStatus = StackHelpers.GetStackStatus(stack, branch1, logger, gitClient, gitHubClient, false); + var stackStatus = StackHelpers.GetStackStatusNew(stack, branch1, logger, gitClient, gitHubClient, false); gitClient.Fetch(true); diff --git a/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs index 8cca002c..978d5a0a 100644 --- a/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs @@ -16,7 +16,7 @@ public async Task WhenNoInputsAreProvided_AsksForName_AndConfirmation_AndDeletes { // Arrange var sourceBranch = Some.BranchName(); - using var repo = new TestGitRepositoryBuilder().Build(); + using var repo = new TestGitRepositoryBuilder().WithBranch(sourceBranch).Build(); var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); @@ -91,7 +91,7 @@ public async Task WhenNameIsProvided_AsksForConfirmation_AndDeletesStack() { // Arrange var sourceBranch = Some.BranchName(); - using var repo = new TestGitRepositoryBuilder().Build(); + using var repo = new TestGitRepositoryBuilder().WithBranch(sourceBranch).Build(); var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); @@ -210,7 +210,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName() { // Arrange var sourceBranch = Some.BranchName(); - using var repo = new TestGitRepositoryBuilder().Build(); + using var repo = new TestGitRepositoryBuilder().WithBranch(sourceBranch).Build(); var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); @@ -244,7 +244,7 @@ public async Task WhenConfirmIsProvided_DoesNotAskForConfirmation_DeletesStack() { // Arrange var sourceBranch = Some.BranchName(); - using var repo = new TestGitRepositoryBuilder().Build(); + using var repo = new TestGitRepositoryBuilder().WithBranch(sourceBranch).Build(); var stackConfig = Substitute.For(); var inputProvider = Substitute.For(); diff --git a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs index a750382d..e89ddb6d 100644 --- a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs @@ -42,33 +42,30 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneHasAPullRequests_Retur stackConfig.Load().Returns(stacks); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); - logger - .WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())) - .Do(ci => ci.ArgAt(1)()); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); - gitHubClient - .GetPullRequest(branch1) - .Returns(pr); + gitHubClient.GetPullRequest(branch1).Returns(pr); // Act var response = await handler.Handle(new StackStatusCommandInputs(null, false, true)); // Assert - var expectedBranchDetails = new Dictionary - { - { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, - { branch1, new BranchDetail { Status = new BranchStatus(true, 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, true, false, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())) } } - }; - response.Statuses.Should().BeEquivalentTo( - new Dictionary - { - { - stack1, new(expectedBranchDetails) - } - }); + var expectedSourceBranch = new global::Stack.Commands.Helpers.Branch(sourceBranch, true, + new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{sourceBranch}", true, 0, 0)); + var expectedBranch1 = new BranchDetail(branch1, true, + new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch1}", true, 0, 0), + pr, new ParentBranchStatus(expectedSourceBranch, 10, 5)); + var expectedBranch2 = new BranchDetail(branch2, true, + new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch2}", true, 0, 0), + null, new ParentBranchStatus(expectedBranch1, 1, 0)); + + var expectedStackDetail = new StackStatus(stack1.Name, expectedSourceBranch, [expectedBranch1, expectedBranch2]); + response.Stacks.Should().BeEquivalentTo([expectedStackDetail]); } [Fact] @@ -102,32 +99,32 @@ public async Task WhenStackNameIsProvided_DoesNotAskForStack_ReturnsStatus() stackConfig.Load().Returns(stacks); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); - logger - .WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())) - .Do(ci => ci.ArgAt(1)()); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); - gitHubClient - .GetPullRequest(branch1) - .Returns(pr); + gitHubClient.GetPullRequest(branch1).Returns(pr); // Act var response = await handler.Handle(new StackStatusCommandInputs("Stack1", false, true)); // Assert - var expectedBranchDetails = new Dictionary - { - { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, - { branch1, new BranchDetail { Status = new BranchStatus(true, 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, true, false, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())) } } - }; - response.Statuses.Should().BeEquivalentTo(new Dictionary - { - { - stack1, new(expectedBranchDetails) - } - }); + var expectedSourceBranch = new global::Stack.Commands.Helpers.Branch(sourceBranch, true, + new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{sourceBranch}", true, 0, 0)); + + var expectedBranch1 = new BranchDetail(branch1, true, + new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch1}", true, 0, 0), + pr, new ParentBranchStatus(expectedSourceBranch, 10, 5)); + + var expectedBranch2 = new BranchDetail(branch2, true, + new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch2}", true, 0, 0), + null, new ParentBranchStatus(expectedBranch1, 1, 0)); + + var expectedStackDetail = new StackStatus(stack1.Name, expectedSourceBranch, [expectedBranch1, expectedBranch2]); + response.Stacks.Should().BeEquivalentTo([expectedStackDetail]); inputProvider.ReceivedCalls().Should().BeEmpty(); } @@ -179,26 +176,30 @@ public async Task WhenAllStacksAreRequested_ReturnsStatusOfEachStack() var response = await handler.Handle(new StackStatusCommandInputs(null, true, true)); // Assert - var expectedBranchDetailsForStack1 = new Dictionary - { - { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, - { branch1, new BranchDetail { Status = new BranchStatus(true, 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, true, false, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())) } } - }; - var expectedBranchDetailsForStack2 = new Dictionary - { - { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, - { branch3, new BranchDetail { Status = new BranchStatus(true, true, true, false, 3, 5, 0, 0, new Commit(tipOfBranch3.Sha[..7], tipOfBranch3.Message.Trim())) } } - }; - response.Statuses.Should().BeEquivalentTo(new Dictionary - { - { - stack1, new(expectedBranchDetailsForStack1) - }, - { - stack2, new(expectedBranchDetailsForStack2) - } - }); + var expectedSourceBranch = new global::Stack.Commands.Helpers.Branch(sourceBranch, true, + new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{sourceBranch}", true, 0, 0)); + + var expectedBranch1 = new BranchDetail(branch1, true, + new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch1}", true, 0, 0), + pr, new ParentBranchStatus(expectedSourceBranch, 10, 5)); + + var expectedBranch2 = new BranchDetail(branch2, true, + new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch2}", true, 0, 0), + null, new ParentBranchStatus(expectedBranch1, 1, 0)); + + var expectedStackDetail1 = new StackStatus(stack1.Name, expectedSourceBranch, [expectedBranch1, expectedBranch2]); + + var expectedBranch3 = new BranchDetail(branch3, true, + new Commit(tipOfBranch3.Sha[..7], tipOfBranch3.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch3}", true, 0, 0), + null, new ParentBranchStatus(expectedSourceBranch, 3, 5)); + + var expectedStackDetail2 = new StackStatus(stack2.Name, expectedSourceBranch, [expectedBranch3]); + + response.Stacks.Should().BeEquivalentTo([expectedStackDetail1, expectedStackDetail2]); } [Fact] @@ -231,44 +232,44 @@ public async Task WhenAllStacksAreRequested_WithStacksInMultipleRepositories_Ret 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 stack3 = new Config.Stack("Stack3", Some.HttpsUri().ToString(), Some.BranchName(), [Some.BranchName()]); var stacks = new List([stack1, stack2]); stackConfig.Load().Returns(stacks); - logger - .WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())) - .Do(ci => ci.ArgAt(1)()); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); - gitHubClient - .GetPullRequest(branch1) - .Returns(pr); + gitHubClient.GetPullRequest(branch1).Returns(pr); // Act var response = await handler.Handle(new StackStatusCommandInputs(null, true, true)); // Assert - var expectedBranchDetailsForStack1 = new Dictionary - { - { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, - { branch1, new BranchDetail { Status = new BranchStatus(true, 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, true, false, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())) } } - }; - var expectedBranchDetailsForStack2 = new Dictionary - { - { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, - { branch3, new BranchDetail { Status = new BranchStatus(true, true, true, false, 3, 5, 0, 0, new Commit(tipOfBranch3.Sha[..7], tipOfBranch3.Message.Trim())) } } - }; - response.Statuses.Should().BeEquivalentTo(new Dictionary - { - { - stack1, new(expectedBranchDetailsForStack1) - }, - { - stack2, new(expectedBranchDetailsForStack2) - } - }); + var expectedSourceBranch = new global::Stack.Commands.Helpers.Branch(sourceBranch, true, + new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{sourceBranch}", true, 0, 0)); + + var expectedBranch1 = new BranchDetail(branch1, true, + new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch1}", true, 0, 0), + pr, new ParentBranchStatus(expectedSourceBranch, 10, 5)); + + var expectedBranch2 = new BranchDetail(branch2, true, + new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch2}", true, 0, 0), + null, new ParentBranchStatus(expectedBranch1, 1, 0)); + + var expectedStackDetail1 = new StackStatus(stack1.Name, expectedSourceBranch, [expectedBranch1, expectedBranch2]); + + var expectedBranch3 = new BranchDetail(branch3, true, + new Commit(tipOfBranch3.Sha[..7], tipOfBranch3.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch3}", true, 0, 0), + null, new ParentBranchStatus(expectedSourceBranch, 3, 5)); + + var expectedStackDetail2 = new StackStatus(stack2.Name, expectedSourceBranch, [expectedBranch3]); + + response.Stacks.Should().BeEquivalentTo([expectedStackDetail1, expectedStackDetail2]); } [Fact] @@ -352,18 +353,21 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem var response = await handler.Handle(new StackStatusCommandInputs(null, false, true)); // Assert - var expectedBranchDetails = new Dictionary - { - { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, false, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, - { branch1, new BranchDetail { Status = new BranchStatus(true, false, false, false, 0, 0, 0, 0, new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim())) } }, - { branch2, new BranchDetail { Status = new BranchStatus(true, true, true, true, 11, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())), PullRequest = pr } } - }; - response.Statuses.Should().BeEquivalentTo(new Dictionary - { - { - stack1, new(expectedBranchDetails) - } - }); + var expectedSourceBranch = new global::Stack.Commands.Helpers.Branch(sourceBranch, true, + new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{sourceBranch}", true, 0, 0)); + + var expectedBranch1 = new BranchDetail(branch1, true, + new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim()), + null, null, new ParentBranchStatus(expectedSourceBranch, 0, 0)); + + var expectedBranch2 = new BranchDetail(branch2, true, + new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch2}", true, 0, 0), + pr, new ParentBranchStatus(expectedSourceBranch, 11, 0)); + + var expectedStackDetail = new StackStatus(stack1.Name, expectedSourceBranch, [expectedBranch1, expectedBranch2]); + response.Stacks.Should().BeEquivalentTo([expectedStackDetail]); } [Fact] @@ -409,18 +413,19 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem var response = await handler.Handle(new StackStatusCommandInputs(null, false, true)); // Assert - var expectedBranchDetails = new Dictionary - { - { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, false, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, - { branch1, new BranchDetail { Status = new BranchStatus(false, false, false, false, 0, 0, 0, 0, null) } }, - { branch2, new BranchDetail { Status = new BranchStatus(true, true, true, true, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())), PullRequest = pr } } - }; - response.Statuses.Should().BeEquivalentTo(new Dictionary - { - { - stack1, new(expectedBranchDetails) - } - }); + var expectedSourceBranch = new global::Stack.Commands.Helpers.Branch(sourceBranch, true, + new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{sourceBranch}", true, 0, 0)); + + var expectedBranch1 = new BranchDetail(branch1, false, null, null, null, null); + + var expectedBranch2 = new BranchDetail(branch2, true, + new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch2}", true, 0, 0), + pr, new ParentBranchStatus(expectedSourceBranch, 1, 0)); + + var expectedStackDetail = new StackStatus(stack1.Name, expectedSourceBranch, [expectedBranch1, expectedBranch2]); + response.Stacks.Should().BeEquivalentTo([expectedStackDetail]); } [Fact] @@ -466,18 +471,20 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_ReturnsStatus() var response = await handler.Handle(new StackStatusCommandInputs(null, false, true)); // Assert - var expectedBranchDetails = new Dictionary - { - { sourceBranch, new BranchDetail { Status = new BranchStatus(true, true, true, true, 0, 0, 0, 0, new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim())) } }, - { branch1, new BranchDetail { Status = new BranchStatus(true, 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, true, false, 1, 0, 0, 0, new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim())) } } - }; - response.Statuses.Should().BeEquivalentTo(new Dictionary - { - { - stack1, new(expectedBranchDetails) - } - }); + var expectedSourceBranch = new global::Stack.Commands.Helpers.Branch(sourceBranch, true, + new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{sourceBranch}", true, 0, 0)); + var expectedBranch1 = new BranchDetail(branch1, true, + new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch1}", true, 0, 0), + pr, new ParentBranchStatus(expectedSourceBranch, 10, 5)); + var expectedBranch2 = new BranchDetail(branch2, true, + new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim()), + new RemoteTrackingBranchStatus($"origin/{branch2}", true, 0, 0), + null, new ParentBranchStatus(expectedBranch1, 1, 0)); + + var expectedStackDetail = new StackStatus(stack1.Name, expectedSourceBranch, [expectedBranch1, expectedBranch2]); + response.Stacks.Should().BeEquivalentTo([expectedStackDetail]); inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any()); } diff --git a/src/Stack.Tests/Helpers/Some.cs b/src/Stack.Tests/Helpers/Some.cs index d1ca36e2..e3dc5e17 100644 --- a/src/Stack.Tests/Helpers/Some.cs +++ b/src/Stack.Tests/Helpers/Some.cs @@ -11,4 +11,5 @@ public static class Some public static string BranchName() => $"branch-{ShortName()}"; public static Uri HttpsUri() => new($"https://{ShortName()}.com"); public static string Email() => $"{ShortName()}@{ShortName()}.com"; + public static string Sha() => Guid.NewGuid().ToString("N").Substring(0, 7); } diff --git a/src/Stack/Commands/Helpers/StackHelpers.cs b/src/Stack/Commands/Helpers/StackHelpers.cs index 4bb9e21f..cc28e309 100644 --- a/src/Stack/Commands/Helpers/StackHelpers.cs +++ b/src/Stack/Commands/Helpers/StackHelpers.cs @@ -6,34 +6,38 @@ namespace Stack.Commands.Helpers; -public class BranchDetail -{ - public BranchStatus Status { get; set; } = new(false, false, false, false, 0, 0, 0, 0, null); - public GitHubPullRequest? PullRequest { get; set; } +public record StackStatus(string Name, Branch SourceBranch, BranchDetail[] Branches); - public bool IsActive => Status.ExistsLocally && Status.ExistsInRemote && (PullRequest is null || PullRequest.State != GitHubPullRequestStates.Merged); - public bool CouldBeCleanedUp => Status.ExistsLocally && ((Status.HasRemoteTrackingBranch && !Status.ExistsInRemote) || (PullRequest is not null && PullRequest.State == GitHubPullRequestStates.Merged)); - public bool HasPullRequest => PullRequest is not null && PullRequest.State != GitHubPullRequestStates.Closed; +public record RemoteTrackingBranchStatus(string Name, bool Exists, int Ahead, int Behind); + +public record Branch(string Name, bool Exists, Commit? Tip, RemoteTrackingBranchStatus? RemoteTrackingBranch) +{ + public virtual bool IsActive => Exists && RemoteTrackingBranch?.Exists == true; + public int AheadOfRemote => RemoteTrackingBranch?.Ahead ?? 0; + public int BehindRemote => RemoteTrackingBranch?.Behind ?? 0; } -public record BranchStatus( - bool ExistsLocally, - bool HasRemoteTrackingBranch, - bool ExistsInRemote, - bool IsCurrentBranch, - int AheadOfParent, - int BehindParent, - int AheadOfRemote, - int BehindRemote, - Commit? Tip); - -public record StackStatus(Dictionary Branches) + +public record BranchDetail( + string Name, + bool Exists, + Commit? Tip, + RemoteTrackingBranchStatus? RemoteTrackingBranch, + GitHubPullRequest? PullRequest, + ParentBranchStatus? Parent) : Branch(Name, Exists, Tip, RemoteTrackingBranch) { - public string[] GetActiveBranches() => Branches.Where(b => b.Value.IsActive).Select(b => b.Key).ToArray(); + public override bool IsActive => base.IsActive && (PullRequest is null || PullRequest.State != GitHubPullRequestStates.Merged); + public bool CouldBeCleanedUp => Exists && ((RemoteTrackingBranch is not null && !RemoteTrackingBranch.Exists) || (PullRequest is not null && PullRequest.State == GitHubPullRequestStates.Merged)); + public bool HasPullRequest => PullRequest is not null && PullRequest.State != GitHubPullRequestStates.Closed; + public int AheadOfParent => Parent?.Ahead ?? 0; + public int BehindParent => Parent?.Behind ?? 0; + public string ParentBranchName => Parent?.Branch.Name ?? string.Empty; } +public record ParentBranchStatus(Branch Branch, int Ahead, int Behind); + public static class StackHelpers { - public static Dictionary GetStackStatus( + public static List GetStackStatusNew( List stacks, string currentBranch, ILogger logger, @@ -41,95 +45,89 @@ public static class StackHelpers IGitHubClient gitHubClient, bool includePullRequestStatus = true) { - var stacksToCheckStatusFor = new Dictionary(); + var stacksToReturnStatusFor = new List(); - stacks - .OrderByCurrentStackThenByName(currentBranch) - .ToList() - .ForEach(stack => stacksToCheckStatusFor.Add(stack, new StackStatus([]))); + var stacksOrderedByCurrentBranch = stacks + .OrderByCurrentStackThenByName(currentBranch); - var allBranchesInStacks = stacks.SelectMany(s => new List([s.SourceBranch]).Concat(s.Branches)).Distinct().ToArray(); + var allBranchesInStacks = stacks + .SelectMany(s => new List([s.SourceBranch]).Concat(s.Branches)) + .Distinct() + .ToArray(); var branchStatuses = gitClient.GetBranchStatuses(allBranchesInStacks); - foreach (var (stack, status) in stacksToCheckStatusFor) + foreach (var stack in stacksOrderedByCurrentBranch) { - var parentBranch = stack.SourceBranch; - - status.Branches.Add(stack.SourceBranch, new BranchDetail()); - branchStatuses.TryGetValue(stack.SourceBranch, out var sourceBranchStatus); - if (sourceBranchStatus is not null) + if (!branchStatuses.TryGetValue(stack.SourceBranch, out var sourceBranchStatus)) { - status.Branches[stack.SourceBranch].Status = new BranchStatus( - true, - sourceBranchStatus.RemoteTrackingBranchName is not null, - sourceBranchStatus.RemoteBranchExists, - sourceBranchStatus.IsCurrentBranch, - 0, - 0, - sourceBranchStatus.Ahead, - sourceBranchStatus.Behind, - sourceBranchStatus.Tip); + logger.Warning($"Source branch '{stack.SourceBranch}' does not exist locally or in the remote repository."); + continue; } - foreach (var branch in stack.Branches) + var sourceBranch = new Branch( + stack.SourceBranch, + true, + sourceBranchStatus.Tip, + sourceBranchStatus.RemoteTrackingBranchName is not null + ? new RemoteTrackingBranchStatus( + sourceBranchStatus.RemoteTrackingBranchName, + sourceBranchStatus.RemoteBranchExists, + sourceBranchStatus.Ahead, + sourceBranchStatus.Behind) + : null); + var parentBranch = sourceBranch; + var stackBranches = new List(); + + foreach (var branchName in stack.Branches) { - status.Branches.Add(branch, new BranchDetail()); - branchStatuses.TryGetValue(branch, out var branchStatus); + branchStatuses.TryGetValue(branchName, out var branchStatus); if (branchStatus is not null) { - var (aheadOfParent, behindParent) = branchStatus.RemoteBranchExists ? gitClient.CompareBranches(branch, parentBranch) : (0, 0); + var (aheadOfParent, behindParent) = branchStatus.RemoteBranchExists ? gitClient.CompareBranches(branchName, parentBranch.Name) : (0, 0); + GitHubPullRequest? pullRequest = null; + + if (includePullRequestStatus) + { + pullRequest = gitHubClient.GetPullRequest(branchName); + } - status.Branches[branch].Status = new BranchStatus( + var branch = new BranchDetail( + branchName, true, - branchStatus.RemoteTrackingBranchName is not null, - branchStatus.RemoteBranchExists, - branchStatus.IsCurrentBranch, - aheadOfParent, - behindParent, - branchStatus.Ahead, - branchStatus.Behind, - branchStatus.Tip); + branchStatus.Tip, + branchStatus.RemoteTrackingBranchName is not null + ? new RemoteTrackingBranchStatus( + branchStatus.RemoteTrackingBranchName, + branchStatus.RemoteBranchExists, + branchStatus.Ahead, + branchStatus.Behind) + : null, + pullRequest, + new ParentBranchStatus(parentBranch, aheadOfParent, behindParent)); + + stackBranches.Add(branch); if (branchStatus.RemoteBranchExists) { parentBranch = branch; } } - } - } - - if (includePullRequestStatus) - { - logger.Status("Checking status of GitHub pull requests...", () => - { - foreach (var (stack, status) in stacksToCheckStatusFor) + else { - try - { - foreach (var branch in stack.Branches) - { - var pr = gitHubClient.GetPullRequest(branch); - - if (pr is not null) - { - status.Branches[branch].PullRequest = pr; - } - } - } - catch (Exception ex) - { - logger.Warning($"Error checking GitHub pull requests: {ex.Message}"); - } + var branch = new BranchDetail(branchName, false, null, null, null, null); + stackBranches.Add(branch); } - }); + } + + stacksToReturnStatusFor.Add(new StackStatus(stack.Name, sourceBranch, [.. stackBranches])); } - return stacksToCheckStatusFor; + return stacksToReturnStatusFor; } - public static StackStatus GetStackStatus( + public static StackStatus GetStackStatusNew( Config.Stack stack, string currentBranch, ILogger logger, @@ -137,7 +135,7 @@ public static StackStatus GetStackStatus( IGitHubClient gitHubClient, bool includePullRequestStatus = true) { - var statuses = GetStackStatus( + var statuses = GetStackStatusNew( [stack], currentBranch, logger, @@ -145,76 +143,104 @@ public static StackStatus GetStackStatus( gitHubClient, includePullRequestStatus); - return statuses[stack]; + return statuses.First(); } public static void OutputStackStatus( - Dictionary stackStatuses, + List stacks, ILogger logger) { - foreach (var (stack, status) in stackStatuses) + foreach (var stack in stacks) { - OutputStackStatus(stack, status, logger); + OutputStackStatus(stack, logger); logger.NewLine(); } } public static void OutputStackStatus( - Config.Stack stack, - StackStatus status, + StackStatus stack, ILogger logger) { - var header = stack.SourceBranch.Branch(); - if (status.Branches.TryGetValue(stack.SourceBranch, out var sourceBranchStatus)) - { - header = GetBranchStatusOutput(stack.SourceBranch, null, sourceBranchStatus); - } + var header = GetBranchStatusOutput(stack.SourceBranch); var items = new List(); - string parentBranch = stack.SourceBranch; - foreach (var branch in stack.Branches) { - if (status.Branches.TryGetValue(branch, out var branchDetail)) - { - items.Add(GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail)); - - if (branchDetail.IsActive) - { - parentBranch = branch; - } - } + items.Add(GetBranchAndPullRequestStatusOutput(branch)); } logger.Information(stack.Name.Stack()); logger.Tree(header, [.. items]); } - public static string GetBranchAndPullRequestStatusOutput( - string branch, - string? parentBranch, - BranchDetail branchDetail) + public static string GetBranchAndPullRequestStatusOutput(BranchDetail branch) { var branchNameBuilder = new StringBuilder(); - branchNameBuilder.Append(GetBranchStatusOutput(branch, parentBranch, branchDetail)); + branchNameBuilder.Append(GetBranchStatusOutput(branch)); - if (branchDetail.PullRequest is not null) + if (branch.PullRequest is not null) { - branchNameBuilder.Append($" {branchDetail.PullRequest.GetPullRequestDisplay()}"); + branchNameBuilder.Append($" {branch.PullRequest.GetPullRequestDisplay()}"); } return branchNameBuilder.ToString(); } - public static string GetBranchStatusOutput( - string branch, - string? parentBranch, - BranchDetail branchDetail) + public static string GetBranchStatusOutput(Branch branch) + { + var branchNameBuilder = new StringBuilder(); + + var branchName = branch.Name; + Color? color = branch.Exists ? null : Color.Grey; + Decoration? decoration = branch.Exists ? null : Decoration.Strikethrough; + + if (color is not null && decoration is not null) + { + branchNameBuilder.Append($"[{decoration} {color}]{branchName}[/]"); + } + else if (color is not null) + { + branchNameBuilder.Append($"[{color}]{branchName}[/]"); + } + else if (decoration is not null) + { + branchNameBuilder.Append($"[{decoration}]{branchName}[/]"); + } + else + { + branchNameBuilder.Append(branchName); + } + + if (branch.IsActive) + { + if (branch.AheadOfRemote > 0 || branch.BehindRemote > 0) + { + branchNameBuilder.Append($" {branch.BehindRemote}{Emoji.Known.DownArrow}{branch.AheadOfRemote}{Emoji.Known.UpArrow}"); + } + } + else if (branch.Exists && branch.RemoteTrackingBranch is null) + { + branchNameBuilder.Append(" (no remote tracking branch)".Muted()); + } + else if (branch.Exists && branch.RemoteTrackingBranch is not null && branch.RemoteTrackingBranch.Exists == false) + { + branchNameBuilder.Append(" (remote branch deleted)".Muted()); + } + + if (branch.Tip is not null) + { + branchNameBuilder.Append($" {branch.Tip.Sha[..7]} {Markup.Escape(branch.Tip.Message)}"); + } + + return branchNameBuilder.ToString(); + } + + public static string GetBranchStatusOutput(BranchDetail branch) { var branchNameBuilder = new StringBuilder(); - var branchName = branchDetail.Status.IsCurrentBranch ? $"* {branch.Branch()}" : branch; - Color? color = branchDetail.Status.ExistsLocally ? null : Color.Grey; - Decoration? decoration = branchDetail.Status.ExistsLocally ? null : Decoration.Strikethrough; + var branchName = branch.Name; + Color? color = branch.Exists ? null : Color.Grey; + Decoration? decoration = branch.Exists ? null : Decoration.Strikethrough; if (color is not null && decoration is not null) { @@ -233,71 +259,66 @@ public static string GetBranchStatusOutput( branchNameBuilder.Append(branchName); } - if (branchDetail.IsActive) + if (branch.IsActive) { - if (branchDetail.Status.AheadOfRemote > 0 || branchDetail.Status.BehindRemote > 0) + if (branch.AheadOfRemote > 0 || branch.BehindRemote > 0) { - branchNameBuilder.Append($" {branchDetail.Status.BehindRemote}{Emoji.Known.DownArrow}{branchDetail.Status.AheadOfRemote}{Emoji.Known.UpArrow}"); + branchNameBuilder.Append($" {branch.BehindRemote}{Emoji.Known.DownArrow}{branch.AheadOfRemote}{Emoji.Known.UpArrow}"); } - if (branchDetail.Status.AheadOfParent > 0 && branchDetail.Status.BehindParent > 0) + if (branch.AheadOfParent > 0 && branch.BehindParent > 0) { - branchNameBuilder.Append($" ({branchDetail.Status.AheadOfParent} ahead, {branchDetail.Status.BehindParent} behind {parentBranch})".Muted()); + branchNameBuilder.Append($" ({branch.AheadOfParent} ahead, {branch.BehindParent} behind {branch.ParentBranchName})".Muted()); } - else if (branchDetail.Status.AheadOfParent > 0) + else if (branch.AheadOfParent > 0) { - branchNameBuilder.Append($" ({branchDetail.Status.AheadOfParent} ahead of {parentBranch})".Muted()); + branchNameBuilder.Append($" ({branch.AheadOfParent} ahead of {branch.ParentBranchName})".Muted()); } - else if (branchDetail.Status.BehindParent > 0) + else if (branch.BehindParent > 0) { - branchNameBuilder.Append($" ({branchDetail.Status.BehindParent} behind {parentBranch})".Muted()); + branchNameBuilder.Append($" ({branch.BehindParent} behind {branch.ParentBranchName})".Muted()); } } - else if (branchDetail.Status.ExistsLocally && !branchDetail.Status.HasRemoteTrackingBranch) + else if (branch.Exists && branch.RemoteTrackingBranch is null) { branchNameBuilder.Append(" (no remote tracking branch)".Muted()); } - else if (branchDetail.Status.ExistsLocally && !branchDetail.Status.ExistsInRemote) + else if (branch.Exists && branch.RemoteTrackingBranch is not null && branch.RemoteTrackingBranch.Exists == false) { branchNameBuilder.Append(" (remote branch deleted)".Muted()); } - else if (branchDetail.PullRequest is not null && branchDetail.PullRequest.State == GitHubPullRequestStates.Merged) + else if (branch.PullRequest is not null && branch.PullRequest.State == GitHubPullRequestStates.Merged) { branchNameBuilder.Append(" (pull request merged)".Muted()); } - if (branchDetail.Status.Tip is not null) + if (branch.Tip is not null) { - branchNameBuilder.Append($" {branchDetail.Status.Tip.Sha[..7]} {Markup.Escape(branchDetail.Status.Tip.Message)}"); + branchNameBuilder.Append($" {branch.Tip.Sha[..7]} {Markup.Escape(branch.Tip.Message)}"); } return branchNameBuilder.ToString(); } public static void OutputBranchAndStackActions( - Config.Stack stack, - StackStatus status, + StackStatus stack, ILogger logger) { - var statusOfBranchesInStack = status.Branches - .Where(b => stack.Branches.Contains(b.Key)) - .Select(b => b.Value).ToList(); - - if (statusOfBranchesInStack.All(branch => branch.CouldBeCleanedUp)) + if (stack.Branches.All(branch => branch.CouldBeCleanedUp)) { logger.Information("All branches exist locally but are either not in the remote repository or the pull request associated with the branch is no longer open. This stack might be able to be deleted."); logger.NewLine(); logger.Information($"Run {$"stack delete --stack \"{stack.Name}\"".Example()} to delete the stack if it's no longer needed."); logger.NewLine(); } - else if (statusOfBranchesInStack.Any(branch => branch.CouldBeCleanedUp)) + else if (stack.Branches.Any(branch => branch.CouldBeCleanedUp)) { logger.Information("Some branches exist locally but are either not in the remote repository or the pull request associated with the branch is no longer open."); logger.NewLine(); logger.Information($"Run {$"stack cleanup --stack \"{stack.Name}\"".Example()} to clean up local branches."); logger.NewLine(); } - else if (statusOfBranchesInStack.All(branch => !branch.Status.ExistsLocally)) + else if (stack.Branches.All(branch => !branch.Exists)) { logger.Information("No branches exist locally. This stack might be able to be deleted."); logger.NewLine(); @@ -305,9 +326,7 @@ public static void OutputBranchAndStackActions( logger.NewLine(); } - if (statusOfBranchesInStack.Any(branch => - branch.Status.ExistsLocally && - (!branch.Status.HasRemoteTrackingBranch || branch.Status.ExistsInRemote && branch.Status.AheadOfRemote > 0))) + if (stack.Branches.Any(branch => branch.Exists && (branch.RemoteTrackingBranch is null || branch.RemoteTrackingBranch.Ahead > 0))) { logger.Information("There are changes in local branches that have not been pushed to the remote repository."); logger.NewLine(); @@ -315,7 +334,7 @@ public static void OutputBranchAndStackActions( logger.NewLine(); } - if (statusOfBranchesInStack.Any(branch => branch.Status.ExistsInRemote && branch.Status.ExistsLocally && branch.Status.BehindParent > 0)) + if (stack.Branches.Any(branch => branch.Exists && branch.RemoteTrackingBranch is not null && branch.RemoteTrackingBranch.Behind > 0)) { logger.Information("There are changes in source branches that have not been applied to the stack."); logger.NewLine(); @@ -388,14 +407,12 @@ public static void UpdateStackUsingMerge( { var sourceBranch = stack.SourceBranch; - foreach (var branch in stack.Branches) + foreach (var branch in status.Branches) { - var branchDetail = status.Branches[branch]; - - if (branchDetail.IsActive) + if (branch.IsActive) { - MergeFromSourceBranch(branch, sourceBranch, gitClient, inputProvider, logger); - sourceBranch = branch; + MergeFromSourceBranch(branch.Name, sourceBranch, gitClient, inputProvider, logger); + sourceBranch = branch.Name; } else { @@ -453,9 +470,9 @@ public static void PushChanges( public static string[] GetBranchesNeedingCleanup(Config.Stack stack, ILogger logger, IGitClient gitClient, IGitHubClient gitHubClient) { var currentBranch = gitClient.GetCurrentBranch(); - var stackStatus = GetStackStatus(stack, currentBranch, logger, gitClient, gitHubClient, true); + var stackStatus = GetStackStatusNew(stack, currentBranch, logger, gitClient, gitHubClient, true); - return [.. stackStatus.Branches.Where(b => b.Value.CouldBeCleanedUp).Select(b => b.Key)]; + return [.. stackStatus.Branches.Where(b => b.CouldBeCleanedUp).Select(b => b.Name)]; } public static void OutputBranchesNeedingCleanup(ILogger logger, string[] branches) @@ -591,13 +608,11 @@ public static void UpdateStackUsingRebase( string? branchToRebaseFrom = null; string? lowestInactiveBranchToReParentFrom = null; - foreach (var branch in stack.Branches) + foreach (var branch in status.Branches) { - var branchDetail = status.Branches[branch]; - - if (branchDetail.IsActive) + if (branch.IsActive) { - branchToRebaseFrom = branch; + branchToRebaseFrom = branch.Name; } } @@ -612,14 +627,16 @@ public static void UpdateStackUsingRebase( branchesToRebaseOnto.Remove(branchToRebaseFrom); branchesToRebaseOnto.Add(stack.SourceBranch); + List allBranchesInStack = [status.SourceBranch, .. status.Branches]; + foreach (var branchToRebaseOnto in branchesToRebaseOnto) { - var branchDetail = status.Branches[branchToRebaseOnto]; + var branchDetail = allBranchesInStack.First(b => b.Name == branchToRebaseOnto); if (branchDetail.IsActive) { - var lowestInactiveBranchToReParentFromDetail = lowestInactiveBranchToReParentFrom is not null ? status.Branches[lowestInactiveBranchToReParentFrom] : null; - var shouldRebaseOntoParent = lowestInactiveBranchToReParentFromDetail is not null && lowestInactiveBranchToReParentFromDetail.Status.ExistsLocally; + var lowestInactiveBranchToReParentFromDetail = lowestInactiveBranchToReParentFrom is not null ? allBranchesInStack.First(b => b.Name == lowestInactiveBranchToReParentFrom) : null; + var shouldRebaseOntoParent = lowestInactiveBranchToReParentFromDetail is not null && lowestInactiveBranchToReParentFromDetail.Exists; if (shouldRebaseOntoParent) { diff --git a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs index 2c71b2ac..23f4fa3a 100644 --- a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs +++ b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs @@ -69,7 +69,7 @@ public override async Task Handle(CreatePullRequestsCommandInputs inputs) throw new InvalidOperationException($"Stack '{inputs.Stack}' not found."); } - var status = StackHelpers.GetStackStatus( + var status = StackHelpers.GetStackStatusNew( stack, currentBranch, logger, @@ -79,22 +79,20 @@ public override async Task Handle(CreatePullRequestsCommandInputs inputs) var sourceBranch = stack.SourceBranch; var pullRequestCreateActions = new List(); - foreach (var branch in stack.Branches) + foreach (var branch in status.Branches) { - var branchDetail = status.Branches[branch]; - - if (branchDetail.IsActive) + if (branch.IsActive) { - if (!branchDetail.HasPullRequest) + if (!branch.HasPullRequest) { - pullRequestCreateActions.Add(new PullRequestCreateAction(branch, sourceBranch)); + pullRequestCreateActions.Add(new PullRequestCreateAction(branch.Name, sourceBranch)); } - sourceBranch = branch; + sourceBranch = branch.Name; } } - StackHelpers.OutputStackStatus(stack, status, logger); + StackHelpers.OutputStackStatus(status, logger); logger.NewLine(); @@ -134,7 +132,7 @@ public override async Task Handle(CreatePullRequestsCommandInputs inputs) { var newPullRequests = CreatePullRequests(logger, gitHubClient, status, pullRequestInformation); - var pullRequestsInStack = status.Branches.Values + var pullRequestsInStack = status.Branches .Where(branch => branch.HasPullRequest) .Select(branch => branch.PullRequest!) .ToList(); @@ -178,7 +176,7 @@ private static List CreatePullRequests( var pullRequests = new List(); foreach (var action in pullRequestCreateActions) { - var branchDetail = status.Branches[action.HeadBranch]; + var branchDetail = status.Branches.First(b => b.Name == action.HeadBranch); logger.Information($"Creating pull request for branch {action.HeadBranch.Branch()} to {action.BaseBranch.Branch()}"); var pullRequest = gitHubClient.CreatePullRequest( action.HeadBranch, @@ -191,7 +189,7 @@ private static List CreatePullRequests( { logger.Information($"Pull request {pullRequest.GetPullRequestDisplay()} created for branch {action.HeadBranch.Branch()} to {action.BaseBranch.Branch()}"); pullRequests.Add(pullRequest); - branchDetail.PullRequest = pullRequest; + branchDetail = branchDetail with { PullRequest = pullRequest }; } } @@ -201,28 +199,25 @@ private static List CreatePullRequests( private static void OutputUpdatedStackStatus(ILogger logger, Config.Stack stack, StackStatus status, List pullRequestCreateActions) { var branchDisplayItems = new List(); - var parentBranch = stack.SourceBranch; - foreach (var branch in stack.Branches) + foreach (var branch in status.Branches) { - var branchDetail = status.Branches[branch]; - if (branchDetail.PullRequest is not null && branchDetail.PullRequest.State != GitHubPullRequestStates.Closed) + if (branch.PullRequest is not null && branch.PullRequest.State != GitHubPullRequestStates.Closed) { - branchDisplayItems.Add(StackHelpers.GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail)); + branchDisplayItems.Add(StackHelpers.GetBranchAndPullRequestStatusOutput(branch)); } else { - var action = pullRequestCreateActions.FirstOrDefault(a => a.HeadBranch == branch); + var action = pullRequestCreateActions.FirstOrDefault(a => a.HeadBranch == branch.Name); if (action is not null) { - branchDisplayItems.Add($"{StackHelpers.GetBranchStatusOutput(branch, parentBranch, branchDetail)} {$"*NEW* {action.Title}".Highlighted()}{(action.Draft == true ? " (draft)".Muted() : string.Empty)}"); + branchDisplayItems.Add($"{StackHelpers.GetBranchStatusOutput(branch)} {$"*NEW* {action.Title}".Highlighted()}{(action.Draft == true ? " (draft)".Muted() : string.Empty)}"); } else { - branchDisplayItems.Add(StackHelpers.GetBranchStatusOutput(branch, parentBranch, branchDetail)); + branchDisplayItems.Add(StackHelpers.GetBranchStatusOutput(branch)); } } - parentBranch = branch; } logger.Tree( diff --git a/src/Stack/Commands/PullRequests/SetPullRequestDescriptionCommand.cs b/src/Stack/Commands/PullRequests/SetPullRequestDescriptionCommand.cs index fe55ea8e..2b28336c 100644 --- a/src/Stack/Commands/PullRequests/SetPullRequestDescriptionCommand.cs +++ b/src/Stack/Commands/PullRequests/SetPullRequestDescriptionCommand.cs @@ -66,7 +66,7 @@ public override async Task Handle(SetPullRequestDescriptionCommandInputs inputs) throw new InvalidOperationException($"Stack '{inputs.Stack}' not found."); } - var status = StackHelpers.GetStackStatus( + var status = StackHelpers.GetStackStatusNew( stack, currentBranch, logger, @@ -76,12 +76,11 @@ public override async Task Handle(SetPullRequestDescriptionCommandInputs inputs) var pullRequestsInStack = new List(); - foreach (var branch in stack.Branches) + foreach (var branch in status.Branches) { - var branchDetail = status.Branches[branch]; - if (branchDetail.PullRequest is not null) + if (branch.PullRequest is not null) { - pullRequestsInStack.Add(branchDetail.PullRequest); + pullRequestsInStack.Add(branch.PullRequest); } } diff --git a/src/Stack/Commands/Remote/SyncStackCommand.cs b/src/Stack/Commands/Remote/SyncStackCommand.cs index 8310b7af..f1dae396 100644 --- a/src/Stack/Commands/Remote/SyncStackCommand.cs +++ b/src/Stack/Commands/Remote/SyncStackCommand.cs @@ -97,7 +97,7 @@ public override async Task Handle(SyncStackCommandInputs inputs) FetchChanges(); - var status = StackHelpers.GetStackStatus( + var status = StackHelpers.GetStackStatusNew( stack, currentBranch, logger, @@ -105,7 +105,7 @@ public override async Task Handle(SyncStackCommandInputs inputs) gitHubClient, true); - StackHelpers.OutputStackStatus(stack, status, logger); + StackHelpers.OutputStackStatus(status, logger); logger.NewLine(); diff --git a/src/Stack/Commands/Stack/StackStatusCommand.cs b/src/Stack/Commands/Stack/StackStatusCommand.cs index 4d81cbb4..28426ff5 100644 --- a/src/Stack/Commands/Stack/StackStatusCommand.cs +++ b/src/Stack/Commands/Stack/StackStatusCommand.cs @@ -41,99 +41,24 @@ protected override async Task Execute(StackStatusCom protected override void WriteDefaultOutput(StackStatusCommandResponse response) { - StackHelpers.OutputStackStatus(response.Statuses, StdOutLogger); + StackHelpers.OutputStackStatus(response.Stacks, StdOutLogger); - if (response.Statuses.Count == 1) + if (response.Stacks.Count == 1) { - var (stack, status) = response.Statuses.First(); - StackHelpers.OutputBranchAndStackActions(stack, status, StdOutLogger); + var stack = response.Stacks.First(); + StackHelpers.OutputBranchAndStackActions(stack, StdOutLogger); } } protected override void WriteJsonOutput(StackStatusCommandResponse response, JsonSerializerOptions options) { - var stackDetails = new List(); - - foreach (var (stack, status) in response.Statuses) - { - status.Branches.TryGetValue(stack.SourceBranch, out var sourceBranchStatus); - - if (sourceBranchStatus is not null) - { - var sourceBranch = new Branch( - stack.SourceBranch, - sourceBranchStatus.Status.ExistsLocally, - sourceBranchStatus.Status.Tip, - sourceBranchStatus.Status.HasRemoteTrackingBranch ? - new RemoteTrackingBranchStatus( - $"origin/{stack.SourceBranch}", - sourceBranchStatus.Status.ExistsInRemote, - sourceBranchStatus.Status.AheadOfRemote, - sourceBranchStatus.Status.BehindRemote) : null); - - var branches = new List(); - var parentBranch = sourceBranch; - - foreach (var branch in stack.Branches) - { - status.Branches.TryGetValue(branch, out var branchStatus); - - if (branchStatus is not null) - { - var pullRequest = branchStatus.PullRequest; - var remoteTrackingBranch = branchStatus.Status.HasRemoteTrackingBranch ? - new RemoteTrackingBranchStatus( - $"origin/{branch}", - branchStatus.Status.ExistsInRemote, - branchStatus.Status.AheadOfRemote, - branchStatus.Status.BehindRemote) : null; - - var parentBranchStatus = new ParentBranchStatus(parentBranch, branchStatus.Status.AheadOfParent, branchStatus.Status.BehindParent); - - var branchDetail = new BranchDetail( - branch, - branchStatus.Status.ExistsLocally, - branchStatus.Status.Tip, - remoteTrackingBranch, - pullRequest, - parentBranchStatus); - - branches.Add(branchDetail); - - if (branchStatus.IsActive) - { - parentBranch = branchDetail; - } - } - } - - stackDetails.Add(new StackDetail(stack.Name, sourceBranch, [.. branches])); - } - } - - var json = JsonSerializer.Serialize(stackDetails, options); + var json = JsonSerializer.Serialize(response.Stacks, options); StdOut.WriteLine(json); } } -record StackDetail(string Name, Branch SourceBranch, BranchDetail[] Branches); - -record RemoteTrackingBranchStatus(string Name, bool Exists, int Ahead, int Behind); - -record Branch(string Name, bool Exists, Commit? Tip, RemoteTrackingBranchStatus? RemoteTrackingBranch); - -record BranchDetail( - string Name, - bool Exists, - Commit? Tip, - RemoteTrackingBranchStatus? RemoteTrackingBranch, - GitHubPullRequest? PullRequest, - ParentBranchStatus? Parent) : Branch(Name, Exists, Tip, RemoteTrackingBranch); - -record ParentBranchStatus(Branch Branch, int Ahead, int Behind); - public record StackStatusCommandInputs(string? Stack, bool All, bool Full); -public record StackStatusCommandResponse(Dictionary Statuses); +public record StackStatusCommandResponse(List Stacks); public class StackStatusCommandHandler( IInputProvider inputProvider, @@ -170,7 +95,7 @@ public override async Task Handle(StackStatusCommand stacksToCheckStatusFor.Add(stack); } - var stackStatusResults = StackHelpers.GetStackStatus( + var stackStatusResults = StackHelpers.GetStackStatusNew( stacksToCheckStatusFor, currentBranch, logger, diff --git a/src/Stack/Commands/Stack/UpdateStackCommand.cs b/src/Stack/Commands/Stack/UpdateStackCommand.cs index 2884a356..1ca290fb 100644 --- a/src/Stack/Commands/Stack/UpdateStackCommand.cs +++ b/src/Stack/Commands/Stack/UpdateStackCommand.cs @@ -78,7 +78,7 @@ public override async Task Handle(UpdateStackCommandInputs inputs) if (stack is null) throw new InvalidOperationException($"Stack '{inputs.Stack}' not found."); - var status = StackHelpers.GetStackStatus( + var status = StackHelpers.GetStackStatusNew( stack, currentBranch, logger, From f820f6ef0a56dc455ba09cb5e92589ccafb31d6d Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 26 May 2025 18:48:38 +1000 Subject: [PATCH 02/10] Fix update tests --- .../Commands/Stack/UpdateStackCommandHandlerTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs index 7b366b7f..82454bfd 100644 --- a/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs @@ -45,7 +45,7 @@ public async Task WhenMultipleBranchesExistInAStack_UpdatesAndMergesEachBranchIn inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); // Act - await handler.Handle(new UpdateStackCommandInputs(null, false, false)); + await handler.Handle(new UpdateStackCommandInputs(null, false, true)); // Assert repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch); @@ -160,7 +160,7 @@ public async Task WhenNameIsProvided_DoesNotAskForName_UpdatesCorrectStack() stackConfig.Load().Returns(stacks); // Act - await handler.Handle(new UpdateStackCommandInputs("Stack1", false, false)); + await handler.Handle(new UpdateStackCommandInputs("Stack1", false, true)); // Assert repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch); @@ -242,7 +242,7 @@ public async Task WhenOnASpecificBranchInTheStack_TheSameBranchIsSetAsCurrentAft inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); // Act - await handler.Handle(new UpdateStackCommandInputs(null, false, false)); + await handler.Handle(new UpdateStackCommandInputs(null, false, true)); // Assert repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch); @@ -281,7 +281,7 @@ public async Task WhenOnlyASingleStackExists_DoesNotAskForStackName_UpdatesStack stackConfig.Load().Returns(stacks); // Act - await handler.Handle(new UpdateStackCommandInputs(null, false, false)); + await handler.Handle(new UpdateStackCommandInputs(null, false, true)); // Assert repo.GetCommitsReachableFromBranch(branch1).Should().Contain(tipOfSourceBranch); From c2980d2a73670dd5f7c634cda246243ca5f8aa71 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 26 May 2025 18:55:45 +1000 Subject: [PATCH 03/10] Fix PR tests --- .../Commands/PullRequests/CreatePullRequestsCommand.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs index 23f4fa3a..b010789c 100644 --- a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs +++ b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs @@ -132,6 +132,14 @@ public override async Task Handle(CreatePullRequestsCommandInputs inputs) { var newPullRequests = CreatePullRequests(logger, gitHubClient, status, pullRequestInformation); + // Re-get the status to pick up PRs + status = StackHelpers.GetStackStatusNew( + stack, + currentBranch, + logger, + gitClient, + gitHubClient); + var pullRequestsInStack = status.Branches .Where(branch => branch.HasPullRequest) .Select(branch => branch.PullRequest!) @@ -189,7 +197,6 @@ private static List CreatePullRequests( { logger.Information($"Pull request {pullRequest.GetPullRequestDisplay()} created for branch {action.HeadBranch.Branch()} to {action.BaseBranch.Branch()}"); pullRequests.Add(pullRequest); - branchDetail = branchDetail with { PullRequest = pullRequest }; } } From c418b956d6fa559a3c1243ecc90ae308b7460728 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 26 May 2025 18:58:26 +1000 Subject: [PATCH 04/10] Cleanup --- src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs | 6 +++--- src/Stack/Commands/Helpers/StackHelpers.cs | 8 ++++---- .../Commands/PullRequests/CreatePullRequestsCommand.cs | 4 ++-- .../PullRequests/SetPullRequestDescriptionCommand.cs | 2 +- src/Stack/Commands/Remote/SyncStackCommand.cs | 2 +- src/Stack/Commands/Stack/StackStatusCommand.cs | 2 +- src/Stack/Commands/Stack/UpdateStackCommand.cs | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs b/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs index 77a815f4..d0c2ffc3 100644 --- a/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs +++ b/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs @@ -145,7 +145,7 @@ public void UpdateStackUsingRebase_WhenARemoteBranchIsDeleted_RebasesOntoThePare repo.DeleteRemoteTrackingBranch(branch1); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, [branch1, branch2]); - var stackStatus = StackHelpers.GetStackStatusNew(stack, branch1, logger, gitClient, gitHubClient, false); + var stackStatus = StackHelpers.GetStackStatus(stack, branch1, logger, gitClient, gitHubClient, false); // Act StackHelpers.UpdateStackUsingRebase(stack, stackStatus, gitClient, inputProvider, logger); @@ -216,7 +216,7 @@ public void UpdateStackUsingRebase_WhenARemoteBranchIsDeleted_ButTheTargetBranch repo.Push(sourceBranch); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, [branch1, branch2]); - var stackStatus = StackHelpers.GetStackStatusNew(stack, branch1, logger, gitClient, gitHubClient, false); + var stackStatus = StackHelpers.GetStackStatus(stack, branch1, logger, gitClient, gitHubClient, false); // Act: Even though the parent branch (branch1) has been deleted on the remote, // we should not explicitly re-parent the target branch (branch2) onto the source branch @@ -265,7 +265,7 @@ public void UpdateStackUsingRebase_WhenARemoteBranchIsDeleted_AndLocalBranchIsDe repo.Commit(); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, [branch1, branch2]); - var stackStatus = StackHelpers.GetStackStatusNew(stack, branch1, logger, gitClient, gitHubClient, false); + var stackStatus = StackHelpers.GetStackStatus(stack, branch1, logger, gitClient, gitHubClient, false); gitClient.Fetch(true); diff --git a/src/Stack/Commands/Helpers/StackHelpers.cs b/src/Stack/Commands/Helpers/StackHelpers.cs index cc28e309..1350c061 100644 --- a/src/Stack/Commands/Helpers/StackHelpers.cs +++ b/src/Stack/Commands/Helpers/StackHelpers.cs @@ -37,7 +37,7 @@ public record ParentBranchStatus(Branch Branch, int Ahead, int Behind); public static class StackHelpers { - public static List GetStackStatusNew( + public static List GetStackStatus( List stacks, string currentBranch, ILogger logger, @@ -127,7 +127,7 @@ branchStatus.RemoteTrackingBranchName is not null return stacksToReturnStatusFor; } - public static StackStatus GetStackStatusNew( + public static StackStatus GetStackStatus( Config.Stack stack, string currentBranch, ILogger logger, @@ -135,7 +135,7 @@ public static StackStatus GetStackStatusNew( IGitHubClient gitHubClient, bool includePullRequestStatus = true) { - var statuses = GetStackStatusNew( + var statuses = GetStackStatus( [stack], currentBranch, logger, @@ -470,7 +470,7 @@ public static void PushChanges( public static string[] GetBranchesNeedingCleanup(Config.Stack stack, ILogger logger, IGitClient gitClient, IGitHubClient gitHubClient) { var currentBranch = gitClient.GetCurrentBranch(); - var stackStatus = GetStackStatusNew(stack, currentBranch, logger, gitClient, gitHubClient, true); + var stackStatus = GetStackStatus(stack, currentBranch, logger, gitClient, gitHubClient, true); return [.. stackStatus.Branches.Where(b => b.CouldBeCleanedUp).Select(b => b.Name)]; } diff --git a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs index b010789c..68f79d9e 100644 --- a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs +++ b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs @@ -69,7 +69,7 @@ public override async Task Handle(CreatePullRequestsCommandInputs inputs) throw new InvalidOperationException($"Stack '{inputs.Stack}' not found."); } - var status = StackHelpers.GetStackStatusNew( + var status = StackHelpers.GetStackStatus( stack, currentBranch, logger, @@ -133,7 +133,7 @@ public override async Task Handle(CreatePullRequestsCommandInputs inputs) var newPullRequests = CreatePullRequests(logger, gitHubClient, status, pullRequestInformation); // Re-get the status to pick up PRs - status = StackHelpers.GetStackStatusNew( + status = StackHelpers.GetStackStatus( stack, currentBranch, logger, diff --git a/src/Stack/Commands/PullRequests/SetPullRequestDescriptionCommand.cs b/src/Stack/Commands/PullRequests/SetPullRequestDescriptionCommand.cs index 2b28336c..5bcad4c4 100644 --- a/src/Stack/Commands/PullRequests/SetPullRequestDescriptionCommand.cs +++ b/src/Stack/Commands/PullRequests/SetPullRequestDescriptionCommand.cs @@ -66,7 +66,7 @@ public override async Task Handle(SetPullRequestDescriptionCommandInputs inputs) throw new InvalidOperationException($"Stack '{inputs.Stack}' not found."); } - var status = StackHelpers.GetStackStatusNew( + var status = StackHelpers.GetStackStatus( stack, currentBranch, logger, diff --git a/src/Stack/Commands/Remote/SyncStackCommand.cs b/src/Stack/Commands/Remote/SyncStackCommand.cs index f1dae396..41e1e45c 100644 --- a/src/Stack/Commands/Remote/SyncStackCommand.cs +++ b/src/Stack/Commands/Remote/SyncStackCommand.cs @@ -97,7 +97,7 @@ public override async Task Handle(SyncStackCommandInputs inputs) FetchChanges(); - var status = StackHelpers.GetStackStatusNew( + var status = StackHelpers.GetStackStatus( stack, currentBranch, logger, diff --git a/src/Stack/Commands/Stack/StackStatusCommand.cs b/src/Stack/Commands/Stack/StackStatusCommand.cs index 28426ff5..91f6306f 100644 --- a/src/Stack/Commands/Stack/StackStatusCommand.cs +++ b/src/Stack/Commands/Stack/StackStatusCommand.cs @@ -95,7 +95,7 @@ public override async Task Handle(StackStatusCommand stacksToCheckStatusFor.Add(stack); } - var stackStatusResults = StackHelpers.GetStackStatusNew( + var stackStatusResults = StackHelpers.GetStackStatus( stacksToCheckStatusFor, currentBranch, logger, diff --git a/src/Stack/Commands/Stack/UpdateStackCommand.cs b/src/Stack/Commands/Stack/UpdateStackCommand.cs index 1ca290fb..2884a356 100644 --- a/src/Stack/Commands/Stack/UpdateStackCommand.cs +++ b/src/Stack/Commands/Stack/UpdateStackCommand.cs @@ -78,7 +78,7 @@ public override async Task Handle(UpdateStackCommandInputs inputs) if (stack is null) throw new InvalidOperationException($"Stack '{inputs.Stack}' not found."); - var status = StackHelpers.GetStackStatusNew( + var status = StackHelpers.GetStackStatus( stack, currentBranch, logger, From 728c0a9615302d4e2f27bba9c6efb17a3fa9165c Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 26 May 2025 19:02:00 +1000 Subject: [PATCH 05/10] Ignore computed props --- src/Stack/Commands/Helpers/StackHelpers.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Stack/Commands/Helpers/StackHelpers.cs b/src/Stack/Commands/Helpers/StackHelpers.cs index 1350c061..1cc7fa22 100644 --- a/src/Stack/Commands/Helpers/StackHelpers.cs +++ b/src/Stack/Commands/Helpers/StackHelpers.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.Json.Serialization; using Spectre.Console; using Stack.Config; using Stack.Git; @@ -12,8 +13,13 @@ public record RemoteTrackingBranchStatus(string Name, bool Exists, int Ahead, in public record Branch(string Name, bool Exists, Commit? Tip, RemoteTrackingBranchStatus? RemoteTrackingBranch) { + [JsonIgnore] public virtual bool IsActive => Exists && RemoteTrackingBranch?.Exists == true; + + [JsonIgnore] public int AheadOfRemote => RemoteTrackingBranch?.Ahead ?? 0; + + [JsonIgnore] public int BehindRemote => RemoteTrackingBranch?.Behind ?? 0; } @@ -25,11 +31,22 @@ public record BranchDetail( GitHubPullRequest? PullRequest, ParentBranchStatus? Parent) : Branch(Name, Exists, Tip, RemoteTrackingBranch) { + [JsonIgnore] public override bool IsActive => base.IsActive && (PullRequest is null || PullRequest.State != GitHubPullRequestStates.Merged); + + [JsonIgnore] public bool CouldBeCleanedUp => Exists && ((RemoteTrackingBranch is not null && !RemoteTrackingBranch.Exists) || (PullRequest is not null && PullRequest.State == GitHubPullRequestStates.Merged)); + + [JsonIgnore] public bool HasPullRequest => PullRequest is not null && PullRequest.State != GitHubPullRequestStates.Closed; + + [JsonIgnore] public int AheadOfParent => Parent?.Ahead ?? 0; + + [JsonIgnore] public int BehindParent => Parent?.Behind ?? 0; + + [JsonIgnore] public string ParentBranchName => Parent?.Branch.Name ?? string.Empty; } From 0c67f78f1625045afba5fb7999fa79226791bbbe Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Tue, 27 May 2025 09:17:27 +1000 Subject: [PATCH 06/10] Use specific command output types --- .../Commands/Stack/StackStatusCommand.cs | 255 +++++++++++++----- 1 file changed, 181 insertions(+), 74 deletions(-) diff --git a/src/Stack/Commands/Stack/StackStatusCommand.cs b/src/Stack/Commands/Stack/StackStatusCommand.cs index 91f6306f..ac19f862 100644 --- a/src/Stack/Commands/Stack/StackStatusCommand.cs +++ b/src/Stack/Commands/Stack/StackStatusCommand.cs @@ -2,107 +2,214 @@ using System.Text.Json; using Spectre.Console; using Spectre.Console.Cli; -using Stack.Commands; using Stack.Commands.Helpers; using Stack.Config; using Stack.Git; using Stack.Infrastructure; -namespace Stack.Commands; - -public class StackStatusCommandSettings : CommandWithOutputSettingsBase +namespace Stack.Commands { - [Description("The name of the stack to show the status of.")] - [CommandOption("-s|--stack")] - public string? Stack { get; init; } + public class StackStatusCommandSettings : CommandWithOutputSettingsBase + { + [Description("The name of the stack to show the status of.")] + [CommandOption("-s|--stack")] + public string? Stack { get; init; } - [Description("Show status of all stacks.")] - [CommandOption("--all")] - public bool All { get; init; } + [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 : CommandWithOutput -{ - protected override async Task Execute(StackStatusCommandSettings settings) - { - var handler = new StackStatusCommandHandler( - InputProvider, - StdErrLogger, - new GitClient(StdErrLogger, settings.GetGitClientSettings()), - new GitHubClient(StdErrLogger, settings.GetGitHubClientSettings()), - new StackConfig()); - - return await handler.Handle(new StackStatusCommandInputs(settings.Stack, settings.All, settings.Full)); + [Description("Show full status including pull requests.")] + [CommandOption("--full")] + public bool Full { get; init; } } - protected override void WriteDefaultOutput(StackStatusCommandResponse response) + public record StackStatusCommandJsonOutput + ( + string Name, + StackStatusCommandJsonOutputBranch SourceBranch, + List Branches + ); + + public record StackStatusCommandJsonOutputBranch + ( + string Name, + bool Exists, + StackStatusCommandJsonOutputCommit? Tip, + StackStatusCommandJsonOutputRemoteTrackingBranchStatus? RemoteTrackingBranch + ); + + public record StackStatusCommandJsonOutputBranchDetail + ( + string Name, + bool Exists, + StackStatusCommandJsonOutputCommit? Tip, + StackStatusCommandJsonOutputRemoteTrackingBranchStatus? RemoteTrackingBranch, + StackStatusCommandJsonOutputGitHubPullRequest? PullRequest, + StackStatusCommandJsonOutputParentBranchStatus? Parent + ) : StackStatusCommandJsonOutputBranch(Name, Exists, Tip, RemoteTrackingBranch); + + public record StackStatusCommandJsonOutputRemoteTrackingBranchStatus + ( + string Name, + bool Exists, + int Ahead, + int Behind + ); + + public record StackStatusCommandJsonOutputCommit + ( + string Sha, + string Message + ); + + public record StackStatusCommandJsonOutputGitHubPullRequest + ( + int Number, + string Title, + string State, + Uri Url, + bool IsDraft + ); + + public record StackStatusCommandJsonOutputParentBranchStatus + ( + StackStatusCommandJsonOutputBranch Branch, + int Ahead, + int Behind + ); + + public class StackStatusCommand : CommandWithOutput { - StackHelpers.OutputStackStatus(response.Stacks, StdOutLogger); - - if (response.Stacks.Count == 1) + protected override async Task Execute(StackStatusCommandSettings settings) { - var stack = response.Stacks.First(); - StackHelpers.OutputBranchAndStackActions(stack, StdOutLogger); + var handler = new StackStatusCommandHandler( + InputProvider, + StdErrLogger, + new GitClient(StdErrLogger, settings.GetGitClientSettings()), + new GitHubClient(StdErrLogger, settings.GetGitHubClientSettings()), + new StackConfig()); + + return await handler.Handle(new StackStatusCommandInputs(settings.Stack, settings.All, settings.Full)); } - } - protected override void WriteJsonOutput(StackStatusCommandResponse response, JsonSerializerOptions options) - { - var json = JsonSerializer.Serialize(response.Stacks, options); - StdOut.WriteLine(json); - } -} + protected override void WriteDefaultOutput(StackStatusCommandResponse response) + { + StackHelpers.OutputStackStatus(response.Stacks, StdOutLogger); -public record StackStatusCommandInputs(string? Stack, bool All, bool Full); -public record StackStatusCommandResponse(List Stacks); + if (response.Stacks.Count == 1) + { + var stack = response.Stacks.First(); + StackHelpers.OutputBranchAndStackActions(stack, StdOutLogger); + } + } -public class StackStatusCommandHandler( - IInputProvider inputProvider, - ILogger logger, - IGitClient gitClient, - IGitHubClient gitHubClient, - IStackConfig stackConfig) - : CommandHandlerBase -{ - public override async Task Handle(StackStatusCommandInputs inputs) - { - await Task.CompletedTask; - var stacks = stackConfig.Load(); + protected override void WriteJsonOutput(StackStatusCommandResponse response, JsonSerializerOptions options) + { + var output = response.Stacks.Select(MapToJsonOutput).ToList(); + var json = JsonSerializer.Serialize(output, options); + StdOut.WriteLine(json); + } - var remoteUri = gitClient.GetRemoteUri(); - var stacksForRemote = stacks.Where(s => s.RemoteUri.Equals(remoteUri, StringComparison.OrdinalIgnoreCase)).ToList(); - var currentBranch = gitClient.GetCurrentBranch(); + private static StackStatusCommandJsonOutput MapToJsonOutput(StackStatus stack) + { + return new StackStatusCommandJsonOutput( + stack.Name, + MapBranch(stack.SourceBranch), + stack.Branches.Select(MapBranchDetail).ToList() + ); + } - var stacksToCheckStatusFor = new List(); + private static StackStatusCommandJsonOutputBranch MapBranch(Branch branch) + { + return new StackStatusCommandJsonOutputBranch( + branch.Name, + branch.Exists, + branch.Tip is null ? null : new StackStatusCommandJsonOutputCommit(branch.Tip.Sha, branch.Tip.Message), + branch.RemoteTrackingBranch is null ? null : new StackStatusCommandJsonOutputRemoteTrackingBranchStatus( + branch.RemoteTrackingBranch.Name, + branch.RemoteTrackingBranch.Exists, + branch.RemoteTrackingBranch.Ahead, + branch.RemoteTrackingBranch.Behind + ) + ); + } - if (inputs.All) + private static StackStatusCommandJsonOutputBranchDetail MapBranchDetail(BranchDetail branch) { - stacksToCheckStatusFor.AddRange(stacksForRemote); + return new StackStatusCommandJsonOutputBranchDetail( + branch.Name, + branch.Exists, + branch.Tip is null ? null : new StackStatusCommandJsonOutputCommit(branch.Tip.Sha, branch.Tip.Message), + branch.RemoteTrackingBranch is null ? null : new StackStatusCommandJsonOutputRemoteTrackingBranchStatus( + branch.RemoteTrackingBranch.Name, + branch.RemoteTrackingBranch.Exists, + branch.RemoteTrackingBranch.Ahead, + branch.RemoteTrackingBranch.Behind + ), + branch.PullRequest is null ? null : new StackStatusCommandJsonOutputGitHubPullRequest( + branch.PullRequest.Number, + branch.PullRequest.Title, + branch.PullRequest.State, + branch.PullRequest.Url, + branch.PullRequest.IsDraft + ), + branch.Parent is null ? null : new StackStatusCommandJsonOutputParentBranchStatus( + MapBranch(branch.Parent.Branch), + branch.Parent.Ahead, + branch.Parent.Behind + ) + ); } - else + } + + public record StackStatusCommandInputs(string? Stack, bool All, bool Full); + public record StackStatusCommandResponse(List Stacks); + + public class StackStatusCommandHandler( + IInputProvider inputProvider, + ILogger logger, + IGitClient gitClient, + IGitHubClient gitHubClient, + IStackConfig stackConfig) + : CommandHandlerBase + { + public override async Task Handle(StackStatusCommandInputs inputs) { - var stack = inputProvider.SelectStack(logger, inputs.Stack, stacksForRemote, currentBranch); + await Task.CompletedTask; + var stacks = stackConfig.Load(); + + var remoteUri = gitClient.GetRemoteUri(); + var stacksForRemote = stacks.Where(s => s.RemoteUri.Equals(remoteUri, StringComparison.OrdinalIgnoreCase)).ToList(); + var currentBranch = gitClient.GetCurrentBranch(); - if (stack is null) + var stacksToCheckStatusFor = new List(); + + if (inputs.All) { - throw new InvalidOperationException($"Stack '{inputs.Stack}' not found."); + stacksToCheckStatusFor.AddRange(stacksForRemote); } + else + { + var stack = inputProvider.SelectStack(logger, inputs.Stack, stacksForRemote, currentBranch); - stacksToCheckStatusFor.Add(stack); - } + if (stack is null) + { + throw new InvalidOperationException($"Stack '{inputs.Stack}' not found."); + } + + stacksToCheckStatusFor.Add(stack); + } - var stackStatusResults = StackHelpers.GetStackStatus( - stacksToCheckStatusFor, - currentBranch, - logger, - gitClient, - gitHubClient, - inputs.Full); + var stackStatusResults = StackHelpers.GetStackStatus( + stacksToCheckStatusFor, + currentBranch, + logger, + gitClient, + gitHubClient, + inputs.Full); - return new StackStatusCommandResponse(stackStatusResults); + return new StackStatusCommandResponse(stackStatusResults); + } } } From 750164a2bd59adaf9c0295c381d16a7de2de6897 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Tue, 27 May 2025 09:21:31 +1000 Subject: [PATCH 07/10] Cleanup --- src/Stack/Commands/Helpers/StackHelpers.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/Stack/Commands/Helpers/StackHelpers.cs b/src/Stack/Commands/Helpers/StackHelpers.cs index 1cc7fa22..1350c061 100644 --- a/src/Stack/Commands/Helpers/StackHelpers.cs +++ b/src/Stack/Commands/Helpers/StackHelpers.cs @@ -1,5 +1,4 @@ using System.Text; -using System.Text.Json.Serialization; using Spectre.Console; using Stack.Config; using Stack.Git; @@ -13,13 +12,8 @@ public record RemoteTrackingBranchStatus(string Name, bool Exists, int Ahead, in public record Branch(string Name, bool Exists, Commit? Tip, RemoteTrackingBranchStatus? RemoteTrackingBranch) { - [JsonIgnore] public virtual bool IsActive => Exists && RemoteTrackingBranch?.Exists == true; - - [JsonIgnore] public int AheadOfRemote => RemoteTrackingBranch?.Ahead ?? 0; - - [JsonIgnore] public int BehindRemote => RemoteTrackingBranch?.Behind ?? 0; } @@ -31,22 +25,11 @@ public record BranchDetail( GitHubPullRequest? PullRequest, ParentBranchStatus? Parent) : Branch(Name, Exists, Tip, RemoteTrackingBranch) { - [JsonIgnore] public override bool IsActive => base.IsActive && (PullRequest is null || PullRequest.State != GitHubPullRequestStates.Merged); - - [JsonIgnore] public bool CouldBeCleanedUp => Exists && ((RemoteTrackingBranch is not null && !RemoteTrackingBranch.Exists) || (PullRequest is not null && PullRequest.State == GitHubPullRequestStates.Merged)); - - [JsonIgnore] public bool HasPullRequest => PullRequest is not null && PullRequest.State != GitHubPullRequestStates.Closed; - - [JsonIgnore] public int AheadOfParent => Parent?.Ahead ?? 0; - - [JsonIgnore] public int BehindParent => Parent?.Behind ?? 0; - - [JsonIgnore] public string ParentBranchName => Parent?.Branch.Name ?? string.Empty; } From 626bef55538dfbd88a712a36cca54ffa67d2183b Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Tue, 27 May 2025 09:24:21 +1000 Subject: [PATCH 08/10] Re-add in status logging --- src/Stack/Commands/Helpers/StackHelpers.cs | 122 +++++++++++---------- 1 file changed, 67 insertions(+), 55 deletions(-) diff --git a/src/Stack/Commands/Helpers/StackHelpers.cs b/src/Stack/Commands/Helpers/StackHelpers.cs index 1350c061..0a7c3c58 100644 --- a/src/Stack/Commands/Helpers/StackHelpers.cs +++ b/src/Stack/Commands/Helpers/StackHelpers.cs @@ -57,74 +57,86 @@ public static List GetStackStatus( var branchStatuses = gitClient.GetBranchStatuses(allBranchesInStacks); - foreach (var stack in stacksOrderedByCurrentBranch) + if (includePullRequestStatus) { - if (!branchStatuses.TryGetValue(stack.SourceBranch, out var sourceBranchStatus)) - { - logger.Warning($"Source branch '{stack.SourceBranch}' does not exist locally or in the remote repository."); - continue; - } + logger.Status("Checking status of GitHub pull requests...", () => EvaluateBranchStatusDetails(logger, gitClient, gitHubClient, includePullRequestStatus, stacksToReturnStatusFor, stacksOrderedByCurrentBranch, branchStatuses)); + } + else + { + EvaluateBranchStatusDetails(logger, gitClient, gitHubClient, includePullRequestStatus, stacksToReturnStatusFor, stacksOrderedByCurrentBranch, branchStatuses); + } + + return stacksToReturnStatusFor; - var sourceBranch = new Branch( - stack.SourceBranch, - true, - sourceBranchStatus.Tip, - sourceBranchStatus.RemoteTrackingBranchName is not null - ? new RemoteTrackingBranchStatus( - sourceBranchStatus.RemoteTrackingBranchName, - sourceBranchStatus.RemoteBranchExists, - sourceBranchStatus.Ahead, - sourceBranchStatus.Behind) - : null); - var parentBranch = sourceBranch; - var stackBranches = new List(); - - foreach (var branchName in stack.Branches) + static void EvaluateBranchStatusDetails(ILogger logger, IGitClient gitClient, IGitHubClient gitHubClient, bool includePullRequestStatus, List stacksToReturnStatusFor, IOrderedEnumerable stacksOrderedByCurrentBranch, Dictionary branchStatuses) + { + foreach (var stack in stacksOrderedByCurrentBranch) { - branchStatuses.TryGetValue(branchName, out var branchStatus); + if (!branchStatuses.TryGetValue(stack.SourceBranch, out var sourceBranchStatus)) + { + logger.Warning($"Source branch '{stack.SourceBranch}' does not exist locally or in the remote repository."); + continue; + } - if (branchStatus is not null) + var sourceBranch = new Branch( + stack.SourceBranch, + true, + sourceBranchStatus.Tip, + sourceBranchStatus.RemoteTrackingBranchName is not null + ? new RemoteTrackingBranchStatus( + sourceBranchStatus.RemoteTrackingBranchName, + sourceBranchStatus.RemoteBranchExists, + sourceBranchStatus.Ahead, + sourceBranchStatus.Behind) + : null); + var parentBranch = sourceBranch; + var stackBranches = new List(); + + foreach (var branchName in stack.Branches) { - var (aheadOfParent, behindParent) = branchStatus.RemoteBranchExists ? gitClient.CompareBranches(branchName, parentBranch.Name) : (0, 0); - GitHubPullRequest? pullRequest = null; + branchStatuses.TryGetValue(branchName, out var branchStatus); - if (includePullRequestStatus) + if (branchStatus is not null) { - pullRequest = gitHubClient.GetPullRequest(branchName); + var (aheadOfParent, behindParent) = branchStatus.RemoteBranchExists ? gitClient.CompareBranches(branchName, parentBranch.Name) : (0, 0); + GitHubPullRequest? pullRequest = null; + + if (includePullRequestStatus) + { + pullRequest = gitHubClient.GetPullRequest(branchName); + } + + var branch = new BranchDetail( + branchName, + true, + branchStatus.Tip, + branchStatus.RemoteTrackingBranchName is not null + ? new RemoteTrackingBranchStatus( + branchStatus.RemoteTrackingBranchName, + branchStatus.RemoteBranchExists, + branchStatus.Ahead, + branchStatus.Behind) + : null, + pullRequest, + new ParentBranchStatus(parentBranch, aheadOfParent, behindParent)); + + stackBranches.Add(branch); + + if (branchStatus.RemoteBranchExists) + { + parentBranch = branch; + } } - - var branch = new BranchDetail( - branchName, - true, - branchStatus.Tip, - branchStatus.RemoteTrackingBranchName is not null - ? new RemoteTrackingBranchStatus( - branchStatus.RemoteTrackingBranchName, - branchStatus.RemoteBranchExists, - branchStatus.Ahead, - branchStatus.Behind) - : null, - pullRequest, - new ParentBranchStatus(parentBranch, aheadOfParent, behindParent)); - - stackBranches.Add(branch); - - if (branchStatus.RemoteBranchExists) + else { - parentBranch = branch; + var branch = new BranchDetail(branchName, false, null, null, null, null); + stackBranches.Add(branch); } } - else - { - var branch = new BranchDetail(branchName, false, null, null, null, null); - stackBranches.Add(branch); - } - } - stacksToReturnStatusFor.Add(new StackStatus(stack.Name, sourceBranch, [.. stackBranches])); + stacksToReturnStatusFor.Add(new StackStatus(stack.Name, sourceBranch, [.. stackBranches])); + } } - - return stacksToReturnStatusFor; } public static StackStatus GetStackStatus( From 6535dc48c3e9ba07a2c2b48792ec1423093e0696 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Tue, 27 May 2025 09:27:14 +1000 Subject: [PATCH 09/10] More cleanup --- .../Commands/Stack/StackStatusCommand.cs | 359 +++++++++--------- 1 file changed, 179 insertions(+), 180 deletions(-) diff --git a/src/Stack/Commands/Stack/StackStatusCommand.cs b/src/Stack/Commands/Stack/StackStatusCommand.cs index ac19f862..4899d01a 100644 --- a/src/Stack/Commands/Stack/StackStatusCommand.cs +++ b/src/Stack/Commands/Stack/StackStatusCommand.cs @@ -7,209 +7,208 @@ using Stack.Git; using Stack.Infrastructure; -namespace Stack.Commands +namespace Stack.Commands; + +public class StackStatusCommandSettings : CommandWithOutputSettingsBase { - public class StackStatusCommandSettings : CommandWithOutputSettingsBase - { - [Description("The name of the stack to show the status of.")] - [CommandOption("-s|--stack")] - public string? Stack { get; init; } + [Description("The name of the stack to show the status of.")] + [CommandOption("-s|--stack")] + public string? Stack { get; init; } - [Description("Show status of all stacks.")] - [CommandOption("--all")] - public bool All { get; init; } + [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; } + [Description("Show full status including pull requests.")] + [CommandOption("--full")] + public bool Full { get; init; } +} + +public record StackStatusCommandJsonOutput +( + string Name, + StackStatusCommandJsonOutputBranch SourceBranch, + List Branches +); + +public record StackStatusCommandJsonOutputBranch +( + string Name, + bool Exists, + StackStatusCommandJsonOutputCommit? Tip, + StackStatusCommandJsonOutputRemoteTrackingBranchStatus? RemoteTrackingBranch +); + +public record StackStatusCommandJsonOutputBranchDetail +( + string Name, + bool Exists, + StackStatusCommandJsonOutputCommit? Tip, + StackStatusCommandJsonOutputRemoteTrackingBranchStatus? RemoteTrackingBranch, + StackStatusCommandJsonOutputGitHubPullRequest? PullRequest, + StackStatusCommandJsonOutputParentBranchStatus? Parent +) : StackStatusCommandJsonOutputBranch(Name, Exists, Tip, RemoteTrackingBranch); + +public record StackStatusCommandJsonOutputRemoteTrackingBranchStatus +( + string Name, + bool Exists, + int Ahead, + int Behind +); + +public record StackStatusCommandJsonOutputCommit +( + string Sha, + string Message +); + +public record StackStatusCommandJsonOutputGitHubPullRequest +( + int Number, + string Title, + string State, + Uri Url, + bool IsDraft +); + +public record StackStatusCommandJsonOutputParentBranchStatus +( + StackStatusCommandJsonOutputBranch Branch, + int Ahead, + int Behind +); + +public class StackStatusCommand : CommandWithOutput +{ + protected override async Task Execute(StackStatusCommandSettings settings) + { + var handler = new StackStatusCommandHandler( + InputProvider, + StdErrLogger, + new GitClient(StdErrLogger, settings.GetGitClientSettings()), + new GitHubClient(StdErrLogger, settings.GetGitHubClientSettings()), + new StackConfig()); + + return await handler.Handle(new StackStatusCommandInputs(settings.Stack, settings.All, settings.Full)); } - public record StackStatusCommandJsonOutput - ( - string Name, - StackStatusCommandJsonOutputBranch SourceBranch, - List Branches - ); - - public record StackStatusCommandJsonOutputBranch - ( - string Name, - bool Exists, - StackStatusCommandJsonOutputCommit? Tip, - StackStatusCommandJsonOutputRemoteTrackingBranchStatus? RemoteTrackingBranch - ); - - public record StackStatusCommandJsonOutputBranchDetail - ( - string Name, - bool Exists, - StackStatusCommandJsonOutputCommit? Tip, - StackStatusCommandJsonOutputRemoteTrackingBranchStatus? RemoteTrackingBranch, - StackStatusCommandJsonOutputGitHubPullRequest? PullRequest, - StackStatusCommandJsonOutputParentBranchStatus? Parent - ) : StackStatusCommandJsonOutputBranch(Name, Exists, Tip, RemoteTrackingBranch); - - public record StackStatusCommandJsonOutputRemoteTrackingBranchStatus - ( - string Name, - bool Exists, - int Ahead, - int Behind - ); - - public record StackStatusCommandJsonOutputCommit - ( - string Sha, - string Message - ); - - public record StackStatusCommandJsonOutputGitHubPullRequest - ( - int Number, - string Title, - string State, - Uri Url, - bool IsDraft - ); - - public record StackStatusCommandJsonOutputParentBranchStatus - ( - StackStatusCommandJsonOutputBranch Branch, - int Ahead, - int Behind - ); - - public class StackStatusCommand : CommandWithOutput + protected override void WriteDefaultOutput(StackStatusCommandResponse response) { - protected override async Task Execute(StackStatusCommandSettings settings) - { - var handler = new StackStatusCommandHandler( - InputProvider, - StdErrLogger, - new GitClient(StdErrLogger, settings.GetGitClientSettings()), - new GitHubClient(StdErrLogger, settings.GetGitHubClientSettings()), - new StackConfig()); - - return await handler.Handle(new StackStatusCommandInputs(settings.Stack, settings.All, settings.Full)); - } + StackHelpers.OutputStackStatus(response.Stacks, StdOutLogger); - protected override void WriteDefaultOutput(StackStatusCommandResponse response) + if (response.Stacks.Count == 1) { - StackHelpers.OutputStackStatus(response.Stacks, StdOutLogger); - - if (response.Stacks.Count == 1) - { - var stack = response.Stacks.First(); - StackHelpers.OutputBranchAndStackActions(stack, StdOutLogger); - } + var stack = response.Stacks.First(); + StackHelpers.OutputBranchAndStackActions(stack, StdOutLogger); } + } - protected override void WriteJsonOutput(StackStatusCommandResponse response, JsonSerializerOptions options) - { - var output = response.Stacks.Select(MapToJsonOutput).ToList(); - var json = JsonSerializer.Serialize(output, options); - StdOut.WriteLine(json); - } + protected override void WriteJsonOutput(StackStatusCommandResponse response, JsonSerializerOptions options) + { + var output = response.Stacks.Select(MapToJsonOutput).ToList(); + var json = JsonSerializer.Serialize(output, options); + StdOut.WriteLine(json); + } - private static StackStatusCommandJsonOutput MapToJsonOutput(StackStatus stack) - { - return new StackStatusCommandJsonOutput( - stack.Name, - MapBranch(stack.SourceBranch), - stack.Branches.Select(MapBranchDetail).ToList() - ); - } + private static StackStatusCommandJsonOutput MapToJsonOutput(StackStatus stack) + { + return new StackStatusCommandJsonOutput( + stack.Name, + MapBranch(stack.SourceBranch), + stack.Branches.Select(MapBranchDetail).ToList() + ); + } - private static StackStatusCommandJsonOutputBranch MapBranch(Branch branch) - { - return new StackStatusCommandJsonOutputBranch( - branch.Name, - branch.Exists, - branch.Tip is null ? null : new StackStatusCommandJsonOutputCommit(branch.Tip.Sha, branch.Tip.Message), - branch.RemoteTrackingBranch is null ? null : new StackStatusCommandJsonOutputRemoteTrackingBranchStatus( - branch.RemoteTrackingBranch.Name, - branch.RemoteTrackingBranch.Exists, - branch.RemoteTrackingBranch.Ahead, - branch.RemoteTrackingBranch.Behind - ) - ); - } + private static StackStatusCommandJsonOutputBranch MapBranch(Branch branch) + { + return new StackStatusCommandJsonOutputBranch( + branch.Name, + branch.Exists, + branch.Tip is null ? null : new StackStatusCommandJsonOutputCommit(branch.Tip.Sha, branch.Tip.Message), + branch.RemoteTrackingBranch is null ? null : new StackStatusCommandJsonOutputRemoteTrackingBranchStatus( + branch.RemoteTrackingBranch.Name, + branch.RemoteTrackingBranch.Exists, + branch.RemoteTrackingBranch.Ahead, + branch.RemoteTrackingBranch.Behind + ) + ); + } - private static StackStatusCommandJsonOutputBranchDetail MapBranchDetail(BranchDetail branch) - { - return new StackStatusCommandJsonOutputBranchDetail( - branch.Name, - branch.Exists, - branch.Tip is null ? null : new StackStatusCommandJsonOutputCommit(branch.Tip.Sha, branch.Tip.Message), - branch.RemoteTrackingBranch is null ? null : new StackStatusCommandJsonOutputRemoteTrackingBranchStatus( - branch.RemoteTrackingBranch.Name, - branch.RemoteTrackingBranch.Exists, - branch.RemoteTrackingBranch.Ahead, - branch.RemoteTrackingBranch.Behind - ), - branch.PullRequest is null ? null : new StackStatusCommandJsonOutputGitHubPullRequest( - branch.PullRequest.Number, - branch.PullRequest.Title, - branch.PullRequest.State, - branch.PullRequest.Url, - branch.PullRequest.IsDraft - ), - branch.Parent is null ? null : new StackStatusCommandJsonOutputParentBranchStatus( - MapBranch(branch.Parent.Branch), - branch.Parent.Ahead, - branch.Parent.Behind - ) - ); - } + private static StackStatusCommandJsonOutputBranchDetail MapBranchDetail(BranchDetail branch) + { + return new StackStatusCommandJsonOutputBranchDetail( + branch.Name, + branch.Exists, + branch.Tip is null ? null : new StackStatusCommandJsonOutputCommit(branch.Tip.Sha, branch.Tip.Message), + branch.RemoteTrackingBranch is null ? null : new StackStatusCommandJsonOutputRemoteTrackingBranchStatus( + branch.RemoteTrackingBranch.Name, + branch.RemoteTrackingBranch.Exists, + branch.RemoteTrackingBranch.Ahead, + branch.RemoteTrackingBranch.Behind + ), + branch.PullRequest is null ? null : new StackStatusCommandJsonOutputGitHubPullRequest( + branch.PullRequest.Number, + branch.PullRequest.Title, + branch.PullRequest.State, + branch.PullRequest.Url, + branch.PullRequest.IsDraft + ), + branch.Parent is null ? null : new StackStatusCommandJsonOutputParentBranchStatus( + MapBranch(branch.Parent.Branch), + branch.Parent.Ahead, + branch.Parent.Behind + ) + ); } +} - public record StackStatusCommandInputs(string? Stack, bool All, bool Full); - public record StackStatusCommandResponse(List Stacks); +public record StackStatusCommandInputs(string? Stack, bool All, bool Full); +public record StackStatusCommandResponse(List Stacks); - public class StackStatusCommandHandler( - IInputProvider inputProvider, - ILogger logger, - IGitClient gitClient, - IGitHubClient gitHubClient, - IStackConfig stackConfig) - : CommandHandlerBase +public class StackStatusCommandHandler( + IInputProvider inputProvider, + ILogger logger, + IGitClient gitClient, + IGitHubClient gitHubClient, + IStackConfig stackConfig) + : CommandHandlerBase +{ + public override async Task Handle(StackStatusCommandInputs inputs) { - public override async Task Handle(StackStatusCommandInputs inputs) - { - await Task.CompletedTask; - var stacks = stackConfig.Load(); + await Task.CompletedTask; + var stacks = stackConfig.Load(); - var remoteUri = gitClient.GetRemoteUri(); - var stacksForRemote = stacks.Where(s => s.RemoteUri.Equals(remoteUri, StringComparison.OrdinalIgnoreCase)).ToList(); - var currentBranch = gitClient.GetCurrentBranch(); + var remoteUri = gitClient.GetRemoteUri(); + var stacksForRemote = stacks.Where(s => s.RemoteUri.Equals(remoteUri, StringComparison.OrdinalIgnoreCase)).ToList(); + var currentBranch = gitClient.GetCurrentBranch(); - var stacksToCheckStatusFor = new List(); + var stacksToCheckStatusFor = new List(); - if (inputs.All) + if (inputs.All) + { + stacksToCheckStatusFor.AddRange(stacksForRemote); + } + else + { + var stack = inputProvider.SelectStack(logger, inputs.Stack, stacksForRemote, currentBranch); + + if (stack is null) { - stacksToCheckStatusFor.AddRange(stacksForRemote); + throw new InvalidOperationException($"Stack '{inputs.Stack}' not found."); } - else - { - var stack = inputProvider.SelectStack(logger, inputs.Stack, stacksForRemote, currentBranch); - if (stack is null) - { - throw new InvalidOperationException($"Stack '{inputs.Stack}' not found."); - } - - stacksToCheckStatusFor.Add(stack); - } + stacksToCheckStatusFor.Add(stack); + } - var stackStatusResults = StackHelpers.GetStackStatus( - stacksToCheckStatusFor, - currentBranch, - logger, - gitClient, - gitHubClient, - inputs.Full); + var stackStatusResults = StackHelpers.GetStackStatus( + stacksToCheckStatusFor, + currentBranch, + logger, + gitClient, + gitHubClient, + inputs.Full); - return new StackStatusCommandResponse(stackStatusResults); - } + return new StackStatusCommandResponse(stackStatusResults); } -} +} \ No newline at end of file From 0b7310b353c6b1c4491f9595d43ff42df9470258 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Tue, 27 May 2025 09:49:20 +1000 Subject: [PATCH 10/10] Fix tests --- .../Commands/Stack/CleanupStackCommandHandlerTests.cs | 7 +++++++ .../Commands/Stack/DeleteStackCommandHandlerTests.cs | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/Stack.Tests/Commands/Stack/CleanupStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/CleanupStackCommandHandlerTests.cs index a9250240..c89d717c 100644 --- a/src/Stack.Tests/Commands/Stack/CleanupStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/CleanupStackCommandHandlerTests.cs @@ -39,6 +39,7 @@ public async Task WhenBranchExistsLocally_ButHasNotBeenPushedToTheRemote_BranchI var remoteUri = Some.HttpsUri().ToString(); stackConfig.Load().Returns(stacks); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); inputProvider.Confirm(Questions.ConfirmDeleteBranches).Returns(true); @@ -80,6 +81,7 @@ public async Task WhenBranchExistsLocally_AndHasBeenDeletedFromTheRemote_BranchI var remoteUri = Some.HttpsUri().ToString(); stackConfig.Load().Returns(stacks); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); inputProvider.Confirm(Questions.ConfirmDeleteBranches).Returns(true); @@ -117,6 +119,7 @@ public async Task WhenBranchExistsLocally_AndInRemote_BranchIsNotDeletedLocally( new("Stack2", repo.RemoteUri, sourceBranch, []) ]); stackConfig.Load().Returns(stacks); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); inputProvider.Confirm(Questions.ConfirmDeleteBranches).Returns(true); @@ -154,6 +157,7 @@ public async Task WhenConfirmationIsFalse_DoesNotDeleteAnyBranches() new("Stack2", repo.RemoteUri, sourceBranch, []) ]); stackConfig.Load().Returns(stacks); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); inputProvider.Confirm(Questions.ConfirmDeleteBranches).Returns(false); @@ -191,6 +195,7 @@ public async Task WhenStackNameIsProvided_ItIsNotAskedFor() new("Stack2", repo.RemoteUri, sourceBranch, []) ]); stackConfig.Load().Returns(stacks); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Confirm(Questions.ConfirmDeleteBranches).Returns(true); @@ -263,6 +268,7 @@ public async Task WhenOnlyASingleStackExists_StackIsSelectedAutomatically() new("Stack1", repo.RemoteUri, sourceBranch, [branchToCleanup, branchToKeep]), ]); stackConfig.Load().Returns(stacks); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Confirm(Questions.ConfirmDeleteBranches).Returns(true); @@ -303,6 +309,7 @@ public async Task WhenConfirmIsProvided_DoesNotAskForConfirmation_DeletesBranche var remoteUri = Some.HttpsUri().ToString(); stackConfig.Load().Returns(stacks); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); diff --git a/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs index 978d5a0a..ac246257 100644 --- a/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs @@ -34,6 +34,7 @@ public async Task WhenNoInputsAreProvided_AsksForName_AndConfirmation_AndDeletes stackConfig .WhenForAnyArgs(s => s.Save(Arg.Any>())) .Do(ci => stacks = ci.ArgAt>(0)); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); inputProvider.Confirm(Questions.ConfirmDeleteStack).Returns(true); @@ -71,6 +72,7 @@ public async Task WhenConfirmationIsFalse_DoesNotDeleteStack() stackConfig .WhenForAnyArgs(s => s.Save(Arg.Any>())) .Do(ci => stacks = ci.ArgAt>(0)); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); inputProvider.Confirm(Questions.ConfirmDeleteStack).Returns(false); @@ -109,6 +111,7 @@ public async Task WhenNameIsProvided_AsksForConfirmation_AndDeletesStack() stackConfig .WhenForAnyArgs(s => s.Save(Arg.Any>())) .Do(ci => stacks = ci.ArgAt>(0)); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Confirm(Questions.ConfirmDeleteStack).Returns(true); @@ -189,6 +192,7 @@ public async Task WhenThereAreLocalBranchesThatAreDeletedInTheRemote_AsksToClean stackConfig .WhenForAnyArgs(s => s.Save(Arg.Any>())) .Do(ci => stacks = ci.ArgAt>(0)); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); inputProvider.Confirm(Questions.ConfirmDeleteStack).Returns(true); @@ -227,6 +231,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName() stackConfig .WhenForAnyArgs(s => s.Save(Arg.Any>())) .Do(ci => stacks = ci.ArgAt>(0)); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Confirm(Questions.ConfirmDeleteStack).Returns(true); @@ -262,6 +267,7 @@ public async Task WhenConfirmIsProvided_DoesNotAskForConfirmation_DeletesStack() stackConfig .WhenForAnyArgs(s => s.Save(Arg.Any>())) .Do(ci => stacks = ci.ArgAt>(0)); + logger.WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())).Do(ci => ci.ArgAt(1)()); inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1");