Skip to content

Commit 1a74bce

Browse files
committed
Fix GitConfigParser ignoring multiple [include] path entries
When an [include] section has multiple entries with the same key (e.g. multiple 'path' values), only the last one was respected. This is because _included_paths() used self.items(section) which delegates to _OMD.items(), and _OMD.__getitem__ returns only the last value for a given key. Fix by using _OMD.items_all() to retrieve all values for each key in the include/includeIf sections, ensuring all paths are processed. Fixes #2099
1 parent 57bbea9 commit 1a74bce

File tree

2 files changed

+51
-4
lines changed

2 files changed

+51
-4
lines changed

git/config.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -549,11 +549,21 @@ def _included_paths(self) -> List[Tuple[str, str]]:
549549
:return:
550550
The list of paths, where each path is a tuple of (option, value).
551551
"""
552+
553+
def _all_items(section: str) -> List[Tuple[str, str]]:
554+
"""Return all (key, value) pairs for a section, including duplicate keys."""
555+
return [
556+
(key, value)
557+
for key, values in self._sections[section].items_all()
558+
if key != "__name__"
559+
for value in values
560+
]
561+
552562
paths = []
553563

554564
for section in self.sections():
555565
if section == "include":
556-
paths += self.items(section)
566+
paths += _all_items(section)
557567

558568
match = CONDITIONAL_INCLUDE_REGEXP.search(section)
559569
if match is None or self._repo is None:
@@ -579,7 +589,7 @@ def _included_paths(self) -> List[Tuple[str, str]]:
579589
)
580590
if self._repo.git_dir:
581591
if fnmatch.fnmatchcase(os.fspath(self._repo.git_dir), value):
582-
paths += self.items(section)
592+
paths += _all_items(section)
583593

584594
elif keyword == "onbranch":
585595
try:
@@ -589,11 +599,11 @@ def _included_paths(self) -> List[Tuple[str, str]]:
589599
continue
590600

591601
if fnmatch.fnmatchcase(branch_name, value):
592-
paths += self.items(section)
602+
paths += _all_items(section)
593603
elif keyword == "hasconfig:remote.*.url":
594604
for remote in self._repo.remotes:
595605
if fnmatch.fnmatchcase(remote.url, value):
596-
paths += self.items(section)
606+
paths += _all_items(section)
597607
break
598608
return paths
599609

test/test_config.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,43 @@ def check_test_value(cr, value):
246246
with GitConfigParser(fpa, read_only=True) as cr:
247247
check_test_value(cr, tv)
248248

249+
@with_rw_directory
250+
def test_multiple_include_paths_with_same_key(self, rw_dir):
251+
"""Test that multiple 'path' entries under [include] are all respected.
252+
253+
Regression test for https://github.com/gitpython-developers/GitPython/issues/2099.
254+
Git config allows multiple ``path`` values under ``[include]``, e.g.::
255+
256+
[include]
257+
path = file1
258+
path = file2
259+
260+
Previously only one of these was included because _OMD.items() returns
261+
only the last value for each key.
262+
"""
263+
# Create two config files to be included.
264+
fp_inc1 = osp.join(rw_dir, "inc1.cfg")
265+
fp_inc2 = osp.join(rw_dir, "inc2.cfg")
266+
fp_main = osp.join(rw_dir, "main.cfg")
267+
268+
with GitConfigParser(fp_inc1, read_only=False) as cw:
269+
cw.set_value("user", "name", "from-inc1")
270+
271+
with GitConfigParser(fp_inc2, read_only=False) as cw:
272+
cw.set_value("core", "bar", "from-inc2")
273+
274+
# Write a config with two path entries under a single [include] section.
275+
# We write it manually because set_value would overwrite the key.
276+
with open(fp_main, "w") as f:
277+
f.write("[include]\n")
278+
f.write(f"\tpath = {fp_inc1}\n")
279+
f.write(f"\tpath = {fp_inc2}\n")
280+
281+
with GitConfigParser(fp_main, read_only=True) as cr:
282+
# Both included files should be loaded.
283+
assert cr.get_value("user", "name") == "from-inc1"
284+
assert cr.get_value("core", "bar") == "from-inc2"
285+
249286
@pytest.mark.xfail(
250287
sys.platform == "win32",
251288
reason='Second config._has_includes() assertion fails (for "config is included if path is matching git_dir")',

0 commit comments

Comments
 (0)