Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f22daab
FunctionalTests: capture mount dumps and preserve logs on failure
tyrielv Aug 27, 2026
d2adb5a
Split enumeration-miss cause and de-duplicate the error telemetry
tyrielv Aug 5, 2026
6ba0bb2
Share transient libgit2 config lookup helper
tyrielv Aug 7, 2026
c511208
Merge pull request #2066 from tyrielv/tyrielv/shared-libgit2-config-l…
tyrielv Aug 27, 2026
82a228d
FunctionalTests: make shared control-repo cache setup resilient
tyrielv Aug 27, 2026
183f5eb
FunctionalTests: build control-repo cache with --mirror, not --bare
tyrielv Aug 27, 2026
6d4d4d7
FunctionalTests: serve control-repo cache SHAs, stop rebuilding cache
tyrielv Aug 27, 2026
b5d75b6
Merge pull request #2062 from tyrielv/tyrielv/ft-failure-diagnostics
tyrielv Aug 31, 2026
3c5ba6e
Merge pull request #2099 from tyrielv/tyrielv/fix-flaky-controlrepo
tyrielv Aug 31, 2026
ff32b9d
Merge pull request #2076 from tyrielv/tyrielv/enum-miss-split
tyrielv Aug 31, 2026
d2215eb
feat: Route GVFS endpoints to dedicated cache servers
derrickstolee Sep 2, 2026
d423ed1
fix: Fall back safely from dedicated cache endpoints
derrickstolee Sep 2, 2026
63f8dc0
docs: Explain endpoint-specific cache routing
derrickstolee Sep 2, 2026
f248096
test: Cover prefetch failure telemetry redaction
derrickstolee Sep 2, 2026
113b3f9
Merge pull request #2103 Route GVFS endpoints to dedicated cache servers
derrickstolee Sep 4, 2026
35272aa
Disable Git fsmonitor in virtual repositories
Sep 14, 2026
d9c9b54
Merge pull request #2106 from tyrielv/tyrielv/core-fsmonitor
tyrielv Sep 16, 2026
092845b
Merge upstream/master into vnext
tyrielv Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/functional-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,18 @@ jobs:
run: |
SET PATH=C:\Program Files\VFS for Git;%PATH%
SET GIT_TRACE2_PERF=C:\temp\git-trace2.log
SET GVFS_TEST_DIAGNOSTICS_DIR=C:\temp\gvfs-ft-diagnostics
ft\GVFS.FunctionalTests.exe /result:TestResult.xml --ci --slice=${{ matrix.nr }},12

- name: Upload failure diagnostics (mount dumps + logs)
if: always() && steps.skip.outputs.result != 'true'
uses: actions/upload-artifact@v7
continue-on-error: true
with:
name: ${{ env.ARTIFACT_PREFIX }}FailureDiagnostics_${{ env.FT_MATRIX_NAME }}
path: C:\temp\gvfs-ft-diagnostics
if-no-files-found: ignore

- name: Upload functional test results
if: always() && steps.skip.outputs.result != 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
4 changes: 3 additions & 1 deletion GVFS/FastFetch/FastFetchVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,9 @@ private int ExecuteWithExitCode()
string fastfetchLogFile = Enlistment.GetNewLogFileName(enlistment.FastFetchLogRoot, "fastfetch");
tracer.AddLogFileEventListener(fastfetchLogFile, EventLevel.Informational, Keywords.Any);

CacheServerInfo cacheServer = new CacheServerInfo(this.GetRemoteUrl(enlistment), null);
CacheServerInfo cacheServer = string.IsNullOrWhiteSpace(this.CacheServerUrl)
? CacheServerResolver.GetCacheServerFromConfig(enlistment)
: new CacheServerInfo(this.GetRemoteUrl(enlistment), null);

tracer.WriteStartEvent(
enlistment.PrimaryEnlistmentRoot,
Expand Down
38 changes: 37 additions & 1 deletion GVFS/GVFS.CommandLine.Tests/GvfsMainCliTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,36 @@ public void Clone_FullCommandLine_ParsesCorrectly()
{
"clone", "https://example.com/repo", @"C:\Users\test\repo",
"--cache-server-url", "https://cache.test",
"--prefetch-cache-server-url", "https://prefetch-cache.test",
"--get-cache-server-url", "https://get-cache.test",
"--post-cache-server-url", "https://post-cache.test",
"--sizes-cache-server-url", "https://sizes-cache.test",
"-b", "develop",
"--single-branch",
"--no-mount",
"--no-prefetch"
});
Assert.That(parseResult.Errors, Is.Empty, "Full clone command should parse without errors");
Assert.Multiple(() =>
{
Assert.That(parseResult.GetValue((Option<string>)FindOptionOnCommand("clone", "--cache-server-url")), Is.EqualTo("https://cache.test"));
Assert.That(parseResult.GetValue((Option<string>)FindOptionOnCommand("clone", "--prefetch-cache-server-url")), Is.EqualTo("https://prefetch-cache.test"));
Assert.That(parseResult.GetValue((Option<string>)FindOptionOnCommand("clone", "--get-cache-server-url")), Is.EqualTo("https://get-cache.test"));
Assert.That(parseResult.GetValue((Option<string>)FindOptionOnCommand("clone", "--post-cache-server-url")), Is.EqualTo("https://post-cache.test"));
Assert.That(parseResult.GetValue((Option<string>)FindOptionOnCommand("clone", "--sizes-cache-server-url")), Is.EqualTo("https://sizes-cache.test"));
});
}

[TestCase("--prefetch-cache-server-url")]
[TestCase("--get-cache-server-url")]
[TestCase("--post-cache-server-url")]
[TestCase("--sizes-cache-server-url")]
public void Clone_EndpointCacheServerUrl_RejectsInvalidUrl(string optionName)
{
var parseResult = rootCommand.Parse(new[] { "clone", "https://example.com/repo", optionName, "not-a-url" });

Assert.That(parseResult.Errors, Has.Count.EqualTo(1));
Assert.That(parseResult.Errors[0].Message, Does.Contain("requires an absolute URL"));
}

[Test]
Expand Down Expand Up @@ -342,7 +366,19 @@ public void Repair_FullCommandLine_ParsesCorrectly()
[Test]
public void Clone_HasAllExpectedOptions()
{
var expected = new[] { "--cache-server-url", "--branch", "--single-branch", "--no-mount", "--no-prefetch", "--local-cache-path" };
var expected = new[]
{
"--cache-server-url",
"--prefetch-cache-server-url",
"--get-cache-server-url",
"--post-cache-server-url",
"--sizes-cache-server-url",
"--branch",
"--single-branch",
"--no-mount",
"--no-prefetch",
"--local-cache-path",
};
foreach (var optName in expected)
{
Assert.That(FindOptionOnCommand("clone", optName), Is.Not.Null,
Expand Down
4 changes: 4 additions & 0 deletions GVFS/GVFS.Common/GVFSConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public static class GitConfig
public const string MountId = GVFSPrefix + "mount-id";
public const string EnlistmentId = GVFSPrefix + "enlistment-id";
public const string CacheServer = GVFSPrefix + "cache-server";
public const string PrefetchCacheServer = GVFSPrefix + "prefetch.cache-server";
public const string GetCacheServer = GVFSPrefix + "get.cache-server";
public const string PostCacheServer = GVFSPrefix + "post.cache-server";
public const string SizesCacheServer = GVFSPrefix + "sizes.cache-server";
public const string DeprecatedCacheEndpointSuffix = ".cache-server-url";
public const string HooksPrefix = GitConfig.GVFSPrefix + "clone.default-";
public const string GVFSTelemetryId = GitConfig.GVFSPrefix + "telemetry-id";
Expand Down
12 changes: 10 additions & 2 deletions GVFS/GVFS.Common/Git/GitObjects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ public virtual bool TryDownloadPrefetchPacks(GitProcess gitProcess, long latestT
"{0}?lastPackTimestamp={1}",
this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl,
latestTimestamp)),
fallbackEndPointGenerator: () => new Uri(
string.Format(
"{0}?lastPackTimestamp={1}",
this.GitObjectRequestor.CacheServer.GlobalPrefetchEndpointUrl,
latestTimestamp)),
requestBodyGenerator: () => null,
cancellationToken: CancellationToken.None,
acceptType: new MediaTypeWithQualityHeaderValue(GVFSConstants.MediaTypes.PrefetchPackFilesAndIndexesMediaType));
Expand All @@ -188,18 +193,21 @@ public virtual bool TryDownloadPrefetchPacks(GitProcess gitProcess, long latestT

if (!result.Succeeded)
{
Uri requestUri = result.Result?.RequestUri
?? new Uri(this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl);
string requestAuthority = HttpRequestor.GetAuthorityForTelemetry(requestUri);
if (result.Result != null && result.Result.HttpStatusCodeResult == HttpStatusCode.NotFound)
{
EventMetadata warning = CreateEventMetadata();
warning.Add(TracingConstants.MessageKey.WarningMessage, "The server does not support " + GVFSConstants.Endpoints.GVFSPrefetch);
warning.Add(nameof(this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl), this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl);
warning.Add("PrefetchEndpointUrl", requestAuthority);
activity.RelatedEvent(EventLevel.Warning, "CommandNotSupported", warning);
}
else
{
EventMetadata error = CreateEventMetadata(result.Error);
error.Add("latestTimestamp", latestTimestamp);
error.Add(nameof(this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl), this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl);
error.Add("PrefetchEndpointUrl", requestAuthority);
activity.RelatedWarning(error, "DownloadPrefetchPacks failed.", Keywords.Telemetry);
}
}
Expand Down
56 changes: 55 additions & 1 deletion GVFS/GVFS.Common/Git/LibGit2Repo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -327,6 +332,55 @@ public virtual string GetConfigString(string name)
}
}

/// <summary>
/// Reads a boolean config value from this already-open repo, falling back to
/// <paramref name="defaultValue"/> if the key is unset or the read fails for any reason
/// (e.g. a corrupt/unreadable config).
/// </summary>
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;
}
}

/// <summary>
/// Reads a single boolean config value from the repo at <paramref name="repoPath"/>,
/// opening and disposing a transient <see cref="LibGit2Repo"/> for the lookup. Prefer
/// this over <see cref="LibGit2RepoInvoker"/> 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
/// <paramref name="defaultValue"/> if the repo can't be opened or the read fails for any
/// reason.
/// </summary>
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)
Expand Down
3 changes: 3 additions & 0 deletions GVFS/GVFS.Common/Git/RequiredGitConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ public static Dictionary<string, string> GetRequiredSettings(GVFSEnlistment enli

// Disable the builtin FS Monitor in case it was enabled globally.
{ "core.useBuiltinFSMonitor", "false" },

// Disable the FS Monitor in case it was enabled globally.
{ "core.fsmonitor", "false" },
};
}
}
Expand Down
86 changes: 83 additions & 3 deletions GVFS/GVFS.Common/Http/CacheServerInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,105 @@ public class CacheServerInfo

[JsonConstructor]
public CacheServerInfo(string url, string name, bool globalDefault = false)
: this(url, name, globalDefault, null, null, null, null)
{
}

public CacheServerInfo(
string url,
string name,
bool globalDefault,
string prefetchCacheServerUrl,
string getCacheServerUrl,
string postCacheServerUrl,
string sizesCacheServerUrl)
{
this.Url = url;
this.Name = name;
this.GlobalDefault = globalDefault;
this.PrefetchCacheServerUrl = prefetchCacheServerUrl;
this.GetCacheServerUrl = getCacheServerUrl;
this.PostCacheServerUrl = postCacheServerUrl;
this.SizesCacheServerUrl = sizesCacheServerUrl;

if (this.Url != null)
{
this.ObjectsEndpointUrl = this.Url + ObjectsEndpointSuffix;
this.PrefetchEndpointUrl = this.Url + PrefetchEndpointSuffix;
this.SizesEndpointUrl = this.Url + SizesEndpointSuffix;
}

this.GlobalPrefetchEndpointUrl = GetEndpointUrl(this.Url, PrefetchEndpointSuffix);
this.GlobalSizesEndpointUrl = GetEndpointUrl(this.Url, SizesEndpointSuffix);
this.PrefetchEndpointUrl = GetEndpointUrl(prefetchCacheServerUrl ?? this.Url, PrefetchEndpointSuffix);
this.ObjectsGetEndpointUrl = GetEndpointUrl(getCacheServerUrl ?? this.Url, ObjectsEndpointSuffix);
this.ObjectsPostEndpointUrl = GetEndpointUrl(postCacheServerUrl ?? this.Url, ObjectsEndpointSuffix);
this.SizesEndpointUrl = GetEndpointUrl(sizesCacheServerUrl ?? this.Url, SizesEndpointSuffix);
}

public string Url { get; }
public string Name { get; }
public bool GlobalDefault { get; }

[JsonIgnore]
public string PrefetchCacheServerUrl { get; }

[JsonIgnore]
public string GetCacheServerUrl { get; }

[JsonIgnore]
public string PostCacheServerUrl { get; }

[JsonIgnore]
public string SizesCacheServerUrl { get; }

public string ObjectsEndpointUrl { get; }
public string PrefetchEndpointUrl { get; }
public string SizesEndpointUrl { get; }

[JsonIgnore]
public string ObjectsGetEndpointUrl { get; }

[JsonIgnore]
public string ObjectsPostEndpointUrl { get; }

[JsonIgnore]
public string GlobalPrefetchEndpointUrl { get; }

[JsonIgnore]
public string GlobalSizesEndpointUrl { get; }

public CacheServerInfo WithEndpointOverrides(
string prefetchCacheServerUrl,
string getCacheServerUrl,
string postCacheServerUrl,
string sizesCacheServerUrl)
{
return new CacheServerInfo(
this.Url,
this.Name,
this.GlobalDefault,
prefetchCacheServerUrl,
getCacheServerUrl,
postCacheServerUrl,
sizesCacheServerUrl);
}

public CacheServerInfo WithEndpointOverridesFrom(CacheServerInfo cacheServer)
{
return this.WithEndpointOverrides(
cacheServer.PrefetchCacheServerUrl,
cacheServer.GetCacheServerUrl,
cacheServer.PostCacheServerUrl,
cacheServer.SizesCacheServerUrl);
}

public bool HasValidUrl()
{
return Uri.IsWellFormedUriString(this.Url, UriKind.Absolute);
return IsValidUrl(this.Url);
}

public static bool IsValidUrl(string url)
{
return Uri.IsWellFormedUriString(url, UriKind.Absolute);
}

public bool IsNone(string repoUrl)
Expand Down Expand Up @@ -64,5 +139,10 @@ public static class ReservedNames
public const string Default = "Default";
public const string UserDefined = "User Defined";
}

private static string GetEndpointUrl(string cacheServerUrl, string endpointSuffix)
{
return cacheServerUrl == null ? null : cacheServerUrl + endpointSuffix;
}
}
}
Loading
Loading