From 62cc99201a1e7da46920bbb9f3f91ac57218411a Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Thu, 25 Sep 2025 20:29:45 +1000 Subject: [PATCH 1/7] Update using local Git data only --- .../Commands/Helpers/StackActionsTests.cs | 56 ++++------- .../Integration/StackActionsTests.cs | 46 +++------ src/Stack/Commands/Helpers/StackActions.cs | 94 +++++++++++-------- 3 files changed, 90 insertions(+), 106 deletions(-) diff --git a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs index ba9a5e0c..8344f2da 100644 --- a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs +++ b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs @@ -21,7 +21,6 @@ public async Task UpdateStack_UsingMerge_WhenConflictResolutionAborted_ThrowsAbo var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, new List { new(feature, []) }); @@ -40,7 +39,7 @@ public async Task UpdateStack_UsingMerge_WhenConflictResolutionAborted_ThrowsAbo var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var actions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act var act = async () => await actions.UpdateStack(stack, UpdateStrategy.Merge, CancellationToken.None); @@ -59,7 +58,6 @@ public async Task UpdateStack_UsingMerge_WhenConflictsResolved_CompletesSuccessf var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, new List { new(feature, []) }); @@ -77,7 +75,7 @@ public async Task UpdateStack_UsingMerge_WhenConflictsResolved_CompletesSuccessf var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var actions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act await actions.UpdateStack(stack, UpdateStrategy.Merge, CancellationToken.None); @@ -96,7 +94,6 @@ public async Task UpdateStack_UsingRebase_WhenConflictResolutionAborted_ThrowsAb var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), source, new List { new(feature, []) }); @@ -114,7 +111,7 @@ public async Task UpdateStack_UsingRebase_WhenConflictResolutionAborted_ThrowsAb var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var actions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act var act = async () => await actions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -133,7 +130,6 @@ public async Task UpdateStack_UsingRebase_WhenConflictsResolved_CompletesSuccess var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), source, new List { new(feature, []) }); @@ -151,7 +147,7 @@ public async Task UpdateStack_UsingRebase_WhenConflictsResolved_CompletesSuccess var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var actions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act await actions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -192,7 +188,7 @@ public void PullChanges_WhenSomeBranchesHaveChanges_AndOthersDoNot_OnlyPullsChan var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(executionContext.WorkingDirectory).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -211,7 +207,6 @@ public void PullChanges_WhenSomeBranchesDoNotExistInRemote_OnlyPullsBranchesThat var branchThatDoesNotExistInRemote = Some.BranchName(); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -234,7 +229,7 @@ public void PullChanges_WhenSomeBranchesDoNotExistInRemote_OnlyPullsBranchesThat var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(executionContext.WorkingDirectory).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -279,7 +274,7 @@ public void PullChanges_WhenOnlyNonCurrentBranchesBehind_FetchesThem() var worktreePath = "/worktree"; factory.Create(executionContext.WorkingDirectory).Returns(gitClient); factory.Create(worktreePath).Returns(worktreeGitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -295,7 +290,6 @@ public void PullChanges_WhenOnlyCurrentBranchBehind_PullsIt() // Arrange var sourceBranch = Some.BranchName(); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); gitClient.GetCurrentBranch().Returns(sourceBranch); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); @@ -312,7 +306,7 @@ public void PullChanges_WhenOnlyCurrentBranchBehind_PullsIt() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -329,7 +323,6 @@ public void PullChanges_WhenCurrentAndOtherBranchesBehind_PullsCurrentAndFetches var sourceBranch = Some.BranchName(); var otherBranch = Some.BranchName(); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -350,7 +343,7 @@ public void PullChanges_WhenCurrentAndOtherBranchesBehind_PullsCurrentAndFetches var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -367,7 +360,6 @@ public void PullChanges_WhenNoBranchesBehind_DoesNothing() var sourceBranch = Some.BranchName(); var otherBranch = Some.BranchName(); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -384,7 +376,7 @@ public void PullChanges_WhenNoBranchesBehind_DoesNothing() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -404,7 +396,6 @@ public void PullChanges_WhenBranchIsBehind_AndCheckedOutInAnotherWorktree_PullsI var defaultGitClient = Substitute.For(); var worktreeGitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -427,7 +418,7 @@ public void PullChanges_WhenBranchIsBehind_AndCheckedOutInAnotherWorktree_PullsI factory.Create(executionContext.WorkingDirectory).Returns(defaultGitClient); factory.Create(worktreePath).Returns(worktreeGitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -446,7 +437,6 @@ public void PushChanges_WhenSomeLocalBranchesAreAhead_OnlyPushesChangesForBranch var branchNotAheadOfRemote = Some.BranchName(); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -475,7 +465,7 @@ public void PushChanges_WhenSomeLocalBranchesAreAhead_OnlyPushesChangesForBranch var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -493,7 +483,6 @@ public void PushChanges_WhenSomeBranchesDoNotExistInRemote_OnlyPushesBranchesTha var branchThatDoesNotExistInRemoteButIsAhead = Some.BranchName(); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -522,7 +511,7 @@ public void PushChanges_WhenSomeBranchesDoNotExistInRemote_OnlyPushesBranchesTha var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -540,7 +529,6 @@ public void PushChanges_WhenSomeBranchesHaveNoRemoteTrackingBranch_PushesThemAsN var newBranchWithNoRemote = Some.BranchName(); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -574,7 +562,7 @@ public void PushChanges_WhenSomeBranchesHaveNoRemoteTrackingBranch_PushesThemAsN var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -594,7 +582,6 @@ public void PushChanges_WhenMaxBatchSizeIsSmaller_PushesBranchesInMultipleBatche var branch3 = Some.BranchName(); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -625,7 +612,7 @@ public void PushChanges_WhenMaxBatchSizeIsSmaller_PushesBranchesInMultipleBatche var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, maxBatchSize: 2, forceWithLease: false); @@ -645,7 +632,6 @@ public void PushChanges_WhenForceWithLeaseIsTrue_PassesForceWithLeaseParameterTo var branchAhead = Some.BranchName(); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -666,7 +652,7 @@ public void PushChanges_WhenForceWithLeaseIsTrue_PassesForceWithLeaseParameterTo var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, maxBatchSize: 5, forceWithLease: true); @@ -684,7 +670,6 @@ public void PushChanges_WhenNoBranchesNeedToBePushed_DoesNotCallPushMethods() var branchBehind = Some.BranchName(); var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -707,7 +692,7 @@ public void PushChanges_WhenNoBranchesNeedToBePushed_DoesNotCallPushMethods() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, maxBatchSize: 5, forceWithLease: false); @@ -726,7 +711,6 @@ public async Task UpdateStack_UsingMerge_WhenBranchIsInWorktree_UsesWorktreeGitC var worktreePath = $"C:/temp/{Some.Name()}"; var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); var inputProvider = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); @@ -752,7 +736,7 @@ public async Task UpdateStack_UsingMerge_WhenBranchIsInWorktree_UsesWorktreeGitC ); var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" }; - var stackActions = new StackActions(gitClientFactory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, executionContext, logger, displayProvider, conflictResolutionDetector); gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); @@ -774,8 +758,6 @@ public async Task UpdateStack_UsingRebase_WhenBranchIsInWorktree_UsesWorktreeGit var worktreePath = $"C:/temp/{Some.Name()}"; var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); - var inputProvider = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var gitClientFactory = Substitute.For(); @@ -800,7 +782,7 @@ public async Task UpdateStack_UsingRebase_WhenBranchIsInWorktree_UsesWorktreeGit ); var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" }; - var stackActions = new StackActions(gitClientFactory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, executionContext, logger, displayProvider, conflictResolutionDetector); gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); diff --git a/src/Stack.Tests/Integration/StackActionsTests.cs b/src/Stack.Tests/Integration/StackActionsTests.cs index 735802df..bf8effd0 100644 --- a/src/Stack.Tests/Integration/StackActionsTests.cs +++ b/src/Stack.Tests/Integration/StackActionsTests.cs @@ -1,5 +1,4 @@ using FluentAssertions; -using NSubstitute; using Meziantou.Extensions.Logging.Xunit; using Stack.Commands.Helpers; using Stack.Git; @@ -25,7 +24,6 @@ public void PullChanges_WhenChangesExistOnSourceAndBranchInStack_PullsChangesCor var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -42,7 +40,7 @@ public void PullChanges_WhenChangesExistOnSourceAndBranchInStack_PullsChangesCor .WithBranch(b => b.WithName(otherBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -71,7 +69,6 @@ public void PullChanges_WhenChangesExistOnWorktreeBranch_PullsChangesCorrectly() var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -90,7 +87,7 @@ public void PullChanges_WhenChangesExistOnWorktreeBranch_PullsChangesCorrectly() .WithBranch(b => b.WithName(worktreeBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -115,7 +112,6 @@ public void PullChanges_WhenLocalBranchHasNoRemoteTrackingBranch_DoesNotPullChan var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -128,7 +124,7 @@ public void PullChanges_WhenLocalBranchHasNoRemoteTrackingBranch_DoesNotPullChan .WithBranch(b => b.WithName(localOnlyBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -152,7 +148,6 @@ public void PullChanges_WhenRemoteTrackingBranchIsDeleted_DoesNotPullChanges() var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -168,7 +163,7 @@ public void PullChanges_WhenRemoteTrackingBranchIsDeleted_DoesNotPullChanges() .WithBranch(b => b.WithName(deletedRemoteBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -196,7 +191,6 @@ public async Task UpdateStack_WhenUpdatingUsingMerge_AndChangesExistOnMultipleBr var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -221,7 +215,7 @@ public async Task UpdateStack_WhenUpdatingUsingMerge_AndChangesExistOnMultipleBr .WithBranch(b => b.WithName(line2Branch1)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Merge, CancellationToken.None); @@ -253,7 +247,6 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndChangesExistOnMultipleB var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -278,7 +271,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndChangesExistOnMultipleB .WithBranch(b => b.WithName(line2Branch1)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -312,7 +305,6 @@ public async Task UpdateStack_WhenUpdatingUsingMerge_AndBranchCheckedOutInWorktr var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -340,7 +332,7 @@ public async Task UpdateStack_WhenUpdatingUsingMerge_AndBranchCheckedOutInWorktr var worktree = repo.CreateWorktree(childBranch); worktree.Should().NotBeNull(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Merge, CancellationToken.None); @@ -367,7 +359,6 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndBranchCheckedOutInWorkt var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -395,7 +386,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndBranchCheckedOutInWorkt var worktreePath = repo.CreateWorktree(childBranch); worktreePath.Should().NotBeNull(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -426,7 +417,6 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndFirstBranchWasSquashMer var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -448,7 +438,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndFirstBranchWasSquashMer .WithChildBranch(e => e.WithName(fourthBranch)))) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -499,7 +489,6 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndFirstBranchWasSquashMer var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -526,7 +515,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndFirstBranchWasSquashMer .WithChildBranch(e => e.WithName(fourthBranch)))) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -558,7 +547,6 @@ public void PushChanges_WhenChangesExistOnCurrentBranch_PushesChangesCorrectly() var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -577,7 +565,7 @@ public void PushChanges_WhenChangesExistOnCurrentBranch_PushesChangesCorrectly() .WithBranch(b => b.WithName(currentBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -602,7 +590,6 @@ public void PushChanges_WhenChangesExistOnNonCurrentBranch_PushesChangesCorrectl var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -623,7 +610,7 @@ public void PushChanges_WhenChangesExistOnNonCurrentBranch_PushesChangesCorrectl .WithBranch(b => b.WithName(nonCurrentBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -648,7 +635,6 @@ public void PushChanges_WhenChangesExistOnWorktreeBranch_PushesChangesCorrectly( var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -671,7 +657,7 @@ public void PushChanges_WhenChangesExistOnWorktreeBranch_PushesChangesCorrectly( .WithBranch(b => b.WithName(worktreeBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -696,7 +682,6 @@ public void PushChanges_WhenLocalOnlyBranchExists_CreatesRemoteTrackingBranch() var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -719,7 +704,7 @@ public void PushChanges_WhenLocalOnlyBranchExists_CreatesRemoteTrackingBranch() .WithBranch(b => b.WithName(localOnlyBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act - should complete without errors and should actually create the remote tracking branch stackActions.PushChanges(stack, 5, false); @@ -750,7 +735,6 @@ public void PushChanges_WhenRemoteTrackingBranchHasBeenDeleted_DoesNotPushChange var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); - var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -773,7 +757,7 @@ public void PushChanges_WhenRemoteTrackingBranchHasBeenDeleted_DoesNotPushChange .WithBranch(b => b.WithName(deletedRemoteBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); // Act - should complete without errors even with deleted remote stackActions.PushChanges(stack, 5, false); diff --git a/src/Stack/Commands/Helpers/StackActions.cs b/src/Stack/Commands/Helpers/StackActions.cs index b3a9994f..ed543609 100644 --- a/src/Stack/Commands/Helpers/StackActions.cs +++ b/src/Stack/Commands/Helpers/StackActions.cs @@ -18,7 +18,6 @@ public interface IStackActions public class StackActions( IGitClientFactory gitClientFactory, CliExecutionContext executionContext, - IGitHubClient gitHubClient, ILogger logger, IDisplayProvider displayProvider, IConflictResolutionDetector conflictResolutionDetector) : IStackActions @@ -143,59 +142,52 @@ public void PushChanges( public async Task UpdateStack(Config.Stack stack, UpdateStrategy strategy, CancellationToken cancellationToken) { var gitClient = GetDefaultGitClient(); - var currentBranch = gitClient.GetCurrentBranch(); - - var status = StackHelpers.GetStackStatus( - stack, - currentBranch, - logger, - gitClient, - gitHubClient, - true); - // Get branch statuses to check for worktrees List allBranchesInStack = [stack.SourceBranch, .. stack.AllBranchNames]; var branchStatuses = gitClient.GetBranchStatuses([.. allBranchesInStack]); + if (!branchStatuses.ContainsKey(stack.SourceBranch)) + { + logger.SourceBranchDoesNotExist(stack.SourceBranch); + return; + } + if (strategy == UpdateStrategy.Rebase) { - await UpdateStackUsingRebase(stack, status, branchStatuses, cancellationToken); + await UpdateStackUsingRebase(stack, branchStatuses, cancellationToken); } else { - await UpdateStackUsingMerge(stack, status, branchStatuses, cancellationToken); + await UpdateStackUsingMerge(stack, branchStatuses, cancellationToken); } } private async Task UpdateStackUsingMerge( Config.Stack stack, - StackStatus status, Dictionary branchStatuses, CancellationToken cancellationToken) { - logger.UpdatingStackUsingMerge(status.Name); + logger.UpdatingStackUsingMerge(stack.Name); - var allBranchLines = status.GetAllBranchLines(); - - foreach (var branchLine in allBranchLines) + foreach (var branchLine in stack.GetAllBranchLines()) { - await UpdateBranchLineUsingMerge(branchLine, status.SourceBranch, branchStatuses, cancellationToken); + await UpdateBranchLineUsingMerge(branchLine, stack.SourceBranch, branchStatuses, cancellationToken); } } private async Task UpdateBranchLineUsingMerge( - List branchLine, - BranchDetailBase parentBranch, + List branchLine, + string parentBranchName, Dictionary branchStatuses, CancellationToken cancellationToken) { - var currentParentBranch = parentBranch; + var currentParentBranch = parentBranchName; foreach (var branch in branchLine) { - if (branch.IsActive) + if (IsBranchActive(branch.Name, branchStatuses)) { - await MergeFromSourceBranch(branch.Name, currentParentBranch.Name, branchStatuses, cancellationToken); - currentParentBranch = branch; + await MergeFromSourceBranch(branch.Name, currentParentBranch, branchStatuses, cancellationToken); + currentParentBranch = branch.Name; } else { @@ -204,6 +196,11 @@ private async Task UpdateBranchLineUsingMerge( } } + private static bool IsBranchActive(string branchName, Dictionary branchStatuses) + { + return branchStatuses.TryGetValue(branchName, out var status) && status.RemoteBranchExists; + } + private async Task MergeFromSourceBranch(string branch, string sourceBranchName, Dictionary branchStatuses, CancellationToken cancellationToken) { logger.MergingBranch(sourceBranchName, branch); @@ -241,21 +238,23 @@ private async Task MergeFromSourceBranch(string branch, string sourceBranchName, private async Task UpdateStackUsingRebase( Config.Stack stack, - StackStatus status, Dictionary branchStatuses, CancellationToken cancellationToken) { - logger.UpdatingStackUsingRebase(status.Name); - - var allBranchLines = status.GetAllBranchLines(); + logger.UpdatingStackUsingRebase(stack.Name); - foreach (var branchLine in allBranchLines) + foreach (var branchLine in stack.GetAllBranchLines()) { - await UpdateBranchLineUsingRebase(status, branchLine, branchStatuses, cancellationToken); + await UpdateBranchLineUsingRebase(stack.Name, stack.SourceBranch, branchLine, branchStatuses, cancellationToken); } } - private async Task UpdateBranchLineUsingRebase(StackStatus status, List branchLine, Dictionary branchStatuses, CancellationToken cancellationToken) + private async Task UpdateBranchLineUsingRebase( + string stackName, + string sourceBranchName, + List branchLine, + Dictionary branchStatuses, + CancellationToken cancellationToken) { // // When rebasing the stack, we need to be able to pick up changes at each level of the stack. @@ -307,19 +306,22 @@ private async Task UpdateBranchLineUsingRebase(StackStatus status, List ", branchLine.Select(b => b.Name))); - List allBranchesInLine = [status.SourceBranch, .. branchLine]; + logger.RebasingStackForBranchLine(stackName, sourceBranchName, string.Join(" -> ", branchLine.Select(b => b.Name))); + var allBranchesInLine = new List { GetBranchState(sourceBranchName, branchStatuses) }; + allBranchesInLine.AddRange(branchLine.Select(b => GetBranchState(b.Name, branchStatuses))); foreach (var branch in branchLine) { - if (!branch.IsActive) + var branchState = allBranchesInLine.First(b => b.Name == branch.Name); + + if (!branchState.IsActive) { logger.TraceSkippingInactiveBranch(branch.Name); continue; } string? lowestInactiveBranchToReParentFrom = null; - List branchesToRebaseOnto = []; + var branchesToRebaseOnto = new List(); // Find all active branches above this one to // rebase onto. Also work out if there is any that @@ -344,8 +346,10 @@ private async Task UpdateBranchLineUsingRebase(StackStatus status, List b.Name == lowestInactiveBranchToReParentFrom) : null; - var couldRebaseOntoParent = lowestInactiveBranchToReParentFromDetail is not null && lowestInactiveBranchToReParentFromDetail.Exists; + BranchState? lowestInactiveBranchToReParentFromDetail = lowestInactiveBranchToReParentFrom is not null + ? allBranchesInLine.First(b => b.Name == lowestInactiveBranchToReParentFrom) + : null; + var couldRebaseOntoParent = lowestInactiveBranchToReParentFromDetail is { Exists: true }; var parentCommitToRebaseFrom = couldRebaseOntoParent ? GetCommitShaToReParentFrom(branch.Name, lowestInactiveBranchToReParentFrom!, branchToRebaseOnto.Name) : null; if (parentCommitToRebaseFrom is not null) @@ -360,6 +364,16 @@ private async Task UpdateBranchLineUsingRebase(StackStatus status, List branchStatuses) + { + if (branchStatuses.TryGetValue(branchName, out var status)) + { + return new BranchState(branchName, true, status.RemoteBranchExists); + } + + return new BranchState(branchName, false, false); + } + private string? GetCommitShaToReParentFrom(string branchToRebase, string lowestInactiveBranchToReParentFrom, string branchToRebaseOnto) { var gitClient = GetDefaultGitClient(); @@ -474,6 +488,7 @@ private async Task RebaseOntoNewParent( } }, cancellationToken); } + private readonly record struct BranchState(string Name, bool Exists, bool IsActive); } } @@ -500,6 +515,9 @@ internal static partial class LoggerExtensionMethods [LoggerMessage(Level = LogLevel.Trace, Message = "Branch {Branch} no longer exists on the remote repository or the associated pull request is no longer open. Skipping...")] public static partial void TraceSkippingInactiveBranch(this ILogger logger, string branch); + [LoggerMessage(Level = LogLevel.Warning, Message = "Source branch \"{SourceBranch}\" does not exist locally. Skipping update.")] + public static partial void SourceBranchDoesNotExist(this ILogger logger, string sourceBranch); + [LoggerMessage(Level = LogLevel.Debug, Message = "Merging {SourceBranch} into {Branch}")] public static partial void MergingBranch(this ILogger logger, string sourceBranch, string branch); From 701a5e27e89cd5b6bb025370808cfd1596ffc6f2 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 26 Sep 2025 08:36:40 +1000 Subject: [PATCH 2/7] Add option for checking pull requests --- README.md | 63 ++++++------ .../Commands/Helpers/StackActionsTests.cs | 96 +++++++++++++++---- .../Remote/SyncStackCommandHandlerTests.cs | 30 +++--- .../Stack/UpdateStackCommandHandlerTests.cs | 24 ++--- .../Helpers/TestGitHubRepositoryBuilder.cs | 19 +++- .../Integration/StackActionsTests.cs | 46 ++++++--- src/Stack/Commands/Helpers/StackActions.cs | 47 ++++++--- src/Stack/Commands/Helpers/StackHelpers.cs | 6 +- .../PullRequests/CreatePullRequestsCommand.cs | 3 +- src/Stack/Commands/Remote/SyncStackCommand.cs | 13 ++- .../Commands/Stack/StackStatusCommand.cs | 12 +-- .../Commands/Stack/UpdateStackCommand.cs | 10 +- src/Stack/Git/CachingGitHubClient.cs | 2 + src/Stack/Git/GitHubClient.cs | 19 ++++ src/Stack/Git/ProcessHelpers.cs | 19 ++++ src/Stack/Git/SafeGitHubClient.cs | 2 + src/Stack/Infrastructure/CommonOptions.cs | 6 ++ 17 files changed, 296 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index e7607aeb..df491f13 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ This is where `stack` comes in: It lets you manage multiple branches that form t - [Adding a new branch](#adding-a-new-branch) - [Incorporating changes from the remote repository](#incorporating-changes-from-the-remote-repository) - [Specifying an update strategy](#specifying-an-update-strategy) + - [Checking pull requests during update](#checking-pull-requests-during-update) - [Creating pull requests](#creating-pull-requests) - [Commands](#commands) @@ -123,7 +124,7 @@ To use the merge strategy, either: Updating a stack using merge, particularly if it has a number of branches in it, can result in lots of merge commits. -If you merge a pull request using "Squash and merge" then you might find that the first update to a stack after that results in merge conflicts that you need to resolve. This can be a bit of a pain. +If you merge a pull request using "Squash and merge" then you might find that the first update to a stack after that results in merge conflicts that you need to resolve. ##### Rebase @@ -144,6 +145,10 @@ Stack has handling to detect when a squash merge happens during updating a stack The remote tracking branch for the branch that was squash merged needs to be deleted for this handling to be enabled. +#### Checking pull requests during update + +A branch will be skipped during the update of a stack if it's remote tracking branch has been deleted, which normally happens when a pull request is merged. If you don't delete the remote tracking branch when merging pull requests you can skip branches where the pull request is merged with the `--check-pull-requests` option. + ### Creating pull requests When you've made your changes you can create a set of pull requests that build off each other. This requires that you have the `gh` CLI installed on your path and authenticated (run `gh auth login`). @@ -209,14 +214,14 @@ Usage: stack status [options] Options: - --working-dir The path to the directory containing the git repository. Defaults to the current directory. - --debug Show debug output. - --verbose Show verbose output. - --json Write output and log messages as JSON. Log messages will be written to stderr. - -s, --stack The name of the stack. - --all Show status of all stacks. - --full Show full status including pull requests. - -?, -h, --help Show help and usage information + --working-dir The path to the directory containing the git repository. Defaults to the current directory. + --debug Show debug output. + --verbose Show verbose output. + --json Write output and log messages as JSON. Log messages will be written to stderr. + -s, --stack The name of the stack. + --all Show status of all stacks. + --check-pull-requests Include the status of pull requests in output. + -?, -h, --help Show help and usage information ``` #### `stack delete` @@ -266,14 +271,15 @@ Usage: stack update [options] Options: - --working-dir The path to the directory containing the git repository. Defaults to the current directory. - --debug Show debug output. - --verbose Show verbose output. - --json Write output and log messages as JSON. Log messages will be written to stderr. - -s, --stack The name of the stack. - --rebase Use rebase when updating the stack. Overrides any setting in Git configuration. - --merge Use merge when updating the stack. Overrides any setting in Git configuration. - -?, -h, --help Show help and usage information + --working-dir The path to the directory containing the git repository. Defaults to the current directory. + --debug Show debug output. + --verbose Show verbose output. + --json Write output and log messages as JSON. Log messages will be written to stderr. + -s, --stack The name of the stack. + --rebase Use rebase when updating the stack. Overrides any setting in Git configuration. + --merge Use merge when updating the stack. Overrides any setting in Git configuration. + --check-pull-requests Check the status of pull requests when determining if a branch should be included in updating the stack. + -?, -h, --help Show help and usage information ``` #### `stack switch` @@ -438,17 +444,18 @@ Usage: stack sync [options] Options: - --working-dir The path to the directory containing the git repository. Defaults to the current directory. - --debug Show debug output. - --verbose Show verbose output. - --json Write output and log messages as JSON. Log messages will be written to stderr. - -s, --stack The name of the stack. - --max-batch-size The maximum number of branches to process at once. [default: 5] - --rebase Use rebase when updating the stack. Overrides any setting in Git configuration. - --merge Use merge when updating the stack. Overrides any setting in Git configuration. - -y, --yes Confirm the command without prompting. - --no-push Don't push changes to the remote repository - -?, -h, --help Show help and usage information + --working-dir The path to the directory containing the git repository. Defaults to the current directory. + --debug Show debug output. + --verbose Show verbose output. + --json Write output and log messages as JSON. Log messages will be written to stderr. + -s, --stack The name of the stack. + --max-batch-size The maximum number of branches to process at once. [default: 5] + --rebase Use rebase when updating the stack. Overrides any setting in Git configuration. + --merge Use merge when updating the stack. Overrides any setting in Git configuration. + -y, --yes Confirm the command without prompting. + --check-pull-requests Check the status of pull requests when determining if a branch should be included in updating the stack. + --no-push Don't push changes to the remote repository + -?, -h, --help Show help and usage information ``` ### GitHub commands diff --git a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs index 8344f2da..e331e1b3 100644 --- a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs +++ b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs @@ -1,3 +1,4 @@ +using System.Net.Http.Headers; using FluentAssertions; using Meziantou.Extensions.Logging.Xunit; using NSubstitute; @@ -21,6 +22,7 @@ public async Task UpdateStack_UsingMerge_WhenConflictResolutionAborted_ThrowsAbo var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, new List { new(feature, []) }); @@ -39,7 +41,7 @@ public async Task UpdateStack_UsingMerge_WhenConflictResolutionAborted_ThrowsAbo var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var actions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act var act = async () => await actions.UpdateStack(stack, UpdateStrategy.Merge, CancellationToken.None); @@ -58,6 +60,7 @@ public async Task UpdateStack_UsingMerge_WhenConflictsResolved_CompletesSuccessf var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, new List { new(feature, []) }); @@ -75,7 +78,7 @@ public async Task UpdateStack_UsingMerge_WhenConflictsResolved_CompletesSuccessf var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var actions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act await actions.UpdateStack(stack, UpdateStrategy.Merge, CancellationToken.None); @@ -94,6 +97,7 @@ public async Task UpdateStack_UsingRebase_WhenConflictResolutionAborted_ThrowsAb var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), source, new List { new(feature, []) }); @@ -111,7 +115,7 @@ public async Task UpdateStack_UsingRebase_WhenConflictResolutionAborted_ThrowsAb var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var actions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act var act = async () => await actions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -130,6 +134,7 @@ public async Task UpdateStack_UsingRebase_WhenConflictsResolved_CompletesSuccess var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); var stack = new Config.Stack("Stack1", Some.HttpsUri().ToString(), source, new List { new(feature, []) }); @@ -147,7 +152,7 @@ public async Task UpdateStack_UsingRebase_WhenConflictsResolved_CompletesSuccess var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var actions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act await actions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -188,7 +193,7 @@ public void PullChanges_WhenSomeBranchesHaveChanges_AndOthersDoNot_OnlyPullsChan var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(executionContext.WorkingDirectory).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -207,6 +212,7 @@ public void PullChanges_WhenSomeBranchesDoNotExistInRemote_OnlyPullsBranchesThat var branchThatDoesNotExistInRemote = Some.BranchName(); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -229,7 +235,7 @@ public void PullChanges_WhenSomeBranchesDoNotExistInRemote_OnlyPullsBranchesThat var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(executionContext.WorkingDirectory).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -274,7 +280,7 @@ public void PullChanges_WhenOnlyNonCurrentBranchesBehind_FetchesThem() var worktreePath = "/worktree"; factory.Create(executionContext.WorkingDirectory).Returns(gitClient); factory.Create(worktreePath).Returns(worktreeGitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -290,6 +296,7 @@ public void PullChanges_WhenOnlyCurrentBranchBehind_PullsIt() // Arrange var sourceBranch = Some.BranchName(); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); gitClient.GetCurrentBranch().Returns(sourceBranch); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); @@ -306,7 +313,7 @@ public void PullChanges_WhenOnlyCurrentBranchBehind_PullsIt() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -323,6 +330,7 @@ public void PullChanges_WhenCurrentAndOtherBranchesBehind_PullsCurrentAndFetches var sourceBranch = Some.BranchName(); var otherBranch = Some.BranchName(); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -343,7 +351,7 @@ public void PullChanges_WhenCurrentAndOtherBranchesBehind_PullsCurrentAndFetches var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -360,6 +368,7 @@ public void PullChanges_WhenNoBranchesBehind_DoesNothing() var sourceBranch = Some.BranchName(); var otherBranch = Some.BranchName(); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -376,7 +385,7 @@ public void PullChanges_WhenNoBranchesBehind_DoesNothing() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -396,6 +405,7 @@ public void PullChanges_WhenBranchIsBehind_AndCheckedOutInAnotherWorktree_PullsI var defaultGitClient = Substitute.For(); var worktreeGitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -418,7 +428,7 @@ public void PullChanges_WhenBranchIsBehind_AndCheckedOutInAnotherWorktree_PullsI factory.Create(executionContext.WorkingDirectory).Returns(defaultGitClient); factory.Create(worktreePath).Returns(worktreeGitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -437,6 +447,7 @@ public void PushChanges_WhenSomeLocalBranchesAreAhead_OnlyPushesChangesForBranch var branchNotAheadOfRemote = Some.BranchName(); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -465,7 +476,7 @@ public void PushChanges_WhenSomeLocalBranchesAreAhead_OnlyPushesChangesForBranch var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -483,6 +494,7 @@ public void PushChanges_WhenSomeBranchesDoNotExistInRemote_OnlyPushesBranchesTha var branchThatDoesNotExistInRemoteButIsAhead = Some.BranchName(); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -511,7 +523,7 @@ public void PushChanges_WhenSomeBranchesDoNotExistInRemote_OnlyPushesBranchesTha var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -529,6 +541,7 @@ public void PushChanges_WhenSomeBranchesHaveNoRemoteTrackingBranch_PushesThemAsN var newBranchWithNoRemote = Some.BranchName(); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -562,7 +575,7 @@ public void PushChanges_WhenSomeBranchesHaveNoRemoteTrackingBranch_PushesThemAsN var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -582,6 +595,7 @@ public void PushChanges_WhenMaxBatchSizeIsSmaller_PushesBranchesInMultipleBatche var branch3 = Some.BranchName(); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -612,7 +626,7 @@ public void PushChanges_WhenMaxBatchSizeIsSmaller_PushesBranchesInMultipleBatche var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, maxBatchSize: 2, forceWithLease: false); @@ -632,6 +646,7 @@ public void PushChanges_WhenForceWithLeaseIsTrue_PassesForceWithLeaseParameterTo var branchAhead = Some.BranchName(); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -652,7 +667,7 @@ public void PushChanges_WhenForceWithLeaseIsTrue_PassesForceWithLeaseParameterTo var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, maxBatchSize: 5, forceWithLease: true); @@ -670,6 +685,7 @@ public void PushChanges_WhenNoBranchesNeedToBePushed_DoesNotCallPushMethods() var branchBehind = Some.BranchName(); var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var conflictResolutionDetector = Substitute.For(); @@ -692,7 +708,7 @@ public void PushChanges_WhenNoBranchesNeedToBePushed_DoesNotCallPushMethods() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; var factory = Substitute.For(); factory.Create(Arg.Any()).Returns(gitClient); - var stackActions = new StackActions(factory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, maxBatchSize: 5, forceWithLease: false); @@ -711,6 +727,7 @@ public async Task UpdateStack_UsingMerge_WhenBranchIsInWorktree_UsesWorktreeGitC var worktreePath = $"C:/temp/{Some.Name()}"; var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); var inputProvider = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); @@ -736,7 +753,7 @@ public async Task UpdateStack_UsingMerge_WhenBranchIsInWorktree_UsesWorktreeGitC ); var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" }; - var stackActions = new StackActions(gitClientFactory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); @@ -758,6 +775,8 @@ public async Task UpdateStack_UsingRebase_WhenBranchIsInWorktree_UsesWorktreeGit var worktreePath = $"C:/temp/{Some.Name()}"; var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); + var inputProvider = Substitute.For(); var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); var gitClientFactory = Substitute.For(); @@ -782,7 +801,7 @@ public async Task UpdateStack_UsingRebase_WhenBranchIsInWorktree_UsesWorktreeGit ); var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" }; - var stackActions = new StackActions(gitClientFactory, executionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); @@ -794,4 +813,43 @@ public async Task UpdateStack_UsingRebase_WhenBranchIsInWorktree_UsesWorktreeGit worktreeGitClient.Received(1).RebaseFromLocalSourceBranch(sourceBranch); gitClient.DidNotReceive().ChangeBranch(branchInWorktree); // Should not change branch since it's in a worktree } + + [Fact] + public async Task UpdateStack_WhenCheckingPullRequests_AndGitHubClientIsNotAvailable_Throws() + { + // Arrange + var sourceBranch = Some.BranchName(); + + var gitClient = Substitute.For(); + var gitHubClient = new TestGitHubRepositoryBuilder().NotAvailable().Build(); + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClientFactory = Substitute.For(); + var conflictResolutionDetector = Substitute.For(); + + gitClient.GetCurrentBranch().Returns(sourceBranch); + gitClientFactory.Create(Arg.Any()).Returns(gitClient); + + var branchStatuses = new Dictionary + { + { sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, true, 0, 0, new Commit(Some.Sha(), Some.Name()), null) } + }; + gitClient.GetBranchStatuses(Arg.Any()).Returns(branchStatuses); + + var stack = new Config.Stack( + "Stack1", + Some.HttpsUri().ToString(), + sourceBranch, + [] + ); + + var executionContext = new CliExecutionContext(); + var stackActions = new StackActions(gitClientFactory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + + gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); + + // Act + Assert + await stackActions.Invoking(async a => await a.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None, true)) + .Should().ThrowAsync(); + } } \ No newline at end of file diff --git a/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs index 27a3ab45..fdd28901 100644 --- a/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs @@ -62,7 +62,7 @@ public async Task WhenNameIsProvided_DoesNotAskForName_SyncsCorrectStack() inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any(), Arg.Any(), Arg.Any>()).Returns(Task.FromResult(UpdateStrategy.Merge)); // Act - await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -111,7 +111,7 @@ public async Task WhenNameIsProvided_ButStackDoesNotExist_Throws() // Act and assert var invalidStackName = Some.Name(); - await handler.Invoking(async h => await h.Handle(new SyncStackCommandInputs(invalidStackName, 5, false, false, false, false), CancellationToken.None)) + await handler.Invoking(async h => await h.Handle(new SyncStackCommandInputs(invalidStackName, 5, false, false, false, false, false), CancellationToken.None)) .Should().ThrowAsync() .WithMessage($"Stack '{invalidStackName}' not found."); } @@ -166,7 +166,7 @@ public async Task WhenOnASpecificBranchInTheStack_TheSameBranchIsSetAsCurrentAft inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any(), Arg.Any(), Arg.Any>()).Returns(Task.FromResult(UpdateStrategy.Merge)); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -219,7 +219,7 @@ public async Task WhenOnlyASingleStackExists_DoesNotAskForStackName_SyncsStack() inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any(), Arg.Any(), Arg.Any>()).Returns(Task.FromResult(UpdateStrategy.Merge)); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -276,7 +276,7 @@ public async Task WhenRebaseIsProvided_SyncsStackUsingRebase() inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, true, false, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, true, false, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -332,7 +332,7 @@ public async Task WhenMergeIsProvided_SyncsStackUsingMerge() inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, true, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, true, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -388,7 +388,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndUpdateSettingIsRebase_SyncsS inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -444,7 +444,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndUpdateSettingIsMerge_SyncsSt inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -500,7 +500,7 @@ public async Task WhenGitConfigValueIsSetToMerge_ButRebaseIsSpecified_SyncsStack inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, true, null, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, true, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -556,7 +556,7 @@ public async Task WhenGitConfigValueIsSetToRebase_ButMergeIsSpecified_SyncsStack inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, true, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, true, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -613,7 +613,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndNoUpdateSettingExists_AndMer inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -670,7 +670,7 @@ public async Task WhenNotSpecifyingRebaseOrMerge_AndNoUpdateSettingsExists_AndRe inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, null, null, false, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -699,7 +699,7 @@ public async Task WhenBothRebaseAndMergeAreSpecified_AnErrorIsThrown() // Act and assert await handler - .Invoking(h => h.Handle(new SyncStackCommandInputs(null, 5, true, true, false, false), CancellationToken.None)) + .Invoking(h => h.Handle(new SyncStackCommandInputs(null, 5, true, true, false, false, false), CancellationToken.None)) .Should().ThrowAsync() .WithMessage("Cannot specify both rebase and merge."); } @@ -751,7 +751,7 @@ public async Task WhenConfirmOptionIsProvided_DoesNotAskForConfirmation() inputProvider.Select(Questions.SelectStack, Arg.Any(), Arg.Any()).Returns("Stack1"); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, true, false), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, true, false, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); @@ -809,7 +809,7 @@ public async Task WhenNoPushOptionIsProvided_DoesNotPushChangesToRemote() inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any()).Returns(true); // Act - await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, true), CancellationToken.None); + await handler.Handle(new SyncStackCommandInputs(null, 5, false, false, false, true, false), CancellationToken.None); // Assert stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); diff --git a/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs index d76c482a..a93136cc 100644 --- a/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs @@ -49,7 +49,7 @@ public async Task WhenNameIsProvided_DoesNotAskForName_UpdatesCorrectStack() gitClient.GetCurrentBranch().Returns(branch1); // Act - await handler.Handle(new UpdateStackCommandInputs("Stack1", false, true), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs("Stack1", false, true, false), CancellationToken.None); // Assert await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any(), Arg.Any()); @@ -92,7 +92,7 @@ public async Task WhenNameIsProvided_ButStackDoesNotExist_Throws() // Act and assert var invalidStackName = Some.Name(); - await handler.Invoking(async h => await h.Handle(new UpdateStackCommandInputs(invalidStackName, false, false), CancellationToken.None)) + await handler.Invoking(async h => await h.Handle(new UpdateStackCommandInputs(invalidStackName, false, false, false), CancellationToken.None)) .Should().ThrowAsync() .WithMessage($"Stack '{invalidStackName}' not found."); } @@ -135,7 +135,7 @@ public async Task WhenOnASpecificBranchInTheStack_TheSameBranchIsSetAsCurrentAft inputProvider.Select(Questions.SelectStack, Arg.Any(), Arg.Any()).Returns("Stack1"); // Act - await handler.Handle(new UpdateStackCommandInputs(null, false, true), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, false, true, false), CancellationToken.None); // Assert current branch preserved gitClient.Received().ChangeBranch(branch1); @@ -172,7 +172,7 @@ public async Task WhenOnlyASingleStackExists_DoesNotAskForStackName_UpdatesStack gitClient.GetCurrentBranch().Returns(branch1); // Act - await handler.Handle(new UpdateStackCommandInputs(null, false, true), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, false, true, false), CancellationToken.None); // Assert await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any(), Arg.Any()); @@ -215,7 +215,7 @@ public async Task WhenRebaseIsSpecified_StackIsUpdatedUsingRebase() gitClient.GetCurrentBranch().Returns(branch1); // Act - await handler.Handle(new UpdateStackCommandInputs(null, true, false), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, true, false, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Rebase, Arg.Any()); @@ -258,7 +258,7 @@ public async Task WhenGitConfigValueIsSetToRebase_StackIsUpdatedUsingRebase() gitClient.GetConfigValue("stack.update.strategy").Returns(UpdateStrategy.Rebase.ToString().ToLower()); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, null), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Rebase, Arg.Any()); @@ -300,7 +300,7 @@ public async Task WhenGitConfigValueIsSetToRebase_ButMergeIsSpecified_StackIsUpd gitClient.GetConfigValue("stack.update.strategy").Returns(UpdateStrategy.Rebase.ToString().ToLower()); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, true), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, true, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any()); @@ -343,7 +343,7 @@ public async Task WhenGitConfigValueIsSetToMerge_StackIsUpdatedUsingMerge() gitClient.GetConfigValue("stack.update.strategy").Returns(UpdateStrategy.Merge.ToString().ToLower()); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, null), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any()); @@ -386,7 +386,7 @@ public async Task WhenGitConfigValueIsSetToMerge_ButRebaseIsSpecified_StackIsUpd gitClient.GetConfigValue("stack.update.strategy").Returns(UpdateStrategy.Merge.ToString().ToLower()); // Act (rebase specified overrides config) - await handler.Handle(new UpdateStackCommandInputs(null, true, null), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, true, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Rebase, Arg.Any()); @@ -430,7 +430,7 @@ public async Task WhenGitConfigValueDoesNotExist_AndRebaseIsSelected_StackIsUpda gitClient.GetConfigValue("stack.update.strategy").Returns((string?)null); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, null), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Rebase, Arg.Any()); @@ -474,7 +474,7 @@ public async Task WhenGitConfigValueDoesNotExist_AndMergeIsSelected_StackIsUpdat gitClient.GetConfigValue("stack.update.strategy").Returns((string?)null); // Act - await handler.Handle(new UpdateStackCommandInputs(null, null, null), CancellationToken.None); + await handler.Handle(new UpdateStackCommandInputs(null, null, null, false), CancellationToken.None); // Assert await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any()); @@ -510,7 +510,7 @@ public async Task WhenBothRebaseAndMergeAreSpecified_AnErrorIsThrown() // Act and assert await handler - .Invoking(h => h.Handle(new UpdateStackCommandInputs(null, true, true), CancellationToken.None)) + .Invoking(h => h.Handle(new UpdateStackCommandInputs(null, true, true, false), CancellationToken.None)) .Should().ThrowAsync() .WithMessage("Cannot specify both rebase and merge."); } diff --git a/src/Stack.Tests/Helpers/TestGitHubRepositoryBuilder.cs b/src/Stack.Tests/Helpers/TestGitHubRepositoryBuilder.cs index d6728cfa..3935066b 100644 --- a/src/Stack.Tests/Helpers/TestGitHubRepositoryBuilder.cs +++ b/src/Stack.Tests/Helpers/TestGitHubRepositoryBuilder.cs @@ -5,6 +5,7 @@ namespace Stack.Tests.Helpers; public class TestGitHubRepositoryBuilder { readonly Dictionary pullRequests = new(); + bool available = true; public TestGitHubRepositoryBuilder WithPullRequest(string branch, Action? pullRequestBuilder = null) { @@ -15,9 +16,15 @@ public TestGitHubRepositoryBuilder WithPullRequest(string branch, Action PullRequests) : IGitHubClient +public class TestGitHubRepository(Dictionary PullRequests, bool available) : IGitHubClient { public Dictionary PullRequests { get; } = PullRequests; @@ -98,4 +105,12 @@ public GitHubPullRequest EditPullRequest(GitHubPullRequest pullRequest, string b public void OpenPullRequest(GitHubPullRequest pullRequest) { } + + public void ThrowIfNotAvailable() + { + if (!available) + { + throw new InvalidOperationException("GitHub client not available."); + } + } } \ No newline at end of file diff --git a/src/Stack.Tests/Integration/StackActionsTests.cs b/src/Stack.Tests/Integration/StackActionsTests.cs index bf8effd0..735802df 100644 --- a/src/Stack.Tests/Integration/StackActionsTests.cs +++ b/src/Stack.Tests/Integration/StackActionsTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using NSubstitute; using Meziantou.Extensions.Logging.Xunit; using Stack.Commands.Helpers; using Stack.Git; @@ -24,6 +25,7 @@ public void PullChanges_WhenChangesExistOnSourceAndBranchInStack_PullsChangesCor var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -40,7 +42,7 @@ public void PullChanges_WhenChangesExistOnSourceAndBranchInStack_PullsChangesCor .WithBranch(b => b.WithName(otherBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -69,6 +71,7 @@ public void PullChanges_WhenChangesExistOnWorktreeBranch_PullsChangesCorrectly() var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -87,7 +90,7 @@ public void PullChanges_WhenChangesExistOnWorktreeBranch_PullsChangesCorrectly() .WithBranch(b => b.WithName(worktreeBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -112,6 +115,7 @@ public void PullChanges_WhenLocalBranchHasNoRemoteTrackingBranch_DoesNotPullChan var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -124,7 +128,7 @@ public void PullChanges_WhenLocalBranchHasNoRemoteTrackingBranch_DoesNotPullChan .WithBranch(b => b.WithName(localOnlyBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -148,6 +152,7 @@ public void PullChanges_WhenRemoteTrackingBranchIsDeleted_DoesNotPullChanges() var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -163,7 +168,7 @@ public void PullChanges_WhenRemoteTrackingBranchIsDeleted_DoesNotPullChanges() .WithBranch(b => b.WithName(deletedRemoteBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PullChanges(stack); @@ -191,6 +196,7 @@ public async Task UpdateStack_WhenUpdatingUsingMerge_AndChangesExistOnMultipleBr var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -215,7 +221,7 @@ public async Task UpdateStack_WhenUpdatingUsingMerge_AndChangesExistOnMultipleBr .WithBranch(b => b.WithName(line2Branch1)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Merge, CancellationToken.None); @@ -247,6 +253,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndChangesExistOnMultipleB var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -271,7 +278,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndChangesExistOnMultipleB .WithBranch(b => b.WithName(line2Branch1)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -305,6 +312,7 @@ public async Task UpdateStack_WhenUpdatingUsingMerge_AndBranchCheckedOutInWorktr var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -332,7 +340,7 @@ public async Task UpdateStack_WhenUpdatingUsingMerge_AndBranchCheckedOutInWorktr var worktree = repo.CreateWorktree(childBranch); worktree.Should().NotBeNull(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Merge, CancellationToken.None); @@ -359,6 +367,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndBranchCheckedOutInWorkt var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -386,7 +395,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndBranchCheckedOutInWorkt var worktreePath = repo.CreateWorktree(childBranch); worktreePath.Should().NotBeNull(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -417,6 +426,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndFirstBranchWasSquashMer var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -438,7 +448,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndFirstBranchWasSquashMer .WithChildBranch(e => e.WithName(fourthBranch)))) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -489,6 +499,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndFirstBranchWasSquashMer var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -515,7 +526,7 @@ public async Task UpdateStack_WhenUpdatingUsingRebase_AndFirstBranchWasSquashMer .WithChildBranch(e => e.WithName(fourthBranch)))) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act await stackActions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); @@ -547,6 +558,7 @@ public void PushChanges_WhenChangesExistOnCurrentBranch_PushesChangesCorrectly() var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -565,7 +577,7 @@ public void PushChanges_WhenChangesExistOnCurrentBranch_PushesChangesCorrectly() .WithBranch(b => b.WithName(currentBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -590,6 +602,7 @@ public void PushChanges_WhenChangesExistOnNonCurrentBranch_PushesChangesCorrectl var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -610,7 +623,7 @@ public void PushChanges_WhenChangesExistOnNonCurrentBranch_PushesChangesCorrectl .WithBranch(b => b.WithName(nonCurrentBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -635,6 +648,7 @@ public void PushChanges_WhenChangesExistOnWorktreeBranch_PushesChangesCorrectly( var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -657,7 +671,7 @@ public void PushChanges_WhenChangesExistOnWorktreeBranch_PushesChangesCorrectly( .WithBranch(b => b.WithName(worktreeBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act stackActions.PushChanges(stack, 5, false); @@ -682,6 +696,7 @@ public void PushChanges_WhenLocalOnlyBranchExists_CreatesRemoteTrackingBranch() var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -704,7 +719,7 @@ public void PushChanges_WhenLocalOnlyBranchExists_CreatesRemoteTrackingBranch() .WithBranch(b => b.WithName(localOnlyBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act - should complete without errors and should actually create the remote tracking branch stackActions.PushChanges(stack, 5, false); @@ -735,6 +750,7 @@ public void PushChanges_WhenRemoteTrackingBranchHasBeenDeleted_DoesNotPushChange var logger = XUnitLogger.CreateLogger(testOutputHelper); var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitHubClient = Substitute.For(); var cliExecutionContext = new CliExecutionContext() { WorkingDirectory = repo.LocalDirectoryPath }; var gitClientFactory = new TestGitClientFactory(testOutputHelper); var conflictResolutionDetector = new ConflictResolutionDetector(); @@ -757,7 +773,7 @@ public void PushChanges_WhenRemoteTrackingBranchHasBeenDeleted_DoesNotPushChange .WithBranch(b => b.WithName(deletedRemoteBranch)) .Build(); - var stackActions = new StackActions(gitClientFactory, cliExecutionContext, logger, displayProvider, conflictResolutionDetector); + var stackActions = new StackActions(gitClientFactory, cliExecutionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); // Act - should complete without errors even with deleted remote stackActions.PushChanges(stack, 5, false); diff --git a/src/Stack/Commands/Helpers/StackActions.cs b/src/Stack/Commands/Helpers/StackActions.cs index ed543609..f9ff5236 100644 --- a/src/Stack/Commands/Helpers/StackActions.cs +++ b/src/Stack/Commands/Helpers/StackActions.cs @@ -11,13 +11,14 @@ public interface IStackActions { void PullChanges(Config.Stack stack); void PushChanges(Config.Stack stack, int maxBatchSize, bool forceWithLease); - Task UpdateStack(Config.Stack stack, UpdateStrategy strategy, CancellationToken cancellationToken); + Task UpdateStack(Config.Stack stack, UpdateStrategy strategy, CancellationToken cancellationToken, bool checkPullRequests = false); } public class StackActions( IGitClientFactory gitClientFactory, CliExecutionContext executionContext, + IGitHubClient gitHubClient, ILogger logger, IDisplayProvider displayProvider, IConflictResolutionDetector conflictResolutionDetector) : IStackActions @@ -139,12 +140,17 @@ public void PushChanges( } } - public async Task UpdateStack(Config.Stack stack, UpdateStrategy strategy, CancellationToken cancellationToken) + public async Task UpdateStack(Config.Stack stack, UpdateStrategy strategy, CancellationToken cancellationToken, bool checkPullRequests = false) { var gitClient = GetDefaultGitClient(); List allBranchesInStack = [stack.SourceBranch, .. stack.AllBranchNames]; - var branchStatuses = gitClient.GetBranchStatuses([.. allBranchesInStack]); + + var branchStatuses = await displayProvider.DisplayStatus("Checking status of branches...", async ct => + { + await Task.CompletedTask; + return gitClient.GetBranchStatuses([.. allBranchesInStack]); + }, cancellationToken); if (!branchStatuses.ContainsKey(stack.SourceBranch)) { @@ -152,9 +158,21 @@ public async Task UpdateStack(Config.Stack stack, UpdateStrategy strategy, Cance return; } + var pullRequests = new Dictionary(); + if (checkPullRequests) + { + gitHubClient.ThrowIfNotAvailable(); + + await displayProvider.DisplayStatus("Checking status of pull requests...", async ct => + { + await Task.CompletedTask; + pullRequests = gitHubClient.GetPullRequests(stack.AllBranchNames); + }, cancellationToken); + } + if (strategy == UpdateStrategy.Rebase) { - await UpdateStackUsingRebase(stack, branchStatuses, cancellationToken); + await UpdateStackUsingRebase(stack, branchStatuses, pullRequests, cancellationToken); } else { @@ -239,13 +257,14 @@ private async Task MergeFromSourceBranch(string branch, string sourceBranchName, private async Task UpdateStackUsingRebase( Config.Stack stack, Dictionary branchStatuses, + Dictionary pullRequests, CancellationToken cancellationToken) { logger.UpdatingStackUsingRebase(stack.Name); foreach (var branchLine in stack.GetAllBranchLines()) { - await UpdateBranchLineUsingRebase(stack.Name, stack.SourceBranch, branchLine, branchStatuses, cancellationToken); + await UpdateBranchLineUsingRebase(stack.Name, stack.SourceBranch, branchLine, branchStatuses, pullRequests, cancellationToken); } } @@ -254,6 +273,7 @@ private async Task UpdateBranchLineUsingRebase( string sourceBranchName, List branchLine, Dictionary branchStatuses, + Dictionary pullRequests, CancellationToken cancellationToken) { // @@ -307,8 +327,8 @@ private async Task UpdateBranchLineUsingRebase( // - feature5 onto main (re-parenting as feature1 was squash merged) // logger.RebasingStackForBranchLine(stackName, sourceBranchName, string.Join(" -> ", branchLine.Select(b => b.Name))); - var allBranchesInLine = new List { GetBranchState(sourceBranchName, branchStatuses) }; - allBranchesInLine.AddRange(branchLine.Select(b => GetBranchState(b.Name, branchStatuses))); + var allBranchesInLine = new List { GetBranchState(sourceBranchName, branchStatuses, pullRequests) }; + allBranchesInLine.AddRange(branchLine.Select(b => GetBranchState(b.Name, branchStatuses, pullRequests))); foreach (var branch in branchLine) { @@ -364,14 +384,14 @@ private async Task UpdateBranchLineUsingRebase( } } - private static BranchState GetBranchState(string branchName, Dictionary branchStatuses) + private static BranchState GetBranchState(string branchName, Dictionary branchStatuses, Dictionary? pullRequests) { if (branchStatuses.TryGetValue(branchName, out var status)) { - return new BranchState(branchName, true, status.RemoteBranchExists); + return new BranchState(branchName, status, pullRequests?.GetValueOrDefault(branchName)); } - return new BranchState(branchName, false, false); + return new BranchState(branchName, null, null); } private string? GetCommitShaToReParentFrom(string branchToRebase, string lowestInactiveBranchToReParentFrom, string branchToRebaseOnto) @@ -488,7 +508,12 @@ private async Task RebaseOntoNewParent( } }, cancellationToken); } - private readonly record struct BranchState(string Name, bool Exists, bool IsActive); + private readonly record struct BranchState(string Name, GitBranchStatus? BranchStatus, GitHubPullRequest? PullRequest) + { + public bool Exists => BranchStatus is not null; + public bool RemoteTrackingBranchExists => BranchStatus?.RemoteBranchExists ?? false; + public bool IsActive => RemoteTrackingBranchExists || (PullRequest?.State == GitHubPullRequestStates.Open); + } } } diff --git a/src/Stack/Commands/Helpers/StackHelpers.cs b/src/Stack/Commands/Helpers/StackHelpers.cs index 7c567cab..26894031 100644 --- a/src/Stack/Commands/Helpers/StackHelpers.cs +++ b/src/Stack/Commands/Helpers/StackHelpers.cs @@ -107,7 +107,7 @@ public static List GetStackStatus( ILogger logger, IGitClient gitClient, IGitHubClient gitHubClient, - bool includePullRequestStatus = true) + bool includePullRequestStatus) { var stacksToReturnStatusFor = new List(); @@ -219,7 +219,7 @@ public static StackStatus GetStackStatus( ILogger logger, IGitClient gitClient, IGitHubClient gitHubClient, - bool includePullRequestStatus = true) + bool includePullRequestStatus) { var statuses = GetStackStatus( [stack], @@ -521,7 +521,7 @@ public static async Task GetUpdateStrategy( 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 = GetStackStatus(stack, currentBranch, logger, gitClient, gitHubClient, false); return [.. stackStatus.GetAllBranches().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 760a2f5a..bb8c2019 100644 --- a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs +++ b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs @@ -82,7 +82,8 @@ public override async Task Handle(CreatePullRequestsCommandInputs inputs, Cancel currentBranch, logger, gitClient, - gitHubClient); + gitHubClient, + true); var pullRequestCreateActions = new List(); diff --git a/src/Stack/Commands/Remote/SyncStackCommand.cs b/src/Stack/Commands/Remote/SyncStackCommand.cs index aff68652..b803639d 100644 --- a/src/Stack/Commands/Remote/SyncStackCommand.cs +++ b/src/Stack/Commands/Remote/SyncStackCommand.cs @@ -32,6 +32,7 @@ public SyncStackCommand( Add(CommonOptions.Rebase); Add(CommonOptions.Merge); Add(CommonOptions.Confirm); + Add(CommonOptions.CheckPullRequests); Add(NoPush); } @@ -44,7 +45,8 @@ await handler.Handle( parseResult.GetValue(CommonOptions.Rebase), parseResult.GetValue(CommonOptions.Merge), parseResult.GetValue(CommonOptions.Confirm), - parseResult.GetValue(NoPush)), + parseResult.GetValue(NoPush), + parseResult.GetValue(CommonOptions.CheckPullRequests)), cancellationToken); } } @@ -55,9 +57,10 @@ public record SyncStackCommandInputs( bool? Rebase, bool? Merge, bool Confirm, - bool NoPush) + bool NoPush, + bool CheckPullRequests) { - public static SyncStackCommandInputs Empty => new(null, 5, null, null, false, false); + public static SyncStackCommandInputs Empty => new(null, 5, null, null, false, false, false); } public class SyncStackCommandHandler( @@ -115,7 +118,7 @@ await displayProvider.DisplayStatusWithSuccess("Fetching changes from remote rep logger, gitClient, gitHubClient, - true); + inputs.CheckPullRequests); }, cancellationToken); await StackHelpers.OutputStackStatus(status, outputProvider, cancellationToken); @@ -138,7 +141,7 @@ await displayProvider.DisplayStatusWithSuccess("Pulling changes from remote repo await displayProvider.DisplayStatus("Updating stack...", async (ct) => { - await stackActions.UpdateStack(stack, updateStrategy, ct); + await stackActions.UpdateStack(stack, updateStrategy, ct, inputs.CheckPullRequests); }, cancellationToken); var forceWithLease = updateStrategy == UpdateStrategy.Rebase; diff --git a/src/Stack/Commands/Stack/StackStatusCommand.cs b/src/Stack/Commands/Stack/StackStatusCommand.cs index e483715f..e8c957c9 100644 --- a/src/Stack/Commands/Stack/StackStatusCommand.cs +++ b/src/Stack/Commands/Stack/StackStatusCommand.cs @@ -88,9 +88,9 @@ public class StackStatusCommand : CommandWithOutput Description = "Show status of all stacks." }; - static readonly Option Full = new("--full") + static readonly Option CheckPullRequests = new("--check-pull-requests") { - Description = "Show full status including pull requests." + Description = "Include the status of pull requests in output." }; private readonly StackStatusCommandHandler handler; @@ -106,7 +106,7 @@ public StackStatusCommand( this.handler = handler; Add(CommonOptions.Stack); Add(All); - Add(Full); + Add(CheckPullRequests); } protected override async Task ExecuteAndReturnResponse(ParseResult parseResult, CancellationToken cancellationToken) @@ -115,7 +115,7 @@ protected override async Task ExecuteAndReturnRespon new StackStatusCommandInputs( parseResult.GetValue(CommonOptions.Stack), parseResult.GetValue(All), - parseResult.GetValue(Full)), + parseResult.GetValue(CheckPullRequests)), cancellationToken); } @@ -193,7 +193,7 @@ private static StackStatusCommandJsonOutputBranchDetail MapBranchDetail(BranchDe } } -public record StackStatusCommandInputs(string? Stack, bool All, bool Full); +public record StackStatusCommandInputs(string? Stack, bool All, bool CheckPullRequests); public record StackStatusCommandResponse(List Stacks); public class StackStatusCommandHandler( @@ -255,7 +255,7 @@ public override async Task Handle(StackStatusCommand logger, gitClient, gitHubClient, - inputs.Full); + inputs.CheckPullRequests); }, cancellationToken); return new StackStatusCommandResponse(stackStatusResults); diff --git a/src/Stack/Commands/Stack/UpdateStackCommand.cs b/src/Stack/Commands/Stack/UpdateStackCommand.cs index fce4947f..a30fc79e 100644 --- a/src/Stack/Commands/Stack/UpdateStackCommand.cs +++ b/src/Stack/Commands/Stack/UpdateStackCommand.cs @@ -24,6 +24,7 @@ public UpdateStackCommand( Add(CommonOptions.Stack); Add(CommonOptions.Rebase); Add(CommonOptions.Merge); + Add(CommonOptions.CheckPullRequests); } protected override async Task Execute(ParseResult parseResult, CancellationToken cancellationToken) @@ -32,14 +33,15 @@ await handler.Handle( new UpdateStackCommandInputs( parseResult.GetValue(CommonOptions.Stack), parseResult.GetValue(CommonOptions.Rebase), - parseResult.GetValue(CommonOptions.Merge)), + parseResult.GetValue(CommonOptions.Merge), + parseResult.GetValue(CommonOptions.CheckPullRequests)), cancellationToken); } } -public record UpdateStackCommandInputs(string? Stack, bool? Rebase, bool? Merge) +public record UpdateStackCommandInputs(string? Stack, bool? Rebase, bool? Merge, bool CheckPullRequests) { - public static UpdateStackCommandInputs Empty => new(null, null, null); + public static UpdateStackCommandInputs Empty => new(null, null, null, false); } public record UpdateStackCommandResponse(); @@ -87,7 +89,7 @@ public override async Task Handle(UpdateStackCommandInputs inputs, CancellationT await displayProvider.DisplayStatus("Updating stack...", async (ct) => { - await stackActions.UpdateStack(stack, updateStrategy, cancellationToken); + await stackActions.UpdateStack(stack, updateStrategy, cancellationToken, inputs.CheckPullRequests); }, cancellationToken); if (stack.SourceBranch.Equals(currentBranch, StringComparison.InvariantCultureIgnoreCase) || diff --git a/src/Stack/Git/CachingGitHubClient.cs b/src/Stack/Git/CachingGitHubClient.cs index a6d3661d..8fd8a9df 100644 --- a/src/Stack/Git/CachingGitHubClient.cs +++ b/src/Stack/Git/CachingGitHubClient.cs @@ -48,5 +48,7 @@ public void OpenPullRequest(GitHubPullRequest pullRequest) inner.OpenPullRequest(pullRequest); } + public void ThrowIfNotAvailable() => inner.ThrowIfNotAvailable(); + static string GetCacheKey(string branch) => $"pr:{branch}"; } diff --git a/src/Stack/Git/GitHubClient.cs b/src/Stack/Git/GitHubClient.cs index 8c24a020..c1b25ba9 100644 --- a/src/Stack/Git/GitHubClient.cs +++ b/src/Stack/Git/GitHubClient.cs @@ -48,7 +48,9 @@ public static string GetPullRequestDisplay(this GitHubPullRequest pullRequest) public interface IGitHubClient { + void ThrowIfNotAvailable(); GitHubPullRequest? GetPullRequest(string branch); + Dictionary GetPullRequests(IEnumerable branches) => branches.ToDictionary(b => b, GetPullRequest); GitHubPullRequest CreatePullRequest( string headBranch, string baseBranch, @@ -71,6 +73,23 @@ internal partial class GitHubClientJsonSerializerContext : JsonSerializerContext public class GitHubClient(ILogger logger, CliExecutionContext context) : IGitHubClient { + public bool IsAvailable => ProcessHelpers.DoesCommandExist("gh"); + + public bool IsAuthenticated => IsAvailable && ExecuteGitHubCommandAndReturnOutput("auth status").Contains("Logged in to"); + + public void ThrowIfNotAvailable() + { + if (!IsAvailable) + { + throw new InvalidOperationException("GitHub CLI (gh) is not installed or not available in PATH."); + } + + if (!IsAuthenticated) + { + throw new InvalidOperationException("GitHub CLI (gh) is not authenticated. Please run 'gh auth login' to authenticate."); + } + } + public GitHubPullRequest? GetPullRequest(string branch) { var output = ExecuteGitHubCommandAndReturnOutput($"pr list --json title,number,body,state,url,isDraft,headRefName --head {branch} --state all"); diff --git a/src/Stack/Git/ProcessHelpers.cs b/src/Stack/Git/ProcessHelpers.cs index 4e74388f..95d9fb53 100644 --- a/src/Stack/Git/ProcessHelpers.cs +++ b/src/Stack/Git/ProcessHelpers.cs @@ -86,6 +86,25 @@ public static ProcessExecutionResult ExecuteProcessAndReturnOutput( return result; } + + public static bool DoesCommandExist(string command) + { + var psi = new ProcessStartInfo + { + FileName = Environment.OSVersion.Platform == PlatformID.Win32NT ? "where" : "which", + Arguments = command, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process is null) return false; + + process.WaitForExit(); + return process.ExitCode == 0; + } } public class ProcessException(string message, string filePath, string command, int exitCode) : Exception(message) diff --git a/src/Stack/Git/SafeGitHubClient.cs b/src/Stack/Git/SafeGitHubClient.cs index ed002011..6ee87c19 100644 --- a/src/Stack/Git/SafeGitHubClient.cs +++ b/src/Stack/Git/SafeGitHubClient.cs @@ -38,6 +38,8 @@ public GitHubPullRequest EditPullRequest(GitHubPullRequest pullRequest, string b public void OpenPullRequest(GitHubPullRequest pullRequest) => inner.OpenPullRequest(pullRequest); + + public void ThrowIfNotAvailable() => inner.ThrowIfNotAvailable(); } internal static partial class LoggerExtensionMethods diff --git a/src/Stack/Infrastructure/CommonOptions.cs b/src/Stack/Infrastructure/CommonOptions.cs index b38dbab5..ce0d0d7a 100644 --- a/src/Stack/Infrastructure/CommonOptions.cs +++ b/src/Stack/Infrastructure/CommonOptions.cs @@ -67,4 +67,10 @@ public static class CommonOptions Description = "The name of the parent branch to put the branch under.", Required = false }; + + public static Option CheckPullRequests { get; } = new Option("--check-pull-requests") + { + Description = "Check the status of pull requests when determining if a branch should be included in updating the stack.", + Required = false + }; } \ No newline at end of file From a97b2fb4fb8ac86725e88d4b62acc3a77e25be66 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 26 Sep 2025 08:47:07 +1000 Subject: [PATCH 3/7] Throw if github client isn't available when checking PR statuses --- src/Stack/Commands/Helpers/StackHelpers.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Stack/Commands/Helpers/StackHelpers.cs b/src/Stack/Commands/Helpers/StackHelpers.cs index 26894031..4b6413e8 100644 --- a/src/Stack/Commands/Helpers/StackHelpers.cs +++ b/src/Stack/Commands/Helpers/StackHelpers.cs @@ -109,6 +109,11 @@ public static List GetStackStatus( IGitHubClient gitHubClient, bool includePullRequestStatus) { + if (includePullRequestStatus) + { + gitHubClient.ThrowIfNotAvailable(); + } + var stacksToReturnStatusFor = new List(); var stacksOrderedByCurrentBranch = stacks From a1e0afcc7ea6b551cc4f1c31ca43f06115413509 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 26 Sep 2025 19:12:22 +1000 Subject: [PATCH 4/7] Check PRs when updating using merge --- src/Stack/Commands/Helpers/StackActions.cs | 50 ++++++++++++++-------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/Stack/Commands/Helpers/StackActions.cs b/src/Stack/Commands/Helpers/StackActions.cs index f9ff5236..1f32939d 100644 --- a/src/Stack/Commands/Helpers/StackActions.cs +++ b/src/Stack/Commands/Helpers/StackActions.cs @@ -176,20 +176,21 @@ await displayProvider.DisplayStatus("Checking status of pull requests...", async } else { - await UpdateStackUsingMerge(stack, branchStatuses, cancellationToken); + await UpdateStackUsingMerge(stack, branchStatuses, pullRequests, cancellationToken); } } private async Task UpdateStackUsingMerge( Config.Stack stack, Dictionary branchStatuses, + Dictionary pullRequests, CancellationToken cancellationToken) { logger.UpdatingStackUsingMerge(stack.Name); foreach (var branchLine in stack.GetAllBranchLines()) { - await UpdateBranchLineUsingMerge(branchLine, stack.SourceBranch, branchStatuses, cancellationToken); + await UpdateBranchLineUsingMerge(branchLine, stack.SourceBranch, branchStatuses, pullRequests, cancellationToken); } } @@ -197,12 +198,14 @@ private async Task UpdateBranchLineUsingMerge( List branchLine, string parentBranchName, Dictionary branchStatuses, + Dictionary pullRequests, CancellationToken cancellationToken) { var currentParentBranch = parentBranchName; foreach (var branch in branchLine) { - if (IsBranchActive(branch.Name, branchStatuses)) + var branchState = GetBranchState(branch.Name, branchStatuses, pullRequests); + if (branchState.IsActive) { await MergeFromSourceBranch(branch.Name, currentParentBranch, branchStatuses, cancellationToken); currentParentBranch = branch.Name; @@ -214,11 +217,6 @@ private async Task UpdateBranchLineUsingMerge( } } - private static bool IsBranchActive(string branchName, Dictionary branchStatuses) - { - return branchStatuses.TryGetValue(branchName, out var status) && status.RemoteBranchExists; - } - private async Task MergeFromSourceBranch(string branch, string sourceBranchName, Dictionary branchStatuses, CancellationToken cancellationToken) { logger.MergingBranch(sourceBranchName, branch); @@ -327,8 +325,7 @@ private async Task UpdateBranchLineUsingRebase( // - feature5 onto main (re-parenting as feature1 was squash merged) // logger.RebasingStackForBranchLine(stackName, sourceBranchName, string.Join(" -> ", branchLine.Select(b => b.Name))); - var allBranchesInLine = new List { GetBranchState(sourceBranchName, branchStatuses, pullRequests) }; - allBranchesInLine.AddRange(branchLine.Select(b => GetBranchState(b.Name, branchStatuses, pullRequests))); + List allBranchesInLine = [GetBranchState(sourceBranchName, branchStatuses, pullRequests), .. branchLine.Select(b => GetBranchState(b.Name, branchStatuses, pullRequests))]; foreach (var branch in branchLine) { @@ -386,12 +383,7 @@ private async Task UpdateBranchLineUsingRebase( private static BranchState GetBranchState(string branchName, Dictionary branchStatuses, Dictionary? pullRequests) { - if (branchStatuses.TryGetValue(branchName, out var status)) - { - return new BranchState(branchName, status, pullRequests?.GetValueOrDefault(branchName)); - } - - return new BranchState(branchName, null, null); + return new BranchState(branchName, branchStatuses.GetValueOrDefault(branchName), pullRequests?.GetValueOrDefault(branchName)); } private string? GetCommitShaToReParentFrom(string branchToRebase, string lowestInactiveBranchToReParentFrom, string branchToRebaseOnto) @@ -512,7 +504,31 @@ private readonly record struct BranchState(string Name, GitBranchStatus? BranchS { public bool Exists => BranchStatus is not null; public bool RemoteTrackingBranchExists => BranchStatus?.RemoteBranchExists ?? false; - public bool IsActive => RemoteTrackingBranchExists || (PullRequest?.State == GitHubPullRequestStates.Open); + public bool IsActive + { + get + { + if (BranchStatus is null) + { + return false; + } + + if (BranchStatus.RemoteTrackingBranchName is null) + { + // Branch has never been pushed to remote, consider it active + return true; + } + + if (!RemoteTrackingBranchExists) + { + // Remote tracking branch doesn't exist, consider it inactive + return false; + } + + // If there's no associated pull request, or if the pull request is not merged, consider it active + return PullRequest is null || PullRequest.State != GitHubPullRequestStates.Merged; + } + } } } } From 0e58bf5b9e902d420d84f8230ad2ae3418651551 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 26 Sep 2025 19:40:47 +1000 Subject: [PATCH 5/7] More tests --- .../Commands/Helpers/StackActionsTests.cs | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs index e331e1b3..d1cf6b70 100644 --- a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs +++ b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs @@ -87,6 +87,92 @@ public async Task UpdateStack_UsingMerge_WhenConflictsResolved_CompletesSuccessf gitClient.Received().ChangeBranch(feature); } + [Fact] + public async Task UpdateStack_UsingMerge_WhenBranchHasMergedPullRequest_SkipsBranch() + { + // Arrange + var sourceBranch = Some.BranchName(); + var inactiveBranch = Some.BranchName(); + + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var gitHubClient = new TestGitHubRepositoryBuilder() + .WithPullRequest(inactiveBranch, pr => pr.Merged()) + .Build(); + var conflictResolutionDetector = Substitute.For(); + + var branchStatuses = new Dictionary + { + { sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, true, 0, 0, new Commit(Some.Sha(), Some.Name())) }, + { inactiveBranch, new GitBranchStatus(inactiveBranch, $"origin/{inactiveBranch}", true, false, 0, 0, new Commit(Some.Sha(), Some.Name())) } + }; + + gitClient.GetBranchStatuses(Arg.Any()).Returns(branchStatuses); + + var stack = new Config.Stack( + "Stack1", + Some.HttpsUri().ToString(), + sourceBranch, + new List { new(inactiveBranch, []) }); + + var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; + var factory = Substitute.For(); + factory.Create(executionContext.WorkingDirectory).Returns(gitClient); + factory.Create(Arg.Any()).Returns(gitClient); + + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + + // Act + await actions.UpdateStack(stack, UpdateStrategy.Merge, CancellationToken.None, true); + + // Assert + gitClient.DidNotReceive().ChangeBranch(inactiveBranch); + gitClient.DidNotReceive().MergeFromLocalSourceBranch(Arg.Any()); + } + + [Fact] + public async Task UpdateStack_UsingMerge_WhenBranchHasNoRemoteTrackingBranch_IsUpdated() + { + // Arrange + var sourceBranch = Some.BranchName(); + var localOnlyBranch = Some.BranchName(); + + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); + var conflictResolutionDetector = Substitute.For(); + + var branchStatuses = new Dictionary + { + { sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, true, 0, 0, new Commit(Some.Sha(), Some.Name())) }, + { localOnlyBranch, new GitBranchStatus(localOnlyBranch, null, false, false, 0, 0, new Commit(Some.Sha(), Some.Name())) } + }; + + gitClient.GetBranchStatuses(Arg.Any()).Returns(branchStatuses); + + var stack = new Config.Stack( + "Stack1", + Some.HttpsUri().ToString(), + sourceBranch, + new List { new(localOnlyBranch, []) }); + + var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; + var factory = Substitute.For(); + factory.Create(executionContext.WorkingDirectory).Returns(gitClient); + factory.Create(Arg.Any()).Returns(gitClient); + + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + + // Act + await actions.UpdateStack(stack, UpdateStrategy.Merge, CancellationToken.None); + + // Assert + gitClient.Received(1).ChangeBranch(localOnlyBranch); + gitClient.Received(1).MergeFromLocalSourceBranch(sourceBranch); + } + [Fact] public async Task UpdateStack_UsingRebase_WhenConflictResolutionAborted_ThrowsAbortException() { @@ -161,6 +247,93 @@ public async Task UpdateStack_UsingRebase_WhenConflictsResolved_CompletesSuccess gitClient.Received().ChangeBranch(feature); } + [Fact] + public async Task UpdateStack_UsingRebase_WhenBranchHasMergedPullRequest_SkipsBranch() + { + // Arrange + var sourceBranch = Some.BranchName(); + var inactiveBranch = Some.BranchName(); + + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var gitHubClient = new TestGitHubRepositoryBuilder() + .WithPullRequest(inactiveBranch, pr => pr.Merged()) + .Build(); + var conflictResolutionDetector = Substitute.For(); + + var branchStatuses = new Dictionary + { + { sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, true, 0, 0, new Commit(Some.Sha(), Some.Name())) }, + { inactiveBranch, new GitBranchStatus(inactiveBranch, $"origin/{inactiveBranch}", true, false, 0, 0, new Commit(Some.Sha(), Some.Name())) } + }; + + gitClient.GetBranchStatuses(Arg.Any()).Returns(branchStatuses); + + var stack = new Config.Stack( + "Stack1", + Some.HttpsUri().ToString(), + sourceBranch, + new List { new(inactiveBranch, []) }); + + var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; + var factory = Substitute.For(); + factory.Create(executionContext.WorkingDirectory).Returns(gitClient); + factory.Create(Arg.Any()).Returns(gitClient); + + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + + // Act + await actions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None, true); + + // Assert + gitClient.DidNotReceive().ChangeBranch(inactiveBranch); + gitClient.DidNotReceive().RebaseFromLocalSourceBranch(Arg.Any()); + gitClient.DidNotReceive().RebaseOntoNewParent(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task UpdateStack_UsingRebase_WhenBranchHasNoRemoteTrackingBranch_IsUpdated() + { + // Arrange + var sourceBranch = Some.BranchName(); + var localOnlyBranch = Some.BranchName(); + + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); + var conflictResolutionDetector = Substitute.For(); + + var branchStatuses = new Dictionary + { + { sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, true, 0, 0, new Commit(Some.Sha(), Some.Name())) }, + { localOnlyBranch, new GitBranchStatus(localOnlyBranch, null, false, false, 0, 0, new Commit(Some.Sha(), Some.Name())) } + }; + + gitClient.GetBranchStatuses(Arg.Any()).Returns(branchStatuses); + + var stack = new Config.Stack( + "Stack1", + Some.HttpsUri().ToString(), + sourceBranch, + new List { new(localOnlyBranch, []) }); + + var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; + var factory = Substitute.For(); + factory.Create(executionContext.WorkingDirectory).Returns(gitClient); + factory.Create(Arg.Any()).Returns(gitClient); + + var actions = new StackActions(factory, executionContext, gitHubClient, logger, displayProvider, conflictResolutionDetector); + + // Act + await actions.UpdateStack(stack, UpdateStrategy.Rebase, CancellationToken.None); + + // Assert + gitClient.Received(1).ChangeBranch(localOnlyBranch); + gitClient.Received(1).RebaseFromLocalSourceBranch(sourceBranch); + } + [Fact] public void PullChanges_WhenSomeBranchesHaveChanges_AndOthersDoNot_OnlyPullsChangesForBranchesThatNeedIt() { From 023f95df59752b200cb71daf5a064da7337d2368 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 29 Sep 2025 20:11:01 +1000 Subject: [PATCH 6/7] Add more tests --- .../Commands/Helpers/StackActionsTests.cs | 1 - .../Remote/SyncStackCommandHandlerTests.cs | 106 ++++++++++++++++++ .../Stack/UpdateStackCommandHandlerTests.cs | 78 +++++++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) diff --git a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs index d1cf6b70..42c09c1b 100644 --- a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs +++ b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs @@ -1,4 +1,3 @@ -using System.Net.Http.Headers; using FluentAssertions; using Meziantou.Extensions.Logging.Xunit; using NSubstitute; diff --git a/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs index fdd28901..f307bf2d 100644 --- a/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Remote/SyncStackCommandHandlerTests.cs @@ -817,4 +817,110 @@ public async Task WhenNoPushOptionIsProvided_DoesNotPushChangesToRemote() stackActions.DidNotReceive().PushChanges(Arg.Any(), Arg.Any(), Arg.Any()); gitClient.Received().ChangeBranch(branch1); } + + [Fact] + public async Task WhenCheckPullRequestsIsTrue_UpdatesStackWithCheckPullRequestsEnabled() + { + // Arrange + var sourceBranch = Some.BranchName(); + var branch1 = Some.BranchName(); + var branch2 = Some.BranchName(); + var remoteUri = Some.HttpsUri().ToString(); + + var stackConfig = new TestStackConfigBuilder() + .WithStack(stack => stack + .WithName("Stack1") + .WithRemoteUri(remoteUri) + .WithSourceBranch(sourceBranch) + .WithBranch(stackBranch => stackBranch.WithName(branch1)) + .WithBranch(stackBranch => stackBranch.WithName(branch2))) + .Build(); + var inputProvider = Substitute.For(); + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); + var stackActions = Substitute.For(); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClientFactory = Substitute.For(); + var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" }; + var outputProvider = Substitute.For(); + var handler = new SyncStackCommandHandler(inputProvider, logger, outputProvider, displayProvider, gitClientFactory, executionContext, gitHubClient, stackConfig, stackActions); + + gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); + + gitClient.GetRemoteUri().Returns(remoteUri); + gitClient.GetCurrentBranch().Returns(branch1); + gitClient.GetBranchStatuses(Arg.Any()).Returns(new Dictionary + { + { sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, false, 0, 0, new Commit("abc1234", "Test commit message")) }, + { branch1, new GitBranchStatus(branch1, $"origin/{branch1}", true, true, 0, 0, new Commit("abc1234", "Test commit message")) }, + { branch2, new GitBranchStatus(branch2, $"origin/{branch2}", true, false, 0, 0, new Commit("def5678", "Test commit message")) } + }); + gitClient.GetConfigValue("stack.update.strategy").Returns((string?)null); + + inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any(), Arg.Any(), Arg.Any>()).Returns(Task.FromResult(UpdateStrategy.Merge)); + + // Act + await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, false, false, true), CancellationToken.None); + + // Assert + stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); + await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any(), true); + stackActions.Received().PushChanges(Arg.Is(s => s.Name == "Stack1"), 5, false); + gitClient.Received().ChangeBranch(branch1); + } + + [Fact] + public async Task WhenCheckPullRequestsIsFalse_UpdatesStackWithCheckPullRequestsDisabled() + { + // Arrange + var sourceBranch = Some.BranchName(); + var branch1 = Some.BranchName(); + var branch2 = Some.BranchName(); + var remoteUri = Some.HttpsUri().ToString(); + + var stackConfig = new TestStackConfigBuilder() + .WithStack(stack => stack + .WithName("Stack1") + .WithRemoteUri(remoteUri) + .WithSourceBranch(sourceBranch) + .WithBranch(stackBranch => stackBranch.WithName(branch1)) + .WithBranch(stackBranch => stackBranch.WithName(branch2))) + .Build(); + var inputProvider = Substitute.For(); + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var gitClient = Substitute.For(); + var gitHubClient = Substitute.For(); + var stackActions = Substitute.For(); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClientFactory = Substitute.For(); + var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" }; + var outputProvider = Substitute.For(); + var handler = new SyncStackCommandHandler(inputProvider, logger, outputProvider, displayProvider, gitClientFactory, executionContext, gitHubClient, stackConfig, stackActions); + + gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); + + gitClient.GetRemoteUri().Returns(remoteUri); + gitClient.GetCurrentBranch().Returns(branch1); + gitClient.GetBranchStatuses(Arg.Any()).Returns(new Dictionary + { + { sourceBranch, new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, false, 0, 0, new Commit("abc1234", "Test commit message")) }, + { branch1, new GitBranchStatus(branch1, $"origin/{branch1}", true, true, 0, 0, new Commit("abc1234", "Test commit message")) }, + { branch2, new GitBranchStatus(branch2, $"origin/{branch2}", true, false, 0, 0, new Commit("def5678", "Test commit message")) } + }); + gitClient.GetConfigValue("stack.update.strategy").Returns((string?)null); + + inputProvider.Confirm(Questions.ConfirmSyncStack, Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + inputProvider.Select(Questions.SelectUpdateStrategy, Arg.Any(), Arg.Any(), Arg.Any>()).Returns(Task.FromResult(UpdateStrategy.Merge)); + + // Act + await handler.Handle(new SyncStackCommandInputs("Stack1", 5, false, false, false, false, false), CancellationToken.None); + + // Assert + stackActions.Received().PullChanges(Arg.Is(s => s.Name == "Stack1")); + await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any(), false); + stackActions.Received().PushChanges(Arg.Is(s => s.Name == "Stack1"), 5, false); + gitClient.Received().ChangeBranch(branch1); + } } diff --git a/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs index a93136cc..79d8638e 100644 --- a/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs @@ -480,6 +480,84 @@ public async Task WhenGitConfigValueDoesNotExist_AndMergeIsSelected_StackIsUpdat await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any()); } + [Fact] + public async Task WhenCheckPullRequestsIsTrue_StackIsUpdatedWithCheckPullRequestsEnabled() + { + // Arrange + var sourceBranch = Some.BranchName(); + var branch1 = Some.BranchName(); + var branch2 = Some.BranchName(); + var remoteUri = Some.HttpsUri().ToString(); + + var stackConfig = new TestStackConfigBuilder() + .WithStack(stack => stack + .WithName("Stack1") + .WithRemoteUri(remoteUri) + .WithSourceBranch(sourceBranch) + .WithBranch(b1 => b1.WithName(branch1).WithChildBranch(b2 => b2.WithName(branch2)))) + .Build(); + var inputProvider = Substitute.For(); + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var stackActions = Substitute.For(); + var gitClientFactory = Substitute.For(); + var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" }; + var handler = new UpdateStackCommandHandler(inputProvider, logger, displayProvider, gitClientFactory, executionContext, stackConfig, stackActions); + + gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); + + inputProvider.Select(Questions.SelectStack, Arg.Any(), Arg.Any()).Returns("Stack1"); + gitClient.GetRemoteUri().Returns(remoteUri); + gitClient.GetCurrentBranch().Returns(branch1); + gitClient.GetConfigValue("stack.update.strategy").Returns((string?)null); + + // Act + await handler.Handle(new UpdateStackCommandInputs(null, null, null, true), CancellationToken.None); + + // Assert + await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any(), true); + } + + [Fact] + public async Task WhenCheckPullRequestsIsFalse_StackIsUpdatedWithCheckPullRequestsDisabled() + { + // Arrange + var sourceBranch = Some.BranchName(); + var branch1 = Some.BranchName(); + var branch2 = Some.BranchName(); + var remoteUri = Some.HttpsUri().ToString(); + + var stackConfig = new TestStackConfigBuilder() + .WithStack(stack => stack + .WithName("Stack1") + .WithRemoteUri(remoteUri) + .WithSourceBranch(sourceBranch) + .WithBranch(b1 => b1.WithName(branch1).WithChildBranch(b2 => b2.WithName(branch2)))) + .Build(); + var inputProvider = Substitute.For(); + var logger = XUnitLogger.CreateLogger(testOutputHelper); + var displayProvider = new TestDisplayProvider(testOutputHelper); + var gitClient = Substitute.For(); + var stackActions = Substitute.For(); + var gitClientFactory = Substitute.For(); + var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" }; + var handler = new UpdateStackCommandHandler(inputProvider, logger, displayProvider, gitClientFactory, executionContext, stackConfig, stackActions); + + gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); + + inputProvider.Select(Questions.SelectStack, Arg.Any(), Arg.Any()).Returns("Stack1"); + gitClient.GetRemoteUri().Returns(remoteUri); + gitClient.GetCurrentBranch().Returns(branch1); + gitClient.GetConfigValue("stack.update.strategy").Returns((string?)null); + + // Act + await handler.Handle(new UpdateStackCommandInputs(null, null, null, false), CancellationToken.None); + + // Assert + await stackActions.Received().UpdateStack(Arg.Is(s => s.Name == "Stack1"), UpdateStrategy.Merge, Arg.Any(), false); + } + [Fact] public async Task WhenBothRebaseAndMergeAreSpecified_AnErrorIsThrown() { From f335798050d3dbce2294a792f38c859d17ddaccf Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Mon, 29 Sep 2025 20:42:54 +1000 Subject: [PATCH 7/7] Improve description --- README.md | 2 +- src/Stack/Infrastructure/CommonOptions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index df491f13..9562d082 100644 --- a/README.md +++ b/README.md @@ -453,7 +453,7 @@ Options: --rebase Use rebase when updating the stack. Overrides any setting in Git configuration. --merge Use merge when updating the stack. Overrides any setting in Git configuration. -y, --yes Confirm the command without prompting. - --check-pull-requests Check the status of pull requests when determining if a branch should be included in updating the stack. + --check-pull-requests Check the status of pull requests as part of determining if a branch should be included when updating the stack. --no-push Don't push changes to the remote repository -?, -h, --help Show help and usage information ``` diff --git a/src/Stack/Infrastructure/CommonOptions.cs b/src/Stack/Infrastructure/CommonOptions.cs index ce0d0d7a..2be312ee 100644 --- a/src/Stack/Infrastructure/CommonOptions.cs +++ b/src/Stack/Infrastructure/CommonOptions.cs @@ -70,7 +70,7 @@ public static class CommonOptions public static Option CheckPullRequests { get; } = new Option("--check-pull-requests") { - Description = "Check the status of pull requests when determining if a branch should be included in updating the stack.", + Description = "Check the status of pull requests as part of determining if a branch should be included when updating the stack.", Required = false }; } \ No newline at end of file