diff --git a/GVFS/FastFetch/FastFetchVerb.cs b/GVFS/FastFetch/FastFetchVerb.cs
index cf6add3aad..0b0c3147a7 100644
--- a/GVFS/FastFetch/FastFetchVerb.cs
+++ b/GVFS/FastFetch/FastFetchVerb.cs
@@ -204,6 +204,24 @@ private int ExecuteWithExitCode()
return ExitFailure;
}
+ // Check this before any code that reads or writes refs assuming the "files"
+ // ref storage format runs, since FastFetch can be pointed at an arbitrary
+ // pre-existing git repository rather than only ones 'gvfs clone' created.
+ // A genuine config-read failure (as opposed to the key simply being absent,
+ // which reports the repository as files-format) is treated as fatal here: a
+ // missing key does not reach this branch, so a read error means the repo's
+ // config is anomalous and continuing risks operating on an unsupported repo.
+ if (!RefStorage.TryIsReftableRepo(enlistment.CreateGitProcess(), out bool isReftableRepo, out string refStorageReadError))
+ {
+ Console.WriteLine("Could not determine the repository's ref storage format: " + refStorageReadError);
+ return ExitFailure;
+ }
+ else if (isReftableRepo)
+ {
+ Console.WriteLine(RefStorage.UnsupportedReftableErrorMessage);
+ return ExitFailure;
+ }
+
string commitish = this.Commit ?? this.Branch;
if (string.IsNullOrWhiteSpace(commitish))
{
diff --git a/GVFS/GVFS.Common/Git/GitProcess.cs b/GVFS/GVFS.Common/Git/GitProcess.cs
index f3100a90cf..e4a80cd5b2 100644
--- a/GVFS/GVFS.Common/Git/GitProcess.cs
+++ b/GVFS/GVFS.Common/Git/GitProcess.cs
@@ -51,6 +51,13 @@ public class GitProcess : ICredentialStore
private static bool failedToSetEncoding = false;
private static string expireTimeDateString;
+ ///
+ /// First git version that supports 'git init --ref-format' and the reftable ref
+ /// backend (git 2.45). Earlier git can neither create a reftable repository nor
+ /// understand the option, so the '--ref-format=files' pin is only added from here on.
+ ///
+ private static readonly GitVersion FirstGitVersionSupportingRefFormat = new GitVersion(2, 45, 0, (string)null);
+
///
/// Lock taken for duration of running executingProcess.
///
@@ -130,13 +137,42 @@ public static string ExpireTimeDateString
public bool LowerPriority { get; set; }
- public static Result Init(Enlistment enlistment)
+ ///
+ /// Runs 'git init' for a new enlistment. The caller passes the already-resolved
+ /// installed git version (the clone flow validates it earlier via
+ /// GVFSVerb.CheckGitVersion) so this does not shell out for 'git --version' again;
+ /// pass null when the version is unknown, which conservatively omits the
+ /// version-gated '--ref-format=files' pin.
+ ///
+ public static Result Init(Enlistment enlistment, GitVersion installedGitVersion)
{
// --object-format=sha1 is pinned explicitly so that GVFS does not silently pick up
// a future git default of sha256. AzDO (the only GVFS-protocol server implementer)
// has no plans to support sha256, and GVFS's own code assumes 20-byte/40-hex-char
// sha1 object IDs throughout, so a sha256 repo would corrupt data or crash.
- return enlistment.CreateGitProcess().InvokeGitOutsideEnlistment("init --object-format=sha1 \"" + enlistment.WorkingDirectoryBackingRoot + "\"");
+ string arguments = "init --object-format=sha1";
+
+ // --ref-format=files is pinned for the same reason on the ref storage axis: GVFS
+ // writes refs directly (e.g. CloneVerb writes .git/packed-refs) rather than through
+ // git's ref backend, and the reftable backend ignores those writes. Pinning here
+ // overrides a user's global init.defaultRefFormat=reftable so 'gvfs clone' produces
+ // a usable files-format enlistment instead of a broken one. The option only exists
+ // in git 2.45+, which is also the first git that can create a reftable repo at all,
+ // so older git needs no pin and would reject the unknown option. A null version
+ // (undetermined) omits the pin; CloneVerb.TryInitRepo still fails fast if the repo
+ // turns out to be reftable.
+ if (SupportsRefFormatOption(installedGitVersion))
+ {
+ arguments += " --ref-format=files";
+ }
+
+ arguments += " \"" + enlistment.WorkingDirectoryBackingRoot + "\"";
+ return enlistment.CreateGitProcess().InvokeGitOutsideEnlistment(arguments);
+ }
+
+ internal static bool SupportsRefFormatOption(GitVersion installedGitVersion)
+ {
+ return installedGitVersion != null && !installedGitVersion.IsLessThan(FirstGitVersionSupportingRefFormat);
}
public static ConfigResult GetFromGlobalConfig(string gitBinPath, string settingName)
diff --git a/GVFS/GVFS.Common/Git/RefStorage.cs b/GVFS/GVFS.Common/Git/RefStorage.cs
new file mode 100644
index 0000000000..12bace313f
--- /dev/null
+++ b/GVFS/GVFS.Common/Git/RefStorage.cs
@@ -0,0 +1,86 @@
+using System;
+
+namespace GVFS.Common.Git
+{
+ ///
+ /// Detects whether a git repository uses the "reftable" ref storage backend
+ /// (git's newer, opt-in binary ref format) rather than the "files" backend
+ /// (loose ref files plus packed-refs) that VFS for Git assumes throughout its
+ /// ref handling.
+ ///
+ /// VFS for Git does not populate refs through git's ref backend during a clone -
+ /// it writes .git/packed-refs directly from the refs returned by the GVFS protocol
+ /// (see CloneVerb.TryInitRepo and GitRefs.ToPackedRefs). A reftable-backed
+ /// repository ignores packed-refs entirely, so those refs would be invisible to
+ /// git and the clone would fail. Other VFS for Git code also reads and writes
+ /// loose ref files and the .git/logs reflog directly (for example
+ /// GitHeadRepairJob). Callers should use this detection to fail fast with a clear
+ /// error rather than let that ref-file-shaped code silently produce a broken
+ /// enlistment.
+ ///
+ public static class RefStorage
+ {
+ public const string RefStorageConfigName = "extensions.refstorage";
+ public const string ReftableValue = "reftable";
+
+ ///
+ /// Name of the directory under .git in which the reftable backend stores refs
+ /// (.git/reftable/). Its presence identifies a reftable repository even when the
+ /// git config is too corrupt for 'git config' to read - useful for 'gvfs repair',
+ /// which runs precisely when the config may be unreadable.
+ ///
+ public const string ReftableDirectoryName = "reftable";
+
+ public const string UnsupportedReftableErrorMessage =
+ "VFS for Git only supports repositories that use the \"files\" ref storage format, but this " +
+ "repository was initialized with the \"reftable\" format. VFS for Git writes refs directly " +
+ "rather than through git's ref backend, so a reftable repository cannot be used. Re-clone or " +
+ "re-initialize the repository with files ref storage (for example, 'git init --ref-format=files').";
+
+ ///
+ /// Returns true if the given value of extensions.refstorage identifies a reftable
+ /// repository. A missing/empty value means the repository defaults to the "files"
+ /// ref storage format.
+ ///
+ public static bool IsReftable(string refStorageConfigValue)
+ {
+ return string.Equals(refStorageConfigValue?.Trim(), ReftableValue, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Reads extensions.refstorage from the local config of the repo backing the given
+ /// GitProcess, and returns true if it identifies a reftable repository.
+ ///
+ /// A config read failure (e.g. a broken git invocation, as opposed to the key simply
+ /// being absent) is treated the same as "not reftable" here, matching the established
+ /// behavior of GitProcess.TryGetFromConfig elsewhere in this codebase for optional
+ /// config reads. Use if the caller wants to distinguish
+ /// and surface that failure instead of silently treating it as the files format.
+ ///
+ public static bool IsReftableRepo(GitProcess git)
+ {
+ TryIsReftableRepo(git, out bool isReftable, out string _);
+ return isReftable;
+ }
+
+ ///
+ /// Reads extensions.refstorage from the local config of the repo backing the given
+ /// GitProcess. Returns false and populates if the config
+ /// could not be read at all (a missing key is not an error - it simply means the
+ /// repository defaults to the "files" ref storage format, and isReftable is set to
+ /// false).
+ ///
+ public static bool TryIsReftableRepo(GitProcess git, out bool isReftable, out string error)
+ {
+ GitProcess.ConfigResult result = git.GetFromLocalConfig(RefStorageConfigName);
+ if (!result.TryParseAsString(out string value, out error))
+ {
+ isReftable = false;
+ return false;
+ }
+
+ isReftable = IsReftable(value);
+ return true;
+ }
+ }
+}
diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs
index 0a5c930b5f..a6b5d9e94b 100644
--- a/GVFS/GVFS.Mount/InProcessMount.cs
+++ b/GVFS/GVFS.Mount/InProcessMount.cs
@@ -280,6 +280,32 @@ private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords)
this.FailMountAndExit("The .git folder is missing or has invalid contents");
}
+ // Check this before TrySetRequiredGitConfigSettings, which unconditionally
+ // forces core.repositoryformatversion back to 0 (matching what 'git init'
+ // writes for a files-format repo) without knowing about extensions.refstorage.
+ // Applying that write to a reftable repo would leave behind an inconsistent
+ // config (repositoryformatversion=0 with a v1-only extension still present)
+ // that git itself then refuses to parse at all - so this check must run first,
+ // before any other git config mutation. VFS for Git also writes refs directly
+ // rather than through git's ref backend, so a reftable repo cannot be used
+ // regardless. A genuine config-read failure is fatal here rather than a
+ // warning: a missing key reports the repo as files-format and does not reach
+ // this branch, so a read error means the config is anomalous and proceeding
+ // into the force-write could brick an unsupported repo.
+ if (!RefStorage.TryIsReftableRepo(git, out bool isReftableRepo, out string refStorageReadError))
+ {
+ // Pass the git error as a format argument, not concatenated into the
+ // format string: FailMountAndExit routes through ITracer.RelatedError(
+ // string, params object[]), which runs string.Format, so a '{' in the
+ // git stderr (possible with a crafted/corrupt .git/config) would throw
+ // a FormatException instead of failing the mount cleanly.
+ this.FailMountAndExit("Could not determine the repository's ref storage format: {0}", refStorageReadError);
+ }
+ else if (isReftableRepo)
+ {
+ this.FailMountAndExit(RefStorage.UnsupportedReftableErrorMessage);
+ }
+
if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(this.enlistment.WorkingDirectoryRoot, out string fsError))
{
this.FailMountAndExit("FileSystem unsupported: " + fsError);
diff --git a/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs b/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs
index 31d6fe2c85..64ba6b71b5 100644
--- a/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs
+++ b/GVFS/GVFS.UnitTests/Git/GitProcessTests.cs
@@ -142,23 +142,97 @@ private static List ReadTokens(string content)
}
[TestCase]
- public void Init_PinsObjectFormatToSha1()
+ public void Init_PinsObjectFormatToSha1_AndOmitsRefFormatWhenVersionUnknown()
{
MockGitProcess gitProcess = new MockGitProcess();
MockGVFSEnlistment enlistment = new MockGVFSEnlistment(gitProcess);
+ // object-format=sha1 is always pinned. A null (undetermined) git version also
+ // omits the version-gated --ref-format pin - CloneVerb.TryInitRepo's reftable
+ // detection is the safety net in that case.
string expectedCommand = "init --object-format=sha1 \"" + enlistment.WorkingDirectoryBackingRoot + "\"";
gitProcess.SetExpectedCommandResult(
expectedCommand,
() => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));
- GitProcess.Result result = GitProcess.Init(enlistment);
+ GitProcess.Result result = GitProcess.Init(enlistment, installedGitVersion: null);
result.ExitCodeIsFailure.ShouldBeFalse();
gitProcess.CommandsRun.Count.ShouldEqual(1);
gitProcess.CommandsRun[0].ShouldEqual(expectedCommand);
}
+ [TestCase]
+ public void Init_PinsRefFormatToFiles_WhenGitSupportsRefFormat()
+ {
+ MockGitProcess gitProcess = new MockGitProcess();
+ MockGVFSEnlistment enlistment = new MockGVFSEnlistment(gitProcess);
+
+ string expectedCommand = "init --object-format=sha1 --ref-format=files \"" + enlistment.WorkingDirectoryBackingRoot + "\"";
+ gitProcess.SetExpectedCommandResult(
+ expectedCommand,
+ () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));
+
+ GitProcess.Result result = GitProcess.Init(enlistment, new GitVersion(2, 45, 0, (string)null));
+
+ result.ExitCodeIsFailure.ShouldBeFalse();
+ gitProcess.CommandsRun.Count.ShouldEqual(1);
+ gitProcess.CommandsRun[0].ShouldEqual(expectedCommand);
+ }
+
+ [TestCase]
+ public void Init_DoesNotPinRefFormat_WhenGitPredatesRefFormat()
+ {
+ MockGitProcess gitProcess = new MockGitProcess();
+ MockGVFSEnlistment enlistment = new MockGVFSEnlistment(gitProcess);
+
+ string expectedCommand = "init --object-format=sha1 \"" + enlistment.WorkingDirectoryBackingRoot + "\"";
+ gitProcess.SetExpectedCommandResult(
+ expectedCommand,
+ () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode));
+
+ GitProcess.Result result = GitProcess.Init(enlistment, new GitVersion(2, 44, 0, (string)null));
+
+ result.ExitCodeIsFailure.ShouldBeFalse();
+ gitProcess.CommandsRun.Count.ShouldEqual(1);
+ gitProcess.CommandsRun[0].ShouldEqual(expectedCommand);
+ }
+
+ [TestCase(2, 45, 0, true)] // first version with --ref-format
+ [TestCase(2, 55, 0, true)] // newer
+ [TestCase(2, 44, 0, false)] // one minor before
+ [TestCase(2, 31, 0, false)] // MinimumGitVersion
+ [TestCase(1, 99, 0, false)] // older major
+ public void SupportsRefFormatOption_GatesOnGit245(int major, int minor, int build, bool expected)
+ {
+ GitProcess.SupportsRefFormatOption(new GitVersion(major, minor, build, (string)null)).ShouldEqual(expected);
+ }
+
+ [TestCase]
+ public void SupportsRefFormatOption_IsTrueForMicrosoftGitBuildOf245()
+ {
+ // Microsoft/Git versions carry a platform + revision suffix (e.g. 2.45.0.vfs.0.1);
+ // the comparison must treat such a build of 2.45 as supporting --ref-format.
+ GitProcess.SupportsRefFormatOption(new GitVersion(2, 45, 0, "vfs", 0, 1)).ShouldEqual(true);
+ }
+
+ [TestCase]
+ public void SupportsRefFormatOption_IsFalseForReleaseCandidateOf245()
+ {
+ // A release candidate of 2.45 sorts before the final 2.45 (GitVersion treats a
+ // version with a release candidate as less than the same version without one), so
+ // the gate conservatively omits the pin for an rc build. That is a missed
+ // optimization, not a correctness problem: the clone falls back to un-pinned init
+ // exactly as on pre-2.45 git, and the reftable detection still catches the repo.
+ GitProcess.SupportsRefFormatOption(new GitVersion(2, 45, 0, releaseCandidate: 0)).ShouldEqual(false);
+ }
+
+ [TestCase]
+ public void SupportsRefFormatOption_IsFalseForNullVersion()
+ {
+ GitProcess.SupportsRefFormatOption(null).ShouldEqual(false);
+ }
+
[TestCase]
public void BoundedGitOutputBuffer_KeepsShortOutput()
{
diff --git a/GVFS/GVFS.UnitTests/Git/RefStorageTests.cs b/GVFS/GVFS.UnitTests/Git/RefStorageTests.cs
new file mode 100644
index 0000000000..b3a02995c0
--- /dev/null
+++ b/GVFS/GVFS.UnitTests/Git/RefStorageTests.cs
@@ -0,0 +1,145 @@
+using GVFS.Common.Git;
+using GVFS.Tests.Should;
+using GVFS.UnitTests.Mock.Git;
+using NUnit.Framework;
+
+namespace GVFS.UnitTests.Git
+{
+ [TestFixture]
+ public class RefStorageTests
+ {
+ private const string ConfigCommand = "config --local " + RefStorage.RefStorageConfigName;
+
+ [TestCase]
+ public void IsReftableIsTrueForReftableValue()
+ {
+ RefStorage.IsReftable("reftable").ShouldEqual(true);
+ }
+
+ [TestCase]
+ public void IsReftableIsTrueForMixedCaseReftableValue()
+ {
+ RefStorage.IsReftable("Reftable").ShouldEqual(true);
+ }
+
+ [TestCase]
+ public void IsReftableIsTrueForReftableValueWithSurroundingWhitespace()
+ {
+ RefStorage.IsReftable(" reftable \n").ShouldEqual(true);
+ }
+
+ [TestCase]
+ public void IsReftableIsFalseForFilesValue()
+ {
+ RefStorage.IsReftable("files").ShouldEqual(false);
+ }
+
+ [TestCase]
+ public void IsReftableIsFalseForNullValue()
+ {
+ RefStorage.IsReftable(null).ShouldEqual(false);
+ }
+
+ [TestCase]
+ public void IsReftableIsFalseForEmptyValue()
+ {
+ RefStorage.IsReftable(string.Empty).ShouldEqual(false);
+ }
+
+ [TestCase]
+ public void IsReftableRepoIsTrueWhenConfigIsReftable()
+ {
+ MockGitProcess git = new MockGitProcess();
+ git.SetExpectedCommandResult(
+ ConfigCommand,
+ () => new GitProcess.Result("reftable\n", string.Empty, GitProcess.Result.SuccessCode));
+
+ RefStorage.IsReftableRepo(git).ShouldEqual(true);
+ }
+
+ [TestCase]
+ public void IsReftableRepoIsFalseWhenConfigIsFiles()
+ {
+ MockGitProcess git = new MockGitProcess();
+ git.SetExpectedCommandResult(
+ ConfigCommand,
+ () => new GitProcess.Result("files\n", string.Empty, GitProcess.Result.SuccessCode));
+
+ RefStorage.IsReftableRepo(git).ShouldEqual(false);
+ }
+
+ [TestCase]
+ public void IsReftableRepoIsFalseWhenConfigIsMissing()
+ {
+ MockGitProcess git = new MockGitProcess();
+
+ // A missing config key causes 'git config' to exit non-zero with no stderr output,
+ // which is the observed behavior for a files-format repo that has no
+ // extensions.refstorage key.
+ git.SetExpectedCommandResult(
+ ConfigCommand,
+ () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode));
+
+ RefStorage.IsReftableRepo(git).ShouldEqual(false);
+ }
+
+ [TestCase]
+ public void IsReftableRepoSwallowsGenuineReadFailureAndReturnsFalse()
+ {
+ MockGitProcess git = new MockGitProcess();
+
+ // The non-Try overload deliberately swallows a genuine config-read failure
+ // (non-zero exit with real stderr content) and reports "not reftable", matching
+ // GitProcess.TryGetFromConfig's "failure == not set" convention. Callers that
+ // need to distinguish a read failure use TryIsReftableRepo instead.
+ git.SetExpectedCommandResult(
+ ConfigCommand,
+ () => new GitProcess.Result(string.Empty, "fatal: not a git repository", GitProcess.Result.GenericFailureCode));
+
+ RefStorage.IsReftableRepo(git).ShouldEqual(false);
+ }
+
+ [TestCase]
+ public void TryIsReftableRepoSucceedsWithNoErrorWhenConfigIsReftable()
+ {
+ MockGitProcess git = new MockGitProcess();
+ git.SetExpectedCommandResult(
+ ConfigCommand,
+ () => new GitProcess.Result("reftable\n", string.Empty, GitProcess.Result.SuccessCode));
+
+ RefStorage.TryIsReftableRepo(git, out bool isReftable, out string error).ShouldEqual(true);
+ isReftable.ShouldEqual(true);
+ error.ShouldEqual(string.Empty);
+ }
+
+ [TestCase]
+ public void TryIsReftableRepoSucceedsWithNoErrorWhenConfigIsMissing()
+ {
+ MockGitProcess git = new MockGitProcess();
+ git.SetExpectedCommandResult(
+ ConfigCommand,
+ () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode));
+
+ RefStorage.TryIsReftableRepo(git, out bool isReftable, out string error).ShouldEqual(true);
+ isReftable.ShouldEqual(false);
+ error.ShouldEqual(string.Empty);
+ }
+
+ [TestCase]
+ public void TryIsReftableRepoFailsAndReportsErrorWhenConfigReadFails()
+ {
+ MockGitProcess git = new MockGitProcess();
+
+ // A genuine 'git config' failure (non-zero exit with real stderr content, as
+ // opposed to a missing key) should be surfaced distinctly rather than silently
+ // treated the same as "not reftable".
+ git.SetExpectedCommandResult(
+ ConfigCommand,
+ () => new GitProcess.Result(string.Empty, "fatal: not a git repository", GitProcess.Result.GenericFailureCode));
+
+ RefStorage.TryIsReftableRepo(git, out bool isReftable, out string error).ShouldEqual(false);
+ isReftable.ShouldEqual(false);
+ error.ShouldNotBeNull();
+ }
+ }
+}
diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs
index 2e9bb52762..ce579d0459 100644
--- a/GVFS/GVFS/CommandLine/CloneVerb.cs
+++ b/GVFS/GVFS/CommandLine/CloneVerb.cs
@@ -665,7 +665,14 @@ private Result CreateClone(
GitRefs refs,
string branch)
{
- Result initRepoResult = this.TryInitRepo(tracer, refs, enlistment);
+ // The clone flow already resolved and validated the installed git version
+ // (GVFSVerb.ValidateClientVersions -> CheckGitVersion, stored on the enlistment),
+ // so parse that instead of shelling out for 'git --version' again inside Init. A
+ // parse failure yields null, which conservatively omits the '--ref-format=files'
+ // pin; TryInitRepo's reftable detection still catches the repo in that case.
+ GitVersion.TryParseVersion(enlistment.GitVersion, out GitVersion installedGitVersion);
+
+ Result initRepoResult = this.TryInitRepo(tracer, refs, enlistment, installedGitVersion);
if (!initRepoResult.Success)
{
return initRepoResult;
@@ -846,10 +853,10 @@ cd src
gitCmd.Attributes = FileAttributes.Hidden;
}
- private Result TryInitRepo(ITracer tracer, GitRefs refs, Enlistment enlistmentToInit)
+ private Result TryInitRepo(ITracer tracer, GitRefs refs, Enlistment enlistmentToInit, GitVersion installedGitVersion)
{
string repoPath = enlistmentToInit.WorkingDirectoryBackingRoot;
- GitProcess.Result initResult = GitProcess.Init(enlistmentToInit);
+ GitProcess.Result initResult = GitProcess.Init(enlistmentToInit, installedGitVersion);
if (initResult.ExitCodeIsFailure)
{
string error = string.Format("Could not init repo at to {0}: {1}", repoPath, initResult.Errors);
@@ -857,6 +864,30 @@ private Result TryInitRepo(ITracer tracer, GitRefs refs, Enlistment enlistmentTo
return new Result(error);
}
+ // 'git init' above pins --ref-format=files when the installed git supports it (git
+ // 2.45+), so a user's init.defaultRefFormat=reftable no longer produces a reftable
+ // repo on a supported git. This check remains as defense-in-depth: it covers the
+ // case where the git version could not be determined (the pin is then omitted) and
+ // any repo that reached v1 by another route. VFS for Git populates refs by writing
+ // .git/packed-refs directly (below, and via GitRefs.ToPackedRefs) rather than
+ // through git's ref backend, and a reftable repo ignores packed-refs - so such a
+ // clone would silently produce an enlistment whose remote refs are invisible to
+ // git. Fail fast with a clear error instead. A genuine config-read failure is also
+ // fatal here: this is our own just-initialized repo, so a read error (as opposed to
+ // the key being absent, which reports files-format) means its config is unreadable
+ // and the clone cannot safely continue.
+ if (!RefStorage.TryIsReftableRepo(enlistmentToInit.CreateGitProcess(), out bool isReftableRepo, out string refStorageReadError))
+ {
+ string readError = "Could not determine the new repository's ref storage format: " + refStorageReadError;
+ tracer.RelatedError(readError);
+ return new Result(readError);
+ }
+ else if (isReftableRepo)
+ {
+ tracer.RelatedError(RefStorage.UnsupportedReftableErrorMessage);
+ return new Result(RefStorage.UnsupportedReftableErrorMessage);
+ }
+
try
{
GVFSPlatform.Instance.FileSystem.EnsureDirectoryIsOwnedByCurrentUser(enlistmentToInit.DotGitRoot);
diff --git a/GVFS/GVFS/RepairJobs/GitConfigRepairJob.cs b/GVFS/GVFS/RepairJobs/GitConfigRepairJob.cs
index 2903bcf72d..a7b33df884 100644
--- a/GVFS/GVFS/RepairJobs/GitConfigRepairJob.cs
+++ b/GVFS/GVFS/RepairJobs/GitConfigRepairJob.cs
@@ -22,6 +22,22 @@ public override string Name
public override IssueType HasIssue(List messages)
{
GitProcess git = new GitProcess(this.Enlistment);
+
+ // A reftable repository is not something 'gvfs repair' can make usable (see
+ // TryFixIssues), so report it as an unfixable issue rather than letting
+ // 'gvfs diagnose'/'gvfs repair' declare the enlistment healthy. Detect it both
+ // from the config extension and from the physical .git/reftable/ directory: the
+ // latter still identifies a reftable repo when the config is too corrupt for
+ // 'git config' to read, which is exactly the state repair runs in. A config-read
+ // failure alone is otherwise swallowed as "not reftable" so the normal config
+ // checks below still run - repair exists to diagnose exactly that kind of broken
+ // config.
+ if (RefStorage.IsReftableRepo(git) || this.ReftableBackendDirectoryExists())
+ {
+ messages.Add(RefStorage.UnsupportedReftableErrorMessage);
+ return IssueType.CantFix;
+ }
+
GitProcess.ConfigResult originResult = git.GetOriginUrl();
string error;
string originUrl;
@@ -74,6 +90,37 @@ public override IssueType HasIssue(List messages)
public override FixResult TryFixIssues(List messages)
{
+ // Check before touching the config file at all: TryFixIssues rebuilds the
+ // entire config from scratch (wiping it to empty, then writing only the
+ // required/optional settings this codebase knows about via
+ // TrySetRequiredGitConfigSettings, which force-writes
+ // core.repositoryformatversion=0 and drops extensions.refstorage). Rebuilding
+ // a reftable repo's config that way would strip the reftable extension while
+ // its refs still live in the reftable backend, producing a differently-broken
+ // repo. There is no way for 'gvfs repair' to make a reftable repo usable by
+ // VFS for Git, so fail clearly instead of attempting the rebuild.
+ //
+ // Detect reftable from the config AND from the physical .git/reftable/ directory.
+ // The directory check is what makes this safe when the config is unreadable: a
+ // corrupt reftable repo would otherwise fall through to the rebuild below and be
+ // silently converted to a files-format config while its refs remain in reftable
+ // storage. A config-read failure with no reftable directory is NOT blocked,
+ // unlike at mount/clone/FastFetch: repair's whole purpose is to rebuild a corrupt
+ // files-format config, so a repo whose config is merely unreadable is exactly what
+ // repair must be allowed to fix.
+ bool reftableDirectoryExists = this.ReftableBackendDirectoryExists();
+ bool configReadable = RefStorage.TryIsReftableRepo(new GitProcess(this.Enlistment), out bool isReftableByConfig, out string refStorageReadError);
+ if (reftableDirectoryExists || (configReadable && isReftableByConfig))
+ {
+ messages.Add(RefStorage.UnsupportedReftableErrorMessage);
+ return FixResult.Failure;
+ }
+
+ if (!configReadable)
+ {
+ this.Tracer.RelatedWarning("Could not determine the repository's ref storage format; proceeding with repair: " + refStorageReadError);
+ }
+
string configPath = Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Config);
string configBackupPath;
if (!this.TryRenameToBackupFile(configPath, out configBackupPath, messages))
@@ -119,5 +166,18 @@ public override FixResult TryFixIssues(List messages)
return FixResult.ManualStepsRequired;
}
+
+ private bool ReftableBackendDirectoryExists()
+ {
+ // DotGitRoot assumes .git is a directory (WorkingDirectoryBackingRoot\.git), which
+ // is how the whole Enlistment abstraction models it. That holds here because
+ // 'gvfs repair' targets the primary enlistment, whose .git is always a real
+ // directory - not a linked git worktree, where .git is a file. If repair is ever
+ // taught to run against a linked worktree, this directory probe would need to
+ // resolve the gitdir from that .git file first.
+ string reftableDirectory = Path.Combine(this.Enlistment.DotGitRoot, RefStorage.ReftableDirectoryName);
+
+ return Directory.Exists(reftableDirectory);
+ }
}
}