diff --git a/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs b/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs index 97f4adea..d0c2ffc3 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)) 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 8cca002c..ac246257 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(); @@ -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); @@ -91,7 +93,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(); @@ -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); @@ -210,7 +214,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(); @@ -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); @@ -244,7 +249,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(); @@ -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"); 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/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); 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..0a7c3c58 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 GetStackStatus( List stacks, string currentBranch, ILogger logger, @@ -41,92 +45,98 @@ 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) + if (includePullRequestStatus) + { + logger.Status("Checking status of GitHub pull requests...", () => EvaluateBranchStatusDetails(logger, gitClient, gitHubClient, includePullRequestStatus, stacksToReturnStatusFor, stacksOrderedByCurrentBranch, branchStatuses)); + } + else { - var parentBranch = stack.SourceBranch; + EvaluateBranchStatusDetails(logger, gitClient, gitHubClient, includePullRequestStatus, stacksToReturnStatusFor, stacksOrderedByCurrentBranch, branchStatuses); + } - status.Branches.Add(stack.SourceBranch, new BranchDetail()); - branchStatuses.TryGetValue(stack.SourceBranch, out var sourceBranchStatus); - if (sourceBranchStatus is not null) - { - 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); - } + return stacksToReturnStatusFor; - foreach (var branch 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) { - status.Branches.Add(branch, new BranchDetail()); - branchStatuses.TryGetValue(branch, out var branchStatus); - - if (branchStatus is not null) + if (!branchStatuses.TryGetValue(stack.SourceBranch, out var sourceBranchStatus)) { - var (aheadOfParent, behindParent) = branchStatus.RemoteBranchExists ? gitClient.CompareBranches(branch, parentBranch) : (0, 0); - - status.Branches[branch].Status = new BranchStatus( - true, - branchStatus.RemoteTrackingBranchName is not null, - branchStatus.RemoteBranchExists, - branchStatus.IsCurrentBranch, - aheadOfParent, - behindParent, - branchStatus.Ahead, - branchStatus.Behind, - branchStatus.Tip); - - if (branchStatus.RemoteBranchExists) - { - parentBranch = branch; - } + logger.Warning($"Source branch '{stack.SourceBranch}' does not exist locally or in the remote repository."); + continue; } - } - } - if (includePullRequestStatus) - { - logger.Status("Checking status of GitHub pull requests...", () => - { - foreach (var (stack, status) in stacksToCheckStatusFor) + 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) { - try + branchStatuses.TryGetValue(branchName, out var branchStatus); + + if (branchStatus is not null) { - foreach (var branch in stack.Branches) + var (aheadOfParent, behindParent) = branchStatus.RemoteBranchExists ? gitClient.CompareBranches(branchName, parentBranch.Name) : (0, 0); + GitHubPullRequest? pullRequest = null; + + if (includePullRequestStatus) { - var pr = gitHubClient.GetPullRequest(branch); + pullRequest = gitHubClient.GetPullRequest(branchName); + } - if (pr is not null) - { - status.Branches[branch].PullRequest = pr; - } + 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; } } - catch (Exception ex) + else { - logger.Warning($"Error checking GitHub pull requests: {ex.Message}"); + var branch = new BranchDetail(branchName, false, null, null, null, null); + stackBranches.Add(branch); } } - }); - } - return stacksToCheckStatusFor; + stacksToReturnStatusFor.Add(new StackStatus(stack.Name, sourceBranch, [.. stackBranches])); + } + } } public static StackStatus GetStackStatus( @@ -145,76 +155,55 @@ 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 = 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 +222,115 @@ 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}"); } + } + 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 = 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 (branchDetail.Status.AheadOfParent > 0 && branchDetail.Status.BehindParent > 0) + if (branch.IsActive) + { + if (branch.AheadOfRemote > 0 || branch.BehindRemote > 0) { - branchNameBuilder.Append($" ({branchDetail.Status.AheadOfParent} ahead, {branchDetail.Status.BehindParent} behind {parentBranch})".Muted()); + branchNameBuilder.Append($" {branch.BehindRemote}{Emoji.Known.DownArrow}{branch.AheadOfRemote}{Emoji.Known.UpArrow}"); } - else if (branchDetail.Status.AheadOfParent > 0) + + if (branch.AheadOfParent > 0 && branch.BehindParent > 0) { - branchNameBuilder.Append($" ({branchDetail.Status.AheadOfParent} ahead of {parentBranch})".Muted()); + branchNameBuilder.Append($" ({branch.AheadOfParent} ahead, {branch.BehindParent} behind {branch.ParentBranchName})".Muted()); } - else if (branchDetail.Status.BehindParent > 0) + else if (branch.AheadOfParent > 0) { - branchNameBuilder.Append($" ({branchDetail.Status.BehindParent} behind {parentBranch})".Muted()); + branchNameBuilder.Append($" ({branch.AheadOfParent} ahead of {branch.ParentBranchName})".Muted()); + } + else if (branch.BehindParent > 0) + { + 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 +338,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 +346,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 +419,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 { @@ -455,7 +484,7 @@ public static string[] GetBranchesNeedingCleanup(Config.Stack stack, ILogger log var currentBranch = gitClient.GetCurrentBranch(); var stackStatus = GetStackStatus(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 +620,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 +639,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..68f79d9e 100644 --- a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs +++ b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs @@ -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,15 @@ public override async Task Handle(CreatePullRequestsCommandInputs inputs) { var newPullRequests = CreatePullRequests(logger, gitHubClient, status, pullRequestInformation); - var pullRequestsInStack = status.Branches.Values + // Re-get the status to pick up PRs + status = StackHelpers.GetStackStatus( + stack, + currentBranch, + logger, + gitClient, + gitHubClient); + + var pullRequestsInStack = status.Branches .Where(branch => branch.HasPullRequest) .Select(branch => branch.PullRequest!) .ToList(); @@ -178,7 +184,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 +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.PullRequest = pullRequest; } } @@ -201,28 +206,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..5bcad4c4 100644 --- a/src/Stack/Commands/PullRequests/SetPullRequestDescriptionCommand.cs +++ b/src/Stack/Commands/PullRequests/SetPullRequestDescriptionCommand.cs @@ -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..41e1e45c 100644 --- a/src/Stack/Commands/Remote/SyncStackCommand.cs +++ b/src/Stack/Commands/Remote/SyncStackCommand.cs @@ -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..4899d01a 100644 --- a/src/Stack/Commands/Stack/StackStatusCommand.cs +++ b/src/Stack/Commands/Stack/StackStatusCommand.cs @@ -2,7 +2,6 @@ using System.Text.Json; using Spectre.Console; using Spectre.Console.Cli; -using Stack.Commands; using Stack.Commands.Helpers; using Stack.Config; using Stack.Git; @@ -25,6 +24,61 @@ public class StackStatusCommandSettings : CommandWithOutputSettingsBase 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) @@ -41,99 +95,76 @@ 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 output = response.Stacks.Select(MapToJsonOutput).ToList(); + var json = JsonSerializer.Serialize(output, 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); + private static StackStatusCommandJsonOutput MapToJsonOutput(StackStatus stack) + { + return new StackStatusCommandJsonOutput( + stack.Name, + MapBranch(stack.SourceBranch), + stack.Branches.Select(MapBranchDetail).ToList() + ); + } -record BranchDetail( - string Name, - bool Exists, - Commit? Tip, - RemoteTrackingBranchStatus? RemoteTrackingBranch, - GitHubPullRequest? PullRequest, - ParentBranchStatus? Parent) : Branch(Name, Exists, Tip, RemoteTrackingBranch); + 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 + ) + ); + } -record ParentBranchStatus(Branch Branch, int Ahead, int 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(Dictionary Statuses); +public record StackStatusCommandResponse(List Stacks); public class StackStatusCommandHandler( IInputProvider inputProvider, @@ -180,4 +211,4 @@ public override async Task Handle(StackStatusCommand return new StackStatusCommandResponse(stackStatusResults); } -} +} \ No newline at end of file