Skip to content
Open
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
204 changes: 199 additions & 5 deletions GVFS/GVFS.Common/Git/LibGit2Repo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,35 @@ public class LibGit2Repo : IDisposable
{
private bool disposedValue = false;

public delegate void MultiVarConfigCallback(string value);

public LibGit2Repo(ITracer tracer, string repoPath)
{
this.Tracer = tracer;

Native.Init();
InitNative();

IntPtr repoHandle;
if (Native.Repo.Open(out repoHandle, repoPath) != Native.ResultCode.Success)
if (TryOpenRepo(repoPath, out repoHandle) != Native.ResultCode.Success)
{
string reason = Native.GetLastError();
string reason = GetLastNativeError();
string message = "Couldn't open repo at " + repoPath + ": " + reason;
tracer.RelatedWarning(message);

Native.Shutdown();
throw new InvalidDataException(message);
if (!reason.EndsWith(" is not owned by current user")
|| !CheckSafeDirectoryConfigForCaseSensitivityIssue(tracer, repoPath, out repoHandle))
{
ShutdownNative();
throw new InvalidDataException(message);
}
}

this.RepoHandle = repoHandle;
}

protected LibGit2Repo()
{
this.Tracer = NullTracer.Instance;
}

~LibGit2Repo()
Expand Down Expand Up @@ -246,7 +253,64 @@ public virtual string GetConfigString(string name)
{
Native.Config.Free(configHandle);
}
}

public void ForEachMultiVarConfig(string key, MultiVarConfigCallback callback)
{
if (Native.Config.GetConfig(out IntPtr configHandle, this.RepoHandle) != Native.ResultCode.Success)
{
throw new LibGit2Exception($"Failed to get config handle: {Native.GetLastError()}");
}
try
{
ForEachMultiVarConfig(configHandle, key, callback);
}
finally
{
Native.Config.Free(configHandle);
}
}

public static void ForEachMultiVarConfigInGlobalAndSystemConfig(string key, MultiVarConfigCallback callback)
{
if (Native.Config.GetGlobalAndSystemConfig(out IntPtr configHandle) != Native.ResultCode.Success)
{
throw new LibGit2Exception($"Failed to get global and system config handle: {Native.GetLastError()}");
}
try
{
ForEachMultiVarConfig(configHandle, key, callback);
}
finally
{
Native.Config.Free(configHandle);
}
}

private static void ForEachMultiVarConfig(IntPtr configHandle, string key, MultiVarConfigCallback callback)
{
Native.Config.GitConfigMultivarCallback nativeCallback = (entryPtr, payload) =>
{
try
{
var entry = Marshal.PtrToStructure<Native.Config.GitConfigEntry>(entryPtr);
callback(entry.GetValue());
}
catch (Exception)
{
return Native.ResultCode.Failure;
}
return 0;
};
if (Native.Config.GetMultivarForeach(
configHandle,
key,
regex:"",
nativeCallback,
IntPtr.Zero) != Native.ResultCode.Success)
{
throw new LibGit2Exception($"Failed to get multivar config for '{key}': {Native.GetLastError()}");
}
}

/// <summary>
Expand Down Expand Up @@ -302,11 +366,86 @@ protected virtual void Dispose(bool disposing)
}
}

/// <summary>
/// Normalize a path for case-insensitive safe.directory comparison:
/// replace backslashes with forward slashes, convert to upper-case,
/// and trim trailing slashes.
/// </summary>
internal static string NormalizePathForSafeDirectoryComparison(string path)
{
if (string.IsNullOrEmpty(path))
{
return path;
}

string normalized = path.Replace('\\', '/').ToUpperInvariant();
return normalized.TrimEnd('/');
}

/// <summary>
/// Retrieve all configured safe.directory values from global and system git config.
/// Virtual so tests can provide fake entries without touching real config.
/// </summary>
protected virtual void GetSafeDirectoryConfigEntries(MultiVarConfigCallback callback)
{
ForEachMultiVarConfigInGlobalAndSystemConfig("safe.directory", callback);
}

/// <summary>
/// Try to open a repository at the given path. Virtual so tests can
/// avoid the native P/Invoke call.
/// </summary>
protected virtual Native.ResultCode TryOpenRepo(string path, out IntPtr repoHandle)
{
return Native.Repo.Open(out repoHandle, path);
}

protected virtual void InitNative()
{
Native.Init();
}

protected virtual void ShutdownNative()
{
Native.Shutdown();
}

protected virtual string GetLastNativeError()
{
return Native.GetLastError();
}

protected bool CheckSafeDirectoryConfigForCaseSensitivityIssue(ITracer tracer, string repoPath, out IntPtr repoHandle)
{
/* Libgit2 has a bug where it is case sensitive for safe.directory (especially the
* drive letter) when git.exe isn't. Until a fix can be made and propagated, work
* around it by matching the repo path we request to the configured safe directory.
*
* See https://github.com/libgit2/libgit2/issues/7037
*/
repoHandle = IntPtr.Zero;

string normalizedRequestedPath = NormalizePathForSafeDirectoryComparison(repoPath);

string configuredMatchingDirectory = null;
GetSafeDirectoryConfigEntries((string value) =>
{
string normalizedConfiguredPath = NormalizePathForSafeDirectoryComparison(value);
if (normalizedConfiguredPath == normalizedRequestedPath)
{
configuredMatchingDirectory = value;
}
});

return configuredMatchingDirectory != null && TryOpenRepo(configuredMatchingDirectory, out repoHandle) == Native.ResultCode.Success;
}

public static class Native
{
public enum ResultCode : int
{
Success = 0,
Failure = -1,
NotFound = -3,
}

Expand Down Expand Up @@ -370,9 +509,64 @@ public static class Config
[DllImport(Git2NativeLibName, EntryPoint = "git_repository_config")]
public static extern ResultCode GetConfig(out IntPtr configHandle, IntPtr repoHandle);

[DllImport(Git2NativeLibName, EntryPoint = "git_config_open_default")]
public static extern ResultCode GetGlobalAndSystemConfig(out IntPtr configHandle);

[DllImport(Git2NativeLibName, EntryPoint = "git_config_get_string")]
public static extern ResultCode GetString(out string value, IntPtr configHandle, string name);

[DllImport(Git2NativeLibName, EntryPoint = "git_config_get_multivar_foreach")]
public static extern ResultCode GetMultivarForeach(
IntPtr configHandle,
string name,
string regex,
GitConfigMultivarCallback callback,
IntPtr payload);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate ResultCode GitConfigMultivarCallback(
IntPtr entryPtr,
IntPtr payload);

[StructLayout(LayoutKind.Sequential)]
public struct GitConfigEntry
{
public IntPtr Name;
public IntPtr Value;
public IntPtr BackendType;
public IntPtr OriginPath;
public uint IncludeDepth;
public int Level;

public string GetValue()
{
return Value != IntPtr.Zero ? MarshalUtf8String(Value) : null;
}

public string GetName()
{
return Name != IntPtr.Zero ? MarshalUtf8String(Name) : null;
}

private static string MarshalUtf8String(IntPtr ptr)
{
if (ptr == IntPtr.Zero)
{
return null;
}

int length = 0;
while (Marshal.ReadByte(ptr, length) != 0)
{
length++;
}

byte[] buffer = new byte[length];
Marshal.Copy(ptr, buffer, 0, length);
return System.Text.Encoding.UTF8.GetString(buffer);
}
}

[DllImport(Git2NativeLibName, EntryPoint = "git_config_get_bool")]
public static extern ResultCode GetBool(out bool value, IntPtr configHandle, string name);

Expand Down
1 change: 1 addition & 0 deletions GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<ProjectReference Include="..\GVFS.NativeTests\GVFS.NativeTests.vcxproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
<ProjectReference Include="..\GVFS.UnitTests\GVFS.UnitTests.csproj" />
<None Include="$(RepoOutPath)GVFS.NativeTests\bin\x64\$(Configuration)\GVFS.NativeTests.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
7 changes: 7 additions & 0 deletions GVFS/GVFS.FunctionalTests/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Tools;
using GVFS.PlatformLoader;
using GVFS.Tests;
using System;
using System.Collections.Generic;
Expand All @@ -13,6 +14,7 @@ public class Program
public static void Main(string[] args)
{
Properties.Settings.Default.Initialize();
GVFSPlatformLoader.Initialize();
Console.WriteLine("Settings.Default.CurrentDirectory: {0}", Settings.Default.CurrentDirectory);
Console.WriteLine("Settings.Default.PathToGit: {0}", Settings.Default.PathToGit);
Console.WriteLine("Settings.Default.PathToGVFS: {0}", Settings.Default.PathToGVFS);
Expand All @@ -21,6 +23,11 @@ public static void Main(string[] args)
NUnitRunner runner = new NUnitRunner(args);
runner.AddGlobalSetupIfNeeded("GVFS.FunctionalTests.GlobalSetup");

if (runner.HasCustomArg("--debug"))
{
Debugger.Launch();
}

if (runner.HasCustomArg("--no-shared-gvfs-cache"))
{
Console.WriteLine("Running without a shared git object cache");
Expand Down
Loading
Loading