@@ -108,24 +108,291 @@ public interface IGitConfiguration
108108 void UnsetAll ( GitConfigurationLevel level , string name , string valueRegex ) ;
109109 }
110110
111+ /// <summary>
112+ /// Represents a single configuration entry with its origin and level.
113+ /// </summary>
114+ internal class ConfigCacheEntry
115+ {
116+ public string Origin { get ; set ; }
117+ public string Value { get ; set ; }
118+ public GitConfigurationLevel Level { get ; set ; }
119+
120+ public ConfigCacheEntry ( string origin , string value )
121+ {
122+ Origin = origin ;
123+ Value = value ;
124+ Level = DetermineLevel ( origin ) ;
125+ }
126+
127+ private static GitConfigurationLevel DetermineLevel ( string origin )
128+ {
129+ if ( string . IsNullOrEmpty ( origin ) )
130+ return GitConfigurationLevel . Unknown ;
131+
132+ // Origins look like: "file:/path/to/config", "command line:", "standard input:"
133+ if ( ! origin . StartsWith ( "file:" ) )
134+ return GitConfigurationLevel . Unknown ;
135+
136+ string path = origin . Substring ( 5 ) ; // Remove "file:" prefix
137+
138+ // System config is typically in /etc/gitconfig or $(prefix)/etc/gitconfig
139+ if ( path . Contains ( "/etc/gitconfig" ) || path . EndsWith ( "/gitconfig" ) )
140+ return GitConfigurationLevel . System ;
141+
142+ // Global config is typically in ~/.gitconfig or ~/.config/git/config
143+ if ( path . Contains ( "/.gitconfig" ) || path . Contains ( "/.config/git/config" ) )
144+ return GitConfigurationLevel . Global ;
145+
146+ // Local config is typically in .git/config within a repository
147+ if ( path . Contains ( "/.git/config" ) )
148+ return GitConfigurationLevel . Local ;
149+
150+ return GitConfigurationLevel . Unknown ;
151+ }
152+ }
153+
154+ /// <summary>
155+ /// Cache for Git configuration entries loaded from 'git config list --show-origin -z'.
156+ /// </summary>
157+ internal class ConfigCache
158+ {
159+ private Dictionary < string , List < ConfigCacheEntry > > _entries ;
160+ private readonly object _lock = new object ( ) ;
161+
162+ public bool IsLoaded => _entries != null ;
163+
164+ public void Load ( string data , ITrace trace )
165+ {
166+ lock ( _lock )
167+ {
168+ var entries = new Dictionary < string , List < ConfigCacheEntry > > ( GitConfigurationKeyComparer . Instance ) ;
169+
170+ var origin = new StringBuilder ( ) ;
171+ var key = new StringBuilder ( ) ;
172+ var value = new StringBuilder ( ) ;
173+
174+ int i = 0 ;
175+ while ( i < data . Length )
176+ {
177+ origin . Clear ( ) ;
178+ key . Clear ( ) ;
179+ value . Clear ( ) ;
180+
181+ // Read origin (NUL terminated)
182+ while ( i < data . Length && data [ i ] != '\0 ' )
183+ {
184+ origin . Append ( data [ i ++ ] ) ;
185+ }
186+
187+ if ( i >= data . Length )
188+ {
189+ trace . WriteLine ( "Invalid Git configuration output. Expected null terminator (\\ 0) after origin." ) ;
190+ break ;
191+ }
192+
193+ // Skip the NUL terminator
194+ i ++ ;
195+
196+ // Read key (newline terminated)
197+ while ( i < data . Length && data [ i ] != '\n ' )
198+ {
199+ key . Append ( data [ i ++ ] ) ;
200+ }
201+
202+ if ( i >= data . Length )
203+ {
204+ trace . WriteLine ( "Invalid Git configuration output. Expected newline terminator (\\ n) after key." ) ;
205+ break ;
206+ }
207+
208+ // Skip the newline terminator
209+ i ++ ;
210+
211+ // Read value (NUL terminated)
212+ while ( i < data . Length && data [ i ] != '\0 ' )
213+ {
214+ value . Append ( data [ i ++ ] ) ;
215+ }
216+
217+ if ( i >= data . Length )
218+ {
219+ trace . WriteLine ( "Invalid Git configuration output. Expected null terminator (\\ 0) after value." ) ;
220+ break ;
221+ }
222+
223+ // Skip the NUL terminator
224+ i ++ ;
225+
226+ string keyStr = key . ToString ( ) ;
227+ var entry = new ConfigCacheEntry ( origin . ToString ( ) , value . ToString ( ) ) ;
228+
229+ if ( ! entries . ContainsKey ( keyStr ) )
230+ {
231+ entries [ keyStr ] = new List < ConfigCacheEntry > ( ) ;
232+ }
233+ entries [ keyStr ] . Add ( entry ) ;
234+ }
235+
236+ _entries = entries ;
237+ }
238+ }
239+
240+ public bool TryGet ( string name , GitConfigurationLevel level , out string value )
241+ {
242+ lock ( _lock )
243+ {
244+ if ( _entries == null )
245+ {
246+ value = null ;
247+ return false ;
248+ }
249+
250+ if ( ! _entries . TryGetValue ( name , out var entryList ) )
251+ {
252+ value = null ;
253+ return false ;
254+ }
255+
256+ // Find the first entry matching the level filter
257+ foreach ( var entry in entryList )
258+ {
259+ if ( level == GitConfigurationLevel . All || entry . Level == level )
260+ {
261+ value = entry . Value ;
262+ return true ;
263+ }
264+ }
265+
266+ value = null ;
267+ return false ;
268+ }
269+ }
270+
271+ public IEnumerable < string > GetAll ( string name , GitConfigurationLevel level )
272+ {
273+ lock ( _lock )
274+ {
275+ if ( _entries == null || ! _entries . TryGetValue ( name , out var entryList ) )
276+ {
277+ return Array . Empty < string > ( ) ;
278+ }
279+
280+ var results = new List < string > ( ) ;
281+ foreach ( var entry in entryList )
282+ {
283+ if ( level == GitConfigurationLevel . All || entry . Level == level )
284+ {
285+ results . Add ( entry . Value ) ;
286+ }
287+ }
288+
289+ return results ;
290+ }
291+ }
292+
293+ public void Enumerate ( GitConfigurationLevel level , GitConfigurationEnumerationCallback cb )
294+ {
295+ lock ( _lock )
296+ {
297+ if ( _entries == null )
298+ return ;
299+
300+ foreach ( var kvp in _entries )
301+ {
302+ foreach ( var entry in kvp . Value )
303+ {
304+ if ( level == GitConfigurationLevel . All || entry . Level == level )
305+ {
306+ var configEntry = new GitConfigurationEntry ( kvp . Key , entry . Value ) ;
307+ if ( ! cb ( configEntry ) )
308+ {
309+ return ;
310+ }
311+ }
312+ }
313+ }
314+ }
315+ }
316+
317+ public void Clear ( )
318+ {
319+ lock ( _lock )
320+ {
321+ _entries = null ;
322+ }
323+ }
324+ }
325+
111326 public class GitProcessConfiguration : IGitConfiguration
112327 {
113328 private static readonly GitVersion TypeConfigMinVersion = new GitVersion ( 2 , 18 , 0 ) ;
114329
115330 private readonly ITrace _trace ;
116331 private readonly GitProcess _git ;
332+ private readonly ConfigCache _cache ;
333+ private readonly bool _useCache ;
334+
335+ internal GitProcessConfiguration ( ITrace trace , GitProcess git ) : this ( trace , git , useCache : true )
336+ {
337+ }
117338
118- internal GitProcessConfiguration ( ITrace trace , GitProcess git )
339+ internal GitProcessConfiguration ( ITrace trace , GitProcess git , bool useCache )
119340 {
120341 EnsureArgument . NotNull ( trace , nameof ( trace ) ) ;
121342 EnsureArgument . NotNull ( git , nameof ( git ) ) ;
122343
123344 _trace = trace ;
124345 _git = git ;
346+ _useCache = useCache ;
347+ _cache = useCache ? new ConfigCache ( ) : null ;
348+ }
349+
350+ private void EnsureCacheLoaded ( )
351+ {
352+ if ( ! _useCache || _cache . IsLoaded )
353+ return ;
354+
355+ using ( ChildProcess git = _git . CreateProcess ( "config list --show-origin -z" ) )
356+ {
357+ git . Start ( Trace2ProcessClass . Git ) ;
358+ // To avoid deadlocks, always read the output stream first and then wait
359+ string data = git . StandardOutput . ReadToEnd ( ) ;
360+ git . WaitForExit ( ) ;
361+
362+ switch ( git . ExitCode )
363+ {
364+ case 0 : // OK
365+ _cache . Load ( data , _trace ) ;
366+ break ;
367+ default :
368+ _trace . WriteLine ( $ "Failed to load config cache (exit={ git . ExitCode } )") ;
369+ // Don't throw - fall back to individual commands
370+ break ;
371+ }
372+ }
373+ }
374+
375+ private void InvalidateCache ( )
376+ {
377+ if ( _useCache )
378+ {
379+ _cache . Clear ( ) ;
380+ }
125381 }
126382
127383 public void Enumerate ( GitConfigurationLevel level , GitConfigurationEnumerationCallback cb )
128384 {
385+ if ( _useCache )
386+ {
387+ EnsureCacheLoaded ( ) ;
388+ if ( _cache . IsLoaded )
389+ {
390+ _cache . Enumerate ( level , cb ) ;
391+ return ;
392+ }
393+ }
394+
395+ // Fall back to original implementation
129396 string levelArg = GetLevelFilterArg ( level ) ;
130397 using ( ChildProcess git = _git . CreateProcess ( $ "config --null { levelArg } --list") )
131398 {
@@ -194,6 +461,17 @@ public void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCa
194461
195462 public bool TryGet ( GitConfigurationLevel level , GitConfigurationType type , string name , out string value )
196463 {
464+ // Use cache for raw types only - typed queries need Git's canonicalization
465+ if ( _useCache && type == GitConfigurationType . Raw )
466+ {
467+ EnsureCacheLoaded ( ) ;
468+ if ( _cache . IsLoaded && _cache . TryGet ( name , level , out value ) )
469+ {
470+ return true ;
471+ }
472+ }
473+
474+ // Fall back to individual git config command for typed queries or cache miss
197475 string levelArg = GetLevelFilterArg ( level ) ;
198476 string typeArg = GetCanonicalizeTypeArg ( type ) ;
199477 using ( ChildProcess git = _git . CreateProcess ( $ "config --null { levelArg } { typeArg } { QuoteCmdArg ( name ) } ") )
@@ -242,6 +520,7 @@ public void Set(GitConfigurationLevel level, string name, string value)
242520 switch ( git . ExitCode )
243521 {
244522 case 0 : // OK
523+ InvalidateCache ( ) ;
245524 break ;
246525 default :
247526 _trace . WriteLine ( $ "Failed to set config entry '{ name } ' to value '{ value } ' (exit={ git . ExitCode } , level={ level } )") ;
@@ -263,6 +542,7 @@ public void Add(GitConfigurationLevel level, string name, string value)
263542 switch ( git . ExitCode )
264543 {
265544 case 0 : // OK
545+ InvalidateCache ( ) ;
266546 break ;
267547 default :
268548 _trace . WriteLine ( $ "Failed to add config entry '{ name } ' with value '{ value } ' (exit={ git . ExitCode } , level={ level } )") ;
@@ -285,6 +565,7 @@ public void Unset(GitConfigurationLevel level, string name)
285565 {
286566 case 0 : // OK
287567 case 5 : // Trying to unset a value that does not exist
568+ InvalidateCache ( ) ;
288569 break ;
289570 default :
290571 _trace . WriteLine ( $ "Failed to unset config entry '{ name } ' (exit={ git . ExitCode } , level={ level } )") ;
@@ -295,6 +576,22 @@ public void Unset(GitConfigurationLevel level, string name)
295576
296577 public IEnumerable < string > GetAll ( GitConfigurationLevel level , GitConfigurationType type , string name )
297578 {
579+ // Use cache for raw types only - typed queries need Git's canonicalization
580+ if ( _useCache && type == GitConfigurationType . Raw )
581+ {
582+ EnsureCacheLoaded ( ) ;
583+ if ( _cache . IsLoaded )
584+ {
585+ var cachedValues = _cache . GetAll ( name , level ) ;
586+ foreach ( var val in cachedValues )
587+ {
588+ yield return val ;
589+ }
590+ yield break ;
591+ }
592+ }
593+
594+ // Fall back to individual git config command
298595 string levelArg = GetLevelFilterArg ( level ) ;
299596 string typeArg = GetCanonicalizeTypeArg ( type ) ;
300597
@@ -392,6 +689,7 @@ public void ReplaceAll(GitConfigurationLevel level, string name, string valueReg
392689 switch ( git . ExitCode )
393690 {
394691 case 0 : // OK
692+ InvalidateCache ( ) ;
395693 break ;
396694 default :
397695 _trace . WriteLine ( $ "Failed to replace all multivar '{ name } ' and value regex '{ valueRegex } ' with new value '{ value } ' (exit={ git . ExitCode } , level={ level } )") ;
@@ -420,6 +718,7 @@ public void UnsetAll(GitConfigurationLevel level, string name, string valueRegex
420718 {
421719 case 0 : // OK
422720 case 5 : // Trying to unset a value that does not exist
721+ InvalidateCache ( ) ;
423722 break ;
424723 default :
425724 _trace . WriteLine ( $ "Failed to unset all multivar '{ name } ' with value regex '{ valueRegex } ' (exit={ git . ExitCode } , level={ level } )") ;
0 commit comments