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))]);
}
}