Skip to content
Merged
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
22 changes: 22 additions & 0 deletions GVFS/GVFS.Common/FileSystem/HooksInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,28 @@ public static bool TryUpdateHooks(GVFSContext context, out string errorMessage)
return false;
}

// Refresh the corresponding .hooks text files. These hold the
// absolute path of GVFS.Hooks.exe that the loader execs at hook
// time, and were originally written at clone time pointing at
// wherever GVFS was installed back then. If GVFS has moved
// (system-to-user migration, version-junction swap, hand-edited
// install), those paths go stale and the loader exits non-zero
// on every git invocation that fires a hook - making the
// enlistment unrecoverable through normal mount. Refreshing on
// every mount makes us self-healing against install-location
// drift, and is a no-op when paths are already current.
string precommandBasePath = Path.Combine(context.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Hooks.PreCommandPath);
if (!GVFSPlatform.Instance.TryInstallGitCommandHooks(context, ExecutingDirectory, GVFSConstants.DotGit.Hooks.PreCommandHookName, precommandBasePath, out errorMessage))
{
return false;
}

string postcommandBasePath = Path.Combine(context.Enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Hooks.PostCommandPath);
if (!GVFSPlatform.Instance.TryInstallGitCommandHooks(context, ExecutingDirectory, GVFSConstants.DotGit.Hooks.PostCommandHookName, postcommandBasePath, out errorMessage))
{
return false;
}

return true;
}

Expand Down
66 changes: 47 additions & 19 deletions GVFS/GVFS.Common/Git/GitProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ public virtual bool TryDeleteCredential(ITracer tracer, string repoUrl, string u
Result result = this.InvokeGitAgainstDotGitFolder(
GenerateCredentialVerbCommand("reject"),
stdin => stdin.Write(stdinConfig),
null);
null,
usePreCommandHook: false);

if (result.ExitCodeIsFailure)
{
Expand All @@ -218,7 +219,8 @@ public virtual bool TryStoreCredential(ITracer tracer, string repoUrl, string us
Result result = this.InvokeGitAgainstDotGitFolder(
GenerateCredentialVerbCommand("approve"),
stdin => stdin.Write(stdinConfig),
null);
null,
usePreCommandHook: false);

if (result.ExitCodeIsFailure)
{
Expand Down Expand Up @@ -249,10 +251,13 @@ public virtual bool TryGetCertificatePassword(

using (ITracer activity = tracer.StartActivity("TryGetCertificatePassword", EventLevel.Informational))
{
// See GetFromConfig for why pre-command hook is disabled
// for bootstrap-time git operations.
Result gitCredentialOutput = this.InvokeGitAgainstDotGitFolder(
"credential fill",
stdin => stdin.Write("protocol=cert\npath=" + certificatePath + "\nusername=\n\n"),
parseStdOutLine: null);
parseStdOutLine: null,
usePreCommandHook: false);

if (gitCredentialOutput.ExitCodeIsFailure)
{
Expand Down Expand Up @@ -300,10 +305,13 @@ public virtual bool TryGetCredential(

using (ITracer activity = tracer.StartActivity(nameof(this.TryGetCredential), EventLevel.Informational))
{
// See GetFromConfig for why pre-command hook is disabled
// for bootstrap-time git operations.
Result gitCredentialOutput = this.InvokeGitAgainstDotGitFolder(
GenerateCredentialVerbCommand("fill"),
stdin => stdin.Write($"url={repoUrl}\n\n"),
parseStdOutLine: null);
parseStdOutLine: null,
usePreCommandHook: false);

if (gitCredentialOutput.ExitCodeIsFailure)
{
Expand Down Expand Up @@ -336,7 +344,10 @@ public virtual bool TryGetCredential(

public bool IsValidRepo()
{
Result result = this.InvokeGitAgainstDotGitFolder("rev-parse --show-toplevel");
// Mount-time bootstrap check - skip pre-command hook so a broken
// hook config in the enlistment can be detected and repaired
// rather than blocking the mount that would fix it.
Result result = this.InvokeGitAgainstDotGitFolder("rev-parse --show-toplevel", usePreCommandHook: false);
return result.ExitCodeIsSuccess;
}

Expand All @@ -352,24 +363,34 @@ public Result GetCurrentBranchName()

public void DeleteFromLocalConfig(string settingName)
{
this.InvokeGitAgainstDotGitFolder("config --local --unset-all " + settingName);
// git config operations never need the pre-command hook (no
// working-tree mutation). Skipping it also keeps mount bootstrap
// robust against a stale hook config that TryUpdateHooks will
// repair shortly. See GetFromConfig for the longer rationale.
this.InvokeGitAgainstDotGitFolder("config --local --unset-all " + settingName, usePreCommandHook: false);
}

public Result SetInLocalConfig(string settingName, string value, bool replaceAll = false)
{
return this.InvokeGitAgainstDotGitFolder(string.Format(
"config --local {0} \"{1}\" \"{2}\"",
replaceAll ? "--replace-all " : string.Empty,
settingName,
value));
// See DeleteFromLocalConfig for why pre-command hook is disabled.
return this.InvokeGitAgainstDotGitFolder(
string.Format(
"config --local {0} \"{1}\" \"{2}\"",
replaceAll ? "--replace-all " : string.Empty,
settingName,
value),
usePreCommandHook: false);
}

public Result AddInLocalConfig(string settingName, string value)
{
return this.InvokeGitAgainstDotGitFolder(string.Format(
"config --local --add {0} {1}",
settingName,
value));
// See DeleteFromLocalConfig for why pre-command hook is disabled.
return this.InvokeGitAgainstDotGitFolder(
string.Format(
"config --local --add {0} {1}",
settingName,
value),
usePreCommandHook: false);
}

public Result SetInFileConfig(string configFile, string settingName, string value, bool replaceAll = false)
Expand All @@ -384,7 +405,8 @@ public Result SetInFileConfig(string configFile, string settingName, string valu

public bool TryGetConfigUrlMatch(string section, string repositoryUrl, out Dictionary<string, GitConfigSetting> configSettings)
{
Result result = this.InvokeGitAgainstDotGitFolder($"config --get-urlmatch {section} {repositoryUrl}");
// See GetFromConfig for why pre-command hook is disabled.
Result result = this.InvokeGitAgainstDotGitFolder($"config --get-urlmatch {section} {repositoryUrl}", usePreCommandHook: false);
if (result.ExitCodeIsFailure)
{
configSettings = null;
Expand All @@ -399,7 +421,8 @@ public bool TryGetAllConfig(bool localOnly, out Dictionary<string, GitConfigSett
{
configSettings = null;
string localParameter = localOnly ? "--local" : string.Empty;
ConfigResult result = new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --list " + localParameter), "--list");
// See GetFromConfig for why pre-command hook is disabled.
ConfigResult result = new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --list " + localParameter, usePreCommandHook: false), "--list");

if (result.TryParseAsString(out string output, out string _, string.Empty))
{
Expand All @@ -425,15 +448,20 @@ public virtual ConfigResult GetFromConfig(string settingName, bool forceOutsideE
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
? new ConfigResult(this.InvokeGitAgainstDotGitFolder(command), settingName)
? new ConfigResult(this.InvokeGitAgainstDotGitFolder(command, usePreCommandHook: false), settingName)
: new ConfigResult(this.InvokeGitOutsideEnlistment(command), settingName);
}

public ConfigResult GetFromLocalConfig(string settingName)
{
return new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --local " + settingName), settingName);
// See GetFromConfig above for why pre-command hook is disabled here.
return new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --local " + settingName, usePreCommandHook: false), settingName);
}

/// <summary>
Expand Down
21 changes: 19 additions & 2 deletions GVFS/GVFS.Common/Git/GitRepo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ public virtual bool ObjectExists(string blobSha)
return output;
}

/// <summary>
/// Checks whether the object can be fully parsed by libgit2 (not just that it exists).
/// Use this to detect corrupt objects. For simple existence checks,
/// prefer <see cref="ObjectExists"/> which is faster.
/// </summary>
public virtual bool ObjectCanBeParsed(string sha)
{
bool output = false;
this.libgit2RepoInvoker.TryInvoke(repo => repo.ObjectCanBeParsed(sha), out output);
return output;
}

/// <summary>
/// Try to find the size of a given blob by SHA1 hash.
///
Expand Down Expand Up @@ -153,8 +165,13 @@ private static bool ReadLooseObjectHeader(Stream input, out long size)
size = 0;

byte[] buffer = new byte[5];
input.Read(buffer, 0, buffer.Length);
if (!Enumerable.SequenceEqual(buffer, LooseBlobHeader))

// Verify bytesRead instead of using ReadExactly: a truncated header must
// return false (Corrupt) so the caller quarantines the file, rather than
// throwing EndOfStreamException which would be caught as IOException
// (Unknown) and skip quarantine.
int bytesRead = input.Read(buffer, 0, buffer.Length);
if (bytesRead < buffer.Length || !Enumerable.SequenceEqual(buffer, LooseBlobHeader))
{
return false;
}
Expand Down
73 changes: 73 additions & 0 deletions GVFS/GVFS.Common/Git/LibGit2Repo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;

namespace GVFS.Common.Git
{
public class LibGit2Repo : IDisposable
{
private bool disposedValue = false;
private IntPtr odbHandle = IntPtr.Zero;

public delegate void MultiVarConfigCallback(string value);

Expand Down Expand Up @@ -104,6 +106,55 @@ public virtual bool CommitAndRootTreeExists(string commitish, out string treeSha
}

public virtual bool ObjectExists(string sha)
{
IntPtr odb = this.odbHandle;
if (odb == IntPtr.Zero)
{
if (Native.Odb.GetOdb(out IntPtr newOdb, this.RepoHandle) != Native.ResultCode.Success)
{
return this.ObjectExistsFallback(sha);
}

IntPtr existing = Interlocked.CompareExchange(ref this.odbHandle, newOdb, IntPtr.Zero);
if (existing != IntPtr.Zero)
{
// Another thread won the race — free our duplicate and use theirs
Native.Odb.Free(newOdb);
odb = existing;
}
else
{
odb = newOdb;
}
}

GitOid oid;
if (Native.Odb.OidFromStr(out oid, sha) != Native.ResultCode.Success)
{
return false;
}

return Native.Odb.Exists(odb, ref oid) == 1;
}

private bool ObjectExistsFallback(string sha)
{
IntPtr objHandle;
if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.ResultCode.Success)
{
return false;
}

Native.Object.Free(objHandle);
return true;
}

/// <summary>
/// Checks whether the object can be fully parsed by libgit2 (not just that it exists).
/// Use this when you need to detect corrupt objects. For simple existence checks,
/// prefer <see cref="ObjectExists"/> which is faster.
/// </summary>
public virtual bool ObjectCanBeParsed(string sha)
{
IntPtr objHandle;
if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.ResultCode.Success)
Expand Down Expand Up @@ -360,6 +411,12 @@ protected virtual void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (this.odbHandle != IntPtr.Zero)
{
Native.Odb.Free(this.odbHandle);
this.odbHandle = IntPtr.Zero;
}

Native.Repo.Free(this.RepoHandle);
Native.Shutdown();
this.disposedValue = true;
Expand Down Expand Up @@ -504,6 +561,22 @@ public static class Repo
public static extern void Free(IntPtr repoHandle);
}

public static class Odb
{
[DllImport(Git2NativeLibName, EntryPoint = "git_repository_odb")]
public static extern ResultCode GetOdb(out IntPtr odbHandle, IntPtr repoHandle);

/// <returns>1 if the object exists, 0 otherwise</returns>
[DllImport(Git2NativeLibName, EntryPoint = "git_odb_exists")]
public static extern int Exists(IntPtr odbHandle, ref GitOid id);

[DllImport(Git2NativeLibName, EntryPoint = "git_odb_free")]
public static extern void Free(IntPtr odbHandle);

[DllImport(Git2NativeLibName, EntryPoint = "git_oid_fromstr")]
public static extern ResultCode OidFromStr(out GitOid oid, string str);
}

public static class Config
{
[DllImport(Git2NativeLibName, EntryPoint = "git_repository_config")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ internal static int GetIndexFileCount(GVFSEnlistment enlistment, PhysicalFileSys
* the 4 bytes at offsets 8-11 of the index file. */
indexFile.Position = 8;
var bytes = new byte[4];
indexFile.Read(
indexFile.ReadExactly(
bytes, // Destination buffer
offset: 0, // Offset in destination buffer, not in indexFile
count: 4);
Expand Down
2 changes: 1 addition & 1 deletion GVFS/GVFS.Common/Maintenance/LooseObjectsStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public void ClearCorruptLooseObjects(EventMetadata metadata)
// may be more bad objects in the next batch after deleting the corrupt objects.
foreach (string objectId in this.GetBatchOfLooseObjects(2 * this.MaxLooseObjectsInPack))
{
if (!this.Context.Repository.ObjectExists(objectId))
if (!this.Context.Repository.ObjectCanBeParsed(objectId))
{
string objectFile = this.GetLooseObjectFileName(objectId);

Expand Down
5 changes: 4 additions & 1 deletion GVFS/GVFS.Common/Prefetch/BlobPrefetcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,10 @@ protected void DownloadMissingCommit(string commitSha, GitObjects gitObjects)
{
using (LibGit2Repo repo = new LibGit2Repo(this.Tracer, this.Enlistment.WorkingDirectoryBackingRoot))
{
if (!repo.ObjectExists(commitSha))
// Use ObjectCanBeParsed (revparse) rather than ObjectExists (odb_exists)
// because commitSha may be a ref name or abbreviated SHA from CLI input
// (e.g. FastFetch --commit), not necessarily a full 40-char hex SHA.
if (!repo.ObjectCanBeParsed(commitSha))
{
if (!gitObjects.TryDownloadCommit(commitSha))
{
Expand Down
2 changes: 1 addition & 1 deletion GVFS/GVFS.UnitTests/Mock/ReusableMemoryStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public string ReadAt(long position, long length)
this.Position = position;

byte[] bytes = new byte[length];
this.Read(bytes, 0, (int)length);
this.ReadExactly(bytes, 0, (int)length);

this.Position = lastPosition;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,11 @@ private FileSystemTaskResult ParseIndex(

private void ReadNextPage()
{
// Last page may be smaller than PageSize; partial fill is safe because
// the parser stops after entryCount entries and never reads stale bytes.
#pragma warning disable CA2022 // Avoid inexact read
this.indexStream.Read(this.page, 0, PageSize);
#pragma warning restore CA2022
this.nextByteIndex = 0;
}

Expand Down
Loading