From a7d659a27c4ac689b9453f0f1cc9d792261e8ebb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 08:26:57 +0000 Subject: [PATCH] Resolve a relative --mirror-root instead of aborting startup [patch] The options validator requires a fully qualified MirrorRoot, and the flag was copied into configuration as typed, so `--mirror-root ./mirrors` ended in an unhandled OptionsValidationException with a stack trace. Resolve the flag against the working directory, as --config already is and as GitLfsCache does for --store. Configuration files and environment variables keep the strict absolute-path rule. Main is now internal so a test can run it with a relative --mirror-root. Fixes ktsu-dev/GitBranchStateCache#77 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01URmxVUpdaWCEUSPudDfLsq --- .../Tool/MirrorRootFlagTests.cs | 40 +++++++++++++++++++ GitBranchStateCache.Tool/Program.cs | 18 ++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 GitBranchStateCache.Tests/Tool/MirrorRootFlagTests.cs diff --git a/GitBranchStateCache.Tests/Tool/MirrorRootFlagTests.cs b/GitBranchStateCache.Tests/Tool/MirrorRootFlagTests.cs new file mode 100644 index 0000000..bc0d21b --- /dev/null +++ b/GitBranchStateCache.Tests/Tool/MirrorRootFlagTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.GitBranchStateCache.Tests.Tool; + +using ktsu.GitBranchStateCache.Tool; + +[TestClass] +public class MirrorRootFlagTests +{ + [TestMethod] + public void MirrorRoot_RelativeDirectory_ResolvesAgainstTheWorkingDirectory() + { + // The options validator refuses a root that is not fully qualified, so passing + // `--mirror-root ./mirrors` through verbatim aborted startup with a validation stack trace. + string resolved = Program.ResolveMirrorRoot("./mirrors"); + + Assert.IsTrue(Path.IsPathFullyQualified(resolved), resolved); + Assert.AreEqual(Path.Combine(Environment.CurrentDirectory, "mirrors"), resolved); + } + + [TestMethod] + public async Task MirrorRoot_RelativeDirectory_IsAcceptedByTheCommandLine() + { + // The invalid upstream stops the run after the flags are read and before a server starts, so a + // relative --mirror-root reaches the same one-line failure path as any other flag mistake. + int exitCode = await Program.Main(["--mirror-root", "./mirrors", "--upstream", "not-a-name-url-pair"]); + + Assert.AreEqual(1, exitCode); + } + + [TestMethod] + public void MirrorRoot_FullyQualifiedDirectory_IsKeptAsGiven() + { + string root = Path.Combine( + Path.GetPathRoot(Path.GetTempPath()) ?? Path.DirectorySeparatorChar.ToString(), + "gitbranchstatecache"); + + Assert.AreEqual(root, Program.ResolveMirrorRoot(root)); + } +} diff --git a/GitBranchStateCache.Tool/Program.cs b/GitBranchStateCache.Tool/Program.cs index bb5ef6f..811e29a 100644 --- a/GitBranchStateCache.Tool/Program.cs +++ b/GitBranchStateCache.Tool/Program.cs @@ -26,7 +26,7 @@ internal static class Program /// The configuration key the --mirror-root flag overrides. private const string MirrorRootKey = "GitBranchStateCache:MirrorRoot"; - private static async Task Main(string[] args) + internal static async Task Main(string[] args) { Option port = new("--port", "-p") { @@ -94,7 +94,7 @@ private static async Task Main(string[] args) if (parseResult.GetValue(mirrorRoot) is string root_) { - overrides[MirrorRootKey] = root_; + overrides[MirrorRootKey] = ResolveMirrorRoot(root_); } if (parseResult.GetValue(gitExecutable) is string git) @@ -266,6 +266,20 @@ private static bool TryApplyUpstreams( return true; } + /// + /// Resolves the --mirror-root flag against the working directory. + /// + /// + /// The options validator requires a fully qualified root, which keeps configuration files and + /// environment variables strict for container deployments. A relative directory is what someone + /// running the tool locally types, though, so the flag resolves it the way --config does + /// rather than letting startup fail with a validation stack trace. This is the same behaviour as + /// --store in ktsu.GitLfsCache. + /// + /// The directory as given on the command line. + /// The fully qualified directory. + internal static string ResolveMirrorRoot(string mirrorRoot) => Path.GetFullPath(mirrorRoot); + /// /// Groups every --allow flag by the upstream it names. ///