Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions GVFS/GVFS.Common/Git/GitProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -577,22 +577,20 @@ public bool TryGetAllConfig(bool localOnly, out Dictionary<string, GitConfigSett
/// <returns>The value found for the setting.</returns>
public virtual ConfigResult GetFromConfig(string settingName, bool forceOutsideEnlistment = false, PhysicalFileSystem fileSystem = null)
{
string command = string.Format("config {0}", settingName);
fileSystem = fileSystem ?? new PhysicalFileSystem();

// This method is called at clone time, so the physical repo may not exist yet.
// Pre-command hook never applies to `git config` reads (no working tree
// mutation happens), and skipping the hook makes us robust to a broken
// hook config in the enlistment - which is exactly what we'd be trying
// to repair via TryUpdateHooks at mount time.
Result result =
fileSystem.DirectoryExists(this.workingDirectoryRoot) && !forceOutsideEnlistment
? this.InvokeConfigRead(settingName, () => this.InvokeGitAgainstDotGitFolder(command, usePreCommandHook: false))
: this.InvokeConfigRead(settingName, () => this.InvokeGitOutsideEnlistment(command));

Result result = this.ReadConfig(settingName, string.Format("config {0}", settingName), forceOutsideEnlistment, fileSystem);
return new ConfigResult(result, settingName);
}

/// <summary>
/// Reads a boolean config value. <paramref name="value"/> is null when the key is unset.
/// Matches <see cref="LibGit2Repo.TryReadConfigBoolWithGit"/>.
/// </summary>
public bool TryGetBoolFromConfig(string settingName, out bool? value, out string error)
{
Result result = this.ReadConfig(settingName, LibGit2Repo.GetConfigBoolArguments(settingName), forceOutsideEnlistment: false, fileSystem: null);
return LibGit2Repo.TryParseConfigBoolOutput(settingName, result.ExitCode, result.Output, result.Errors, out value, out error);
}

public ConfigResult GetFromLocalConfig(string settingName)
{
// See GetFromConfig above for why pre-command hook is disabled here.
Expand Down Expand Up @@ -1293,6 +1291,21 @@ private static bool IsCredentialConfigReadFailure(Result result)
return configReadFailed && transientAccessFailure;
}

private Result ReadConfig(string settingName, string command, bool forceOutsideEnlistment, PhysicalFileSystem fileSystem)
{
fileSystem = fileSystem ?? new PhysicalFileSystem();

// This method is called at clone time, so the physical repo may not exist yet.
// Pre-command hook never applies to `git config` reads (no working tree
// mutation happens), and skipping the hook makes us robust to a broken
// hook config in the enlistment - which is exactly what we'd be trying
// to repair via TryUpdateHooks at mount time.
return
fileSystem.DirectoryExists(this.workingDirectoryRoot) && !forceOutsideEnlistment
? this.InvokeConfigRead(settingName, () => this.InvokeGitAgainstDotGitFolder(command, usePreCommandHook: false))
: this.InvokeConfigRead(settingName, () => this.InvokeGitOutsideEnlistment(command));
}

private Result InvokeConfigRead(string configName, Func<Result> readConfig)
{
return GitConfigReadRetry.InvokeResult(
Expand Down
8 changes: 7 additions & 1 deletion GVFS/GVFS.Common/Git/GitRepo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public GitRepo(ITracer tracer, Enlistment enlistment, PhysicalFileSystem fileSys

this.libgit2RepoInvoker = new LibGit2RepoInvoker(
tracer,
repoFactory ?? (() => new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot)));
repoFactory ?? (() => new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot)),
this.TryReadConfigBoolWithGit);
}

// For Unit Testing
Expand Down Expand Up @@ -224,6 +225,11 @@ public void Dispose()
}
}

private bool TryReadConfigBoolWithGit(string key, out bool? value, out string error)
{
return this.enlistment.CreateGitProcess(this.tracer).TryGetBoolFromConfig(key, out value, out error);
}

private static bool ReadLooseObjectHeader(Stream input, out long size)
{
size = 0;
Expand Down
93 changes: 92 additions & 1 deletion GVFS/GVFS.Common/Git/LibGit2Repo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ public class LibGit2Repo : IDisposable

public delegate void MultiVarConfigCallback(string value);

/// <summary>
/// Reads a boolean config value with git.exe. <paramref name="value"/> is null when the key
/// is unset. Returns false and sets <paramref name="error"/> when the read fails.
/// </summary>
public delegate bool TryReadConfigBoolWithGit(string key, out bool? value, out string error);

public LibGit2Repo(ITracer tracer, string repoPath)
: this(tracer, repoPath, GitConfigReadRetry.Delay, GitConfigReadRetry.MaxAttempts)
{
Expand Down Expand Up @@ -634,8 +640,35 @@ public bool GetConfigBoolOrDefault(string key, bool defaultValue)
/// reason. <paramref name="configReadMaxAttempts"/> bounds the transient-failure retries
/// for latency-sensitive callers.
/// </summary>
public static bool GetConfigBoolOrDefault(ITracer tracer, string repoPath, string key, bool defaultValue, int configReadMaxAttempts = GitConfigReadRetry.MaxAttempts)
/// <param name="readBoolWithGit">
/// Reads the key with git.exe. This method uses it instead of libgit2 when
/// <see cref="EnvironmentChangesGitConfig()"/> is true.
/// </param>
public static bool GetConfigBoolOrDefault(
ITracer tracer,
string repoPath,
string key,
bool defaultValue,
TryReadConfigBoolWithGit readBoolWithGit,
int configReadMaxAttempts = GitConfigReadRetry.MaxAttempts)
{
return GetConfigBoolOrDefault(tracer, repoPath, key, defaultValue, readBoolWithGit, Environment.GetEnvironmentVariables(), configReadMaxAttempts);
}

internal static bool GetConfigBoolOrDefault(
ITracer tracer,
string repoPath,
string key,
bool defaultValue,
TryReadConfigBoolWithGit readBoolWithGit,
IDictionary environment,
int configReadMaxAttempts = GitConfigReadRetry.MaxAttempts)
{
if (EnvironmentChangesGitConfig(environment))
{
return GetConfigBoolWithGitOrDefault(tracer, key, defaultValue, readBoolWithGit);
}

try
{
using (LibGit2Repo repo = new LibGit2Repo(tracer, repoPath, GitConfigReadRetry.Delay, configReadMaxAttempts))
Expand All @@ -656,6 +689,64 @@ public static bool GetConfigBoolOrDefault(ITracer tracer, string repoPath, strin
}
}

/// <summary>
/// Reads a boolean config value with git.exe through <paramref name="readBoolWithGit"/>.
/// Falls back to <paramref name="defaultValue"/> if the key is unset or the read fails.
/// </summary>
internal static bool GetConfigBoolWithGitOrDefault(ITracer tracer, string key, bool defaultValue, TryReadConfigBoolWithGit readBoolWithGit)
{
tracer = tracer ?? NullTracer.Instance;

try
{
if (readBoolWithGit(key, out bool? value, out string error))
{
return value ?? defaultValue;
}

tracer.RelatedWarning($"Failed to read {key} config, using default: {error}");
}
catch (Exception e)
{
tracer.RelatedWarning($"Failed to read {key} config, using default: {e.Message}");
}

return defaultValue;
}

/// <summary>
/// The git.exe arguments that read <paramref name="key"/> for <see cref="TryParseConfigBoolOutput"/>.
/// </summary>
internal static string GetConfigBoolArguments(string key)
{
return "config --type=bool " + key;
}

/// <summary>
/// Parses the result of <c>git config --type=bool</c>. Git normalizes a boolean to "true" or
/// "false", exits with 1 when the key is unset, and fails when the value is not a boolean.
/// </summary>
internal static bool TryParseConfigBoolOutput(string key, int exitCode, string output, string errors, out bool? value, out string error)
{
value = null;
error = null;

if (exitCode == 1 && string.IsNullOrWhiteSpace(errors))
{
return true;
}

string trimmedOutput = output?.Trim();
if (exitCode != 0 || !bool.TryParse(trimmedOutput, out bool parsed))
{
error = $"Error while reading '{key}' from config: exit code {exitCode}, output '{trimmedOutput}', errors '{errors?.Trim()}'";
return false;
}

value = parsed;
return true;
}

/// <summary>
/// Reads a string config value as git.exe would, without loading the object store. Reads
/// the repo at <paramref name="repoPath"/>, or the global, XDG, system, and ProgramData
Expand Down
21 changes: 21 additions & 0 deletions GVFS/GVFS.Common/Git/LibGit2RepoInvoker.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using GVFS.Common.Tracing;
using System;
using System.Collections;
using System.Threading;

namespace GVFS.Common.Git
{
public class LibGit2RepoInvoker : IDisposable
{
private readonly Func<LibGit2Repo> createRepo;
private readonly LibGit2Repo.TryReadConfigBoolWithGit readBoolWithGit;
private readonly IDictionary environment;
private readonly ITracer tracer;
private readonly Lock sharedRepoLock = new Lock();
private volatile bool disposing;
Expand All @@ -19,9 +22,21 @@ public LibGit2RepoInvoker(ITracer tracer, string repoPath)
}

public LibGit2RepoInvoker(ITracer tracer, Func<LibGit2Repo> createRepo)
: this(tracer, createRepo, readBoolWithGit: null)
{
}

/// <param name="readBoolWithGit">
/// Reads a key with git.exe. <see cref="GetConfigBoolOrDefault"/> uses it instead of libgit2
/// when <see cref="LibGit2Repo.EnvironmentChangesGitConfig()"/> is true.
/// </param>
/// <param name="environment">The environment to check. Null selects the process environment.</param>
public LibGit2RepoInvoker(ITracer tracer, Func<LibGit2Repo> createRepo, LibGit2Repo.TryReadConfigBoolWithGit readBoolWithGit, IDictionary environment = null)
{
this.tracer = tracer;
this.createRepo = createRepo;
this.readBoolWithGit = readBoolWithGit;
this.environment = environment;

this.InitializeSharedRepo();
}
Expand Down Expand Up @@ -89,6 +104,12 @@ public void InitializeSharedRepo()

public bool GetConfigBoolOrDefault(string key, bool defaultValue)
{
if (this.readBoolWithGit != null &&
LibGit2Repo.EnvironmentChangesGitConfig(this.environment ?? Environment.GetEnvironmentVariables()))
{
return LibGit2Repo.GetConfigBoolWithGitOrDefault(this.tracer, key, defaultValue, this.readBoolWithGit);
}

bool? value = defaultValue;
if (this.TryInvoke(repo => repo.GetConfigBool(key), out value))
{
Expand Down
17 changes: 17 additions & 0 deletions GVFS/GVFS.Hooks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,26 @@ private static bool ConfigurationAllowsHydrationStatus()
normalizedCurrentDirectory,
GVFSConstants.GitConfig.ShowHydrationStatus,
GVFSConstants.GitConfig.ShowHydrationStatusDefault,
TryReadBoolConfigWithGit,
ConfigReadMaxAttempts);
}

private static bool TryReadBoolConfigWithGit(string key, out bool? value, out string error)
{
// Git runs this hook, so the child git.exe reads the config of the same repository.
ProcessResult result = GitConfigReadRetry.InvokeResult(
key,
() => ProcessHelper.Run("git", LibGit2Repo.GetConfigBoolArguments(key)),
(ProcessResult processResult) =>
processResult.ExitCode != 0 &&
GitConfigReadRetry.IsTransientFileAccessError(processResult.Errors, lineMustContain: "unable to access"),
NullTracer.Instance,
GitConfigReadRetry.Delay,
ConfigReadMaxAttempts);

return LibGit2Repo.TryParseConfigBoolOutput(key, result.ExitCode, result.Output, result.Errors, out value, out error);
}

/// <summary>
/// Query the mount process for the cached hydration summary via named pipe.
/// The entire operation (connect + send + receive + parse) is bounded to
Expand Down
21 changes: 8 additions & 13 deletions GVFS/GVFS.Mount/InProcessMount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,8 @@ private bool IsBackgroundCacheAuthEnabled()
this.tracer,
this.enlistment.WorkingDirectoryBackingRoot,
GVFSConstants.GitConfig.BackgroundCacheAuth,
GVFSConstants.GitConfig.BackgroundCacheAuthDefault);
GVFSConstants.GitConfig.BackgroundCacheAuthDefault,
this.enlistment.CreateGitProcess(this.tracer).TryGetBoolFromConfig);
}

private void ValidateMountPoints()
Expand Down Expand Up @@ -629,18 +630,12 @@ private bool ShouldReportMountProgress()
// pipe starts), and its constructor force-loads the object store, which we must
// not do on the pre-pipe critical path. A raw LibGit2Repo opens only the repo
// handle + config (no object store) and gives native git-bool parsing.
try
{
using (LibGit2Repo repo = new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot))
{
return repo.GetConfigBool(GVFSConstants.GitConfig.MountProgress) ?? GVFSConstants.GitConfig.MountProgressDefault;
}
}
catch (Exception e)
{
this.tracer.RelatedWarning($"Failed to read {GVFSConstants.GitConfig.MountProgress} config, using default: {e.Message}");
return GVFSConstants.GitConfig.MountProgressDefault;
}
return LibGit2Repo.GetConfigBoolOrDefault(
this.tracer,
this.enlistment.WorkingDirectoryBackingRoot,
GVFSConstants.GitConfig.MountProgress,
GVFSConstants.GitConfig.MountProgressDefault,
this.enlistment.CreateGitProcess(this.tracer).TryGetBoolFromConfig);
}

private void FailMountAndExit(string error, params object[] args)
Expand Down
Loading
Loading