diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index fe238c532..bfac2917c 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 }}`) diff --git a/GVFS/GVFS.Common/Git/GitRepo.cs b/GVFS/GVFS.Common/Git/GitRepo.cs index ee9d8b96d..cd11436d8 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/HashingStream.cs b/GVFS/GVFS.Common/Git/HashingStream.cs index c0630362c..3be13b2b3 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/Git/LibGit2Repo.cs b/GVFS/GVFS.Common/Git/LibGit2Repo.cs index 0849dd6b7..f9edcce64 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,82 @@ 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 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 rawEntrySha = Native.IntPtrToGitOid(entryId); + entrySha = rawEntrySha.ToString(); + + 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 +324,26 @@ 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 TreeEntryFileModeDirectory = 0x4000; + + } } } } \ No newline at end of file diff --git a/GVFS/GVFS.Common/Http/CacheServerResolver.cs b/GVFS/GVFS.Common/Http/CacheServerResolver.cs index 9f3a7d311..bc1df9727 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.Common/SHA1Util.cs b/GVFS/GVFS.Common/SHA1Util.cs index 800a2d48b..0fc20019d 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.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index a9dd95b70..52426075b 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -27,6 +27,12 @@ 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. 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; 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) diff --git a/GVFS/GVFS.ReadObjectHook/main.cpp b/GVFS/GVFS.ReadObjectHook/main.cpp index 7a3f60887..83f58beab 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"); } diff --git a/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FolderData.cs b/GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FolderData.cs index bc942d3b2..4777cabbc 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++) { diff --git a/GVFS/GVFS/CommandLine/CacheServerVerb.cs b/GVFS/GVFS/CommandLine/CacheServerVerb.cs index 55edb8853..86754ae67 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 e81030837..e0d858360 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 @@ -162,12 +164,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 +191,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,18 +244,26 @@ public override void Execute() exitCode = (int)result; } } - 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.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"); + } + 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."); + } } } diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index fa183c7a3..fe0731a00 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -493,6 +493,50 @@ protected RetryConfig GetRetryConfig(ITracer tracer, GVFSEnlistment enlistment, return retryConfig; } + /// + /// 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, + 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 90db430cf..5183ec434 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 5d6373f8e..ab72b5e9f 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"),