diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a1a1ae77..8f0742bf 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,7 +7,7 @@ Purpose: Enable AI agents to quickly understand and extend the `stack` CLI (bran - **Entry**: `Program.cs` builds DI host then invokes `StackRootCommand` (System.CommandLine pattern). - **Command Structure**: Each feature = command class inheriting `Infrastructure/Commands/Command.cs` (adds common options, logging, error handling, input prompts). Command handlers are split: command class parses & constructs handler (e.g. `NewStackCommand` + `NewStackCommandHandler`). Follow this separation religiously. - **Core Domains**: - 1. **Config (`Config/`)**: Persistent model (`Stack`, `Branch`) with schema migration v1→v2 in `FileStackConfig`. Config path is `%AppData%/stack/config.json` (Windows). v1 = linear list, v2 = tree structure. Backward compatibility is critical—use existing mapping helpers. **Stack Repository Pattern**: Command handlers access stacks through `IStackRepository` (scoped service) instead of `IStackConfig` directly. Repository automatically filters stacks by current git remote URI—handlers don't need to know about remote filtering. See "Stack Repository Pattern" section below. + 1. **Config (`Config/`)**: Persistent model (`Stack`, `Branch`) with schema migration v1→v2 in `FileStackDataStore`. Config path is `%AppData%/stack/config.json` (Windows). v1 = linear list, v2 = tree structure. Backward compatibility is critical—use existing mapping helpers. **Stack Repository Pattern**: Command handlers access stacks through `IStackRepository` (scoped service) instead of `IStackConfig` directly. Repository automatically filters stacks by current git remote URI—handlers don't need to know about remote filtering. See "Stack Repository Pattern" section below. 2. **Git (`Git/`)**: Thin wrapper over `git` & `gh` CLIs via `ProcessHelpers`. Never re-implement git porcelain—compose existing commands. Conflict detection surfaces as `ConflictException`. 3. **Stack orchestration (`Commands/Helpers/StackActions.cs` & `StackHelpers.cs`)**: Implements update strategies (merge vs rebase), batch operations, PR list maintenance, status tree rendering (Spectre.Console). Pull request status lookups are best-effort and may be skipped if the GitHub CLI is unavailable. 4. **GitHub integration (`GitHubClient`, `SafeGitHubClient`, `CachingGitHubClient`)**: Uses `gh pr` CLI JSON output. `SafeGitHubClient` swallows failures (missing CLI, auth, network) for status lookups only (`GetPullRequest`) and logs a single warning, ensuring commands still succeed; create/edit/open operations still propagate errors. Extend by adding fields to the source gen context. @@ -38,7 +38,7 @@ Purpose: Enable AI agents to quickly understand and extend the `stack` CLI (bran 1. **Create Command Class**: `Commands//Command.cs` inheriting `Command` base class. 2. **Define Options**: Define any new `Option` locally; reuse existing ones from `CommonOptions` static properties. 3. **Wire Dependencies**: In constructor, `Add(...)` options and set description; register in `StackRootCommand` constructor. -4. **Implement Execute**: Override `Execute` method. Inject `IStackRepository` (instead of `IStackConfig`), `IGitClientFactory`, and `CliExecutionContext` via handler constructor. +4. **Implement Execute**: Override `Execute` method. Inject `IStackRepository` (instead of `IStackDataStore`), `IGitClientFactory`, and `CliExecutionContext` via handler constructor. 5. **Separate Handler Logic**: Put non-trivial business logic in separate handler class (suffix `CommandHandler`) inheriting `CommandHandlerBase` to keep command class thin and testable. 6. **Exception Handling**: Follow existing patterns—let `ProcessException` bubble for standardized error output via base `Command` class. diff --git a/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs b/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs index 71b46d62..16478910 100644 --- a/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Branch/AddBranchCommandHandlerTests.cs @@ -54,8 +54,8 @@ public async Task WhenNoInputsProvided_AsksForStackAndBranchAndParentBranchAndCo // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(firstBranch, [new Model.Branch(childBranch, []), new Model.Branch(branchToAdd, [])])]), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, [new Model.Branch(firstBranch, [new Model.Branch(childBranch, []), new Model.Branch(branchToAdd, [])])]), + new("Stack2", sourceBranch, []) }); } @@ -96,8 +96,8 @@ public async Task WhenStackNameProvided_DoesNotAskForStackName_AddsBranchFromSta await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any(), Arg.Any()); stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(branchToAdd, [])])]), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(branchToAdd, [])])]), + new("Stack2", sourceBranch, []) }); } @@ -137,7 +137,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_AddsBranchFromSt await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any(), Arg.Any()); stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(branchToAdd, [])])]) + new("Stack1", sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(branchToAdd, [])])]) }); } @@ -212,8 +212,8 @@ public async Task WhenBranchNameProvided_DoesNotAskForBranchName_AddsBranchFromS // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(branchToAdd, [])])]), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(branchToAdd, [])])]), + new("Stack2", sourceBranch, []) }); await inputProvider.DidNotReceive().Select(Questions.SelectBranch, Arg.Any(), Arg.Any()); } @@ -327,8 +327,8 @@ public async Task WhenAllInputsProvided_DoesNotAskForAnything_AddsBranchFromStac // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(branchToAdd, [])])]), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(branchToAdd, [])])]), + new("Stack2", sourceBranch, []) }); inputProvider.ReceivedCalls().Should().BeEmpty(); } @@ -372,8 +372,8 @@ public async Task WhenParentBranchProvided_DoesNotAskForParentBranch_CreatesNewB // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(firstBranch, [new Model.Branch(childBranch, []), new Model.Branch(branchToAdd, [])])]), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, [new Model.Branch(firstBranch, [new Model.Branch(childBranch, []), new Model.Branch(branchToAdd, [])])]), + new("Stack2", sourceBranch, []) }); await inputProvider.DidNotReceive().Select(Questions.SelectParentBranch, Arg.Any(), Arg.Any()); diff --git a/src/Stack.Tests/Commands/Branch/MoveBranchCommandHandlerTests.cs b/src/Stack.Tests/Commands/Branch/MoveBranchCommandHandlerTests.cs index c6e36c57..ce0dacbe 100644 --- a/src/Stack.Tests/Commands/Branch/MoveBranchCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Branch/MoveBranchCommandHandlerTests.cs @@ -52,7 +52,7 @@ public async Task WhenMovingBranchWithoutChildren_MovesBranchToNewParent() // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [ + new("Stack1", sourceBranch, [ new Model.Branch(firstBranch, [new Model.Branch(branchToMove, [])]), new Model.Branch(secondBranch, []) ]) @@ -100,7 +100,7 @@ public async Task WhenMovingBranchWithChildren_AndMoveChildrenOption_MovesBranch // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [ + new("Stack1", sourceBranch, [ new Model.Branch(firstBranch, [new Model.Branch(branchToMove, [new Model.Branch(childBranch, [])])]) ]) }); @@ -147,7 +147,7 @@ public async Task WhenMovingBranchWithChildren_AndReParentChildrenOption_MovesBr // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [ + new("Stack1", sourceBranch, [ new Model.Branch(firstBranch, [new Model.Branch(branchToMove, [])]), new Model.Branch(childBranch, []) ]) @@ -191,7 +191,7 @@ public async Task WhenMovingBranchToSourceBranch_MovesBranchToRootLevel() // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [ + new("Stack1", sourceBranch, [ new Model.Branch(firstBranch, []), new Model.Branch(branchToMove, []) ]) @@ -231,7 +231,7 @@ public async Task WhenAllInputsProvided_DoesNotPromptUser() // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [ + new("Stack1", sourceBranch, [ new Model.Branch(firstBranch, [new Model.Branch(branchToMove, [])]) ]) }); diff --git a/src/Stack.Tests/Commands/Branch/NewBranchCommandHandlerTests.cs b/src/Stack.Tests/Commands/Branch/NewBranchCommandHandlerTests.cs index 3c30e08b..e18d5dae 100644 --- a/src/Stack.Tests/Commands/Branch/NewBranchCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Branch/NewBranchCommandHandlerTests.cs @@ -53,8 +53,8 @@ public async Task WhenNoInputsProvided_AsksForStackAndBranchAndParentBranch_Crea // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(firstBranch, [new Model.Branch(childBranch, []), new Model.Branch(newBranch, [])])]), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, [new Model.Branch(firstBranch, [new Model.Branch(childBranch, []), new Model.Branch(newBranch, [])])]), + new("Stack2", sourceBranch, []) }); gitClient.Received().CreateNewBranch(newBranch, firstBranch); gitClient.Received().ChangeBranch(newBranch); @@ -99,8 +99,8 @@ public async Task WhenStackNameProvided_DoesNotAskForStackName_CreatesAndAddsBra await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any(), Arg.Any()); stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(newBranch, [])])]), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(newBranch, [])])]), + new("Stack2", sourceBranch, []) }); } @@ -140,7 +140,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_CreatesAndAddsBr await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any(), Arg.Any()); stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(newBranch, [])])]), + new("Stack1", sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(newBranch, [])])]), }); } @@ -218,8 +218,8 @@ public async Task WhenBranchNameProvided_DoesNotAskForBranchName_CreatesAndAddsB // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(newBranch, [])])]), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(newBranch, [])])]), + new("Stack2", sourceBranch, []) }); await inputProvider.DidNotReceive().Text(Questions.BranchName, Arg.Any(), Arg.Any()); } @@ -341,8 +341,8 @@ public async Task WhenPushToTheRemoteFails_StillCreatesTheBranchLocallyAndAddsIt // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(newBranch, [])])]), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, [new Model.Branch(anotherBranch, [new Model.Branch(newBranch, [])])]), + new("Stack2", sourceBranch, []) }); gitClient.Received().CreateNewBranch(newBranch, anotherBranch); gitClient.Received().ChangeBranch(newBranch); @@ -388,8 +388,8 @@ public async Task WhenParentBranchNotProvided_AsksForParentBranch_CreatesNewBran // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(firstBranch, [new Model.Branch(childBranch, []), new Model.Branch(newBranch, [])])]), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, [new Model.Branch(firstBranch, [new Model.Branch(childBranch, []), new Model.Branch(newBranch, [])])]), + new("Stack2", sourceBranch, []) }); gitClient.Received().CreateNewBranch(newBranch, firstBranch); gitClient.Received().ChangeBranch(newBranch); @@ -434,8 +434,8 @@ public async Task WhenParentBranchProvided_DoesNotAskForParentBranch_CreatesNewB // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(firstBranch, [new Model.Branch(childBranch, []), new Model.Branch(newBranch, [])])]), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, [new Model.Branch(firstBranch, [new Model.Branch(childBranch, []), new Model.Branch(newBranch, [])])]), + new("Stack2", sourceBranch, []) }); gitClient.Received().CreateNewBranch(newBranch, firstBranch); gitClient.Received().ChangeBranch(newBranch); diff --git a/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs b/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs index 48afe34e..5e29f4f0 100644 --- a/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Branch/RemoveBranchCommandHandlerTests.cs @@ -51,8 +51,8 @@ public async Task WhenNoInputsProvided_AsksForAllInputsAndConfirms_RemovesBranch // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new Model.Stack("Stack1", stackRepository.RemoteUri, sourceBranch, []), - new Model.Stack("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new Model.Stack("Stack1", sourceBranch, []), + new Model.Stack("Stack2", sourceBranch, []) }); } @@ -91,8 +91,8 @@ public async Task WhenStackNameProvided_DoesNotAskForStackName_RemovesBranchFrom await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any(), Arg.Any()); stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, []), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, []), + new("Stack2", sourceBranch, []) }); } @@ -163,8 +163,8 @@ public async Task WhenBranchNameProvided_DoesNotAskForBranchName_RemovesBranchFr // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, []), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, []), + new("Stack2", sourceBranch, []) }); await inputProvider.DidNotReceive().Select(Questions.SelectBranch, Arg.Any(), Arg.Any()); } @@ -235,7 +235,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName() // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, []) }); await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any(), Arg.Any()); @@ -275,8 +275,8 @@ public async Task WhenConfirmProvided_DoesNotAskForConfirmation_RemovesBranchFro // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, []), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, []), + new("Stack2", sourceBranch, []) }); await inputProvider.DidNotReceive().Confirm(Questions.ConfirmRemoveBranch, Arg.Any()); } @@ -316,7 +316,7 @@ public async Task WhenChildActionIsMoveChildrenToParent_RemovesBranchAndMovesChi // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(childBranch, [])]) + new("Stack1", sourceBranch, [new Model.Branch(childBranch, [])]) }); } @@ -355,7 +355,7 @@ public async Task WhenChildActionIsRemoveChildren_RemovesBranchAndDeletesChildre // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, []) }); } @@ -392,7 +392,7 @@ public async Task WhenRemoveChildrenIsProvided_RemovesBranchAndDeletesChildren() // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, []) }); await inputProvider.DidNotReceive().Select(Questions.RemoveBranchChildAction, Arg.Any(), Arg.Any(), Arg.Any>()); @@ -431,7 +431,7 @@ public async Task WhenMoveChildrenToParentIsProvided_RemovesBranchAndMovesChildr // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(childBranch, [])]) + new("Stack1", sourceBranch, [new Model.Branch(childBranch, [])]) }); await inputProvider.DidNotReceive().Select(Questions.RemoveBranchChildAction, Arg.Any(), Arg.Any(), Arg.Any>()); @@ -469,7 +469,7 @@ public async Task WhenBranchHasNoChildren_DoesNotAskForChildAction() // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, []) }); // Should not ask for child action when branch has no children @@ -508,7 +508,7 @@ public async Task WhenBranchHasNoChildrenButRemoveChildrenIsProvided_DoesNotAskF // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, []) }); // Should not ask for child action when explicitly provided, even if branch has no children @@ -547,7 +547,7 @@ public async Task WhenBranchHasNoChildrenButMoveChildrenToParentIsProvided_DoesN // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, []) }); // Should not ask for child action when explicitly provided, even if branch has no children diff --git a/src/Stack.Tests/Commands/Helpers/InputProviderExtensionMethodsTests.cs b/src/Stack.Tests/Commands/Helpers/InputProviderExtensionMethodsTests.cs index ef9d4c61..fae26c10 100644 --- a/src/Stack.Tests/Commands/Helpers/InputProviderExtensionMethodsTests.cs +++ b/src/Stack.Tests/Commands/Helpers/InputProviderExtensionMethodsTests.cs @@ -14,15 +14,14 @@ public class InputProviderExtensionMethodsTests(ITestOutputHelper testOutputHelp public async Task SelectStack_WhenNameIsProvided_ReturnsStackByName() { // Arrange - var remoteUri = Some.HttpsUri().ToString(); var sourceBranch = Some.BranchName(); var currentBranch = Some.BranchName(); var stackName = "TestStack"; var stacks = new List { - new(stackName, remoteUri, sourceBranch, []), - new("OtherStack", remoteUri, sourceBranch, []) + new(stackName, sourceBranch, []), + new("OtherStack", sourceBranch, []) }; var inputProvider = Substitute.For(); @@ -41,14 +40,13 @@ public async Task SelectStack_WhenNameIsProvided_ReturnsStackByName() public async Task SelectStack_WhenOnlyOneStackExists_AutoSelectsStack() { // Arrange - var remoteUri = Some.HttpsUri().ToString(); var sourceBranch = Some.BranchName(); var currentBranch = Some.BranchName(); var stackName = "OnlyStack"; var stacks = new List { - new(stackName, remoteUri, sourceBranch, []) + new(stackName, sourceBranch, []) }; var inputProvider = Substitute.For(); @@ -67,7 +65,6 @@ public async Task SelectStack_WhenOnlyOneStackExists_AutoSelectsStack() public async Task SelectStack_WhenCurrentBranchIsInOnlyOneStack_AutoSelectsThatStack() { // Arrange - var remoteUri = Some.HttpsUri().ToString(); var sourceBranch = Some.BranchName(); var currentBranch = Some.BranchName(); var stackWithCurrentBranch = "StackWithBranch"; @@ -75,8 +72,8 @@ public async Task SelectStack_WhenCurrentBranchIsInOnlyOneStack_AutoSelectsThatS var stacks = new List { - new(stackWithCurrentBranch, remoteUri, sourceBranch, [new Model.Branch(currentBranch, [])]), - new(stackWithoutCurrentBranch, remoteUri, sourceBranch, [new Model.Branch(Some.BranchName(), [])]) + new(stackWithCurrentBranch, sourceBranch, [new Model.Branch(currentBranch, [])]), + new(stackWithoutCurrentBranch, sourceBranch, [new Model.Branch(Some.BranchName(), [])]) }; var inputProvider = Substitute.For(); @@ -95,7 +92,6 @@ public async Task SelectStack_WhenCurrentBranchIsInOnlyOneStack_AutoSelectsThatS public async Task SelectStack_WhenCurrentBranchIsInMultipleStacks_PromptsUser() { // Arrange - var remoteUri = Some.HttpsUri().ToString(); var sourceBranch = Some.BranchName(); var currentBranch = Some.BranchName(); var stack1Name = "Stack1"; @@ -103,8 +99,8 @@ public async Task SelectStack_WhenCurrentBranchIsInMultipleStacks_PromptsUser() var stacks = new List { - new(stack1Name, remoteUri, sourceBranch, [new Model.Branch(currentBranch, [])]), - new(stack2Name, remoteUri, sourceBranch, [new Model.Branch(currentBranch, [])]) + new(stack1Name, sourceBranch, [new Model.Branch(currentBranch, [])]), + new(stack2Name, sourceBranch, [new Model.Branch(currentBranch, [])]) }; var inputProvider = Substitute.For(); @@ -126,7 +122,6 @@ public async Task SelectStack_WhenCurrentBranchIsInMultipleStacks_PromptsUser() public async Task SelectStack_WhenCurrentBranchIsInNoStacks_PromptsUser() { // Arrange - var remoteUri = Some.HttpsUri().ToString(); var sourceBranch = Some.BranchName(); var currentBranch = Some.BranchName(); var stack1Name = "Stack1"; @@ -134,8 +129,8 @@ public async Task SelectStack_WhenCurrentBranchIsInNoStacks_PromptsUser() var stacks = new List { - new(stack1Name, remoteUri, sourceBranch, [new Model.Branch(Some.BranchName(), [])]), - new(stack2Name, remoteUri, sourceBranch, [new Model.Branch(Some.BranchName(), [])]) + new(stack1Name, sourceBranch, [new Model.Branch(Some.BranchName(), [])]), + new(stack2Name, sourceBranch, [new Model.Branch(Some.BranchName(), [])]) }; var inputProvider = Substitute.For(); @@ -157,7 +152,6 @@ public async Task SelectStack_WhenCurrentBranchIsInNoStacks_PromptsUser() public async Task SelectStack_WhenCurrentBranchIsSourceBranchInOnlyOneStack_AutoSelectsThatStack() { // Arrange - var remoteUri = Some.HttpsUri().ToString(); var sourceBranch = Some.BranchName(); var currentBranch = sourceBranch; // Current branch is the source branch var stackWithCurrentAsSource = "StackWithSourceBranch"; @@ -165,8 +159,8 @@ public async Task SelectStack_WhenCurrentBranchIsSourceBranchInOnlyOneStack_Auto var stacks = new List { - new(stackWithCurrentAsSource, remoteUri, sourceBranch, []), - new(stackWithDifferentSource, remoteUri, Some.BranchName(), []) + new(stackWithCurrentAsSource, sourceBranch, []), + new(stackWithDifferentSource, Some.BranchName(), []) }; var inputProvider = Substitute.For(); diff --git a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs index 4460e06c..8fae200c 100644 --- a/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs +++ b/src/Stack.Tests/Commands/Helpers/StackActionsTests.cs @@ -23,7 +23,7 @@ public async Task UpdateStack_UsingMerge_WhenConflictResolutionAborted_ThrowsAbo var gitClient = Substitute.For(); var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); - var stack = new Model.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, new List { new(feature, []) }); + var stack = new Model.Stack("Stack1", sourceBranch, new List { new(feature, []) }); gitClient.GetBranchStatuses(Arg.Any()).Returns(new Dictionary { @@ -61,7 +61,7 @@ public async Task UpdateStack_UsingMerge_WhenConflictsResolved_CompletesSuccessf var gitClient = Substitute.For(); var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); - var stack = new Model.Stack("Stack1", Some.HttpsUri().ToString(), sourceBranch, new List { new(feature, []) }); + var stack = new Model.Stack("Stack1", sourceBranch, new List { new(feature, []) }); gitClient.GetBranchStatuses(Arg.Any()).Returns(new Dictionary { @@ -111,7 +111,6 @@ public async Task UpdateStack_UsingMerge_WhenBranchHasMergedPullRequest_SkipsBra var stack = new Model.Stack( "Stack1", - Some.HttpsUri().ToString(), sourceBranch, new List { new(inactiveBranch, []) }); @@ -153,7 +152,6 @@ public async Task UpdateStack_UsingMerge_WhenBranchHasNoRemoteTrackingBranch_IsU var stack = new Model.Stack( "Stack1", - Some.HttpsUri().ToString(), sourceBranch, new List { new(localOnlyBranch, []) }); @@ -184,7 +182,7 @@ public async Task UpdateStack_UsingRebase_WhenConflictResolutionAborted_ThrowsAb var gitClient = Substitute.For(); var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); - var stack = new Model.Stack("Stack1", Some.HttpsUri().ToString(), source, new List { new(feature, []) }); + var stack = new Model.Stack("Stack1", source, new List { new(feature, []) }); gitClient.GetBranchStatuses(Arg.Any()).Returns(new Dictionary { @@ -221,7 +219,7 @@ public async Task UpdateStack_UsingRebase_WhenConflictsResolved_CompletesSuccess var gitClient = Substitute.For(); var gitHubClient = Substitute.For(); var conflictResolutionDetector = Substitute.For(); - var stack = new Model.Stack("Stack1", Some.HttpsUri().ToString(), source, new List { new(feature, []) }); + var stack = new Model.Stack("Stack1", source, new List { new(feature, []) }); gitClient.GetBranchStatuses(Arg.Any()).Returns(new Dictionary { @@ -271,7 +269,6 @@ public async Task UpdateStack_UsingRebase_WhenBranchHasMergedPullRequest_SkipsBr var stack = new Model.Stack( "Stack1", - Some.HttpsUri().ToString(), sourceBranch, new List { new(inactiveBranch, []) }); @@ -314,7 +311,6 @@ public async Task UpdateStack_UsingRebase_WhenBranchHasNoRemoteTrackingBranch_Is var stack = new Model.Stack( "Stack1", - Some.HttpsUri().ToString(), sourceBranch, new List { new(localOnlyBranch, []) }); @@ -919,7 +915,6 @@ public async Task UpdateStack_UsingMerge_WhenBranchIsInWorktree_UsesWorktreeGitC var stack = new Model.Stack( "Stack1", - Some.HttpsUri().ToString(), sourceBranch, new List { new Model.Branch(branchInWorktree, new List()) } ); @@ -967,7 +962,6 @@ public async Task UpdateStack_UsingRebase_WhenBranchIsInWorktree_UsesWorktreeGit var stack = new Model.Stack( "Stack1", - Some.HttpsUri().ToString(), sourceBranch, new List { new Model.Branch(branchInWorktree, new List()) } ); @@ -1010,7 +1004,6 @@ public async Task UpdateStack_WhenCheckingPullRequests_AndGitHubClientIsNotAvail var stack = new Model.Stack( "Stack1", - Some.HttpsUri().ToString(), sourceBranch, [] ); diff --git a/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs index 882a3ffb..55cc2714 100644 --- a/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/DeleteStackCommandHandlerTests.cs @@ -50,7 +50,7 @@ public async Task WhenNoInputsAreProvided_AsksForName_AndConfirmation_AndDeletes // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack2", sourceBranch, []) }); } @@ -90,8 +90,8 @@ public async Task WhenConfirmationIsFalse_DoesNotDeleteStack() // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, []), - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, []), + new("Stack2", sourceBranch, []) }); } @@ -130,7 +130,7 @@ public async Task WhenNameIsProvided_AsksForConfirmation_AndDeletesStack() // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack2", sourceBranch, []) }); await inputProvider.DidNotReceive().Select(Questions.SelectStack, Arg.Any(), Arg.Any()); @@ -230,7 +230,7 @@ public async Task WhenThereAreLocalBranchesThatAreDeletedInTheRemote_AsksToClean // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack2", sourceBranch, []) }); gitClient.Received(1).DeleteLocalBranch(branchToCleanup); gitClient.DidNotReceive().DeleteLocalBranch(branchToKeep); @@ -306,7 +306,7 @@ public async Task WhenConfirmIsProvided_DoesNotAskForConfirmation_DeletesStack() // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack2", stackRepository.RemoteUri, sourceBranch, []) + new("Stack2", sourceBranch, []) }); await inputProvider.DidNotReceive().Confirm(Questions.ConfirmDeleteStack, Arg.Any()); diff --git a/src/Stack.Tests/Commands/Stack/NewStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/NewStackCommandHandlerTests.cs index 881044cf..93412b54 100644 --- a/src/Stack.Tests/Commands/Stack/NewStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/NewStackCommandHandlerTests.cs @@ -45,7 +45,7 @@ public async Task WithAnExistingBranch_TheStackIsCreatedAndTheCurrentBranchIsCha // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new(stackName, stackRepository.RemoteUri, sourceBranch, [new Model.Branch(existingBranch, [])]) + new(stackName, sourceBranch, [new Model.Branch(existingBranch, [])]) }); gitClient.Received().ChangeBranch(existingBranch); } @@ -81,7 +81,7 @@ public async Task WithNoBranch_TheStackIsCreatedAndTheCurrentBranchIsNotChanged( // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new(stackName, stackRepository.RemoteUri, sourceBranch, []) + new(stackName, sourceBranch, []) }); gitClient.DidNotReceive().ChangeBranch(Arg.Any()); } @@ -117,7 +117,7 @@ public async Task WhenStackNameIsProvidedInInputs_TheProviderIsNotAskedForAName_ // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new(stackName, stackRepository.RemoteUri, sourceBranch, []) + new(stackName, sourceBranch, []) }); await inputProvider.DidNotReceive().Text(Questions.StackName, Arg.Any()); } @@ -152,7 +152,7 @@ public async Task WhenSourceBranchIsProvidedInInputs_TheProviderIsNotAskedForThe // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, []) + new("Stack1", sourceBranch, []) }); await inputProvider.DidNotReceive().Select(Questions.SelectBranch, Arg.Any(), Arg.Any()); } @@ -188,7 +188,7 @@ public async Task WhenBranchNameIsProvidedInInputs_TheProviderIsNotAskedForTheBr // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(newBranch, [])]) + new("Stack1", sourceBranch, [new Model.Branch(newBranch, [])]) }); gitClient.Received().CreateNewBranch(newBranch, sourceBranch); gitClient.Received().PushNewBranch(newBranch); @@ -226,7 +226,7 @@ public async Task WithANewBranch_TheStackIsCreatedAndTheBranchExistsOnTheRemote( // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(newBranch, [])]) + new("Stack1", sourceBranch, [new Model.Branch(newBranch, [])]) }); gitClient.Received().CreateNewBranch(newBranch, sourceBranch); gitClient.Received().PushNewBranch(newBranch); @@ -266,7 +266,7 @@ public async Task WithANewBranch_AndThePushFails_TheStackIsStillCreatedSuccessfu // Assert stackRepository.Stacks.Should().BeEquivalentTo(new List { - new("Stack1", stackRepository.RemoteUri, sourceBranch, [new Model.Branch(newBranch, [])]) + new("Stack1", sourceBranch, [new Model.Branch(newBranch, [])]) }); gitClient.Received().CreateNewBranch(newBranch, sourceBranch); gitClient.Received().PushNewBranch(newBranch); diff --git a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs index 3b6f7dce..800f380b 100644 --- a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs @@ -234,92 +234,6 @@ public async Task WhenAllStacksAreRequested_ReturnsStatusOfEachStack() response.Stacks.Should().BeEquivalentTo([expectedStackDetail1, expectedStackDetail2]); } - [Fact] - public async Task WhenAllStacksAreRequested_WithStacksInMultipleRepositories_ReturnsStatusOfEachStackInTheCorrectRepository() - { - // Arrange - var sourceBranch = Some.BranchName(); - var branch1 = Some.BranchName(); - var branch2 = Some.BranchName(); - var branch3 = Some.BranchName(); - var otherRemoteUri = Some.HttpsUri().ToString(); - var tipOfSourceBranch = new Commit(Some.Sha(), Some.Name()); - var tipOfBranch1 = new Commit(Some.Sha(), Some.Name()); - var tipOfBranch2 = new Commit(Some.Sha(), Some.Name()); - var tipOfBranch3 = new Commit(Some.Sha(), Some.Name()); - - var stackRepository = new TestStackRepositoryBuilder() - .WithStack(stack => stack - .WithName("Stack1") - .WithSourceBranch(sourceBranch) - .WithBranch(b1 => b1.WithName(branch1).WithChildBranch(b2 => b2.WithName(branch2)))) - .WithStack(stack => stack - .WithName("Stack2") - .WithSourceBranch(sourceBranch) - .WithBranch(b => b.WithName(branch3))) - .WithStack(stack => stack - .WithName("Stack3") - .WithRemoteUri(otherRemoteUri) - .WithSourceBranch(Some.BranchName()) - .WithBranch(b => b.WithName(Some.BranchName()))) - .Build(); - var inputProvider = Substitute.For(); - var logger = XUnitLogger.CreateLogger(testOutputHelper); - var gitClient = Substitute.For(); - var gitHubClient = Substitute.For(); - var console = new TestDisplayProvider(testOutputHelper); - var gitClientFactory = Substitute.For(); - var executionContext = new CliExecutionContext { WorkingDirectory = "/some/path" }; - var handler = new StackStatusCommandHandler(inputProvider, logger, console, gitClientFactory, executionContext, gitHubClient, stackRepository); - - gitClientFactory.Create(executionContext.WorkingDirectory).Returns(gitClient); - - var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false, branch1); - - gitHubClient.GetPullRequest(branch1).Returns(pr); - gitClient.GetCurrentBranch().Returns(sourceBranch); - gitClient.GetBranchStatuses(Arg.Any()).Returns(new Dictionary - { - [sourceBranch] = new GitBranchStatus(sourceBranch, $"origin/{sourceBranch}", true, false, 0, 0, tipOfSourceBranch), - [branch1] = new GitBranchStatus(branch1, $"origin/{branch1}", true, false, 0, 0, tipOfBranch1), - [branch2] = new GitBranchStatus(branch2, $"origin/{branch2}", true, false, 0, 0, tipOfBranch2), - [branch3] = new GitBranchStatus(branch3, $"origin/{branch3}", true, false, 0, 0, tipOfBranch3), - }); - gitClient.CompareBranches(branch1, sourceBranch).Returns((10, 5)); - gitClient.CompareBranches(branch2, branch1).Returns((1, 0)); - gitClient.CompareBranches(branch3, sourceBranch).Returns((3, 5)); - - // Act - var response = await handler.Handle(new StackStatusCommandInputs(null, true, true), CancellationToken.None); - - // Assert - var expectedSourceBranch = new SourceBranchDetail(sourceBranch, true, - new Commit(tipOfSourceBranch.Sha[..7], tipOfSourceBranch.Message.Trim()), - new RemoteTrackingBranchStatus($"origin/{sourceBranch}", true, 0, 0)); - - var expectedBranch1 = new BranchDetail(branch1, true, - new Commit(tipOfBranch1.Sha[..7], tipOfBranch1.Message.Trim()), - new RemoteTrackingBranchStatus($"origin/{branch1}", true, 0, 0), - pr, new ParentBranchStatus(sourceBranch, 10, 5), - [ - new BranchDetail(branch2, true, - new Commit(tipOfBranch2.Sha[..7], tipOfBranch2.Message.Trim()), - new RemoteTrackingBranchStatus($"origin/{branch2}", true, 0, 0), - null, new ParentBranchStatus(branch1, 1, 0), []) - ]); - - var expectedStackDetail1 = new StackStatus("Stack1", expectedSourceBranch, [expectedBranch1]); - - var expectedBranch3 = new BranchDetail(branch3, true, - new Commit(tipOfBranch3.Sha[..7], tipOfBranch3.Message.Trim()), - new RemoteTrackingBranchStatus($"origin/{branch3}", true, 0, 0), - null, new ParentBranchStatus(sourceBranch, 3, 5), []); - - var expectedStackDetail2 = new StackStatus("Stack2", expectedSourceBranch, [expectedBranch3]); - - response.Stacks.Should().BeEquivalentTo([expectedStackDetail1, expectedStackDetail2]); - } - [Fact] public async Task WhenStackNameIsProvided_ButStackDoesNotExist_Throws() { diff --git a/src/Stack.Tests/Helpers/TestStackRepositoryBuilder.cs b/src/Stack.Tests/Helpers/TestStackRepositoryBuilder.cs index ad6b78fc..6b126d7d 100644 --- a/src/Stack.Tests/Helpers/TestStackRepositoryBuilder.cs +++ b/src/Stack.Tests/Helpers/TestStackRepositoryBuilder.cs @@ -6,7 +6,6 @@ namespace Stack.Tests.Helpers; public class TestStackRepositoryBuilder { readonly List> stackBuilders = []; - string remoteUri = Some.HttpsUri().ToString(); public TestStackRepositoryBuilder WithStack(Action stackBuilder) { @@ -14,30 +13,22 @@ public TestStackRepositoryBuilder WithStack(Action stackBuilde return this; } - public TestStackRepositoryBuilder WithRemoteUri(string uri) - { - remoteUri = uri; - return this; - } - public TestStackRepository Build() { - var stackData = new StackData([.. stackBuilders.Select(builder => + List stacks = [.. stackBuilders.Select(builder => { var stackBuilder = new TestStackBuilder(); - stackBuilder = stackBuilder.WithRemoteUri(remoteUri); builder(stackBuilder); return stackBuilder.Build(); - })]); + })]; - return new TestStackRepository(stackData, remoteUri); + return new TestStackRepository(stacks); } } public class TestStackBuilder { string? name; - string? remoteUri; string? sourceBranch; List> branchBuilders = []; @@ -47,12 +38,6 @@ public TestStackBuilder WithName(string name) return this; } - public TestStackBuilder WithRemoteUri(string remoteUri) - { - this.remoteUri = remoteUri; - return this; - } - public TestStackBuilder WithSourceBranch(string sourceBranch) { this.sourceBranch = sourceBranch; @@ -76,7 +61,7 @@ public Model.Stack Build() }) .ToList(); - var stack = new Model.Stack(name ?? Some.Name(), remoteUri ?? Some.HttpsUri().ToString(), sourceBranch ?? Some.BranchName(), branches); + var stack = new Model.Stack(name ?? Some.Name(), sourceBranch ?? Some.BranchName(), branches); return stack; } @@ -113,31 +98,14 @@ [.. childBranchBuilders.Select(builder => } } -public class TestStackConfig(StackData initialData) : IStackConfig -{ - StackData stackData = initialData; - - public List Stacks => stackData.Stacks; - - public string GetConfigPath() => throw new NotImplementedException("TestStackConfig does not support GetConfigPath."); - - public StackData Load() => stackData; - - public void Save(StackData newStackData) - { - stackData = newStackData; - } -} - public class TestStackRepository : IStackRepository { - private readonly StackData stackData; - private readonly string remoteUri; + private readonly List stacks; + private readonly string remoteUri = Some.HttpsUri().ToString(); - public TestStackRepository(StackData initialData, string remoteUri) + public TestStackRepository(List initialData) { - this.stackData = initialData; - this.remoteUri = remoteUri; + this.stacks = initialData; } public string RemoteUri => remoteUri; @@ -149,19 +117,17 @@ public TestStackRepository(StackData initialData, string remoteUri) return []; } - return stackData.Stacks - .Where(s => s.RemoteUri.Equals(remoteUri, StringComparison.OrdinalIgnoreCase)) - .ToList(); + return stacks.ToList(); } public void AddStack(Model.Stack stack) { - stackData.Stacks.Add(stack); + stacks.Add(stack); } public void RemoveStack(Model.Stack stack) { - stackData.Stacks.Remove(stack); + stacks.Remove(stack); } public void SaveChanges() @@ -169,5 +135,5 @@ public void SaveChanges() // No-op for testing - changes are already in memory } - public List Stacks => stackData.Stacks; + public List Stacks => stacks; } diff --git a/src/Stack.Tests/Persistence/FileStackConfigTests.cs b/src/Stack.Tests/Persistence/FileStackDataStoreTests.cs similarity index 89% rename from src/Stack.Tests/Persistence/FileStackConfigTests.cs rename to src/Stack.Tests/Persistence/FileStackDataStoreTests.cs index c9dbee5a..868acc3f 100644 --- a/src/Stack.Tests/Persistence/FileStackConfigTests.cs +++ b/src/Stack.Tests/Persistence/FileStackDataStoreTests.cs @@ -7,7 +7,7 @@ // use a fully-qualified name in all other tests. namespace Stack.Tests; -public class FileStackConfigTests +public class FileStackDataStoreTests { [Fact] public void Load_WhenConfigFileDoesNotExist_ReturnsEmptyList() @@ -15,10 +15,10 @@ public void Load_WhenConfigFileDoesNotExist_ReturnsEmptyList() // Arrange using var tempDirectory = TemporaryDirectory.Create(); - var fileStackConfig = new FileStackConfig(tempDirectory.DirectoryPath); + var dataStore = new FileStackDataStore(tempDirectory.DirectoryPath); // Act - var stackData = fileStackConfig.Load(); + var stackData = dataStore.Load(); // Assert stackData.Should().BeEquivalentTo(new StackData([])); @@ -49,8 +49,8 @@ public void Load_WhenConfigFileIsInV1Format_LoadsCorrectly_MigratesAndSavesFileI ]"; File.WriteAllText(configPath, v1Json); - var fileStackConfig = new FileStackConfig(tempDirectory.DirectoryPath); - var expectedStack = new Model.Stack( + var dataStore = new FileStackDataStore(tempDirectory.DirectoryPath); + var expectedStack = new StackDataItem( stackName, remoteUri, sourceBranch, @@ -62,7 +62,7 @@ public void Load_WhenConfigFileIsInV1Format_LoadsCorrectly_MigratesAndSavesFileI ]); // Act - var stackData = fileStackConfig.Load(); + var stackData = dataStore.Load(); // Assert stackData.Should().BeEquivalentTo(new StackData([expectedStack])); @@ -96,7 +96,7 @@ public void Load_WhenConfigFileIsInV1Format_LoadsCorrectly_MigratesAndSavesFileI normalizedSavedJson.Should().Be(normalizedExpectedJson); // Original backup should be in V1 format - var backupPath = fileStackConfig.GetV1ConfigBackupFilePath(); + var backupPath = dataStore.GetV1ConfigBackupFilePath(); File.Exists(backupPath).Should().BeTrue(); var backupJson = File.ReadAllText(backupPath); backupJson.Should().Be(v1Json); @@ -145,9 +145,9 @@ public void Load_WhenConfigFileIsInV2Format_LoadsCorrectly() }}"; File.WriteAllText(configPath, v2Json); - var fileStackConfig = new FileStackConfig(tempDirectory.DirectoryPath); + var dataStore = new FileStackDataStore(tempDirectory.DirectoryPath); - var expectedStack = new Model.Stack( + var expectedStack = new StackDataItem( stackName, remoteUri, sourceBranch, @@ -157,7 +157,7 @@ public void Load_WhenConfigFileIsInV2Format_LoadsCorrectly() ]); // Act - var stackData = fileStackConfig.Load(); + var stackData = dataStore.Load(); // Assert stackData.Should().BeEquivalentTo(new StackData([expectedStack])); @@ -189,7 +189,7 @@ public void Save_WhenConfigFileIsInV1Format_AndStackIsChangedToHaveTreeStructure ]"; File.WriteAllText(configPath, v1Json); - var stack = new Model.Stack( + var stack = new StackDataItem( stackName, remoteUri, sourceBranch, @@ -198,10 +198,10 @@ public void Save_WhenConfigFileIsInV1Format_AndStackIsChangedToHaveTreeStructure new(branch3, []) ]); - var fileStackConfig = new FileStackConfig(tempDirectory.DirectoryPath); + var dataStore = new FileStackDataStore(tempDirectory.DirectoryPath); // Act - fileStackConfig.Save(new StackData([stack])); + dataStore.Save(new StackData([stack])); // Assert var savedJson = File.ReadAllText(configPath); @@ -238,7 +238,7 @@ public void Save_WhenConfigFileIsInV1Format_AndStackIsChangedToHaveTreeStructure normalizedSavedJson.Should().Be(normalizedExpectedJson); // Original backup should be in V1 format - var backupPath = fileStackConfig.GetV1ConfigBackupFilePath(); + var backupPath = dataStore.GetV1ConfigBackupFilePath(); File.Exists(backupPath).Should().BeTrue(); var backupJson = File.ReadAllText(backupPath); backupJson.Should().Be(v1Json); diff --git a/src/Stack.Tests/Persistence/StackRepositoryTests.cs b/src/Stack.Tests/Persistence/StackRepositoryTests.cs index 5244b146..231cbdcd 100644 --- a/src/Stack.Tests/Persistence/StackRepositoryTests.cs +++ b/src/Stack.Tests/Persistence/StackRepositoryTests.cs @@ -4,23 +4,41 @@ using Stack.Infrastructure.Settings; using Stack.Persistence; using Stack.Tests.Helpers; -using StackModel = Stack.Model.Stack; namespace Stack.Tests; public class StackRepositoryTests { + private class MockStackDataStore(StackData stackData) : IStackDataStore + { + public StackData Data { get; private set; } = stackData; + + public void Save(StackData data) + { + Data = data; + } + + public StackData Load() + { + return Data; + } + + public string GetConfigPath() + { + throw new NotImplementedException(); + } + } + [Fact] public void GetStacks_FiltersStacksByRemoteUri_CaseInsensitive() { // Arrange var remoteUri = Some.HttpsUri().ToString(); - var stack1 = new StackModel("Stack1", remoteUri, "main", []); - var stack2 = new StackModel("Stack2", Some.HttpsUri().ToString(), "main", []); - var stack3 = new StackModel("Stack3", remoteUri.ToUpper(), "main", []); + var stack1 = new StackDataItem("Stack1", remoteUri, "main", []); + var stack2 = new StackDataItem("Stack2", Some.HttpsUri().ToString(), "main", []); + var stack3 = new StackDataItem("Stack3", remoteUri.ToUpper(), "main", []); - var stackConfig = Substitute.For(); - stackConfig.Load().Returns(new StackData([stack1, stack2, stack3])); + var dataStore = new MockStackDataStore(new StackData([stack1, stack2, stack3])); var gitClient = Substitute.For(); gitClient.GetRemoteUri().Returns(remoteUri); @@ -31,21 +49,23 @@ public void GetStacks_FiltersStacksByRemoteUri_CaseInsensitive() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; // Act - var repository = new StackRepository(stackConfig, gitClientFactory, executionContext); + var repository = new StackRepository(dataStore, gitClientFactory, executionContext); var stacks = repository.GetStacks(); // Assert - stacks.Should().BeEquivalentTo([stack1, stack3]); + stacks.Should().BeEquivalentTo([ + new Model.Stack("Stack1", "main", []), + new Model.Stack("Stack3", "main", []) + ]); } [Fact] public void GetStacks_WhenNoRemoteUri_ReturnsEmptyList() { // Arrange - var stack1 = new StackModel("Stack1", Some.HttpsUri().ToString(), "main", []); + var stack1 = new StackDataItem("Stack1", Some.HttpsUri().ToString(), "main", []); - var stackConfig = Substitute.For(); - stackConfig.Load().Returns(new StackData([stack1])); + var dataStore = new MockStackDataStore(new StackData([stack1])); var gitClient = Substitute.For(); gitClient.GetRemoteUri().Returns((string?)null); @@ -56,7 +76,7 @@ public void GetStacks_WhenNoRemoteUri_ReturnsEmptyList() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; // Act - var repository = new StackRepository(stackConfig, gitClientFactory, executionContext); + var repository = new StackRepository(dataStore, gitClientFactory, executionContext); var stacks = repository.GetStacks(); // Assert @@ -68,10 +88,9 @@ public void GetStacks_WhenNoStacksMatchRemote_ReturnsEmptyList() { // Arrange var remoteUri = Some.HttpsUri().ToString(); - var stack1 = new StackModel("Stack1", Some.HttpsUri().ToString(), "main", []); + var stack1 = new StackDataItem("Stack1", Some.HttpsUri().ToString(), "main", []); - var stackConfig = Substitute.For(); - stackConfig.Load().Returns(new StackData([stack1])); + var dataStore = new MockStackDataStore(new StackData([stack1])); var gitClient = Substitute.For(); gitClient.GetRemoteUri().Returns(remoteUri); @@ -82,7 +101,7 @@ public void GetStacks_WhenNoStacksMatchRemote_ReturnsEmptyList() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; // Act - var repository = new StackRepository(stackConfig, gitClientFactory, executionContext); + var repository = new StackRepository(dataStore, gitClientFactory, executionContext); var stacks = repository.GetStacks(); // Assert @@ -94,11 +113,9 @@ public void AddStack_AddsStackToCollection() { // Arrange var remoteUri = Some.HttpsUri().ToString(); - var existingStack = new StackModel("Stack1", remoteUri, "main", []); - var newStack = new StackModel("Stack2", remoteUri, "main", []); + var existingStackInStorage = new StackDataItem("Stack1", remoteUri, "main", []); - var stackConfig = Substitute.For(); - stackConfig.Load().Returns(new StackData([existingStack])); + var dataStore = new MockStackDataStore(new StackData([existingStackInStorage])); var gitClient = Substitute.For(); gitClient.GetRemoteUri().Returns(remoteUri); @@ -108,14 +125,18 @@ public void AddStack_AddsStackToCollection() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; - var repository = new StackRepository(stackConfig, gitClientFactory, executionContext); + var repository = new StackRepository(dataStore, gitClientFactory, executionContext); // Act + var newStack = new Model.Stack("Stack2", "main", []); repository.AddStack(newStack); var stacks = repository.GetStacks(); // Assert - stacks.Should().BeEquivalentTo([existingStack, newStack]); + stacks.Should().BeEquivalentTo([ + new Model.Stack("Stack1", "main", []), + new Model.Stack("Stack2", "main", []) + ]); } [Fact] @@ -123,11 +144,10 @@ public void RemoveStack_RemovesStackFromCollection() { // Arrange var remoteUri = Some.HttpsUri().ToString(); - var stack1 = new StackModel("Stack1", remoteUri, "main", []); - var stack2 = new StackModel("Stack2", remoteUri, "main", []); + var stack1 = new StackDataItem("Stack1", remoteUri, "main", []); + var stack2 = new StackDataItem("Stack2", remoteUri, "main", []); - var stackConfig = Substitute.For(); - stackConfig.Load().Returns(new StackData([stack1, stack2])); + var dataStore = new MockStackDataStore(new StackData([stack1, stack2])); var gitClient = Substitute.For(); gitClient.GetRemoteUri().Returns(remoteUri); @@ -137,42 +157,14 @@ public void RemoveStack_RemovesStackFromCollection() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; - var repository = new StackRepository(stackConfig, gitClientFactory, executionContext); + var repository = new StackRepository(dataStore, gitClientFactory, executionContext); // Act - repository.RemoveStack(stack1); + repository.RemoveStack(new Model.Stack("Stack1", "main", [])); var stacks = repository.GetStacks(); // Assert - stacks.Should().BeEquivalentTo([stack2]); - } - - [Fact] - public void SaveChanges_CallsStackConfigSave() - { - // Arrange - var remoteUri = Some.HttpsUri().ToString(); - var stack1 = new StackModel("Stack1", remoteUri, "main", []); - var originalStackData = new StackData([stack1]); - - var stackConfig = Substitute.For(); - stackConfig.Load().Returns(originalStackData); - - var gitClient = Substitute.For(); - gitClient.GetRemoteUri().Returns(remoteUri); - - var gitClientFactory = Substitute.For(); - gitClientFactory.Create(Arg.Any()).Returns(gitClient); - - var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; - - var repository = new StackRepository(stackConfig, gitClientFactory, executionContext); - - // Act - repository.SaveChanges(); - - // Assert - stackConfig.Received(1).Save(Arg.Is(sd => sd == originalStackData)); + stacks.Should().BeEquivalentTo([new Model.Stack("Stack2", "main", [])]); } [Fact] @@ -180,11 +172,10 @@ public void SaveChanges_AfterAddingStack_SavesUpdatedData() { // Arrange var remoteUri = Some.HttpsUri().ToString(); - var stack1 = new StackModel("Stack1", remoteUri, "main", []); - var stack2 = new StackModel("Stack2", remoteUri, "main", []); + var stack1 = new StackDataItem("Stack1", remoteUri, "main", []); + var stack2 = new StackDataItem("Stack2", remoteUri, "main", []); - var stackConfig = Substitute.For(); - stackConfig.Load().Returns(new StackData([stack1])); + var dataStore = new MockStackDataStore(new StackData([stack1])); var gitClient = Substitute.For(); gitClient.GetRemoteUri().Returns(remoteUri); @@ -194,17 +185,14 @@ public void SaveChanges_AfterAddingStack_SavesUpdatedData() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; - var repository = new StackRepository(stackConfig, gitClientFactory, executionContext); + var repository = new StackRepository(dataStore, gitClientFactory, executionContext); // Act - repository.AddStack(stack2); + repository.AddStack(new Model.Stack("Stack2", "main", [])); repository.SaveChanges(); // Assert - stackConfig.Received(1).Save(Arg.Is(sd => - sd.Stacks.Count == 2 && - sd.Stacks.Contains(stack1) && - sd.Stacks.Contains(stack2))); + dataStore.Data.Stacks.Should().BeEquivalentTo([stack1, stack2]); } [Fact] @@ -212,11 +200,10 @@ public void SaveChanges_AfterRemovingStack_SavesUpdatedData() { // Arrange var remoteUri = Some.HttpsUri().ToString(); - var stack1 = new StackModel("Stack1", remoteUri, "main", []); - var stack2 = new StackModel("Stack2", remoteUri, "main", []); + var stack1 = new StackDataItem("Stack1", remoteUri, "main", []); + var stack2 = new StackDataItem("Stack2", remoteUri, "main", []); - var stackConfig = Substitute.For(); - stackConfig.Load().Returns(new StackData([stack1, stack2])); + var dataStore = new MockStackDataStore(new StackData([stack1, stack2])); var gitClient = Substitute.For(); gitClient.GetRemoteUri().Returns(remoteUri); @@ -226,29 +213,25 @@ public void SaveChanges_AfterRemovingStack_SavesUpdatedData() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; - var repository = new StackRepository(stackConfig, gitClientFactory, executionContext); + var repository = new StackRepository(dataStore, gitClientFactory, executionContext); // Act - repository.RemoveStack(stack1); + repository.RemoveStack(new Model.Stack("Stack1", "main", [])); repository.SaveChanges(); // Assert - stackConfig.Received(1).Save(Arg.Is(sd => - sd.Stacks.Count == 1 && - sd.Stacks.Contains(stack2) && - !sd.Stacks.Contains(stack1))); + dataStore.Data.Stacks.Should().BeEquivalentTo([stack2]); } [Fact] - public void AddStack_ThenRemoveStack_ResultsInOriginalState() + public void RemoveStack_RemoteUriComparisonIsCaseInsensitive() { // Arrange var remoteUri = Some.HttpsUri().ToString(); - var stack1 = new StackModel("Stack1", remoteUri, "main", []); - var stack2 = new StackModel("Stack2", remoteUri, "main", []); + var stack1 = new StackDataItem("Stack1", remoteUri, "main", []); + var stack2 = new StackDataItem("Stack2", remoteUri.ToUpper(), "main", []); - var stackConfig = Substitute.For(); - stackConfig.Load().Returns(new StackData([stack1])); + var dataStore = new MockStackDataStore(new StackData([stack1, stack2])); var gitClient = Substitute.For(); gitClient.GetRemoteUri().Returns(remoteUri); @@ -258,26 +241,24 @@ public void AddStack_ThenRemoveStack_ResultsInOriginalState() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; - var repository = new StackRepository(stackConfig, gitClientFactory, executionContext); + var repository = new StackRepository(dataStore, gitClientFactory, executionContext); // Act - repository.AddStack(stack2); - repository.RemoveStack(stack2); - var stacks = repository.GetStacks(); + repository.RemoveStack(new Model.Stack("Stack1", "main", [])); + repository.SaveChanges(); // Assert - stacks.Should().BeEquivalentTo([stack1]); + dataStore.Data.Stacks.Should().BeEquivalentTo([stack2]); } [Fact] - public void GetStacks_ReturnsNewListEachTime() + public void AddStack_ThenRemoveStack_ResultsInOriginalState() { // Arrange var remoteUri = Some.HttpsUri().ToString(); - var stack1 = new StackModel("Stack1", remoteUri, "main", []); + var stack1 = new StackDataItem("Stack1", remoteUri, "main", []); - var stackConfig = Substitute.For(); - stackConfig.Load().Returns(new StackData([stack1])); + var dataStore = new MockStackDataStore(new StackData([stack1])); var gitClient = Substitute.For(); gitClient.GetRemoteUri().Returns(remoteUri); @@ -287,14 +268,16 @@ public void GetStacks_ReturnsNewListEachTime() var executionContext = new CliExecutionContext { WorkingDirectory = "/repo" }; - var repository = new StackRepository(stackConfig, gitClientFactory, executionContext); + var repository = new StackRepository(dataStore, gitClientFactory, executionContext); // Act - var stacks1 = repository.GetStacks(); - var stacks2 = repository.GetStacks(); + var stack2Model = new Model.Stack("Stack2", "main", []); + repository.AddStack(stack2Model); + repository.RemoveStack(stack2Model); + repository.SaveChanges(); // Assert - stacks1.Should().NotBeSameAs(stacks2, "each call should return a new list instance"); + dataStore.Data.Stacks.Should().BeEquivalentTo([stack1]); } [Fact] @@ -304,8 +287,7 @@ public void WhenExecutionContextHasSpecificWorkingDirectory_UsesGitClientForThat var workingDirectory = "/custom/path"; var remoteUri = Some.HttpsUri().ToString(); - var stackConfig = Substitute.For(); - stackConfig.Load().Returns(new StackData([])); + var dataStore = new MockStackDataStore(new StackData([])); var gitClient = Substitute.For(); gitClient.GetRemoteUri().Returns(remoteUri); @@ -316,7 +298,7 @@ public void WhenExecutionContextHasSpecificWorkingDirectory_UsesGitClientForThat var executionContext = new CliExecutionContext { WorkingDirectory = workingDirectory }; // Act - var repository = new StackRepository(stackConfig, gitClientFactory, executionContext); + var repository = new StackRepository(dataStore, gitClientFactory, executionContext); // Assert repository.GetStacks(); diff --git a/src/Stack.Tests/Persistence/StackTests.cs b/src/Stack.Tests/Persistence/StackTests.cs index 17a7b9c0..007ed0fe 100644 --- a/src/Stack.Tests/Persistence/StackTests.cs +++ b/src/Stack.Tests/Persistence/StackTests.cs @@ -21,7 +21,6 @@ public void GetAllBranchLines_ReturnsAllRootToLeafPaths() // - G var stack = new Model.Stack( "TestStack", - Some.HttpsUri().ToString(), "main", [ new Model.Branch("A", [ @@ -60,7 +59,6 @@ public void MoveBranch_WhenMovingBranchWithoutChildren_MovesBranchToNewLocation( // - C var stack = new Model.Stack( "TestStack", - Some.HttpsUri().ToString(), "main", [ new Model.Branch("A", []), @@ -91,7 +89,6 @@ public void MoveBranch_WhenMovingBranchToSourceBranch_MovesBranchToRootLevel() // - B var stack = new Model.Stack( "TestStack", - Some.HttpsUri().ToString(), "main", [ new Model.Branch("A", [ @@ -122,7 +119,6 @@ public void MoveBranch_WhenMovingBranchWithChildren_AndMoveChildrenAction_MovesB // - D var stack = new Model.Stack( "TestStack", - Some.HttpsUri().ToString(), "main", [ new Model.Branch("A", []), @@ -163,7 +159,6 @@ public void MoveBranch_WhenMovingBranchWithChildren_AndReParentChildrenAction_Mo // - E var stack = new Model.Stack( "TestStack", - Some.HttpsUri().ToString(), "main", [ new Model.Branch("A", []), @@ -207,7 +202,6 @@ public void MoveBranch_WhenMovingDeepNestedBranch_CorrectlyMovesFromAnyDepth() // - E var stack = new Model.Stack( "TestStack", - Some.HttpsUri().ToString(), "main", [ new Model.Branch("A", [ @@ -248,7 +242,6 @@ public void MoveBranch_WhenBranchNotFound_ThrowsException() // Arrange var stack = new Model.Stack( "TestStack", - Some.HttpsUri().ToString(), "main", [ new Model.Branch("A", []) @@ -268,7 +261,6 @@ public void MoveBranch_WhenNewParentNotFound_ThrowsException() // Arrange var stack = new Model.Stack( "TestStack", - Some.HttpsUri().ToString(), "main", [ new Model.Branch("A", [ @@ -293,7 +285,6 @@ public void MoveBranch_WhenMovingRootLevelBranchWithChildrenToAnotherRootLevelBr // - C var stack = new Model.Stack( "TestStack", - Some.HttpsUri().ToString(), "main", [ new Model.Branch("A", [ diff --git a/src/Stack/Commands/Config/OpenConfigCommand.cs b/src/Stack/Commands/Config/OpenConfigCommand.cs index e6bd3fae..5d4fc129 100644 --- a/src/Stack/Commands/Config/OpenConfigCommand.cs +++ b/src/Stack/Commands/Config/OpenConfigCommand.cs @@ -9,24 +9,24 @@ namespace Stack.Commands; public class OpenConfigCommand : Command { - readonly IStackConfig stackConfig; + readonly IStackDataStore dataStore; public OpenConfigCommand( - IStackConfig stackConfig, + IStackDataStore dataStore, CliExecutionContext executionContext, IInputProvider inputProvider, IOutputProvider outputProvider, ILogger logger) : base("open", "Open the configuration file in the default editor.", executionContext, inputProvider, outputProvider, logger) { - this.stackConfig = stackConfig; + this.dataStore = dataStore; } protected override async Task Execute(ParseResult parseResult, CancellationToken cancellationToken) { await Task.CompletedTask; - var configPath = stackConfig.GetConfigPath(); + var configPath = dataStore.GetConfigPath(); if (!File.Exists(configPath)) { diff --git a/src/Stack/Commands/Stack/NewStackCommand.cs b/src/Stack/Commands/Stack/NewStackCommand.cs index 73222134..104eb615 100644 --- a/src/Stack/Commands/Stack/NewStackCommand.cs +++ b/src/Stack/Commands/Stack/NewStackCommand.cs @@ -94,7 +94,7 @@ public override async Task Handle(NewStackCommandInputs inputs, CancellationToke var sourceBranch = await inputProvider.Select(logger, Questions.SelectSourceBranch, inputs.SourceBranch, branches, cancellationToken); var remoteUri = gitClient.GetRemoteUri(); - var stack = new Model.Stack(name, remoteUri, sourceBranch, []); + var stack = new Model.Stack(name, sourceBranch, []); string? branchName = null; BranchAction? branchAction = null; diff --git a/src/Stack/Infrastructure/HostApplicationBuilderExtensions.cs b/src/Stack/Infrastructure/HostApplicationBuilderExtensions.cs index 741c5e94..106816e6 100644 --- a/src/Stack/Infrastructure/HostApplicationBuilderExtensions.cs +++ b/src/Stack/Infrastructure/HostApplicationBuilderExtensions.cs @@ -76,7 +76,7 @@ private static void ConfigureServices(this IServiceCollection services, string[] services.AddSingleton(); } services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddScoped(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Stack/Model/Stack.cs b/src/Stack/Model/Stack.cs index 17183c46..9fe0c7af 100644 --- a/src/Stack/Model/Stack.cs +++ b/src/Stack/Model/Stack.cs @@ -3,7 +3,7 @@ namespace Stack.Model; -public record Stack(string Name, string RemoteUri, string SourceBranch, List Branches) +public record Stack(string Name, string SourceBranch, List Branches) { public List GetAllBranches() { diff --git a/src/Stack/Persistence/StackConfig.cs b/src/Stack/Persistence/FileStackDataStore.cs similarity index 77% rename from src/Stack/Persistence/StackConfig.cs rename to src/Stack/Persistence/FileStackDataStore.cs index 36f5a4f2..8f74f4ef 100644 --- a/src/Stack/Persistence/StackConfig.cs +++ b/src/Stack/Persistence/FileStackDataStore.cs @@ -4,9 +4,12 @@ namespace Stack.Persistence; -public record StackData(List Stacks); +public record StackDataItem(string Name, string RemoteUri, string SourceBranch, List Branches); +public record StackBranchItem(string Name, List Children); -public interface IStackConfig +public record StackData(List Stacks); + +public interface IStackDataStore { string GetConfigPath(); StackData Load(); @@ -21,7 +24,7 @@ internal partial class StackConfigJsonSerializerContext : JsonSerializerContext { } -public class FileStackConfig(string? configDirectory = null) : IStackConfig +public class FileStackDataStore(string? configDirectory = null) : IStackDataStore { readonly string? configDirectory = configDirectory; @@ -101,7 +104,7 @@ private bool IsStackConfigInV2Format(string jsonString) } } - private List LoadStacksFromV2Format(string jsonString) + private List LoadStacksFromV2Format(string jsonString) { var stacksV2 = JsonSerializer.Deserialize(jsonString, StackConfigJsonSerializerContext.Default.StackConfigV2); @@ -112,18 +115,18 @@ private bool IsStackConfigInV2Format(string jsonString) return [.. stacksV2.Stacks.Select(MapFromV2Format)]; } - private static StackV2 MapToV2Format(Model.Stack stack) + private static StackV2 MapToV2Format(StackDataItem stack) { var branchesV2 = stack.Branches.Select(MapToV2Format).ToList(); return new StackV2(stack.Name, stack.RemoteUri, stack.SourceBranch, branchesV2); } - private static StackV2Branch MapToV2Format(Branch branch) + private static StackV2Branch MapToV2Format(StackBranchItem branch) { return new StackV2Branch(branch.Name, [.. branch.Children.Select(MapToV2Format)]); } - private List LoadStacksFromV1Format(string jsonString) + private List LoadStacksFromV1Format(string jsonString) { var stacksV1 = JsonSerializer.Deserialize(jsonString, StackConfigJsonSerializerContext.Default.ListStackV1); if (stacksV1 == null) @@ -134,31 +137,26 @@ private static StackV2Branch MapToV2Format(Branch branch) return [.. stacksV1.Select(MapFromV1Format)]; } - private static Model.Stack MapFromV2Format(StackV2 stackV2) - { - var branches = stackV2.Branches.Select(b => new Model.Branch(b.Name, [.. b.Children.Select(MapFromV2Format)])).ToList(); - return new Model.Stack(stackV2.Name, stackV2.RemoteUri, stackV2.SourceBranch, branches); - } - - private static Model.Branch MapFromV2Format(StackV2Branch branchV2) + private static StackDataItem MapFromV2Format(StackV2 stackV2) { - return new Model.Branch(branchV2.Name, [.. branchV2.Children.Select(MapFromV2Format)]); + var branches = stackV2.Branches.Select(b => new StackBranchItem(b.Name, [.. b.Children.Select(MapFromV2Format)])).ToList(); + return new StackDataItem(stackV2.Name, stackV2.RemoteUri, stackV2.SourceBranch, branches); } - private static StackV1 MapToV1Format(Model.Stack stack) + private static StackBranchItem MapFromV2Format(StackV2Branch branchV2) { - return new StackV1(stack.Name, stack.RemoteUri, stack.SourceBranch, stack.AllBranchNames); + return new StackBranchItem(branchV2.Name, [.. branchV2.Children.Select(MapFromV2Format)]); } - private static Model.Stack MapFromV1Format(StackV1 stackV1) + private static StackDataItem MapFromV1Format(StackV1 stackV1) { // In v1, the branches are a flat list, but this actually represents a tree structure // where each branch is the child of the previous one. - var childBranches = new List(); - Model.Branch? currentParent = null; + var childBranches = new List(); + StackBranchItem? currentParent = null; foreach (var branch in stackV1.Branches) { - var newBranch = new Model.Branch(branch, []); + var newBranch = new StackBranchItem(branch, []); if (currentParent == null) { childBranches.Add(newBranch); @@ -170,7 +168,7 @@ private static Model.Stack MapFromV1Format(StackV1 stackV1) currentParent = newBranch; } - return new Model.Stack(stackV1.Name, stackV1.RemoteUri, stackV1.SourceBranch, childBranches); + return new StackDataItem(stackV1.Name, stackV1.RemoteUri, stackV1.SourceBranch, childBranches); } } diff --git a/src/Stack/Persistence/StackRepository.cs b/src/Stack/Persistence/StackRepository.cs index cbd3d7d6..52b99cbd 100644 --- a/src/Stack/Persistence/StackRepository.cs +++ b/src/Stack/Persistence/StackRepository.cs @@ -16,41 +16,78 @@ public interface IStackRepository public class StackRepository : IStackRepository { - private readonly IStackConfig stackConfig; + private readonly IStackDataStore dataStore; private readonly IGitClientFactory gitClientFactory; private readonly CliExecutionContext executionContext; private readonly Lazy stackData; public StackRepository( - IStackConfig stackConfig, + IStackDataStore dataStore, IGitClientFactory gitClientFactory, CliExecutionContext executionContext) { - this.stackConfig = stackConfig; + this.dataStore = dataStore; this.gitClientFactory = gitClientFactory; this.executionContext = executionContext; - this.stackData = new Lazy(() => stackConfig.Load()); + this.stackData = new Lazy(() => dataStore.Load()); } public List GetStacks() { - var remoteUri = gitClientFactory.Create(executionContext.WorkingDirectory).GetRemoteUri(); + var remoteUri = GetRemoteUri(); - return [.. stackData.Value.Stacks.Where(s => s.RemoteUri.Equals(remoteUri, StringComparison.OrdinalIgnoreCase))]; + return [.. stackData.Value.Stacks + .Where(s => s.RemoteUri.Equals(remoteUri, StringComparison.OrdinalIgnoreCase)) + .Select(s => new Model.Stack( + s.Name, + s.SourceBranch, + [.. s.Branches.Select(b => MapToModelBranch(b))]))]; } public void AddStack(Model.Stack stack) { - stackData.Value.Stacks.Add(stack); + var remoteUri = GetRemoteUri(); + + stackData.Value.Stacks.Add( + new StackDataItem(stack.Name, remoteUri, stack.SourceBranch, [.. stack.Branches.Select(b => MapToDataBranch(b))])); } public void RemoveStack(Model.Stack stack) { - stackData.Value.Stacks.Remove(stack); + var remoteUri = GetRemoteUri(); + var stackToRemove = stackData.Value.Stacks.FirstOrDefault(s => + s.Name == stack.Name && + s.RemoteUri.Equals(remoteUri, StringComparison.OrdinalIgnoreCase)); + + if (stackToRemove == null) + { + throw new InvalidOperationException($"Stack '{stack.Name}' does not exist in the current repository."); + } + + stackData.Value.Stacks.Remove(stackToRemove); } public void SaveChanges() { - stackConfig.Save(stackData.Value); + dataStore.Save(stackData.Value); + } + + private string GetRemoteUri() + { + return gitClientFactory.Create(executionContext.WorkingDirectory).GetRemoteUri(); + } + + private static Model.Branch MapToModelBranch(StackBranchItem branchItem) + { + return new Model.Branch( + branchItem.Name, + [.. branchItem.Children.Select(b => MapToModelBranch(b))]); + } + + private static StackBranchItem MapToDataBranch(Model.Branch branch) + { + return new StackBranchItem( + branch.Name, + [.. branch.Children.Select(b => MapToDataBranch(b))]); } }