Skip to content

Commit eb0d445

Browse files
derrickstoleeclaude
andcommitted
refactor: Use git config --show-scope for reliable level detection
Context: The original implementation inferred GitConfigurationLevel by parsing file paths from config origins (e.g., checking if path contains "/.gitconfig" for global). This approach is error-prone because: - Path conventions vary across platforms and Git installations - System config can be in multiple locations (prefix-dependent) - Custom config paths break the path-based detection - Worktree configs weren't handled Justification: Git's --show-scope flag directly reports whether each config entry is from system, global, local, worktree, or command line scope. Using this flag eliminates path-parsing heuristics and defers to Git's authoritative knowledge of config scope. Implementation: - Replaced DetermineLevel() path-parsing logic with ParseScope() that maps Git's scope strings directly to GitConfigurationLevel enum - Updated ConfigCache.Load() to parse scope field from git output - Updated EnsureCacheLoaded() to include --show-scope flag - ConfigCacheEntry constructor now accepts level directly Scope parsing maps: system/global/local/worktree → enum values, treating worktree as local (both are repository-specific). All 52 tests pass with this change. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 625d254 commit eb0d445

File tree

1 file changed

+38
-29
lines changed

1 file changed

+38
-29
lines changed

src/shared/Core/GitConfiguration.cs

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -117,37 +117,27 @@ internal class ConfigCacheEntry
117117
public string Value { get; set; }
118118
public GitConfigurationLevel Level { get; set; }
119119

120-
public ConfigCacheEntry(string origin, string value)
120+
public ConfigCacheEntry(string origin, string value, GitConfigurationLevel level)
121121
{
122122
Origin = origin;
123123
Value = value;
124-
Level = DetermineLevel(origin);
124+
Level = level;
125125
}
126126

127-
private static GitConfigurationLevel DetermineLevel(string origin)
127+
/// <summary>
128+
/// Parse scope string from 'git config --show-scope' to GitConfigurationLevel.
129+
/// </summary>
130+
public static GitConfigurationLevel ParseScope(string scope)
128131
{
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;
132+
return scope switch
133+
{
134+
"system" => GitConfigurationLevel.System,
135+
"global" => GitConfigurationLevel.Global,
136+
"local" => GitConfigurationLevel.Local,
137+
"worktree" => GitConfigurationLevel.Local, // Treat worktree as local
138+
"command" => GitConfigurationLevel.Unknown,
139+
_ => GitConfigurationLevel.Unknown
140+
};
151141
}
152142
}
153143

@@ -167,17 +157,36 @@ public void Load(string data, ITrace trace)
167157
{
168158
var entries = new Dictionary<string, List<ConfigCacheEntry>>(GitConfigurationKeyComparer.Instance);
169159

160+
var scope = new StringBuilder();
170161
var origin = new StringBuilder();
171162
var key = new StringBuilder();
172163
var value = new StringBuilder();
173164

174165
int i = 0;
175166
while (i < data.Length)
176167
{
168+
scope.Clear();
177169
origin.Clear();
178170
key.Clear();
179171
value.Clear();
180172

173+
// Read scope (NUL terminated)
174+
while (i < data.Length && data[i] != '\0')
175+
{
176+
scope.Append(data[i++]);
177+
}
178+
179+
if (i >= data.Length)
180+
{
181+
trace.WriteLine("Invalid Git configuration output. Expected null terminator (\\0) after scope.");
182+
break;
183+
}
184+
185+
// Skip the NUL terminator
186+
i++;
187+
188+
GitConfigurationLevel level = ConfigCacheEntry.ParseScope(scope.ToString());
189+
181190
// Read origin (NUL terminated)
182191
while (i < data.Length && data[i] != '\0')
183192
{
@@ -224,7 +233,7 @@ public void Load(string data, ITrace trace)
224233
i++;
225234

226235
string keyStr = key.ToString();
227-
var entry = new ConfigCacheEntry(origin.ToString(), value.ToString());
236+
var entry = new ConfigCacheEntry(origin.ToString(), value.ToString(), level);
228237

229238
if (!entries.ContainsKey(keyStr))
230239
{
@@ -359,7 +368,7 @@ private void EnsureCacheLoaded()
359368
if (!_useCache || _cache.IsLoaded)
360369
return;
361370

362-
using (ChildProcess git = _git.CreateProcess("config list --show-origin -z"))
371+
using (ChildProcess git = _git.CreateProcess("config list --show-scope --show-origin -z"))
363372
{
364373
git.Start(Trace2ProcessClass.Git);
365374
// To avoid deadlocks, always read the output stream first and then wait
@@ -372,8 +381,8 @@ private void EnsureCacheLoaded()
372381
_cache.Load(data, _trace);
373382
break;
374383
default:
375-
_trace.WriteLine($"Failed to load config cache (exit={git.ExitCode})");
376-
// Don't throw - fall back to individual commands
384+
_trace.WriteLine($"Failed to load config cache (exit={git.ExitCode}), will use individual git config commands");
385+
// Don't throw - cache stays unloaded and operations fall back to individual commands
377386
break;
378387
}
379388
}

0 commit comments

Comments
 (0)