From 589c6578bdd0ae5bcd92858047b643a2257dc8c0 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 12 Sep 2025 08:20:29 -0700 Subject: [PATCH 01/11] Warn-and-continue if gvfs/config download fails The verbs 'clone', 'mount', 'prefetch', and 'cache-server' each perform a query to gvfs/config endpoint for two purposes: to get the list of allowed client versions for the server, and to get the list of preconfigured cache servers. Currently, if the query fails then the verb fails. The most common reason for the query to fail appears to be transient network/authentication issues, making automount in particular more flaky than desired. Mount and Prefetch verbs should always have a cache server configuration available already, and Clone and Cache-Server both have command-line options to allow specifying the cache server configuration. This change modifies the verbs so that if a cache server url is already available (from local config or command-line option) then the verb will warn that the version could not be checked and continue instead of failing if the query to gvfs/config fails. --- GVFS/GVFS.Common/Http/CacheServerResolver.cs | 6 +- GVFS/GVFS/CommandLine/CacheServerVerb.cs | 59 ++++++++++----- GVFS/GVFS/CommandLine/CloneVerb.cs | 14 ++-- GVFS/GVFS/CommandLine/GVFSVerb.cs | 42 +++++++++++ GVFS/GVFS/CommandLine/MountVerb.cs | 40 +++++----- GVFS/GVFS/CommandLine/PrefetchVerb.cs | 78 +++++++++++--------- 6 files changed, 160 insertions(+), 79 deletions(-) diff --git a/GVFS/GVFS.Common/Http/CacheServerResolver.cs b/GVFS/GVFS.Common/Http/CacheServerResolver.cs index 9f3a7d3116..bc1df9727b 100644 --- a/GVFS/GVFS.Common/Http/CacheServerResolver.cs +++ b/GVFS/GVFS.Common/Http/CacheServerResolver.cs @@ -54,12 +54,12 @@ public bool TryResolveUrlFromRemote( if (cacheServerName.Equals(CacheServerInfo.ReservedNames.Default, StringComparison.OrdinalIgnoreCase)) { cacheServer = - serverGVFSConfig.CacheServers.FirstOrDefault(cache => cache.GlobalDefault) + serverGVFSConfig?.CacheServers.FirstOrDefault(cache => cache.GlobalDefault) ?? this.CreateNone(); } else { - cacheServer = serverGVFSConfig.CacheServers.FirstOrDefault(cache => + cacheServer = serverGVFSConfig?.CacheServers.FirstOrDefault(cache => cache.Name.Equals(cacheServerName, StringComparison.OrdinalIgnoreCase)); if (cacheServer == null) @@ -87,7 +87,7 @@ public CacheServerInfo ResolveNameFromRemote( } return - serverGVFSConfig.CacheServers.FirstOrDefault(cache => cache.Url.Equals(cacheServerUrl, StringComparison.OrdinalIgnoreCase)) + serverGVFSConfig?.CacheServers.FirstOrDefault(cache => cache.Url.Equals(cacheServerUrl, StringComparison.OrdinalIgnoreCase)) ?? new CacheServerInfo(cacheServerUrl, CacheServerInfo.ReservedNames.UserDefined); } diff --git a/GVFS/GVFS/CommandLine/CacheServerVerb.cs b/GVFS/GVFS/CommandLine/CacheServerVerb.cs index 55edb88537..86754ae672 100644 --- a/GVFS/GVFS/CommandLine/CacheServerVerb.cs +++ b/GVFS/GVFS/CommandLine/CacheServerVerb.cs @@ -48,34 +48,25 @@ protected override void Execute(GVFSEnlistment enlistment) this.ReportErrorAndExit(tracer, "Authentication failed: " + authErrorMessage); } - ServerGVFSConfig serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); + ServerGVFSConfig serverGVFSConfig = null; string error = null; - if (this.CacheToSet != null) + // Handle the three operation types: list, set, and get (default) + if (this.ListCacheServers) { - CacheServerInfo cacheServer = cacheServerResolver.ParseUrlOrFriendlyName(this.CacheToSet); - cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); + // For listing, require config endpoint to succeed + serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); - if (!cacheServerResolver.TrySaveUrlToLocalConfig(cacheServer, out error)) - { - this.ReportErrorAndExit("Failed to save cache to config: " + error); - } - - this.Output.WriteLine("You must remount GVFS for this to take effect."); - } - else if (this.ListCacheServers) - { List cacheServers = serverGVFSConfig.CacheServers.ToList(); if (cacheServers != null && cacheServers.Any()) { this.Output.WriteLine(); this.Output.WriteLine("Available cache servers for: " + enlistment.RepoUrl); - foreach (CacheServerInfo cacheServer in cacheServers) + foreach (CacheServerInfo cacheServerInfo in cacheServers) { - this.Output.WriteLine(cacheServer); + this.Output.WriteLine(cacheServerInfo); } } else @@ -83,12 +74,42 @@ protected override void Execute(GVFSEnlistment enlistment) this.Output.WriteLine("There are no available cache servers for: " + enlistment.RepoUrl); } } + else if (this.CacheToSet != null) + { + // Setting a new cache server + CacheServerInfo cacheServer = cacheServerResolver.ParseUrlOrFriendlyName(this.CacheToSet); + + // For set operation, allow fallback if config endpoint fails but cache server URL is valid + serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + tracer, + enlistment, + retryConfig, + cacheServer); + + cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); + + if (!cacheServerResolver.TrySaveUrlToLocalConfig(cacheServer, out error)) + { + this.ReportErrorAndExit("Failed to save cache to config: " + error); + } + + this.Output.WriteLine("You must remount GVFS for this to take effect."); + } else { - string cacheServerUrl = CacheServerResolver.GetUrlFromConfig(enlistment); - CacheServerInfo cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerUrl, serverGVFSConfig); + // Default operation: get current cache server info + CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); + + // For get operation, allow fallback if config endpoint fails but cache server URL is valid + serverGVFSConfig =this.QueryGVFSConfigWithFallbackCacheServer( + tracer, + enlistment, + retryConfig, + cacheServer); + + CacheServerInfo resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverGVFSConfig); - this.Output.WriteLine("Using cache server: " + cacheServer); + this.Output.WriteLine("Using cache server: " + resolvedCacheServer); } } } diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs index e810308377..f9985f7e08 100644 --- a/GVFS/GVFS/CommandLine/CloneVerb.cs +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -162,12 +162,12 @@ public override void Execute() string resolvedLocalCacheRoot; if (string.IsNullOrWhiteSpace(this.LocalCacheRoot)) { - string localCacheRootError; - if (!LocalCacheResolver.TryGetDefaultLocalCacheRoot(enlistment, out resolvedLocalCacheRoot, out localCacheRootError)) + string localCacheRootError; + if (!LocalCacheResolver.TryGetDefaultLocalCacheRoot(enlistment, out resolvedLocalCacheRoot, out localCacheRootError)) { this.ReportErrorAndExit( tracer, - $"Failed to determine the default location for the local GVFS cache: `{localCacheRootError}`"); + $"Failed to determine the default location for the local GVFS cache: `{localCacheRootError}`"); } } else @@ -189,7 +189,12 @@ public override void Execute() } RetryConfig retryConfig = this.GetRetryConfig(tracer, enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); - serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); + + serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + tracer, + enlistment, + retryConfig, + cacheServer); cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); @@ -237,7 +242,6 @@ public override void Execute() exitCode = (int)result; } } - else { Process.Start(new ProcessStartInfo( diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index a9fcb95876..70248bf66d 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -493,6 +493,48 @@ protected RetryConfig GetRetryConfig(ITracer tracer, GVFSEnlistment enlistment, return retryConfig; } + /// + /// Attempts to query the GVFS config endpoint. If it fails, but a valid cache server URL is available, returns a null object. + /// If neither is available, . If config endpoint fails, skips minimum version check and warns (unless skip-version-check is set). + /// + protected ServerGVFSConfig QueryGVFSConfigWithFallbackCacheServer( + ITracer tracer, + GVFSEnlistment enlistment, + RetryConfig retryConfig, + CacheServerInfo fallbackCacheServer) + { + ServerGVFSConfig serverGVFSConfig = null; + string errorMessage = null; + bool configSuccess = this.ShowStatusWhileRunning( + () => + { + using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig)) + { + const bool LogErrors = true; + return configRequestor.TryQueryGVFSConfig(LogErrors, out serverGVFSConfig, out _, out errorMessage); + } + }, + "Querying remote for config", + suppressGvfsLogMessage: true); + + if (!configSuccess) + { + // If a valid cache server URL is available, warn and continue + if (fallbackCacheServer != null && !string.IsNullOrWhiteSpace(fallbackCacheServer.Url)) + { + // Continue without config + // Warning will be logged/displayed when version check is run + return null; + } + else + { + this.ReportErrorAndExit(tracer, "Unable to query /gvfs/config" + Environment.NewLine + errorMessage); + } + } + return serverGVFSConfig; + } + + // Restore original QueryGVFSConfig for other callers protected ServerGVFSConfig QueryGVFSConfig(ITracer tracer, GVFSEnlistment enlistment, RetryConfig retryConfig) { ServerGVFSConfig serverGVFSConfig = null; diff --git a/GVFS/GVFS/CommandLine/MountVerb.cs b/GVFS/GVFS/CommandLine/MountVerb.cs index 90db430cf2..5183ec4342 100644 --- a/GVFS/GVFS/CommandLine/MountVerb.cs +++ b/GVFS/GVFS/CommandLine/MountVerb.cs @@ -95,7 +95,8 @@ protected override void Execute(GVFSEnlistment enlistment) this.ReportErrorAndExit("Error installing hooks: " + errorMessage); } - CacheServerInfo cacheServer = this.ResolvedCacheServer ?? CacheServerResolver.GetCacheServerFromConfig(enlistment); + var resolvedCacheServer = this.ResolvedCacheServer; + var cacheServerFromConfig = resolvedCacheServer ?? CacheServerResolver.GetCacheServerFromConfig(enlistment); tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.MountVerb), @@ -104,7 +105,7 @@ protected override void Execute(GVFSEnlistment enlistment) tracer.WriteStartEvent( enlistment.EnlistmentRoot, enlistment.RepoUrl, - cacheServer.Url, + cacheServerFromConfig.Url, new EventMetadata { { "Unattended", this.Unattended }, @@ -122,7 +123,7 @@ protected override void Execute(GVFSEnlistment enlistment) { { "KernelDriver.IsReady_Error", errorMessage }, { TracingConstants.MessageKey.InfoMessage, "Service will retry" } - }); + }); if (!this.ShowStatusWhileRunning( () => { return this.TryEnableAndAttachPrjFltThroughService(enlistment.EnlistmentRoot, out errorMessage); }, @@ -134,7 +135,8 @@ protected override void Execute(GVFSEnlistment enlistment) RetryConfig retryConfig = null; ServerGVFSConfig serverGVFSConfig = this.DownloadedGVFSConfig; - if (!this.SkipVersionCheck) + /* If resolved cache server was passed in, we've already checked server config and version check in previous operation. */ + if (resolvedCacheServer == null) { string authErrorMessage; if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) @@ -150,17 +152,21 @@ protected override void Execute(GVFSEnlistment enlistment) retryConfig = this.GetRetryConfig(tracer, enlistment); } - serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); + serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + tracer, + enlistment, + retryConfig, + cacheServerFromConfig); } this.ValidateClientVersions(tracer, enlistment, serverGVFSConfig, showWarnings: true); CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); - cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverGVFSConfig); - this.Output.WriteLine("Configured cache server: " + cacheServer); + resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerFromConfig.Url, serverGVFSConfig); + this.Output.WriteLine("Configured cache server: " + cacheServerFromConfig); } - this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig, cacheServer); + this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig, resolvedCacheServer); if (!this.ShowStatusWhileRunning( () => { return this.PerformPreMountValidation(tracer, enlistment, out mountExecutableLocation, out errorMessage); }, @@ -193,23 +199,23 @@ protected override void Execute(GVFSEnlistment enlistment) "Mounting")) { this.ReportErrorAndExit(tracer, errorMessage); - } - - if (!this.Unattended) + } + + if (!this.Unattended) { tracer.RelatedInfo($"{nameof(this.Execute)}: Registering for automount"); - - if (this.ShowStatusWhileRunning( - () => { return this.RegisterMount(enlistment, out errorMessage); }, - "Registering for automount")) + + if (this.ShowStatusWhileRunning( + () => { return this.RegisterMount(enlistment, out errorMessage); }, + "Registering for automount")) { - tracer.RelatedInfo($"{nameof(this.Execute)}: Registered for automount"); + tracer.RelatedInfo($"{nameof(this.Execute)}: Registered for automount"); } else { this.Output.WriteLine(" WARNING: " + errorMessage); tracer.RelatedInfo($"{nameof(this.Execute)}: Failed to register for automount"); - } + } } } } diff --git a/GVFS/GVFS/CommandLine/PrefetchVerb.cs b/GVFS/GVFS/CommandLine/PrefetchVerb.cs index 5d6373f8e6..ab72b5e9f1 100644 --- a/GVFS/GVFS/CommandLine/PrefetchVerb.cs +++ b/GVFS/GVFS/CommandLine/PrefetchVerb.cs @@ -53,12 +53,12 @@ public class PrefetchVerb : GVFSVerb.ForExistingEnlistment Required = false, Default = false, HelpText = "Specify this flag to load file list from stdin. Same format as when loading from file.")] - public bool FilesFromStdIn { get; set; } - - [Option( - "stdin-folders-list", - Required = false, - Default = false, + public bool FilesFromStdIn { get; set; } + + [Option( + "stdin-folders-list", + Required = false, + Default = false, HelpText = "Specify this flag to load folder list from stdin. Same format as when loading from file.")] public bool FoldersFromStdIn { get; set; } @@ -109,7 +109,7 @@ protected override void Execute(GVFSEnlistment enlistment) tracer.AddDiagnosticConsoleEventListener(EventLevel.Informational, Keywords.Any); } - string cacheServerUrl = CacheServerResolver.GetUrlFromConfig(enlistment); + var cacheServerFromConfig = CacheServerResolver.GetCacheServerFromConfig(enlistment); tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.Prefetch), @@ -118,7 +118,7 @@ protected override void Execute(GVFSEnlistment enlistment) tracer.WriteStartEvent( enlistment.EnlistmentRoot, enlistment.RepoUrl, - cacheServerUrl); + cacheServerFromConfig.Url); try { @@ -127,8 +127,8 @@ protected override void Execute(GVFSEnlistment enlistment) metadata.Add("Files", this.Files); metadata.Add("Folders", this.Folders); metadata.Add("FileListFile", this.FilesListFile); - metadata.Add("FoldersListFile", this.FoldersListFile); - metadata.Add("FilesFromStdIn", this.FilesFromStdIn); + metadata.Add("FoldersListFile", this.FoldersListFile); + metadata.Add("FilesFromStdIn", this.FilesFromStdIn); metadata.Add("FoldersFromStdIn", this.FoldersFromStdIn); metadata.Add("HydrateFiles", this.HydrateFiles); tracer.RelatedEvent(EventLevel.Informational, "PerformPrefetch", metadata); @@ -151,14 +151,14 @@ protected override void Execute(GVFSEnlistment enlistment) } GitObjectsHttpRequestor objectRequestor; - CacheServerInfo cacheServer; + CacheServerInfo resolvedCacheServer; this.InitializeServerConnection( tracer, enlistment, - cacheServerUrl, + cacheServerFromConfig, out objectRequestor, - out cacheServer); - this.PrefetchCommits(tracer, enlistment, objectRequestor, cacheServer); + out resolvedCacheServer); + this.PrefetchCommits(tracer, enlistment, objectRequestor, resolvedCacheServer); } else { @@ -167,8 +167,8 @@ protected override void Execute(GVFSEnlistment enlistment) List foldersList; FileBasedDictionary lastPrefetchArgs; - this.LoadBlobPrefetchArgs(tracer, enlistment, out headCommitId, out filesList, out foldersList, out lastPrefetchArgs); - + this.LoadBlobPrefetchArgs(tracer, enlistment, out headCommitId, out filesList, out foldersList, out lastPrefetchArgs); + if (BlobPrefetcher.IsNoopPrefetch(tracer, lastPrefetchArgs, headCommitId, filesList, foldersList, this.HydrateFiles)) { Console.WriteLine("All requested files are already available. Nothing new to prefetch."); @@ -176,14 +176,14 @@ protected override void Execute(GVFSEnlistment enlistment) else { GitObjectsHttpRequestor objectRequestor; - CacheServerInfo cacheServer; + CacheServerInfo resolvedCacheServer; this.InitializeServerConnection( tracer, enlistment, - cacheServerUrl, + cacheServerFromConfig, out objectRequestor, - out cacheServer); - this.PrefetchBlobs(tracer, enlistment, headCommitId, filesList, foldersList, lastPrefetchArgs, objectRequestor, cacheServer); + out resolvedCacheServer); + this.PrefetchBlobs(tracer, enlistment, headCommitId, filesList, foldersList, lastPrefetchArgs, objectRequestor, resolvedCacheServer); } } } @@ -230,15 +230,18 @@ protected override void Execute(GVFSEnlistment enlistment) private void InitializeServerConnection( ITracer tracer, GVFSEnlistment enlistment, - string cacheServerUrl, + CacheServerInfo cacheServerFromConfig, out GitObjectsHttpRequestor objectRequestor, - out CacheServerInfo cacheServer) + out CacheServerInfo resolvedCacheServer) { RetryConfig retryConfig = this.GetRetryConfig(tracer, enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); - cacheServer = this.ResolvedCacheServer; + // These this.* arguments are set if this is a follow-on operation from clone or mount. + resolvedCacheServer = this.ResolvedCacheServer; ServerGVFSConfig serverGVFSConfig = this.ServerGVFSConfig; - if (!this.SkipVersionCheck) + + // If ResolvedCacheServer is set, then we have already tried querying the server config and checking versions. + if (resolvedCacheServer == null) { string authErrorMessage; if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) @@ -246,24 +249,29 @@ private void InitializeServerConnection( this.ReportErrorAndExit(tracer, "Unable to prefetch because authentication failed: " + authErrorMessage); } + CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); + if (serverGVFSConfig == null) { - serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); + serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + tracer, + enlistment, + retryConfig, + cacheServerFromConfig); } - if (cacheServer == null) + resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerFromConfig.Url, serverGVFSConfig); + + if (!this.SkipVersionCheck) { - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); - cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerUrl, serverGVFSConfig); + this.ValidateClientVersions(tracer, enlistment, serverGVFSConfig, showWarnings: false); } - this.ValidateClientVersions(tracer, enlistment, serverGVFSConfig, showWarnings: false); - - this.Output.WriteLine("Configured cache server: " + cacheServer); + this.Output.WriteLine("Configured cache server: " + resolvedCacheServer); } - this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig, cacheServer); - objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, cacheServer, retryConfig); + this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig, resolvedCacheServer); + objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, resolvedCacheServer, retryConfig); } private void PrefetchCommits(ITracer tracer, GVFSEnlistment enlistment, GitObjectsHttpRequestor objectRequestor, CacheServerInfo cacheServer) @@ -300,8 +308,8 @@ private void LoadBlobPrefetchArgs( out List foldersList, out FileBasedDictionary lastPrefetchArgs) { - string error; - + string error; + if (!FileBasedDictionary.TryCreate( tracer, Path.Combine(enlistment.DotGVFSRoot, "LastBlobPrefetch.dat"), From 566d81f652a236b97f13eef4fb82e6e90bce8748 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:18:26 +0000 Subject: [PATCH 02/11] build(deps): bump actions/github-script from 7 to 8 Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index fe238c532a..bfac2917c4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -29,7 +29,7 @@ jobs: - name: Look for prior successful runs id: check if: github.event.inputs.git_version == '' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{secrets.GITHUB_TOKEN}} result-encoding: string @@ -130,7 +130,7 @@ jobs: - name: Skip this job if there is a previous successful run if: needs.validate.outputs.skip != '' id: skip - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | core.info(`Skipping: There already is a successful run: ${{ needs.validate.outputs.skip }}`) @@ -212,7 +212,7 @@ jobs: - name: Skip this job if there is a previous successful run if: needs.validate.outputs.skip != '' id: skip - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | core.info(`Skipping: There already is a successful run: ${{ needs.validate.outputs.skip }}`) From 92c2b3228331a66ef8b877e7169ec7b985f21ff0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 24 Sep 2025 15:19:37 +0200 Subject: [PATCH 03/11] ReadObjectHook: suppress false positive Currently, CodeQL mis-identifies a couple instances of the usually-legitimate "User-controlled data may not be null terminated" problem. In these instances, CodeQL thinks that `packet_txt_read()` fails to NUL-terminate (not actually "null"... tsk, tsk) the string. But it totally does! Here is the current definition of that function: size_t packet_txt_read(char *buf, size_t count, FILE *stream) { size_t len; len = packet_bin_read(buf, count, stream); if (len && buf[len - 1] == '\n') { len--; } buf[len] = 0; return len; } The `buf[len] = 0` statement guarantees that the string is NUL-terminated. Signed-off-by: Johannes Schindelin --- GVFS/GVFS.ReadObjectHook/main.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/GVFS/GVFS.ReadObjectHook/main.cpp b/GVFS/GVFS.ReadObjectHook/main.cpp index 7a3f608874..83f58beabb 100644 --- a/GVFS/GVFS.ReadObjectHook/main.cpp +++ b/GVFS/GVFS.ReadObjectHook/main.cpp @@ -84,13 +84,13 @@ int main(int, char *argv[]) DisableCRLFTranslationOnStdPipes(); packet_txt_read(packet_buffer, sizeof(packet_buffer)); - if (strcmp(packet_buffer, "git-read-object-client")) + if (strcmp(packet_buffer, "git-read-object-client")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s { die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad welcome message\n"); } packet_txt_read(packet_buffer, sizeof(packet_buffer)); - if (strcmp(packet_buffer, "version=1")) + if (strcmp(packet_buffer, "version=1")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s { die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad version\n"); } @@ -105,7 +105,7 @@ int main(int, char *argv[]) packet_flush(); packet_txt_read(packet_buffer, sizeof(packet_buffer)); - if (strcmp(packet_buffer, "capability=get")) + if (strcmp(packet_buffer, "capability=get")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s { die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad capability\n"); } @@ -125,13 +125,13 @@ int main(int, char *argv[]) while (1) { packet_txt_read(packet_buffer, sizeof(packet_buffer)); - if (strcmp(packet_buffer, "command=get")) + if (strcmp(packet_buffer, "command=get")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s { die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad command\n"); } len = packet_txt_read(packet_buffer, sizeof(packet_buffer)); - if ((len != SHA1_LENGTH + 5) || strncmp(packet_buffer, "sha1=", 5)) + if ((len != SHA1_LENGTH + 5) || strncmp(packet_buffer, "sha1=", 5)) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s { die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad sha1 in get command\n"); } From 96cbde24c66f0f7c90a9b18657aa9ead072cc6a2 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Wed, 24 Sep 2025 09:06:16 -0700 Subject: [PATCH 04/11] Handle errors on background prefetch start In some environments "gvfs" may not be properly resolved by the shell when running the background prefetch process, so this change uses the current process path instead. If the background prefetch process fails to start, the error is now displayed to the user with remediation steps and clone continues to the next step. --- GVFS/GVFS/CommandLine/CloneVerb.cs | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs index e810308377..e1aa098a63 100644 --- a/GVFS/GVFS/CommandLine/CloneVerb.cs +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -6,9 +6,11 @@ using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using System; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Text; namespace GVFS.CommandLine @@ -240,15 +242,24 @@ public override void Execute() else { - Process.Start(new ProcessStartInfo( - fileName: "gvfs", - arguments: "prefetch --commits") + try { - UseShellExecute = true, - WindowStyle = ProcessWindowStyle.Hidden, - WorkingDirectory = enlistment.EnlistmentRoot - }); - this.Output.WriteLine("\r\nPrefetch of commit graph has been started as a background process. Git operations involving history may be slower until prefetch has completed.\r\n"); + string gvfsExecutable = Assembly.GetExecutingAssembly().Location; + Process.Start(new ProcessStartInfo( + fileName: gvfsExecutable, + arguments: "prefetch --commits") + { + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden, + WorkingDirectory = enlistment.EnlistmentRoot + }); + this.Output.WriteLine("\r\nPrefetch of commit graph has been started as a background process. Git operations involving history may be slower until prefetch has completed.\r\n"); + } + catch (Win32Exception ex) + { + this.Output.WriteLine("\r\nError starting prefetch: " + ex.Message); + this.Output.WriteLine("Run 'gvfs prefetch --commits' from within your enlistment to prefetch the commit graph."); + } } } From 3c72233ea6b893e47a250fc0016691ee8c9bdcb0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 1 Oct 2025 13:17:34 +0200 Subject: [PATCH 05/11] Suppress false positives about Git's usage of SHA-1 Git uses SHA-1 as its hashing algorithm, and therefore VFSforGit must use the same. Signed-off-by: Johannes Schindelin --- GVFS/GVFS.Common/Git/HashingStream.cs | 2 +- GVFS/GVFS.Common/SHA1Util.cs | 2 +- .../Projection/GitIndexProjection.FolderData.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/GVFS/GVFS.Common/Git/HashingStream.cs b/GVFS/GVFS.Common/Git/HashingStream.cs index c0630362c8..3be13b2b33 100644 --- a/GVFS/GVFS.Common/Git/HashingStream.cs +++ b/GVFS/GVFS.Common/Git/HashingStream.cs @@ -17,7 +17,7 @@ public HashingStream(Stream stream) { this.stream = stream; - this.hash = SHA1.Create(); + this.hash = SHA1.Create(); // CodeQL [SM02196] SHA-1 is acceptable here because this is Git's hashing algorithm, not used for cryptographic purposes this.hashResult = null; this.hash.Initialize(); this.closed = false; diff --git a/GVFS/GVFS.Common/SHA1Util.cs b/GVFS/GVFS.Common/SHA1Util.cs index 800a2d48bc..0fc20019de 100644 --- a/GVFS/GVFS.Common/SHA1Util.cs +++ b/GVFS/GVFS.Common/SHA1Util.cs @@ -21,7 +21,7 @@ public static byte[] SHA1ForUTF8String(string s) { byte[] bytes = Encoding.UTF8.GetBytes(s); - using (SHA1 sha1 = SHA1.Create()) + using (SHA1 sha1 = SHA1.Create()) // CodeQL [SM02196] SHA-1 is acceptable here because this is Git's hashing algorithm, not used for cryptographic purposes { return sha1.ComputeHash(bytes); } diff --git a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FolderData.cs b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FolderData.cs index bc942d3b26..4777cabbc3 100644 --- a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FolderData.cs +++ b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FolderData.cs @@ -54,7 +54,7 @@ public void Include() public string HashedChildrenNamesSha() { - using (HashAlgorithm hash = SHA1.Create()) + using (HashAlgorithm hash = SHA1.Create()) // CodeQL [SM02196] SHA-1 is acceptable here because this is Git's hashing algorithm, not used for cryptographic purposes { for (int i = 0; i < this.ChildEntries.Count; i++) { From d69080c97c67ccb8adef2bad8ba7f7182036bbeb Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 3 Oct 2025 09:16:40 -0700 Subject: [PATCH 06/11] Fix summary of QueryGVFSConfigWithFallbackCacheServer --- GVFS/GVFS/CommandLine/GVFSVerb.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index 70248bf66d..f0c7d984da 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -494,8 +494,10 @@ protected RetryConfig GetRetryConfig(ITracer tracer, GVFSEnlistment enlistment, } /// - /// Attempts to query the GVFS config endpoint. If it fails, but a valid cache server URL is available, returns a null object. - /// If neither is available, . If config endpoint fails, skips minimum version check and warns (unless skip-version-check is set). + /// Attempts to query the GVFS config endpoint. If successful, returns the config. + /// If the query fails but a valid fallback cache server URL is available, returns null and continues. + /// (A warning will be logged later.) + /// If the query fails and no valid fallback is available, reports an error and exits. /// protected ServerGVFSConfig QueryGVFSConfigWithFallbackCacheServer( ITracer tracer, From 0b0693d375c7b03777301bcc236227459f95bed6 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Wed, 24 Sep 2025 09:06:16 -0700 Subject: [PATCH 07/11] Use current program instead of having shell locate gvfs --- GVFS/GVFS/CommandLine/CloneVerb.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs index e810308377..5bcca3a97d 100644 --- a/GVFS/GVFS/CommandLine/CloneVerb.cs +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Text; namespace GVFS.CommandLine @@ -241,7 +242,7 @@ public override void Execute() else { Process.Start(new ProcessStartInfo( - fileName: "gvfs", + fileName: Assembly.GetExecutingAssembly().Location, arguments: "prefetch --commits") { UseShellExecute = true, From 29af4e41bb9e3f6ee0ec3eb1328e8b7dc8a5e7d3 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 3 Oct 2025 09:28:43 -0700 Subject: [PATCH 08/11] Launch prefetch on clone minimized instead of hidden --- GVFS/GVFS/CommandLine/CloneVerb.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs index 5bcca3a97d..a36ec67f00 100644 --- a/GVFS/GVFS/CommandLine/CloneVerb.cs +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -246,10 +246,10 @@ public override void Execute() arguments: "prefetch --commits") { UseShellExecute = true, - WindowStyle = ProcessWindowStyle.Hidden, + WindowStyle = ProcessWindowStyle.Minimized, WorkingDirectory = enlistment.EnlistmentRoot }); - this.Output.WriteLine("\r\nPrefetch of commit graph has been started as a background process. Git operations involving history may be slower until prefetch has completed.\r\n"); + this.Output.WriteLine("\r\nPrefetch of commit graph has been started in another window. Git operations involving history may be slower until prefetch has completed.\r\n"); } } From 1a051327fe0f870d1afd2b8b89a0a5917d2f44e4 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 3 Oct 2025 09:53:37 -0700 Subject: [PATCH 09/11] Update heuristic for pre-prefetch commit loading This change is intended to improve the heuristic for loading commits when a prefetch has not completed, either because the clone was run with `--no-prefetch` or because `gvfs.trustPackIndexes` is set to `false`. Previously the heuristic for loading commits before prefetch has completed is: - When a commit is requested to be downloaded, record the tree it points to. - At most once per 5 minutes - When a tree is requested to be downloaded that was previously recorded and it has been at least 5 minutes since the last time a commit - Then download the commit. This works for the most basic case where a user clones a repo, then checks out a branch other than the default. It also limits over-downloading when history or commands are run that load many commits but need few objects from them. However, it doesn't work well when these are combined (eg a history command is run first, then a checkout), or when multiple commands are run in far-apart sections of the commit graph in less than a 5-minute period. The new heuristic is: - When a commit is requested to be downloaded, record the tree it points to, associated with the commit. - When a tree is downloaded that was previously recorded, record the subtrees that it references that have not been downloaded yet and associate them with the same commit. - When a tree is requested to be downloaded, if the number of trees associated with a commit is greater than N, then download the commit. N is currently set to 200, which is approximately the number of trees that are downloaded in the first 3 seconds of attempting to checkout a branch where many trees would be downloaded without batching. Downloading the entire tree graph for a commit as a pack takes about 1 second, so this should limit the amount of time to download all trees for a commit to about 4 seconds, but also be unlikely to download the commit packs for history/blame operations that only need a few trees per commit. --- GVFS/GVFS.Common/Git/GitRepo.cs | 15 +++++ GVFS/GVFS.Common/Git/LibGit2Repo.cs | 100 ++++++++++++++++++++++++++++ GVFS/GVFS.Mount/InProcessMount.cs | 69 +++++++++++++++---- 3 files changed, 172 insertions(+), 12 deletions(-) diff --git a/GVFS/GVFS.Common/Git/GitRepo.cs b/GVFS/GVFS.Common/Git/GitRepo.cs index ee9d8b96d2..cd11436d8c 100644 --- a/GVFS/GVFS.Common/Git/GitRepo.cs +++ b/GVFS/GVFS.Common/Git/GitRepo.cs @@ -113,6 +113,21 @@ public virtual bool TryGetBlobLength(string blobSha, out long size) return this.GetLooseBlobState(blobSha, null, out size) == LooseBlobState.Exists; } + /// + /// Try to find the SHAs of subtrees missing from the given tree. + /// + /// Tree to look up + /// SHAs of subtrees of this tree which are not downloaded yet. + /// + public virtual bool TryGetMissingSubTrees(string treeSha, out string[] subtrees) + { + string[] missingSubtrees = null; + var succeeded = this.libgit2RepoInvoker.TryInvoke(repo => + repo.GetMissingSubTrees(treeSha), out missingSubtrees); + subtrees = missingSubtrees; + return succeeded; + } + public void Dispose() { if (this.libgit2RepoInvoker != null) diff --git a/GVFS/GVFS.Common/Git/LibGit2Repo.cs b/GVFS/GVFS.Common/Git/LibGit2Repo.cs index 0849dd6b7f..41a4a4cf8c 100644 --- a/GVFS/GVFS.Common/Git/LibGit2Repo.cs +++ b/GVFS/GVFS.Common/Git/LibGit2Repo.cs @@ -1,5 +1,6 @@ using GVFS.Common.Tracing; using System; +using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; @@ -147,6 +148,84 @@ public virtual bool TryCopyBlob(string sha, Action writeAction) return true; } + /// + /// Get the list of missing subtrees for the given treeSha. + /// + /// Tree to look up + /// SHAs of subtrees of this tree which are not downloaded yet. + public virtual string[] GetMissingSubTrees(string treeSha) + { + List missingSubtreesList = new List(); + IntPtr treeHandle; + if (Native.RevParseSingle(out treeHandle, this.RepoHandle, treeSha) != Native.SuccessCode + || treeHandle == IntPtr.Zero) + { + return Array.Empty(); + } + + try + { + if (Native.Object.GetType(treeHandle) != Native.ObjectTypes.Tree) + { + return Array.Empty(); + } + + uint entryCount = Native.Tree.GetEntryCount(treeHandle); + for (uint i = 0; i < entryCount; i++) + { + if (this.IsMissingSubtree(treeHandle, i, out string entrySha)) + { + missingSubtreesList.Add(entrySha); + } + } + } + finally + { + Native.Object.Free(treeHandle); + } + + return missingSubtreesList.ToArray(); + } + + /// + /// Determine if the given index of a tree is a subtree and if it is missing. + /// If it is a missing subtree, return the SHA of the subtree. + /// + private bool IsMissingSubtree(IntPtr treeHandle, uint i, out string entrySha) + { + entrySha = null; + IntPtr entryHandle = Native.Tree.GetEntryByIndex(treeHandle, i); + if (entryHandle == IntPtr.Zero) + { + return false; + } + + var entryId = Native.Tree.GetEntryId(entryHandle); + if (entryId == IntPtr.Zero) + { + return false; + } + + var entryMode = Native.Tree.GetEntryFileMode(entryHandle); + var rawEntrySha = Native.IntPtrToGitOid(entryId); + entrySha = rawEntrySha.ToString(); + + /* Trees may be listed as executable files instead of trees */ + if (entryMode != Native.Tree.TreeEntryFileModeTree + && entryMode != Native.Tree.TreeEntryFileModeExecutableFile) + { + return false; + } + + if (this.ObjectExists(entrySha)) + { + return false; + } + return true; + /* Both the entryHandle and the entryId handle are owned by the treeHandle, so we shouldn't free them or it will lead to corruption of the later entries */ + } + + public void Dispose() { this.Dispose(true); @@ -247,6 +326,27 @@ public static class Blob [DllImport(Git2NativeLibName, EntryPoint = "git_blob_rawcontent")] public static unsafe extern byte* GetRawContent(IntPtr objectHandle); } + + public static class Tree + { + [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entrycount")] + public static extern uint GetEntryCount(IntPtr treeHandle); + + [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_byindex")] + public static extern IntPtr GetEntryByIndex(IntPtr treeHandle, uint index); + + [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_id")] + public static extern IntPtr GetEntryId(IntPtr entryHandle); + + /* git_tree_entry_type requires the object to exist, so we can't use it to check if + * a missing entry is a tree. Instead, we can use the file mode to determine if it is a tree. */ + [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_filemode")] + public static extern uint GetEntryFileMode(IntPtr entryHandle); + + public const uint TreeEntryFileModeTree = 0x4000; + public const uint TreeEntryFileModeExecutableFile = 0x81ED; // 100755 in Octal + + } } } } \ No newline at end of file diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index a9dd95b708..e39f410f20 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -9,6 +9,7 @@ using GVFS.PlatformLoader; using GVFS.Virtualization; using GVFS.Virtualization.FileSystem; +using Microsoft.Isam.Esent.Interop; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -27,6 +28,11 @@ public class InProcessMount private const int MaxPipeNameLength = 250; private const int MutexMaxWaitTimeMS = 500; + // This is value chosen based on tested scenarios to limit the required download time for + // all the trees in a commit to a few seconds, while not downloading pack files for commits + // where only a few missing trees are needed. + private const int MissingTreeThresholdForDownloadingCommitPack = 200; + private readonly bool showDebugWindow; private FileSystemCallbacks fileSystemCallbacks; @@ -47,7 +53,6 @@ public class InProcessMount private ManualResetEvent unmountEvent; private readonly Dictionary treesWithDownloadedCommits = new Dictionary(); - private DateTime lastCommitPackDownloadTime = DateTime.MinValue; // True if InProcessMount is calling git reset as part of processing // a folder dehydrate request @@ -518,13 +523,14 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name if (this.ShouldDownloadCommitPack(objectSha, out string commitSha) && this.gitObjects.TryDownloadCommit(commitSha)) { - this.DownloadedCommitPack(objectSha: objectSha, commitSha: commitSha); + this.DownloadedCommitPack(commitSha); response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult); // FUTURE: Should the stats be updated to reflect all the trees in the pack? // FUTURE: Should we try to clean up duplicate trees or increase depth of the commit download? } else if (this.gitObjects.TryDownloadAndSaveObject(objectSha, GVFSGitObjects.RequestSource.NamedPipeMessage) == GitObjects.DownloadAndSaveObjectResult.Success) { + this.UpdateTreesForDownloadedCommits(objectSha); response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult); } else @@ -548,7 +554,7 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name * Otherwise, the trees for the commit may be needed soon depending on the context. * e.g. git log (without a pathspec) doesn't need trees, but git checkout does. * - * Save the tree/commit so if the tree is requested soon we can download all the trees for the commit in a batch. + * Save the tree/commit so if more trees are requested we can download all the trees for the commit in a batch. */ this.treesWithDownloadedCommits[treeSha] = objectSha; } @@ -561,28 +567,67 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name private bool PrefetchHasBeenDone() { var prefetchPacks = this.gitObjects.ReadPackFileNames(this.enlistment.GitPackRoot, GVFSConstants.PrefetchPackPrefix); - return prefetchPacks.Length > 0; + var result = prefetchPacks.Length > 0; + if (result) + { + this.treesWithDownloadedCommits.Clear(); + } + return result; } private bool ShouldDownloadCommitPack(string objectSha, out string commitSha) { - if (!this.treesWithDownloadedCommits.TryGetValue(objectSha, out commitSha) || this.PrefetchHasBeenDone()) { return false; } - /* This is a heuristic to prevent downloading multiple packs related to git history commands, - * since commits downloaded close together likely have similar trees. */ - var timePassed = DateTime.UtcNow - this.lastCommitPackDownloadTime; - return (timePassed > TimeSpan.FromMinutes(5)); + /* This is a heuristic to prevent downloading multiple packs related to git history commands. + * Closely related commits are likely to have similar trees, so we'll find fewer missing trees in them. + * Conversely, if we know (from previously downloaded missing trees) that a commit has a lot of missing + * trees left, we'll probably need to download many more trees for the commit so we should download the pack. + */ + var commitShaLocal = commitSha; // can't use out parameter in lambda + int missingTreeCount = this.treesWithDownloadedCommits.Where(x => x.Value == commitShaLocal).Count(); + return missingTreeCount > MissingTreeThresholdForDownloadingCommitPack; + } + + private void UpdateTreesForDownloadedCommits(string objectSha) + { + /* If we are downloading missing trees, we probably are missing more trees for the commit. + * Update our list of trees associated with the commit so we can use the # of missing trees + * as a heuristic to decide whether to batch download all the trees for the commit the + * next time a missing one is requested. + */ + if (!this.treesWithDownloadedCommits.TryGetValue(objectSha, out var commitSha) + || this.PrefetchHasBeenDone()) + { + return; + } + + if (!this.context.Repository.TryGetObjectType(objectSha, out var objectType) + || objectType != Native.ObjectTypes.Tree) + { + return; + } + + if (this.context.Repository.TryGetMissingSubTrees(objectSha, out var missingSubTrees)) + { + foreach (var missingSubTree in missingSubTrees) + { + this.treesWithDownloadedCommits[missingSubTree] = commitSha; + } + } } - private void DownloadedCommitPack(string objectSha, string commitSha) + private void DownloadedCommitPack(string commitSha) { - this.lastCommitPackDownloadTime = DateTime.UtcNow; - this.treesWithDownloadedCommits.Remove(objectSha); + var toRemove = this.treesWithDownloadedCommits.Where(x => x.Value == commitSha).ToList(); + foreach (var tree in toRemove) + { + this.treesWithDownloadedCommits.Remove(tree.Key); + } } private void HandlePostFetchJobRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection) From b13b95904df71eed261b011514d3683eed668811 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 17 Oct 2025 08:57:12 -0700 Subject: [PATCH 10/11] Update comments, fix tree entry type check --- GVFS/GVFS.Common/Git/LibGit2Repo.cs | 17 +++++++---------- GVFS/GVFS.Mount/InProcessMount.cs | 5 +++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/GVFS/GVFS.Common/Git/LibGit2Repo.cs b/GVFS/GVFS.Common/Git/LibGit2Repo.cs index 41a4a4cf8c..f9edcce64a 100644 --- a/GVFS/GVFS.Common/Git/LibGit2Repo.cs +++ b/GVFS/GVFS.Common/Git/LibGit2Repo.cs @@ -200,23 +200,21 @@ private bool IsMissingSubtree(IntPtr treeHandle, uint i, out string entrySha) return false; } + var entryMode = Native.Tree.GetEntryFileMode(entryHandle); + if (entryMode != Native.Tree.TreeEntryFileModeDirectory) + { + return false; + } + var entryId = Native.Tree.GetEntryId(entryHandle); if (entryId == IntPtr.Zero) { return false; } - var entryMode = Native.Tree.GetEntryFileMode(entryHandle); var rawEntrySha = Native.IntPtrToGitOid(entryId); entrySha = rawEntrySha.ToString(); - /* Trees may be listed as executable files instead of trees */ - if (entryMode != Native.Tree.TreeEntryFileModeTree - && entryMode != Native.Tree.TreeEntryFileModeExecutableFile) - { - return false; - } - if (this.ObjectExists(entrySha)) { return false; @@ -343,8 +341,7 @@ public static class Tree [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_filemode")] public static extern uint GetEntryFileMode(IntPtr entryHandle); - public const uint TreeEntryFileModeTree = 0x4000; - public const uint TreeEntryFileModeExecutableFile = 0x81ED; // 100755 in Octal + public const uint TreeEntryFileModeDirectory = 0x4000; } } diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index e39f410f20..7e74581a18 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -29,8 +29,9 @@ public class InProcessMount private const int MutexMaxWaitTimeMS = 500; // This is value chosen based on tested scenarios to limit the required download time for - // all the trees in a commit to a few seconds, while not downloading pack files for commits - // where only a few missing trees are needed. + // all the trees. This is approximately the amount of trees that can be downloaded in 1 second. + // Downloading an entire commit pack also takes around 1 second, so this should limit downloading + // all the trees in a commit to ~2-3 seconds. private const int MissingTreeThresholdForDownloadingCommitPack = 200; private readonly bool showDebugWindow; From 0b13dcca384bd9aa6d9907462e0c9bbb09b18948 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 17 Oct 2025 08:57:37 -0700 Subject: [PATCH 11/11] Remove unnecessary using --- GVFS/GVFS.Mount/InProcessMount.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index 7e74581a18..52426075bb 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -9,7 +9,6 @@ using GVFS.PlatformLoader; using GVFS.Virtualization; using GVFS.Virtualization.FileSystem; -using Microsoft.Isam.Esent.Interop; using Newtonsoft.Json; using System; using System.Collections.Generic;