Skip to content

Commit 4b3bc38

Browse files
authored
Merge pull request microsoft#1899 from tyrielv/tyrielv/safedirectory-caseinsensitive
Workaround libgit2 safe.directory case mismatch bug
2 parents 717137d + abea3ba commit 4b3bc38

4 files changed

Lines changed: 490 additions & 5 deletions

File tree

GVFS/GVFS.Common/Git/LibGit2Repo.cs

Lines changed: 199 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,35 @@ public class LibGit2Repo : IDisposable
1010
{
1111
private bool disposedValue = false;
1212

13+
public delegate void MultiVarConfigCallback(string value);
14+
1315
public LibGit2Repo(ITracer tracer, string repoPath)
1416
{
1517
this.Tracer = tracer;
1618

17-
Native.Init();
19+
InitNative();
1820

1921
IntPtr repoHandle;
20-
if (Native.Repo.Open(out repoHandle, repoPath) != Native.ResultCode.Success)
22+
if (TryOpenRepo(repoPath, out repoHandle) != Native.ResultCode.Success)
2123
{
22-
string reason = Native.GetLastError();
24+
string reason = GetLastNativeError();
2325
string message = "Couldn't open repo at " + repoPath + ": " + reason;
2426
tracer.RelatedWarning(message);
2527

26-
Native.Shutdown();
27-
throw new InvalidDataException(message);
28+
if (!reason.EndsWith(" is not owned by current user")
29+
|| !CheckSafeDirectoryConfigForCaseSensitivityIssue(tracer, repoPath, out repoHandle))
30+
{
31+
ShutdownNative();
32+
throw new InvalidDataException(message);
33+
}
2834
}
2935

3036
this.RepoHandle = repoHandle;
3137
}
3238

3339
protected LibGit2Repo()
3440
{
41+
this.Tracer = NullTracer.Instance;
3542
}
3643

3744
~LibGit2Repo()
@@ -246,7 +253,64 @@ public virtual string GetConfigString(string name)
246253
{
247254
Native.Config.Free(configHandle);
248255
}
256+
}
257+
258+
public void ForEachMultiVarConfig(string key, MultiVarConfigCallback callback)
259+
{
260+
if (Native.Config.GetConfig(out IntPtr configHandle, this.RepoHandle) != Native.ResultCode.Success)
261+
{
262+
throw new LibGit2Exception($"Failed to get config handle: {Native.GetLastError()}");
263+
}
264+
try
265+
{
266+
ForEachMultiVarConfig(configHandle, key, callback);
267+
}
268+
finally
269+
{
270+
Native.Config.Free(configHandle);
271+
}
272+
}
273+
274+
public static void ForEachMultiVarConfigInGlobalAndSystemConfig(string key, MultiVarConfigCallback callback)
275+
{
276+
if (Native.Config.GetGlobalAndSystemConfig(out IntPtr configHandle) != Native.ResultCode.Success)
277+
{
278+
throw new LibGit2Exception($"Failed to get global and system config handle: {Native.GetLastError()}");
279+
}
280+
try
281+
{
282+
ForEachMultiVarConfig(configHandle, key, callback);
283+
}
284+
finally
285+
{
286+
Native.Config.Free(configHandle);
287+
}
288+
}
249289

290+
private static void ForEachMultiVarConfig(IntPtr configHandle, string key, MultiVarConfigCallback callback)
291+
{
292+
Native.Config.GitConfigMultivarCallback nativeCallback = (entryPtr, payload) =>
293+
{
294+
try
295+
{
296+
var entry = Marshal.PtrToStructure<Native.Config.GitConfigEntry>(entryPtr);
297+
callback(entry.GetValue());
298+
}
299+
catch (Exception)
300+
{
301+
return Native.ResultCode.Failure;
302+
}
303+
return 0;
304+
};
305+
if (Native.Config.GetMultivarForeach(
306+
configHandle,
307+
key,
308+
regex:"",
309+
nativeCallback,
310+
IntPtr.Zero) != Native.ResultCode.Success)
311+
{
312+
throw new LibGit2Exception($"Failed to get multivar config for '{key}': {Native.GetLastError()}");
313+
}
250314
}
251315

252316
/// <summary>
@@ -302,11 +366,86 @@ protected virtual void Dispose(bool disposing)
302366
}
303367
}
304368

369+
/// <summary>
370+
/// Normalize a path for case-insensitive safe.directory comparison:
371+
/// replace backslashes with forward slashes, convert to upper-case,
372+
/// and trim trailing slashes.
373+
/// </summary>
374+
internal static string NormalizePathForSafeDirectoryComparison(string path)
375+
{
376+
if (string.IsNullOrEmpty(path))
377+
{
378+
return path;
379+
}
380+
381+
string normalized = path.Replace('\\', '/').ToUpperInvariant();
382+
return normalized.TrimEnd('/');
383+
}
384+
385+
/// <summary>
386+
/// Retrieve all configured safe.directory values from global and system git config.
387+
/// Virtual so tests can provide fake entries without touching real config.
388+
/// </summary>
389+
protected virtual void GetSafeDirectoryConfigEntries(MultiVarConfigCallback callback)
390+
{
391+
ForEachMultiVarConfigInGlobalAndSystemConfig("safe.directory", callback);
392+
}
393+
394+
/// <summary>
395+
/// Try to open a repository at the given path. Virtual so tests can
396+
/// avoid the native P/Invoke call.
397+
/// </summary>
398+
protected virtual Native.ResultCode TryOpenRepo(string path, out IntPtr repoHandle)
399+
{
400+
return Native.Repo.Open(out repoHandle, path);
401+
}
402+
403+
protected virtual void InitNative()
404+
{
405+
Native.Init();
406+
}
407+
408+
protected virtual void ShutdownNative()
409+
{
410+
Native.Shutdown();
411+
}
412+
413+
protected virtual string GetLastNativeError()
414+
{
415+
return Native.GetLastError();
416+
}
417+
418+
protected bool CheckSafeDirectoryConfigForCaseSensitivityIssue(ITracer tracer, string repoPath, out IntPtr repoHandle)
419+
{
420+
/* Libgit2 has a bug where it is case sensitive for safe.directory (especially the
421+
* drive letter) when git.exe isn't. Until a fix can be made and propagated, work
422+
* around it by matching the repo path we request to the configured safe directory.
423+
*
424+
* See https://github.com/libgit2/libgit2/issues/7037
425+
*/
426+
repoHandle = IntPtr.Zero;
427+
428+
string normalizedRequestedPath = NormalizePathForSafeDirectoryComparison(repoPath);
429+
430+
string configuredMatchingDirectory = null;
431+
GetSafeDirectoryConfigEntries((string value) =>
432+
{
433+
string normalizedConfiguredPath = NormalizePathForSafeDirectoryComparison(value);
434+
if (normalizedConfiguredPath == normalizedRequestedPath)
435+
{
436+
configuredMatchingDirectory = value;
437+
}
438+
});
439+
440+
return configuredMatchingDirectory != null && TryOpenRepo(configuredMatchingDirectory, out repoHandle) == Native.ResultCode.Success;
441+
}
442+
305443
public static class Native
306444
{
307445
public enum ResultCode : int
308446
{
309447
Success = 0,
448+
Failure = -1,
310449
NotFound = -3,
311450
}
312451

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

512+
[DllImport(Git2NativeLibName, EntryPoint = "git_config_open_default")]
513+
public static extern ResultCode GetGlobalAndSystemConfig(out IntPtr configHandle);
514+
373515
[DllImport(Git2NativeLibName, EntryPoint = "git_config_get_string")]
374516
public static extern ResultCode GetString(out string value, IntPtr configHandle, string name);
375517

518+
[DllImport(Git2NativeLibName, EntryPoint = "git_config_get_multivar_foreach")]
519+
public static extern ResultCode GetMultivarForeach(
520+
IntPtr configHandle,
521+
string name,
522+
string regex,
523+
GitConfigMultivarCallback callback,
524+
IntPtr payload);
525+
526+
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
527+
public delegate ResultCode GitConfigMultivarCallback(
528+
IntPtr entryPtr,
529+
IntPtr payload);
530+
531+
[StructLayout(LayoutKind.Sequential)]
532+
public struct GitConfigEntry
533+
{
534+
public IntPtr Name;
535+
public IntPtr Value;
536+
public IntPtr BackendType;
537+
public IntPtr OriginPath;
538+
public uint IncludeDepth;
539+
public int Level;
540+
541+
public string GetValue()
542+
{
543+
return Value != IntPtr.Zero ? MarshalUtf8String(Value) : null;
544+
}
545+
546+
public string GetName()
547+
{
548+
return Name != IntPtr.Zero ? MarshalUtf8String(Name) : null;
549+
}
550+
551+
private static string MarshalUtf8String(IntPtr ptr)
552+
{
553+
if (ptr == IntPtr.Zero)
554+
{
555+
return null;
556+
}
557+
558+
int length = 0;
559+
while (Marshal.ReadByte(ptr, length) != 0)
560+
{
561+
length++;
562+
}
563+
564+
byte[] buffer = new byte[length];
565+
Marshal.Copy(ptr, buffer, 0, length);
566+
return System.Text.Encoding.UTF8.GetString(buffer);
567+
}
568+
}
569+
376570
[DllImport(Git2NativeLibName, EntryPoint = "git_config_get_bool")]
377571
public static extern ResultCode GetBool(out bool value, IntPtr configHandle, string name);
378572

GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<ProjectReference Include="..\GVFS.NativeTests\GVFS.NativeTests.vcxproj">
2727
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
2828
</ProjectReference>
29+
<ProjectReference Include="..\GVFS.UnitTests\GVFS.UnitTests.csproj" />
2930
<None Include="$(RepoOutPath)GVFS.NativeTests\bin\x64\$(Configuration)\GVFS.NativeTests.dll">
3031
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3132
</None>

GVFS/GVFS.FunctionalTests/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using GVFS.FunctionalTests.Properties;
22
using GVFS.FunctionalTests.Tools;
3+
using GVFS.PlatformLoader;
34
using GVFS.Tests;
45
using System;
56
using System.Collections.Generic;
@@ -13,6 +14,7 @@ public class Program
1314
public static void Main(string[] args)
1415
{
1516
Properties.Settings.Default.Initialize();
17+
GVFSPlatformLoader.Initialize();
1618
Console.WriteLine("Settings.Default.CurrentDirectory: {0}", Settings.Default.CurrentDirectory);
1719
Console.WriteLine("Settings.Default.PathToGit: {0}", Settings.Default.PathToGit);
1820
Console.WriteLine("Settings.Default.PathToGVFS: {0}", Settings.Default.PathToGVFS);
@@ -21,6 +23,11 @@ public static void Main(string[] args)
2123
NUnitRunner runner = new NUnitRunner(args);
2224
runner.AddGlobalSetupIfNeeded("GVFS.FunctionalTests.GlobalSetup");
2325

26+
if (runner.HasCustomArg("--debug"))
27+
{
28+
Debugger.Launch();
29+
}
30+
2431
if (runner.HasCustomArg("--no-shared-gvfs-cache"))
2532
{
2633
Console.WriteLine("Running without a shared git object cache");

0 commit comments

Comments
 (0)