From 9aa13ac6af7dfb8b98bc7494ed882571e975c433 Mon Sep 17 00:00:00 2001 From: Marc Becker Date: Sun, 13 Sep 2026 02:05:52 +0200 Subject: [PATCH 1/3] config: add test for conditional config inclusion redirect global config location test special conditional global include based on local Git remote value verify unconditional include directive for local scope --- src/Core.Tests/GitConfigurationTests.cs | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/Core.Tests/GitConfigurationTests.cs b/src/Core.Tests/GitConfigurationTests.cs index 5005cf4319..36bc0a2db8 100644 --- a/src/Core.Tests/GitConfigurationTests.cs +++ b/src/Core.Tests/GitConfigurationTests.cs @@ -656,5 +656,47 @@ public void GitConfiguration_TypedQuery_CanonicalizesValues() // Value should be canonicalized path, not raw "~/example" Assert.NotEqual("~/example", value); } + + [Fact] + public void GitConfiguration_ScopedConfig_ProcessesIncludes() + { + const string configKey = "test.value"; + const string configTrigger = "remote.*.url:http://localhost/**"; + const string configValue = "foo123"; + string repoPath = CreateRepository(out string workDirPath); + + string gitGlobal = System.Environment.GetEnvironmentVariable("GIT_CONFIG_GLOBAL"); + + try { + System.Environment.SetEnvironmentVariable("GIT_CONFIG_GLOBAL", repoPath + "/global.config"); + + ExecGit(repoPath, workDirPath, $"config --file {workDirPath}/git.config.glob {configKey} {configValue}").AssertSuccess(); + ExecGit(repoPath, workDirPath, $"config --global includeif.hasconfig:{configTrigger}.path {workDirPath}/git.config.glob").AssertSuccess(); + + ExecGit(repoPath, workDirPath, $"config --file {workDirPath}/git.config.loc {configKey} {configValue}").AssertSuccess(); + ExecGit(repoPath, workDirPath, $"config --local include.path {workDirPath}/git.config.loc").AssertSuccess(); + + ExecGit(repoPath, workDirPath, $"config --local remote.test.url http://localhost/my/repo").AssertSuccess(); + + string gitPath = GetGitPath(); + var trace = new NullTrace(); + var trace2 = new NullTrace2(); + var processManager = new TestProcessManager(); + + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + IGitConfiguration config = git.GetConfiguration(); + + // value of included file set in scopes + foreach (var scope in new GitConfigurationLevel[] { GitConfigurationLevel.Global, GitConfigurationLevel.Local }) + { + Assert.True(config.TryGet(scope, GitConfigurationType.Raw, configKey, out string value)); + Assert.Equal(configValue, value); + } + } + finally + { + System.Environment.SetEnvironmentVariable("GIT_CONFIG_GLOBAL", gitGlobal); + } + } } } From f64cc3a40ec17ba9492a0334e8e4fa536bb5ce74 Mon Sep 17 00:00:00 2001 From: Marc Becker Date: Sun, 13 Sep 2026 02:38:45 +0200 Subject: [PATCH 2/3] config: enable cache for Raw config in Git v2.26 split Git version checks for support of "raw" and "typed" config listing use compatible argument to create config listing in older Git versions drop `--no-type` for Raw values (default) to support older Git versions --- src/Core/GitConfiguration.cs | 52 ++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/Core/GitConfiguration.cs b/src/Core/GitConfiguration.cs index 83a10d5918..9bdc5afcfa 100644 --- a/src/Core/GitConfiguration.cs +++ b/src/Core/GitConfiguration.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using System.Text; namespace GitCredentialManager @@ -326,11 +324,14 @@ public class GitProcessConfiguration : IGitConfiguration private static readonly GitVersion ConfigListTypeMinVersion = new GitVersion(2, 54, 0); private static readonly GitVersion ConfigListTypeMinVfsBase = new GitVersion(2, 53, 0); private static readonly GitVersion ConfigListTypeMinVfsSuffix = new GitVersion(0, 1); + private static readonly GitVersion ConfigListRawMinVersion = new GitVersion(2, 26, 0); + private static readonly GitVersion ConfigListSubcommandMinVersion = new GitVersion(2, 46, 0); private readonly ITrace _trace; private readonly GitProcess _git; private readonly Dictionary _cache; - private readonly bool _useCache; + private readonly bool _useConfigTypeCache; + private readonly bool _useConfigRawCache; internal GitProcessConfiguration(ITrace trace, GitProcess git) : this(trace, git, useCache: true) { @@ -348,12 +349,24 @@ internal GitProcessConfiguration(ITrace trace, GitProcess git, bool useCache) // or microsoft/git fork 2.53.0.vfs.0.1+ if (useCache && !SupportsConfigListType(git)) { - trace.WriteLine($"Git version {git.Version.OriginalString} does not support 'git config list --type'; config cache disabled"); - useCache = false; + trace.WriteLine($"Git version {git.Version.OriginalString} does not support 'git config --list --type'; config cache disabled"); + _useConfigTypeCache = false; + } + else + { + _useConfigTypeCache = useCache; } - _useCache = useCache; - _cache = useCache ? new Dictionary() : null; + if (useCache && !SupportsConfigListRaw(git)) + { + trace.WriteLine($"Git version {git.Version.OriginalString} does not support 'git config list --type'; config cache disabled"); + _useConfigRawCache = false; + } + else + { + _useConfigRawCache = useCache; + } + _cache = _useConfigTypeCache || _useConfigRawCache ? new Dictionary() : null; } private static bool SupportsConfigListType(GitProcess git) @@ -381,10 +394,20 @@ private static bool SupportsConfigListType(GitProcess git) return false; } + private static bool SupportsConfigListRaw(GitProcess git) + { + return git.Version >= ConfigListRawMinVersion; + } + + private bool UseConfigCache(GitConfigurationType type) + { + return type != GitConfigurationType.Raw ? _useConfigTypeCache : _useConfigRawCache; + } + private void EnsureCacheLoaded(GitConfigurationType type) { ConfigCache cache; - if (!_useCache || (_cache.TryGetValue(type, out cache) && cache.IsLoaded)) + if (!UseConfigCache(type) || (_cache.TryGetValue(type, out cache) && cache.IsLoaded)) { return; } @@ -400,7 +423,7 @@ private void EnsureCacheLoaded(GitConfigurationType type) switch (type) { case GitConfigurationType.Raw: - typeArg = "--no-type"; + typeArg = ""; break; case GitConfigurationType.Path: @@ -415,7 +438,8 @@ private void EnsureCacheLoaded(GitConfigurationType type) return; } - using (ChildProcess git = _git.CreateProcess($"config list --show-scope -z {typeArg}")) + string argList = (_git.Version >= ConfigListSubcommandMinVersion) ? "list" : "--list"; + using (ChildProcess git = _git.CreateProcess($"config {argList} --show-scope -z {typeArg}")) { git.Start(Trace2ProcessClass.Git); // To avoid deadlocks, always read the output stream first and then wait @@ -437,7 +461,7 @@ private void EnsureCacheLoaded(GitConfigurationType type) private void InvalidateCache() { - if (_useCache) + if (_useConfigTypeCache ||_useConfigRawCache) { foreach (ConfigCache cache in _cache.Values) { @@ -448,7 +472,7 @@ private void InvalidateCache() public void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCallback cb) { - if (_useCache) + if (_useConfigRawCache) { EnsureCacheLoaded(GitConfigurationType.Raw); @@ -530,7 +554,7 @@ public void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCa public bool TryGet(GitConfigurationLevel level, GitConfigurationType type, string name, out string value) { - if (_useCache) + if (UseConfigCache(type)) { EnsureCacheLoaded(type); @@ -647,7 +671,7 @@ public void Unset(GitConfigurationLevel level, string name) public IEnumerable GetAll(GitConfigurationLevel level, GitConfigurationType type, string name) { - if (_useCache) + if (UseConfigCache(type)) { EnsureCacheLoaded(type); From 228be3d08b690ebdd0676d5ba784555faf4f3a5b Mon Sep 17 00:00:00 2001 From: Marc Becker Date: Sun, 13 Sep 2026 02:39:34 +0200 Subject: [PATCH 3/3] config: enable file inclusion in config queries match query results of `ConfigCache` as close as possible --- src/Core/GitConfiguration.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Core/GitConfiguration.cs b/src/Core/GitConfiguration.cs index 9bdc5afcfa..7c6dc87302 100644 --- a/src/Core/GitConfiguration.cs +++ b/src/Core/GitConfiguration.cs @@ -487,7 +487,7 @@ public void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCa // Fall back to original implementation string levelArg = GetLevelFilterArg(level); - using (ChildProcess git = _git.CreateProcess($"config --null {levelArg} --list")) + using (ChildProcess git = _git.CreateProcess($"config --null {levelArg} --includes --list")) { git.Start(Trace2ProcessClass.Git); // To avoid deadlocks, always read the output stream first and then wait @@ -569,7 +569,7 @@ public bool TryGet(GitConfigurationLevel level, GitConfigurationType type, strin // Fall back to individual git config command if cache not available string levelArg = GetLevelFilterArg(level); string typeArg = GetCanonicalizeTypeArg(type); - using (ChildProcess git = _git.CreateProcess($"config --null {levelArg} {typeArg} {QuoteCmdArg(name)}")) + using (ChildProcess git = _git.CreateProcess($"config --null {levelArg} --includes {typeArg} {QuoteCmdArg(name)}")) { git.Start(Trace2ProcessClass.Git); // To avoid deadlocks, always read the output stream first and then wait @@ -691,7 +691,7 @@ public IEnumerable GetAll(GitConfigurationLevel level, GitConfigurationT string levelArg = GetLevelFilterArg(level); string typeArg = GetCanonicalizeTypeArg(type); - var gitArgs = $"config --null {levelArg} {typeArg} --get-all {QuoteCmdArg(name)}"; + var gitArgs = $"config --null {levelArg} --includes {typeArg} --get-all {QuoteCmdArg(name)}"; using (ChildProcess git = _git.CreateProcess(gitArgs)) { @@ -729,7 +729,7 @@ public IEnumerable GetRegex(GitConfigurationLevel level, GitConfiguratio string levelArg = GetLevelFilterArg(level); string typeArg = GetCanonicalizeTypeArg(type); - var gitArgs = $"config --null {levelArg} {typeArg} --get-regex {QuoteCmdArg(nameRegex)}"; + var gitArgs = $"config --null {levelArg} --includes {typeArg} --get-regex {QuoteCmdArg(nameRegex)}"; if (valueRegex != null) { gitArgs += $" {QuoteCmdArg(valueRegex)}";