diff --git a/GVFS/GVFS.Common/Git/GitAuthentication.cs b/GVFS/GVFS.Common/Git/GitAuthentication.cs index 27796f0e8..bb81a86c1 100644 --- a/GVFS/GVFS.Common/Git/GitAuthentication.cs +++ b/GVFS/GVFS.Common/Git/GitAuthentication.cs @@ -183,34 +183,98 @@ public bool TryGetCredentials(ITracer tracer, out string credentialString, out s return true; } + /// + /// Initialize authentication by probing the server. Determines whether + /// anonymous access is supported and, if not, fetches credentials. + /// Callers that also need the GVFS config should use + /// instead to avoid a + /// redundant HTTP round-trip. + /// public bool TryInitialize(ITracer tracer, Enlistment enlistment, out string errorMessage) + { + // Delegate to the combined method, discarding the config result. + // This avoids duplicating the anonymous-probe + credential-fetch logic. + return this.TryInitializeAndQueryGVFSConfig( + tracer, + enlistment, + new RetryConfig(), + out _, + out errorMessage); + } + + /// + /// Combines authentication initialization with the GVFS config query, + /// eliminating a redundant HTTP round-trip. The anonymous probe and + /// config query use the same request to /gvfs/config: + /// 1. Config query → /gvfs/config → 200 (anonymous) or 401 + /// 2. If 401: credential fetch, then retry → 200 + /// This saves one HTTP request compared to probing auth separately + /// and then querying config, and reuses the same TCP/TLS connection. + /// + public bool TryInitializeAndQueryGVFSConfig( + ITracer tracer, + Enlistment enlistment, + RetryConfig retryConfig, + out ServerGVFSConfig serverGVFSConfig, + out string errorMessage) { if (this.isInitialized) { throw new InvalidOperationException("Already initialized"); } + serverGVFSConfig = null; errorMessage = null; - bool isAnonymous; - if (!this.TryAnonymousQuery(tracer, enlistment, out isAnonymous)) + using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig)) { - errorMessage = $"Unable to determine if authentication is required"; - return false; - } + HttpStatusCode? httpStatus; - if (!isAnonymous && - !this.TryCallGitCredential(tracer, out errorMessage)) - { + // First attempt without credentials. If anonymous access works, + // we get the config in a single request. + if (configRequestor.TryQueryGVFSConfig(false, out serverGVFSConfig, out httpStatus, out _)) + { + this.IsAnonymous = true; + this.isInitialized = true; + tracer.RelatedInfo("{0}: Anonymous access succeeded, config obtained in one request", nameof(this.TryInitializeAndQueryGVFSConfig)); + return true; + } + + if (httpStatus != HttpStatusCode.Unauthorized) + { + errorMessage = "Unable to query /gvfs/config"; + tracer.RelatedWarning("{0}: Config query failed with status {1}", nameof(this.TryInitializeAndQueryGVFSConfig), httpStatus?.ToString() ?? "None"); + return false; + } + + // Server requires authentication — fetch credentials + this.IsAnonymous = false; + + if (!this.TryCallGitCredential(tracer, out errorMessage)) + { + tracer.RelatedWarning("{0}: Credential fetch failed: {1}", nameof(this.TryInitializeAndQueryGVFSConfig), errorMessage); + return false; + } + + this.isInitialized = true; + + // Retry with credentials using the same ConfigHttpRequestor (reuses HttpClient/connection) + if (configRequestor.TryQueryGVFSConfig(true, out serverGVFSConfig, out _, out errorMessage)) + { + tracer.RelatedInfo("{0}: Config obtained with credentials", nameof(this.TryInitializeAndQueryGVFSConfig)); + return true; + } + + tracer.RelatedWarning("{0}: Config query failed with credentials: {1}", nameof(this.TryInitializeAndQueryGVFSConfig), errorMessage); return false; } - - this.IsAnonymous = isAnonymous; - this.isInitialized = true; - return true; } - public bool TryInitializeAndRequireAuth(ITracer tracer, out string errorMessage) + /// + /// Test-only initialization that skips the network probe and goes + /// straight to credential fetch. Not for production use. + /// + internal bool TryInitializeAndRequireAuth(ITracer tracer, out string errorMessage) { if (this.isInitialized) { @@ -267,45 +331,6 @@ private static bool TryParseCredentialString(string credentialString, out string return false; } - private bool TryAnonymousQuery(ITracer tracer, Enlistment enlistment, out bool isAnonymous) - { - bool querySucceeded; - using (ITracer anonymousTracer = tracer.StartActivity("AttemptAnonymousAuth", EventLevel.Informational)) - { - HttpStatusCode? httpStatus; - - using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(anonymousTracer, enlistment, new RetryConfig())) - { - ServerGVFSConfig gvfsConfig; - const bool LogErrors = false; - if (configRequestor.TryQueryGVFSConfig(LogErrors, out gvfsConfig, out httpStatus, out _)) - { - querySucceeded = true; - isAnonymous = true; - } - else if (httpStatus == HttpStatusCode.Unauthorized) - { - querySucceeded = true; - isAnonymous = false; - } - else - { - querySucceeded = false; - isAnonymous = false; - } - } - - anonymousTracer.Stop(new EventMetadata - { - { "HttpStatus", httpStatus.HasValue ? ((int)httpStatus).ToString() : "None" }, - { "QuerySucceeded", querySucceeded }, - { "IsAnonymous", isAnonymous }, - }); - } - - return querySucceeded; - } - private DateTime GetNextAuthAttemptTime() { if (this.numberOfAttempts <= 1) diff --git a/GVFS/GVFS.Common/Git/GitCoreGVFSFlags.cs b/GVFS/GVFS.Common/Git/GitCoreGVFSFlags.cs new file mode 100644 index 000000000..411c5bc3c --- /dev/null +++ b/GVFS/GVFS.Common/Git/GitCoreGVFSFlags.cs @@ -0,0 +1,58 @@ +using System; + +namespace GVFS.Common.Git +{ + [Flags] + public enum GitCoreGVFSFlags + { + // GVFS_SKIP_SHA_ON_INDEX + // Disables the calculation of the sha when writing the index + SkipShaOnIndex = 1 << 0, + + // GVFS_BLOCK_COMMANDS + // Blocks git commands that are not allowed in a GVFS/Scalar repo + BlockCommands = 1 << 1, + + // GVFS_MISSING_OK + // Normally git write-tree ensures that the objects referenced by the + // directory exist in the object database.This option disables this check. + MissingOk = 1 << 2, + + // GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT + // When marking entries to remove from the index and the working + // directory this option will take into account what the + // skip-worktree bit was set to so that if the entry has the + // skip-worktree bit set it will not be removed from the working + // directory. This will allow virtualized working directories to + // detect the change to HEAD and use the new commit tree to show + // the files that are in the working directory. + NoDeleteOutsideSparseCheckout = 1 << 3, + + // GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK + // While performing a fetch with a virtual file system we know + // that there will be missing objects and we don't want to download + // them just because of the reachability of the commits. We also + // don't want to download a pack file with commits, trees, and blobs + // since these will be downloaded on demand. This flag will skip the + // checks on the reachability of objects during a fetch as well as + // the upload pack so that extraneous objects don't get downloaded. + FetchSkipReachabilityAndUploadPack = 1 << 4, + + // 1 << 5 has been deprecated + + // GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS + // With a virtual file system we only know the file size before any + // CRLF or smudge/clean filters processing is done on the client. + // To prevent file corruption due to truncation or expansion with + // garbage at the end, these filters must not run when the file + // is first accessed and brought down to the client. Git.exe can't + // currently tell the first access vs subsequent accesses so this + // flag just blocks them from occurring at all. + BlockFiltersAndEolConversions = 1 << 6, + + // GVFS_PREFETCH_DURING_FETCH + // While performing a `git fetch` command, use the gvfs-helper to + // perform a "prefetch" of commits and trees. + PrefetchDuringFetch = 1 << 7, + } +} diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index e6d43a842..c56627703 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -15,8 +15,10 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Security; using System.Text; using System.Threading; +using System.Threading.Tasks; using static GVFS.Common.Git.LibGit2Repo; namespace GVFS.Mount @@ -83,6 +85,40 @@ public void Mount(EventLevel verbosity, Keywords keywords) { this.currentState = MountState.Mounting; + // Start auth + config query immediately — these are network-bound and don't + // depend on repo metadata or cache paths. Every millisecond of network latency + // we can overlap with local I/O is a win. + // TryInitializeAndQueryGVFSConfig combines the anonymous probe, credential fetch, + // and config query into at most 2 HTTP requests (1 for anonymous repos), reusing + // the same HttpClient/TCP connection. + Stopwatch parallelTimer = Stopwatch.StartNew(); + + var networkTask = Task.Run(() => + { + Stopwatch sw = Stopwatch.StartNew(); + ServerGVFSConfig config; + string authConfigError; + + if (!this.enlistment.Authentication.TryInitializeAndQueryGVFSConfig( + this.tracer, this.enlistment, this.retryConfig, + out config, out authConfigError)) + { + if (this.cacheServer != null && !string.IsNullOrWhiteSpace(this.cacheServer.Url)) + { + this.tracer.RelatedWarning("Mount will proceed with fallback cache server: " + authConfigError); + config = null; + } + else + { + this.FailMountAndExit("Unable to query /gvfs/config" + Environment.NewLine + authConfigError); + } + } + + this.ValidateGVFSVersion(config); + this.tracer.RelatedInfo("ParallelMount: Auth + config completed in {0}ms", sw.ElapsedMilliseconds); + return config; + }); + // We must initialize repo metadata before starting the pipe server so it // can immediately handle status requests string error; @@ -121,6 +157,58 @@ public void Mount(EventLevel verbosity, Keywords keywords) this.enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot); + // Local validations and git config run while we wait for the network + var localTask = Task.Run(() => + { + Stopwatch sw = Stopwatch.StartNew(); + + this.ValidateGitVersion(); + this.tracer.RelatedInfo("ParallelMount: ValidateGitVersion completed in {0}ms", sw.ElapsedMilliseconds); + + this.ValidateHooksVersion(); + this.ValidateFileSystemSupportsRequiredFeatures(); + + GitProcess git = new GitProcess(this.enlistment); + if (!git.IsValidRepo()) + { + this.FailMountAndExit("The .git folder is missing or has invalid contents"); + } + + if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(this.enlistment.EnlistmentRoot, out string fsError)) + { + this.FailMountAndExit("FileSystem unsupported: " + fsError); + } + + this.tracer.RelatedInfo("ParallelMount: Local validations completed in {0}ms", sw.ElapsedMilliseconds); + + if (!this.TrySetRequiredGitConfigSettings()) + { + this.FailMountAndExit("Unable to configure git repo"); + } + + this.LogEnlistmentInfoAndSetConfigValues(); + this.tracer.RelatedInfo("ParallelMount: Local validations + git config completed in {0}ms", sw.ElapsedMilliseconds); + }); + + try + { + Task.WaitAll(networkTask, localTask); + } + catch (AggregateException ae) + { + this.FailMountAndExit(ae.Flatten().InnerExceptions[0].Message); + } + + parallelTimer.Stop(); + this.tracer.RelatedInfo("ParallelMount: All parallel tasks completed in {0}ms", parallelTimer.ElapsedMilliseconds); + + ServerGVFSConfig serverGVFSConfig = networkTask.Result; + + CacheServerResolver cacheServerResolver = new CacheServerResolver(this.tracer, this.enlistment); + this.cacheServer = cacheServerResolver.ResolveNameFromRemote(this.cacheServer.Url, serverGVFSConfig); + + this.EnsureLocalCacheIsHealthy(serverGVFSConfig); + using (NamedPipeServer pipeServer = this.StartNamedPipe()) { this.tracer.RelatedEvent( @@ -772,13 +860,6 @@ private void HandleUnmountRequest(NamedPipeServer.Connection connection) private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache, bool alreadyInitialized = false) { string error; - if (!alreadyInitialized) - { - if (!this.context.Enlistment.Authentication.TryInitialize(this.context.Tracer, this.context.Enlistment, out error)) - { - this.FailMountAndExit("Failed to obtain git credentials: " + error); - } - } GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(this.context.Tracer, this.context.Enlistment, cache, this.retryConfig); this.gitObjects = new GVFSGitObjects(this.context, objectRequestor); @@ -846,6 +927,462 @@ private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache, bool this.heartbeat.Start(); } + private void ValidateGitVersion() + { + GitVersion gitVersion = null; + if (string.IsNullOrEmpty(this.enlistment.GitBinPath) || !GitProcess.TryGetVersion(this.enlistment.GitBinPath, out gitVersion, out string _)) + { + this.FailMountAndExit("Error: Unable to retrieve the Git version"); + } + + this.enlistment.SetGitVersion(gitVersion.ToString()); + + if (gitVersion.Platform != GVFSConstants.SupportedGitVersion.Platform) + { + this.FailMountAndExit("Error: Invalid version of Git {0}. Must use vfs version.", gitVersion); + } + + if (gitVersion.IsLessThan(GVFSConstants.SupportedGitVersion)) + { + this.FailMountAndExit( + "Error: Installed Git version {0} is less than the minimum supported version of {1}.", + gitVersion, + GVFSConstants.SupportedGitVersion); + } + else if (gitVersion.Revision != GVFSConstants.SupportedGitVersion.Revision) + { + this.FailMountAndExit( + "Error: Installed Git version {0} has revision number {1} instead of {2}." + + " This Git version is too new, so either downgrade Git or upgrade VFS for Git." + + " The minimum supported version of Git is {3}.", + gitVersion, + gitVersion.Revision, + GVFSConstants.SupportedGitVersion.Revision, + GVFSConstants.SupportedGitVersion); + } + } + + private void ValidateHooksVersion() + { + string hooksVersion; + string error; + if (!GVFSPlatform.Instance.TryGetGVFSHooksVersion(out hooksVersion, out error)) + { + this.FailMountAndExit(error); + } + + string gvfsVersion = ProcessHelper.GetCurrentProcessVersion(); + if (hooksVersion != gvfsVersion) + { + this.FailMountAndExit("GVFS.Hooks version ({0}) does not match GVFS version ({1}).", hooksVersion, gvfsVersion); + } + + this.enlistment.SetGVFSHooksVersion(hooksVersion); + } + + private void ValidateFileSystemSupportsRequiredFeatures() + { + try + { + string warning; + string error; + if (!GVFSPlatform.Instance.KernelDriver.IsSupported(this.enlistment.EnlistmentRoot, out warning, out error)) + { + this.FailMountAndExit("Error: {0}", error); + } + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Exception", e.ToString()); + this.tracer.RelatedError(metadata, "Failed to determine if file system supports features required by GVFS"); + this.FailMountAndExit("Error: Failed to determine if file system supports features required by GVFS."); + } + } + + private ServerGVFSConfig QueryAndValidateGVFSConfig() + { + ServerGVFSConfig serverGVFSConfig = null; + string errorMessage = null; + + using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(this.tracer, this.enlistment, this.retryConfig)) + { + const bool LogErrors = true; + if (!configRequestor.TryQueryGVFSConfig(LogErrors, out serverGVFSConfig, out _, out errorMessage)) + { + // If we have a valid cache server, continue without config (matches verb fallback behavior) + if (this.cacheServer != null && !string.IsNullOrWhiteSpace(this.cacheServer.Url)) + { + this.tracer.RelatedWarning("Unable to query /gvfs/config: " + errorMessage); + serverGVFSConfig = null; + } + else + { + this.FailMountAndExit("Unable to query /gvfs/config" + Environment.NewLine + errorMessage); + } + } + } + + this.ValidateGVFSVersion(serverGVFSConfig); + + return serverGVFSConfig; + } + + private void ValidateGVFSVersion(ServerGVFSConfig config) + { + using (ITracer activity = this.tracer.StartActivity("ValidateGVFSVersion", EventLevel.Informational)) + { + if (ProcessHelper.IsDevelopmentVersion()) + { + return; + } + + string recordedVersion = ProcessHelper.GetCurrentProcessVersion(); + int plus = recordedVersion.IndexOf('+'); + Version currentVersion = new Version(plus < 0 ? recordedVersion : recordedVersion.Substring(0, plus)); + IEnumerable allowedGvfsClientVersions = + config != null + ? config.AllowedGVFSClientVersions + : null; + + if (allowedGvfsClientVersions == null || !allowedGvfsClientVersions.Any()) + { + string warningMessage = "WARNING: Unable to validate your GVFS version" + Environment.NewLine; + if (config == null) + { + warningMessage += "Could not query valid GVFS versions from: " + Uri.EscapeUriString(this.enlistment.RepoUrl); + } + else + { + warningMessage += "Server not configured to provide supported GVFS versions"; + } + + this.tracer.RelatedWarning(warningMessage); + return; + } + + foreach (ServerGVFSConfig.VersionRange versionRange in config.AllowedGVFSClientVersions) + { + if (currentVersion >= versionRange.Min && + (versionRange.Max == null || currentVersion <= versionRange.Max)) + { + activity.RelatedEvent( + EventLevel.Informational, + "GVFSVersionValidated", + new EventMetadata + { + { "SupportedVersionRange", versionRange }, + }); + + this.enlistment.SetGVFSVersion(currentVersion.ToString()); + return; + } + } + + activity.RelatedError("GVFS version {0} is not supported", currentVersion); + this.FailMountAndExit("ERROR: Your GVFS version is no longer supported. Install the latest and try again."); + } + } + + private void EnsureLocalCacheIsHealthy(ServerGVFSConfig serverGVFSConfig) + { + if (!Directory.Exists(this.enlistment.LocalCacheRoot)) + { + try + { + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Local cache root: {this.enlistment.LocalCacheRoot} missing, recreating it"); + Directory.CreateDirectory(this.enlistment.LocalCacheRoot); + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Exception", e.ToString()); + metadata.Add("enlistment.LocalCacheRoot", this.enlistment.LocalCacheRoot); + this.tracer.RelatedError(metadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create local cache root"); + this.FailMountAndExit("Failed to create local cache: " + this.enlistment.LocalCacheRoot); + } + } + + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + if (Directory.Exists(this.enlistment.GitObjectsRoot)) + { + bool gitObjectsRootInAlternates = false; + string alternatesFilePath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Objects.Info.Alternates); + if (File.Exists(alternatesFilePath)) + { + try + { + using (Stream stream = fileSystem.OpenFileStream( + alternatesFilePath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + callFlushFileBuffers: false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + while (!reader.EndOfStream) + { + string alternatesLine = reader.ReadLine(); + if (string.Equals(alternatesLine, this.enlistment.GitObjectsRoot, GVFSPlatform.Instance.Constants.PathComparison)) + { + gitObjectsRootInAlternates = true; + } + } + } + } + } + catch (Exception e) + { + EventMetadata exceptionMetadata = new EventMetadata(); + exceptionMetadata.Add("Exception", e.ToString()); + this.tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to validate alternates file"); + this.FailMountAndExit($"Failed to validate that alternates file includes git objects root: {e.Message}"); + } + } + else + { + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Alternates file not found"); + } + + if (!gitObjectsRootInAlternates) + { + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({this.enlistment.GitObjectsRoot}) missing from alternates files, recreating alternates"); + string error; + if (!this.TryCreateAlternatesFile(fileSystem, out error)) + { + this.FailMountAndExit($"Failed to update alternates file to include git objects root: {error}"); + } + } + } + else + { + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({this.enlistment.GitObjectsRoot}) missing, determining new root"); + + if (serverGVFSConfig == null) + { + using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(this.tracer, this.enlistment, this.retryConfig)) + { + string configError; + if (!configRequestor.TryQueryGVFSConfig(true, out serverGVFSConfig, out _, out configError)) + { + this.FailMountAndExit("Unable to query /gvfs/config" + Environment.NewLine + configError); + } + } + } + + string localCacheKey; + string error; + LocalCacheResolver localCacheResolver = new LocalCacheResolver(this.enlistment); + if (!localCacheResolver.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers( + this.tracer, + serverGVFSConfig, + this.cacheServer, + this.enlistment.LocalCacheRoot, + localCacheKey: out localCacheKey, + errorMessage: out error)) + { + this.FailMountAndExit($"Previous git objects root ({this.enlistment.GitObjectsRoot}) not found, and failed to determine new local cache key: {error}"); + } + + EventMetadata keyMetadata = new EventMetadata(); + keyMetadata.Add("localCacheRoot", this.enlistment.LocalCacheRoot); + keyMetadata.Add("localCacheKey", localCacheKey); + keyMetadata.Add(TracingConstants.MessageKey.InfoMessage, "Initializing and persisting updated paths"); + this.tracer.RelatedEvent(EventLevel.Informational, "EnsureLocalCacheIsHealthy_InitializePathsFromKey", keyMetadata); + this.enlistment.InitializeCachePathsFromKey(this.enlistment.LocalCacheRoot, localCacheKey); + + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating GitObjectsRoot ({this.enlistment.GitObjectsRoot}), GitPackRoot ({this.enlistment.GitPackRoot}), and BlobSizesRoot ({this.enlistment.BlobSizesRoot})"); + try + { + Directory.CreateDirectory(this.enlistment.GitObjectsRoot); + Directory.CreateDirectory(this.enlistment.GitPackRoot); + } + catch (Exception e) + { + EventMetadata exceptionMetadata = new EventMetadata(); + exceptionMetadata.Add("Exception", e.ToString()); + exceptionMetadata.Add("enlistment.GitObjectsRoot", this.enlistment.GitObjectsRoot); + exceptionMetadata.Add("enlistment.GitPackRoot", this.enlistment.GitPackRoot); + this.tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create objects and pack folders"); + this.FailMountAndExit("Failed to create objects and pack folders"); + } + + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating new alternates file"); + if (!this.TryCreateAlternatesFile(fileSystem, out error)) + { + this.FailMountAndExit($"Failed to update alternates file with new objects path: {error}"); + } + + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving git objects root ({this.enlistment.GitObjectsRoot}) in repo metadata"); + RepoMetadata.Instance.SetGitObjectsRoot(this.enlistment.GitObjectsRoot); + + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving blob sizes root ({this.enlistment.BlobSizesRoot}) in repo metadata"); + RepoMetadata.Instance.SetBlobSizesRoot(this.enlistment.BlobSizesRoot); + } + + if (!Directory.Exists(this.enlistment.BlobSizesRoot)) + { + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: BlobSizesRoot ({this.enlistment.BlobSizesRoot}) not found, re-creating"); + try + { + Directory.CreateDirectory(this.enlistment.BlobSizesRoot); + } + catch (Exception e) + { + EventMetadata exceptionMetadata = new EventMetadata(); + exceptionMetadata.Add("Exception", e.ToString()); + exceptionMetadata.Add("enlistment.BlobSizesRoot", this.enlistment.BlobSizesRoot); + this.tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create blob sizes folder"); + this.FailMountAndExit("Failed to create blob sizes folder"); + } + } + } + + private bool TryCreateAlternatesFile(PhysicalFileSystem fileSystem, out string errorMessage) + { + try + { + string alternatesFilePath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Objects.Info.Alternates); + string tempFilePath = alternatesFilePath + ".tmp"; + fileSystem.WriteAllText(tempFilePath, this.enlistment.GitObjectsRoot); + fileSystem.MoveAndOverwriteFile(tempFilePath, alternatesFilePath); + } + catch (SecurityException e) { errorMessage = e.Message; return false; } + catch (IOException e) { errorMessage = e.Message; return false; } + + errorMessage = null; + return true; + } + + + private bool TrySetRequiredGitConfigSettings() + { + string expectedHooksPath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Hooks.Root); + expectedHooksPath = Paths.ConvertPathToGitFormat(expectedHooksPath); + + string gitStatusCachePath = null; + if (!GVFSEnlistment.IsUnattended(tracer: null) && GVFSPlatform.Instance.IsGitStatusCacheSupported()) + { + gitStatusCachePath = Path.Combine( + this.enlistment.EnlistmentRoot, + GVFSPlatform.Instance.Constants.DotGVFSRoot, + GVFSConstants.DotGVFS.GitStatusCache.CachePath); + + gitStatusCachePath = Paths.ConvertPathToGitFormat(gitStatusCachePath); + } + + string coreGVFSFlags = Convert.ToInt32( + GitCoreGVFSFlags.SkipShaOnIndex | + GitCoreGVFSFlags.BlockCommands | + GitCoreGVFSFlags.MissingOk | + GitCoreGVFSFlags.NoDeleteOutsideSparseCheckout | + GitCoreGVFSFlags.FetchSkipReachabilityAndUploadPack | + GitCoreGVFSFlags.BlockFiltersAndEolConversions) + .ToString(); + + Dictionary requiredSettings = new Dictionary + { + { "am.keepcr", "true" }, + { "checkout.optimizenewbranch", "true" }, + { "core.autocrlf", "false" }, + { "core.commitGraph", "true" }, + { "core.fscache", "true" }, + { "core.gvfs", coreGVFSFlags }, + { "core.multiPackIndex", "true" }, + { "core.preloadIndex", "true" }, + { "core.safecrlf", "false" }, + { "core.untrackedCache", "false" }, + { "core.repositoryformatversion", "0" }, + { "core.filemode", GVFSPlatform.Instance.FileSystem.SupportsFileMode ? "true" : "false" }, + { "core.bare", "false" }, + { "core.logallrefupdates", "true" }, + { GitConfigSetting.CoreVirtualizeObjectsName, "true" }, + { GitConfigSetting.CoreVirtualFileSystemName, Paths.ConvertPathToGitFormat(GVFSConstants.DotGit.Hooks.VirtualFileSystemPath) }, + { "core.hookspath", expectedHooksPath }, + { GitConfigSetting.CredentialUseHttpPath, "true" }, + { "credential.validate", "false" }, + { "diff.autoRefreshIndex", "true" }, + { "feature.manyFiles", "false" }, + { "feature.experimental", "false" }, + { "fetch.writeCommitGraph", "false" }, + { "gc.auto", "0" }, + { "gui.gcwarning", "false" }, + { "index.threads", "true" }, + { "index.version", "4" }, + { "merge.stat", "false" }, + { "merge.renames", "false" }, + { "pack.useBitmaps", "false" }, + { "pack.useSparse", "true" }, + { "receive.autogc", "false" }, + { "reset.quiet", "true" }, + { "status.deserializePath", gitStatusCachePath }, + { "status.submoduleSummary", "false" }, + { "commitGraph.generationVersion", "1" }, + { "core.useBuiltinFSMonitor", "false" }, + }; + + GitProcess git = new GitProcess(this.enlistment); + + Dictionary existingConfigSettings; + if (!git.TryGetAllConfig(localOnly: true, configSettings: out existingConfigSettings)) + { + return false; + } + + foreach (KeyValuePair setting in requiredSettings) + { + GitConfigSetting existingSetting; + if (setting.Value != null) + { + if (!existingConfigSettings.TryGetValue(setting.Key, out existingSetting) || + !existingSetting.HasValue(setting.Value)) + { + GitProcess.Result setConfigResult = git.SetInLocalConfig(setting.Key, setting.Value); + if (setConfigResult.ExitCodeIsFailure) + { + return false; + } + } + } + else + { + if (existingConfigSettings.TryGetValue(setting.Key, out existingSetting)) + { + git.DeleteFromLocalConfig(setting.Key); + } + } + } + + return true; + } + + private void LogEnlistmentInfoAndSetConfigValues() + { + string mountId = Guid.NewGuid().ToString("N"); + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId); + metadata.Add(nameof(mountId), mountId); + metadata.Add("Enlistment", this.enlistment); + metadata.Add("PhysicalDiskInfo", GVFSPlatform.Instance.GetPhysicalDiskInfo(this.enlistment.WorkingDirectoryRoot, sizeStatsOnly: false)); + this.tracer.RelatedEvent(EventLevel.Informational, "EnlistmentInfo", metadata, Keywords.Telemetry); + + GitProcess git = new GitProcess(this.enlistment); + GitProcess.Result configResult = git.SetInLocalConfig(GVFSConstants.GitConfig.EnlistmentId, RepoMetadata.Instance.EnlistmentId, replaceAll: true); + if (configResult.ExitCodeIsFailure) + { + string error = "Could not update config with enlistment id, error: " + configResult.Errors; + this.tracer.RelatedWarning(error); + } + + configResult = git.SetInLocalConfig(GVFSConstants.GitConfig.MountId, mountId, replaceAll: true); + if (configResult.ExitCodeIsFailure) + { + string error = "Could not update config with mount id, error: " + configResult.Errors; + this.tracer.RelatedWarning(error); + } + } + private void UnmountAndStopWorkingDirectoryCallbacks(bool willRemountInSameProcess = false) { if (this.maintenanceScheduler != null) diff --git a/GVFS/GVFS/CommandLine/CacheServerVerb.cs b/GVFS/GVFS/CommandLine/CacheServerVerb.cs index 86754ae67..9fedad0b0 100644 --- a/GVFS/GVFS/CommandLine/CacheServerVerb.cs +++ b/GVFS/GVFS/CommandLine/CacheServerVerb.cs @@ -42,12 +42,6 @@ protected override void Execute(GVFSEnlistment enlistment) using (ITracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "CacheVerb")) { - string authErrorMessage; - if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) - { - this.ReportErrorAndExit(tracer, "Authentication failed: " + authErrorMessage); - } - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); ServerGVFSConfig serverGVFSConfig = null; string error = null; @@ -55,8 +49,12 @@ protected override void Execute(GVFSEnlistment enlistment) // Handle the three operation types: list, set, and get (default) if (this.ListCacheServers) { - // For listing, require config endpoint to succeed - serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); + // For listing, require config endpoint to succeed (no fallback) + if (!this.TryAuthenticateAndQueryGVFSConfig( + tracer, enlistment, retryConfig, out serverGVFSConfig, out error)) + { + this.ReportErrorAndExit(tracer, "Unable to query /gvfs/config" + Environment.NewLine + error); + } List cacheServers = serverGVFSConfig.CacheServers.ToList(); @@ -80,11 +78,12 @@ protected override void Execute(GVFSEnlistment enlistment) 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); + if (!this.TryAuthenticateAndQueryGVFSConfig( + tracer, enlistment, retryConfig, out serverGVFSConfig, out error, + fallbackCacheServer: cacheServer)) + { + this.ReportErrorAndExit(tracer, "Authentication failed: " + error); + } cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); @@ -101,11 +100,12 @@ protected override void Execute(GVFSEnlistment enlistment) 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); + if (!this.TryAuthenticateAndQueryGVFSConfig( + tracer, enlistment, retryConfig, out serverGVFSConfig, out error, + fallbackCacheServer: cacheServer)) + { + this.ReportErrorAndExit(tracer, "Authentication failed: " + error); + } CacheServerInfo resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverGVFSConfig); diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs index 8bbc5b9fb..bd37c7d4b 100644 --- a/GVFS/GVFS/CommandLine/CloneVerb.cs +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -185,19 +185,19 @@ public override void Execute() this.Output.WriteLine(" Local Cache: " + resolvedLocalCacheRoot); this.Output.WriteLine(" Destination: " + enlistment.EnlistmentRoot); - string authErrorMessage; - if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) - { - this.ReportErrorAndExit(tracer, "Cannot clone because authentication failed: " + authErrorMessage); - } - RetryConfig retryConfig = this.GetRetryConfig(tracer, enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); - serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + string authErrorMessage; + if (!this.TryAuthenticateAndQueryGVFSConfig( tracer, enlistment, retryConfig, - cacheServer); + out serverGVFSConfig, + out authErrorMessage, + fallbackCacheServer: cacheServer)) + { + this.ReportErrorAndExit(tracer, "Cannot clone because authentication failed: " + authErrorMessage); + } cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index fe0731a00..c2a4060d1 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -36,59 +36,6 @@ public GVFSVerb(bool validateOrigin = true) this.InitializeDefaultParameterValues(); } - [Flags] - private enum GitCoreGVFSFlags - { - // GVFS_SKIP_SHA_ON_INDEX - // Disables the calculation of the sha when writing the index - SkipShaOnIndex = 1 << 0, - - // GVFS_BLOCK_COMMANDS - // Blocks git commands that are not allowed in a GVFS/Scalar repo - BlockCommands = 1 << 1, - - // GVFS_MISSING_OK - // Normally git write-tree ensures that the objects referenced by the - // directory exist in the object database.This option disables this check. - MissingOk = 1 << 2, - - // GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT - // When marking entries to remove from the index and the working - // directory this option will take into account what the - // skip-worktree bit was set to so that if the entry has the - // skip-worktree bit set it will not be removed from the working - // directory. This will allow virtualized working directories to - // detect the change to HEAD and use the new commit tree to show - // the files that are in the working directory. - NoDeleteOutsideSparseCheckout = 1 << 3, - - // GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK - // While performing a fetch with a virtual file system we know - // that there will be missing objects and we don't want to download - // them just because of the reachability of the commits. We also - // don't want to download a pack file with commits, trees, and blobs - // since these will be downloaded on demand. This flag will skip the - // checks on the reachability of objects during a fetch as well as - // the upload pack so that extraneous objects don't get downloaded. - FetchSkipReachabilityAndUploadPack = 1 << 4, - - // 1 << 5 has been deprecated - - // GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS - // With a virtual file system we only know the file size before any - // CRLF or smudge/clean filters processing is done on the client. - // To prevent file corruption due to truncation or expansion with - // garbage at the end, these filters must not run when the file - // is first accessed and brought down to the client. Git.exe can't - // currently tell the first access vs subsequent accesses so this - // flag just blocks them from occurring at all. - BlockFiltersAndEolConversions = 1 << 6, - - // GVFS_PREFETCH_DURING_FETCH - // While performing a `git fetch` command, use the gvfs-helper to - // perform a "prefetch" of commits and trees. - PrefetchDuringFetch = 1 << 7, - } public abstract string EnlistmentRootPathParameter { get; set; } @@ -429,14 +376,50 @@ protected bool ShowStatusWhileRunning( protected bool TryAuthenticate(ITracer tracer, GVFSEnlistment enlistment, out string authErrorMessage) { - string authError = null; + return this.TryAuthenticateAndQueryGVFSConfig(tracer, enlistment, null, out _, out authErrorMessage); + } + + /// + /// Combines authentication and GVFS config query into a single operation, + /// eliminating a redundant HTTP round-trip. If + /// is null, a default RetryConfig is used. + /// If the config query fails but a valid + /// URL is available, auth succeeds but + /// will be null (caller should handle this gracefully). + /// + protected bool TryAuthenticateAndQueryGVFSConfig( + ITracer tracer, + GVFSEnlistment enlistment, + RetryConfig retryConfig, + out ServerGVFSConfig serverGVFSConfig, + out string errorMessage, + CacheServerInfo fallbackCacheServer = null) + { + ServerGVFSConfig config = null; + string error = null; bool result = this.ShowStatusWhileRunning( - () => enlistment.Authentication.TryInitialize(tracer, enlistment, out authError), + () => enlistment.Authentication.TryInitializeAndQueryGVFSConfig( + tracer, + enlistment, + retryConfig ?? new RetryConfig(), + out config, + out error), "Authenticating", enlistment.EnlistmentRoot); - authErrorMessage = authError; + if (!result && fallbackCacheServer != null && !string.IsNullOrWhiteSpace(fallbackCacheServer.Url)) + { + // Auth/config query failed, but we have a fallback cache server. + // Allow auth to succeed so mount/clone can proceed; config will be null. + tracer.RelatedWarning("Config query failed but continuing with fallback cache server: " + error); + serverGVFSConfig = null; + errorMessage = null; + return true; + } + + serverGVFSConfig = config; + errorMessage = error; return result; } @@ -493,50 +476,7 @@ 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 + // QueryGVFSConfig for callers that require config to succeed (no fallback) 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 5183ec434..312a9f1c6 100644 --- a/GVFS/GVFS/CommandLine/MountVerb.cs +++ b/GVFS/GVFS/CommandLine/MountVerb.cs @@ -1,15 +1,11 @@ using CommandLine; using GVFS.Common; -using GVFS.Common.FileSystem; -using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using GVFS.DiskLayoutUpgrades; -using GVFS.Virtualization.Projection; using System; using System.IO; -using System.Security.Principal; namespace GVFS.CommandLine { @@ -86,17 +82,11 @@ protected override void Execute(GVFSEnlistment enlistment) string mountExecutableLocation = null; using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "ExecuteMount")) { - PhysicalFileSystem fileSystem = new PhysicalFileSystem(); - GitRepo gitRepo = new GitRepo(tracer, enlistment, fileSystem); - GVFSContext context = new GVFSContext(tracer, fileSystem, gitRepo, enlistment); + // Validate these before handing them to the background process + // which cannot tell the user when they are bad + this.ValidateEnumArgs(); - if (!this.SkipInstallHooks && !HooksInstaller.InstallHooks(context, out errorMessage)) - { - this.ReportErrorAndExit("Error installing hooks: " + errorMessage); - } - - var resolvedCacheServer = this.ResolvedCacheServer; - var cacheServerFromConfig = resolvedCacheServer ?? CacheServerResolver.GetCacheServerFromConfig(enlistment); + CacheServerInfo cacheServerFromConfig = CacheServerResolver.GetCacheServerFromConfig(enlistment); tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.MountVerb), @@ -133,65 +123,11 @@ protected override void Execute(GVFSEnlistment enlistment) } } - RetryConfig retryConfig = null; - ServerGVFSConfig serverGVFSConfig = this.DownloadedGVFSConfig; - /* If resolved cache server was passed in, we've already checked server config and version check in previous operation. */ - if (resolvedCacheServer == null) + // Verify mount executable exists before launching + mountExecutableLocation = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), GVFSPlatform.Instance.Constants.MountExecutableName); + if (!File.Exists(mountExecutableLocation)) { - string authErrorMessage; - if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) - { - this.Output.WriteLine(" WARNING: " + authErrorMessage); - this.Output.WriteLine(" Mount will proceed, but new files cannot be accessed until GVFS can authenticate."); - } - - if (serverGVFSConfig == null) - { - if (retryConfig == null) - { - retryConfig = this.GetRetryConfig(tracer, enlistment); - } - - serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( - tracer, - enlistment, - retryConfig, - cacheServerFromConfig); - } - - this.ValidateClientVersions(tracer, enlistment, serverGVFSConfig, showWarnings: true); - - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); - resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerFromConfig.Url, serverGVFSConfig); - this.Output.WriteLine("Configured cache server: " + cacheServerFromConfig); - } - - this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig, resolvedCacheServer); - - if (!this.ShowStatusWhileRunning( - () => { return this.PerformPreMountValidation(tracer, enlistment, out mountExecutableLocation, out errorMessage); }, - "Validating repo")) - { - this.ReportErrorAndExit(tracer, errorMessage); - } - - if (!this.SkipVersionCheck) - { - string error; - if (!RepoMetadata.TryInitialize(tracer, enlistment.DotGVFSRoot, out error)) - { - this.ReportErrorAndExit(tracer, error); - } - - try - { - GitProcess git = new GitProcess(enlistment); - this.LogEnlistmentInfoAndSetConfigValues(tracer, git, enlistment); - } - finally - { - RepoMetadata.Shutdown(); - } + this.ReportErrorAndExit(tracer, $"Could not find {GVFSPlatform.Instance.Constants.MountExecutableName}. You may need to reinstall GVFS."); } if (!this.ShowStatusWhileRunning( @@ -220,62 +156,8 @@ protected override void Execute(GVFSEnlistment enlistment) } } - private bool PerformPreMountValidation(ITracer tracer, GVFSEnlistment enlistment, out string mountExecutableLocation, out string errorMessage) - { - errorMessage = string.Empty; - mountExecutableLocation = string.Empty; - - // We have to parse these parameters here to make sure they are valid before - // handing them to the background process which cannot tell the user when they are bad - EventLevel verbosity; - Keywords keywords; - this.ParseEnumArgs(out verbosity, out keywords); - - mountExecutableLocation = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), GVFSPlatform.Instance.Constants.MountExecutableName); - if (!File.Exists(mountExecutableLocation)) - { - errorMessage = $"Could not find {GVFSPlatform.Instance.Constants.MountExecutableName}. You may need to reinstall GVFS."; - return false; - } - - GitProcess git = new GitProcess(enlistment); - if (!git.IsValidRepo()) - { - errorMessage = "The .git folder is missing or has invalid contents"; - return false; - } - - try - { - GitIndexProjection.ReadIndex(tracer, Path.Combine(enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Index)); - } - catch (Exception e) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Exception", e.ToString()); - tracer.RelatedError(metadata, "Index validation failed"); - errorMessage = "Index validation failed, run 'gvfs repair' to repair index."; - - return false; - } - - if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(enlistment.EnlistmentRoot, out string error)) - { - errorMessage = $"FileSystem unsupported: {error}"; - return false; - } - - return true; - } - private bool TryMount(ITracer tracer, GVFSEnlistment enlistment, string mountExecutableLocation, out string errorMessage) { - if (!GVFSVerb.TrySetRequiredGitConfigSettings(enlistment)) - { - errorMessage = "Unable to configure git repo"; - return false; - } - const string ParamPrefix = "--"; tracer.RelatedInfo($"{nameof(this.TryMount)}: Launching background process('{mountExecutableLocation}') for {enlistment.EnlistmentRoot}"); @@ -355,14 +237,14 @@ private bool RegisterMount(GVFSEnlistment enlistment, out string errorMessage) } } - private void ParseEnumArgs(out EventLevel verbosity, out Keywords keywords) + private void ValidateEnumArgs() { - if (!Enum.TryParse(this.KeywordsCsv, out keywords)) + if (!Enum.TryParse(this.KeywordsCsv, out Keywords _)) { this.ReportErrorAndExit("Error: Invalid logging filter keywords: " + this.KeywordsCsv); } - if (!Enum.TryParse(this.Verbosity, out verbosity)) + if (!Enum.TryParse(this.Verbosity, out EventLevel _)) { this.ReportErrorAndExit("Error: Invalid logging verbosity: " + this.Verbosity); } diff --git a/GVFS/GVFS/CommandLine/PrefetchVerb.cs b/GVFS/GVFS/CommandLine/PrefetchVerb.cs index ab72b5e9f..1dd31b3b1 100644 --- a/GVFS/GVFS/CommandLine/PrefetchVerb.cs +++ b/GVFS/GVFS/CommandLine/PrefetchVerb.cs @@ -243,23 +243,23 @@ private void InitializeServerConnection( // 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)) - { - this.ReportErrorAndExit(tracer, "Unable to prefetch because authentication failed: " + authErrorMessage); - } - - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); - if (serverGVFSConfig == null) { - serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + string authErrorMessage; + if (!this.TryAuthenticateAndQueryGVFSConfig( tracer, enlistment, retryConfig, - cacheServerFromConfig); + out serverGVFSConfig, + out authErrorMessage, + fallbackCacheServer: cacheServerFromConfig)) + { + this.ReportErrorAndExit(tracer, "Unable to prefetch because authentication failed: " + authErrorMessage); + } } + CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); + resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerFromConfig.Url, serverGVFSConfig); if (!this.SkipVersionCheck)