@@ -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
0 commit comments