From 5471e4670a24e165c40caa936a32c8fc42684121 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 7 Aug 2026 15:51:24 -0700 Subject: [PATCH 1/2] Share transient libgit2 config lookup helper Add LibGit2Repo.GetConfigBoolOrDefault(...) (instance + static overloads) for one-off boolean config reads, replacing scattered short-lived LibGit2Repo/LibGit2RepoInvoker usage at 4 call sites: - GVFS/CommandLine/CloneVerb.cs (gvfs.trust-pack-indexes) - GVFS.Hooks/Program.cs (gvfs.show-hydration-status) - GVFS.Mount/InProcessMount.cs (gvfs.background-cache-auth) - GVFS/CommandLine/PrefetchVerb.cs (gvfs.prefetch-offload) LibGit2RepoInvoker.InitializeSharedRepo() intentionally forces an object-store probe so long-lived/shared callers can amortize object-store load costs. That is wasted work for one-off config reads that immediately dispose the repo. The helper methods live directly on LibGit2Repo rather than a separate extension class, matching the repo.GetConfigBoolOrDefault(name, default) convention already documented in AGENTS.md, and avoiding unnecessary indirection for a class the team owns in the same assembly. Both methods fall back to defaultValue and log a RelatedWarning on any failure, matching the "default on any failure" contract each call site previously implemented independently. Added a protected LibGit2Repo(ITracer tracer) constructor to support test doubles that inject a mock tracer without opening a real repo. Surveyed master for other short-lived config-only LibGit2Repo/ LibGit2RepoInvoker usage; PrefetchStep.cs, GitStatusCache.cs, and GitRepo.cs were left alone because they use shared/long-lived repo access, not the transient anti-pattern this change addresses. Reviewed with an internal 6-lens review-swarm pass (correctness, security, design, tests, async-parallelism, risk-rollout); addressed all actionable findings: - Widened the shared helper's exception handling to a plain catch (Exception), restoring the "default on any failure" guarantee InProcessMount/PrefetchVerb relied on before this refactor. - Fixed a double-RelatedWarning log on the repo-open-failure path. - Replaced a hardcoded, non-portable "Z:\..." path in a unit test with a GUID-suffixed temp path. - Added test coverage for the unset-key (null-coalescing) branch and the InvalidDataException catch arm. - Simplified the parameterless constructor to delegate to the tracer-accepting one. Full unit test suite: 891 passed, 0 failed, 11 skipped (pre-existing, unrelated). Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella --- GVFS/GVFS.Common/Git/LibGit2Repo.cs | 56 +++++++- GVFS/GVFS.Hooks/GVFS.Hooks.csproj | 1 - GVFS/GVFS.Hooks/Program.cs | 9 +- GVFS/GVFS.Mount/InProcessMount.cs | 26 +--- .../Common/LibGit2RepoConfigLookupTests.cs | 127 ++++++++++++++++++ GVFS/GVFS/CommandLine/CloneVerb.cs | 11 +- GVFS/GVFS/CommandLine/PrefetchVerb.cs | 18 +-- 7 files changed, 204 insertions(+), 44 deletions(-) create mode 100644 GVFS/GVFS.UnitTests/Common/LibGit2RepoConfigLookupTests.cs diff --git a/GVFS/GVFS.Common/Git/LibGit2Repo.cs b/GVFS/GVFS.Common/Git/LibGit2Repo.cs index dafcc8d54..09f2e0e16 100644 --- a/GVFS/GVFS.Common/Git/LibGit2Repo.cs +++ b/GVFS/GVFS.Common/Git/LibGit2Repo.cs @@ -39,8 +39,13 @@ public LibGit2Repo(ITracer tracer, string repoPath) } protected LibGit2Repo() + : this(NullTracer.Instance) { - this.Tracer = NullTracer.Instance; + } + + protected LibGit2Repo(ITracer tracer) + { + this.Tracer = tracer; } ~LibGit2Repo() @@ -306,6 +311,55 @@ public virtual string GetConfigString(string name) } } + /// + /// Reads a boolean config value from this already-open repo, falling back to + /// if the key is unset or the read fails for any reason + /// (e.g. a corrupt/unreadable config). + /// + public bool GetConfigBoolOrDefault(string key, bool defaultValue) + { + try + { + return this.GetConfigBool(key) ?? defaultValue; + } + catch (Exception e) + { + this.Tracer.RelatedWarning($"Failed to read {key} config, using default: {e.Message}"); + return defaultValue; + } + } + + /// + /// Reads a single boolean config value from the repo at , + /// opening and disposing a transient for the lookup. Prefer + /// this over for one-off config reads: + /// LibGit2RepoInvoker.InitializeSharedRepo forces the object store to load, which is + /// wasted work when all that's needed is a single config value. Falls back to + /// if the repo can't be opened or the read fails for any + /// reason. + /// + public static bool GetConfigBoolOrDefault(ITracer tracer, string repoPath, string key, bool defaultValue) + { + try + { + using (LibGit2Repo repo = new LibGit2Repo(tracer, repoPath)) + { + return repo.GetConfigBoolOrDefault(key, defaultValue); + } + } + catch (InvalidDataException) + { + // The LibGit2Repo constructor already logged a RelatedWarning with the native + // failure reason before throwing; avoid logging the same failure twice. + return defaultValue; + } + catch (Exception e) + { + tracer.RelatedWarning($"Failed to read {key} config, using default: {e.Message}"); + return defaultValue; + } + } + public void ForEachMultiVarConfig(string key, MultiVarConfigCallback callback) { if (Native.Config.GetConfig(out IntPtr configHandle, this.RepoHandle) != Native.ResultCode.Success) diff --git a/GVFS/GVFS.Hooks/GVFS.Hooks.csproj b/GVFS/GVFS.Hooks/GVFS.Hooks.csproj index 69988ac80..3b996578e 100644 --- a/GVFS/GVFS.Hooks/GVFS.Hooks.csproj +++ b/GVFS/GVFS.Hooks/GVFS.Hooks.csproj @@ -118,4 +118,3 @@ - diff --git a/GVFS/GVFS.Hooks/Program.cs b/GVFS/GVFS.Hooks/Program.cs index 00db23872..940b2cf37 100644 --- a/GVFS/GVFS.Hooks/Program.cs +++ b/GVFS/GVFS.Hooks/Program.cs @@ -171,10 +171,11 @@ private static bool HasShortFlag(string arg, string flag) private static bool ConfigurationAllowsHydrationStatus() { - using (LibGit2RepoInvoker repo = new LibGit2RepoInvoker(NullTracer.Instance, normalizedCurrentDirectory)) - { - return repo.GetConfigBoolOrDefault(GVFSConstants.GitConfig.ShowHydrationStatus, GVFSConstants.GitConfig.ShowHydrationStatusDefault); - } + return LibGit2Repo.GetConfigBoolOrDefault( + NullTracer.Instance, + normalizedCurrentDirectory, + GVFSConstants.GitConfig.ShowHydrationStatus, + GVFSConstants.GitConfig.ShowHydrationStatusDefault); } /// diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index 1e2c69ffb..4cde0a876 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -475,27 +475,11 @@ private GVFSContext CreateContext() private bool IsBackgroundCacheAuthEnabled() { - // Read the flag via libgit2 (in-process) rather than spawning git.exe. - // The GVFSContext (and its shared libgit2 repo) is not created until - // later in mount, so open a short-lived repo here just for the config - // read. Default to off on any failure. - try - { - using (LibGit2Repo repo = new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot)) - { - return repo.GetConfigBool(GVFSConstants.GitConfig.BackgroundCacheAuth) - ?? GVFSConstants.GitConfig.BackgroundCacheAuthDefault; - } - } - catch (Exception e) - { - this.tracer.RelatedWarning( - "Failed to read {0} config, defaulting to {1}: {2}", - GVFSConstants.GitConfig.BackgroundCacheAuth, - GVFSConstants.GitConfig.BackgroundCacheAuthDefault, - e.Message); - return GVFSConstants.GitConfig.BackgroundCacheAuthDefault; - } + return LibGit2Repo.GetConfigBoolOrDefault( + this.tracer, + this.enlistment.WorkingDirectoryBackingRoot, + GVFSConstants.GitConfig.BackgroundCacheAuth, + GVFSConstants.GitConfig.BackgroundCacheAuthDefault); } private void ValidateMountPoints() diff --git a/GVFS/GVFS.UnitTests/Common/LibGit2RepoConfigLookupTests.cs b/GVFS/GVFS.UnitTests/Common/LibGit2RepoConfigLookupTests.cs new file mode 100644 index 000000000..70031e2ec --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/LibGit2RepoConfigLookupTests.cs @@ -0,0 +1,127 @@ +using GVFS.Common.Git; +using GVFS.Tests.Should; +using GVFS.UnitTests.Mock.Common; +using NUnit.Framework; +using System; +using System.IO; + +namespace GVFS.UnitTests.Common +{ + [TestFixture] + public class LibGit2RepoConfigLookupTests + { + [TestCase] + public void GetConfigBoolOrDefaultOnRepoReturnsConfiguredValue() + { + MockTracer tracer = new MockTracer(); + + using (MockConfigRepo repo = new MockConfigRepo(tracer, true)) + { + bool value = repo.GetConfigBoolOrDefault("gvfs.test", false); + + value.ShouldEqual(true); + tracer.RelatedWarningEvents.Count.ShouldEqual(0); + } + } + + [TestCase] + public void GetConfigBoolOrDefaultOnRepoReturnsDefaultWhenKeyIsUnset() + { + MockTracer tracer = new MockTracer(); + + using (MockConfigRepo repo = new MockConfigRepo(tracer, (bool?)null)) + { + bool value = repo.GetConfigBoolOrDefault("gvfs.test", true); + + value.ShouldEqual(true); + tracer.RelatedWarningEvents.Count.ShouldEqual(0); + } + } + + [TestCase] + public void GetConfigBoolOrDefaultOnRepoReturnsDefaultOnLibGit2ExceptionAndLogsOnce() + { + MockTracer tracer = new MockTracer(); + + using (MockConfigRepo repo = new MockConfigRepo(tracer, new LibGit2Exception("boom"))) + { + bool value = repo.GetConfigBoolOrDefault("gvfs.test", false); + + value.ShouldEqual(false); + tracer.RelatedWarningEvents.Count.ShouldEqual(1); + tracer.RelatedWarningEvents[0].ShouldContain("Failed to read gvfs.test config, using default: boom"); + } + } + + [TestCase] + public void GetConfigBoolOrDefaultOnRepoReturnsDefaultOnInvalidDataExceptionAndLogsOnce() + { + MockTracer tracer = new MockTracer(); + + using (MockConfigRepo repo = new MockConfigRepo(tracer, new InvalidDataException("corrupt config"))) + { + bool value = repo.GetConfigBoolOrDefault("gvfs.test", false); + + value.ShouldEqual(false); + tracer.RelatedWarningEvents.Count.ShouldEqual(1); + tracer.RelatedWarningEvents[0].ShouldContain("Failed to read gvfs.test config, using default: corrupt config"); + } + } + + [TestCase] + public void GetConfigBoolOrDefaultOnPathReturnsDefaultForMissingRepoAndLogsExactlyOnce() + { + MockTracer tracer = new MockTracer(); + + // A GUID-suffixed path under the OS temp directory is guaranteed not to exist and + // does not depend on any particular drive letter being unmapped (unlike a + // hardcoded "Z:\..." path, which could resolve on a host with that drive mapped). + string missingRepoPath = Path.Combine( + Path.GetTempPath(), + "LibGit2RepoConfigLookupTests_" + Guid.NewGuid().ToString("N")); + + bool value = LibGit2Repo.GetConfigBoolOrDefault( + tracer, + missingRepoPath, + "gvfs.test", + false); + + value.ShouldEqual(false); + + // The LibGit2Repo constructor logs a RelatedWarning with the native open-failure + // reason before throwing InvalidDataException; the static helper's catch does not + // log a second time for that case (see LibGit2Repo.GetConfigBoolOrDefault), so + // exactly one warning is expected here. + tracer.RelatedWarningEvents.Count.ShouldEqual(1); + tracer.RelatedWarningEvents[0].ShouldContain("Couldn't open repo at"); + } + + private class MockConfigRepo : LibGit2Repo + { + private readonly bool? value; + private readonly Exception exceptionToThrow; + + public MockConfigRepo(MockTracer tracer, bool? value) + : base(tracer) + { + this.value = value; + } + + public MockConfigRepo(MockTracer tracer, Exception exceptionToThrow) + : base(tracer) + { + this.exceptionToThrow = exceptionToThrow; + } + + public override bool? GetConfigBool(string name) + { + if (this.exceptionToThrow != null) + { + throw this.exceptionToThrow; + } + + return this.value; + } + } + } +} diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs index 977c5b082..1277355ab 100644 --- a/GVFS/GVFS/CommandLine/CloneVerb.cs +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -152,7 +152,7 @@ public override void Execute() CacheServerInfo cacheServer = null; ServerGVFSConfig serverGVFSConfig = null; - bool trustPackIndexes; + bool trustPackIndexes = GVFSConstants.GitConfig.TrustPackIndexesDefault; using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "GVFSClone")) { @@ -248,10 +248,13 @@ public override void Execute() { tracer.RelatedError(cloneResult.ErrorMessage); } - - using (var repo = new LibGit2RepoInvoker(tracer, enlistment.WorkingDirectoryBackingRoot)) + else { - trustPackIndexes = repo.GetConfigBoolOrDefault(GVFSConstants.GitConfig.TrustPackIndexes, GVFSConstants.GitConfig.TrustPackIndexesDefault); + trustPackIndexes = LibGit2Repo.GetConfigBoolOrDefault( + tracer, + enlistment.WorkingDirectoryBackingRoot, + GVFSConstants.GitConfig.TrustPackIndexes, + GVFSConstants.GitConfig.TrustPackIndexesDefault); } } diff --git a/GVFS/GVFS/CommandLine/PrefetchVerb.cs b/GVFS/GVFS/CommandLine/PrefetchVerb.cs index 945984d62..6fa0d91f4 100644 --- a/GVFS/GVFS/CommandLine/PrefetchVerb.cs +++ b/GVFS/GVFS/CommandLine/PrefetchVerb.cs @@ -700,19 +700,11 @@ private string GetCacheServerDisplay(CacheServerInfo cacheServer, string repoUrl private bool IsPrefetchOffloadEnabled(ITracer tracer, GVFSEnlistment enlistment) { - try - { - using (LibGit2Repo repo = new LibGit2Repo(tracer, enlistment.WorkingDirectoryBackingRoot)) - { - bool? enabled = repo.GetConfigBool(GVFSConstants.GitConfig.PrefetchOffload); - return enabled ?? GVFSConstants.GitConfig.PrefetchOffloadDefault; - } - } - catch (Exception ex) - { - tracer.RelatedWarning($"Failed to read '{GVFSConstants.GitConfig.PrefetchOffload}' config; defaulting to {GVFSConstants.GitConfig.PrefetchOffloadDefault}: {ex.GetType().Name}: {ex.Message}"); - return GVFSConstants.GitConfig.PrefetchOffloadDefault; - } + return LibGit2Repo.GetConfigBoolOrDefault( + tracer, + enlistment.WorkingDirectoryBackingRoot, + GVFSConstants.GitConfig.PrefetchOffload, + GVFSConstants.GitConfig.PrefetchOffloadDefault); } /// From 332b4fc42a0f99906f8180650b0d9d65462d0e07 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 7 Aug 2026 15:52:59 -0700 Subject: [PATCH 2/2] Fix NullReferenceException when cloning into a non-empty directory `gvfs clone ` into a non-empty directory throws an unhandled NullReferenceException instead of reporting the expected error: Cannot clone @ : System.NullReferenceException: Object reference not set to an instance of an object. at GVFS.CommandLine.CloneVerb.Execute() + 0x9fc v1.0 behavior (expected): Cannot clone @ Error: Clone directory '' exists and is not empty Root cause: CloneVerb.Execute() unconditionally read enlistment.WorkingDirectoryBackingRoot to determine trustPackIndexes, even when TryCreateEnlistment failed (e.g. because the target directory already exists and is not empty). In that failure path enlistment is null, so the dereference throws. Extract the lookup into internal bool GetTrustPackIndexes(ITracer, Result, GVFSEnlistment), which only touches enlistment when cloneResult.Success is true; otherwise it returns the default without dereferencing enlistment. Delegates to the shared LibGit2Repo.GetConfigBoolOrDefault helper. Widen TryCreateEnlistment/Result from private to internal so they are directly unit-testable (GVFS assembly already grants InternalsVisibleTo to GVFS.UnitTests). Add GVFS.UnitTests.CommandLine.CloneVerbTests: - TryCreateEnlistmentFailsWithoutEnlistmentWhenTargetDirectoryIsNotEmpty: confirms the failure precondition (null enlistment on non-empty target dir). - TryCreateEnlistmentDoesNotFailForEmptyTargetDirectory: boundary case, an empty target directory does not trigger the "exists and is not empty" error. - TryCreateEnlistmentReportsNormalizedPathWhenItDiffersFromFullPath: covers the divergent full-vs-normalized-path error message branch. - GetTrustPackIndexesDoesNotThrowWhenCloneFailedAndEnlistmentIsNull: the actual regression test, driving the exact failed-clone/null-enlistment composition that used to throw. Verified by temporarily removing the cloneResult.Success gate: the test failed with the original NullReferenceException, then passed again once the gate was restored. Reviewed with an internal 6-lens review-swarm pass; the main finding was that an earlier draft of the regression test only proved TryCreateEnlistment's own contract (already true pre-fix) without exercising the actual Execute() null-dereference, addressed by the extraction and test above. Full unit test suite: 895 passed, 0 failed, 11 skipped (pre-existing, unrelated). Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella --- .../CommandLine/CloneVerbTests.cs | 97 +++++++++++++++++++ GVFS/GVFS/CommandLine/CloneVerb.cs | 37 +++++-- 2 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 GVFS/GVFS.UnitTests/CommandLine/CloneVerbTests.cs diff --git a/GVFS/GVFS.UnitTests/CommandLine/CloneVerbTests.cs b/GVFS/GVFS.UnitTests/CommandLine/CloneVerbTests.cs new file mode 100644 index 000000000..e8055ce5e --- /dev/null +++ b/GVFS/GVFS.UnitTests/CommandLine/CloneVerbTests.cs @@ -0,0 +1,97 @@ +using GVFS.Common; +using GVFS.CommandLine; +using GVFS.UnitTests.Mock.Common; +using NUnit.Framework; +using System; +using System.IO; + +namespace GVFS.UnitTests.CommandLine +{ + [TestFixture] + public class CloneVerbTests + { + private CloneVerb cloneVerb; + private string testDir; + + [SetUp] + public void Setup() + { + this.cloneVerb = new CloneVerb(); + this.testDir = Path.Combine(Path.GetTempPath(), "CloneVerbTests_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(this.testDir); + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(this.testDir)) + { + Directory.Delete(this.testDir, recursive: true); + } + } + + [TestCase] + public void TryCreateEnlistmentFailsWithoutEnlistmentWhenTargetDirectoryIsNotEmpty() + { + File.WriteAllText(Path.Combine(this.testDir, "preexisting.txt"), "content"); + + CloneVerb.Result result = this.cloneVerb.TryCreateEnlistment( + this.testDir, + this.testDir, + out GVFSEnlistment enlistment); + + Assert.IsFalse(result.Success); + Assert.IsNull(enlistment); + StringAssert.Contains("exists and is not empty", result.ErrorMessage); + } + + [TestCase] + public void TryCreateEnlistmentDoesNotFailForEmptyTargetDirectory() + { + // testDir is created empty by Setup and never written to in this test. + CloneVerb.Result result = this.cloneVerb.TryCreateEnlistment( + this.testDir, + this.testDir, + out GVFSEnlistment enlistment); + + StringAssert.DoesNotContain("exists and is not empty", result.ErrorMessage ?? string.Empty); + } + + [TestCase] + public void TryCreateEnlistmentReportsNormalizedPathWhenItDiffersFromFullPath() + { + File.WriteAllText(Path.Combine(this.testDir, "preexisting.txt"), "content"); + string fullPath = this.testDir + Path.DirectorySeparatorChar; + + CloneVerb.Result result = this.cloneVerb.TryCreateEnlistment( + fullPath, + this.testDir, + out GVFSEnlistment enlistment); + + Assert.IsFalse(result.Success); + Assert.IsNull(enlistment); + StringAssert.Contains($"'{fullPath}'", result.ErrorMessage); + StringAssert.Contains($"['{this.testDir}']", result.ErrorMessage); + } + + // Regression test: this is the actual code path that used to throw a + // NullReferenceException when `gvfs clone` targeted a non-empty directory. + // TryCreateEnlistment (above) fails and returns a null enlistment; CloneVerb.Execute() + // used to unconditionally dereference that null enlistment to read the trustPackIndexes + // config, crashing instead of reporting the "exists and is not empty" error. Execute() + // itself cannot be unit-tested (it terminates the process via Environment.Exit()), so + // GetTrustPackIndexes was extracted as the smallest testable seam that reproduces the + // exact failure condition: a failed clone result with a null enlistment. + [TestCase] + public void GetTrustPackIndexesDoesNotThrowWhenCloneFailedAndEnlistmentIsNull() + { + MockTracer tracer = new MockTracer(); + CloneVerb.Result failedCloneResult = new CloneVerb.Result("Clone directory exists and is not empty"); + + bool trustPackIndexes = true; + Assert.DoesNotThrow(() => trustPackIndexes = this.cloneVerb.GetTrustPackIndexes(tracer, failedCloneResult, enlistment: null)); + + Assert.AreEqual(GVFSConstants.GitConfig.TrustPackIndexesDefault, trustPackIndexes); + } + } +} diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs index 1277355ab..23e6b2303 100644 --- a/GVFS/GVFS/CommandLine/CloneVerb.cs +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -248,14 +248,8 @@ public override void Execute() { tracer.RelatedError(cloneResult.ErrorMessage); } - else - { - trustPackIndexes = LibGit2Repo.GetConfigBoolOrDefault( - tracer, - enlistment.WorkingDirectoryBackingRoot, - GVFSConstants.GitConfig.TrustPackIndexes, - GVFSConstants.GitConfig.TrustPackIndexesDefault); - } + + trustPackIndexes = this.GetTrustPackIndexes(tracer, cloneResult, enlistment); } if (cloneResult.Success) @@ -364,7 +358,30 @@ private static bool IsForceCheckoutErrorCloneFailure(string checkoutError) return true; } - private Result TryCreateEnlistment( + /// + /// Determines whether pack indexes should be trusted for the newly cloned enlistment. + /// Only reads the enlistment's git config when indicates + /// the clone succeeded; is null when the clone failed + /// (e.g. TryCreateEnlistment failed because the target directory was not empty), and must + /// not be dereferenced in that case. This gating is what fixes the NullReferenceException + /// regression where `gvfs clone` into a non-empty directory used to crash instead of + /// reporting "exists and is not empty". + /// + internal bool GetTrustPackIndexes(ITracer tracer, Result cloneResult, GVFSEnlistment enlistment) + { + if (!cloneResult.Success) + { + return GVFSConstants.GitConfig.TrustPackIndexesDefault; + } + + return LibGit2Repo.GetConfigBoolOrDefault( + tracer, + enlistment.WorkingDirectoryBackingRoot, + GVFSConstants.GitConfig.TrustPackIndexes, + GVFSConstants.GitConfig.TrustPackIndexesDefault); + } + + internal Result TryCreateEnlistment( string fullEnlistmentRootPathParameter, string normalizedEnlistementRootPath, out GVFSEnlistment enlistment) @@ -816,7 +833,7 @@ private Result TryInitRepo(ITracer tracer, GitRefs refs, Enlistment enlistmentTo return new Result(true); } - private class Result + internal class Result { public Result(bool success) {