From 6244f1d030136491716186212f975408eae323a8 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Sat, 11 Apr 2026 19:55:27 +0000 Subject: [PATCH 1/2] config: resolve relative includes from relative config paths Normalize path-based config inputs before using them for include cycle checks. This lets GitConfigParser resolve relative include.path values when the root config file was provided as a relative path, without re-reading the same config under a different spelling. Add regression coverage with a relative root config and a relative include cycle. --- git/config.py | 15 +++++++++++---- test/test_config.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/git/config.py b/git/config.py index c6eaf8f7b..b60df2ca8 100644 --- a/git/config.py +++ b/git/config.py @@ -32,6 +32,7 @@ List, Dict, Sequence, + Set, TYPE_CHECKING, Tuple, TypeVar, @@ -631,11 +632,17 @@ def read(self) -> None: # type: ignore[override] files_to_read = list(self._file_or_files) # END ensure we have a copy of the paths to handle - seen = set(files_to_read) + def path_key(file_path: Union[PathLike, IO]) -> Union[str, IO]: + if isinstance(file_path, (str, os.PathLike)): + return osp.normpath(osp.abspath(file_path)) + return file_path + + seen: Set[Union[str, IO]] = {path_key(file_path) for file_path in files_to_read} num_read_include_files = 0 while files_to_read: file_path = files_to_read.pop(0) file_ok = False + abs_file_path: Union[str, None] = None if hasattr(file_path, "seek"): # Must be a file-object. @@ -644,6 +651,7 @@ def read(self) -> None: # type: ignore[override] self._read(file_path, file_path.name) else: try: + abs_file_path = osp.normpath(osp.abspath(file_path)) with open(file_path, "rb") as fp: file_ok = True self._read(fp, fp.name) @@ -660,9 +668,8 @@ def read(self) -> None: # type: ignore[override] if not file_ok: continue # END ignore relative paths if we don't know the configuration file path - file_path = cast(PathLike, file_path) - assert osp.isabs(file_path), "Need absolute paths to be sure our cycle checks will work" - include_path = osp.join(osp.dirname(file_path), include_path) + assert abs_file_path is not None, "Need a source path to resolve relative include paths" + include_path = osp.join(osp.dirname(abs_file_path), include_path) # END make include path absolute include_path = osp.normpath(include_path) if include_path in seen or not os.access(include_path, os.R_OK): diff --git a/test/test_config.py b/test/test_config.py index 11ea52d16..eb7ec27f2 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -246,6 +246,24 @@ def check_test_value(cr, value): with GitConfigParser(fpa, read_only=True) as cr: check_test_value(cr, tv) + @with_rw_directory + def test_config_include_from_relative_config_path(self, rw_dir): + fpa = osp.join(rw_dir, "a") + fpb = osp.join(rw_dir, "b") + + with GitConfigParser(fpa, read_only=False) as cw: + cw.set_value("a", "value", "a") + cw.set_value("include", "path", "b") + + with GitConfigParser(fpb, read_only=False) as cw: + cw.set_value("b", "value", "b") + cw.set_value("include", "path", "a") + + with GitConfigParser(osp.relpath(fpa), read_only=True) as cr: + assert cr.get_value("a", "value") == "a" + assert cr.get_value("b", "value") == "b" + assert cr.get_values("include", "path") == ["b", "a"] + @with_rw_directory def test_multiple_include_paths_with_same_key(self, rw_dir): """Test that multiple 'path' entries under [include] are all respected. From 483f0c666ee7f3ce0e59ade9c1f125ebe486680b Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Mon, 13 Apr 2026 03:42:25 +0000 Subject: [PATCH 2/2] Test relative include cycles through nested paths --- test/test_config.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/test_config.py b/test/test_config.py index eb7ec27f2..1eb81103c 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -249,20 +249,22 @@ def check_test_value(cr, value): @with_rw_directory def test_config_include_from_relative_config_path(self, rw_dir): fpa = osp.join(rw_dir, "a") - fpb = osp.join(rw_dir, "b") + nested_dir = osp.join(rw_dir, "subdir") + os.makedirs(nested_dir) + fpb = osp.join(nested_dir, "b") with GitConfigParser(fpa, read_only=False) as cw: cw.set_value("a", "value", "a") - cw.set_value("include", "path", "b") + cw.set_value("include", "path", "subdir/b") with GitConfigParser(fpb, read_only=False) as cw: cw.set_value("b", "value", "b") - cw.set_value("include", "path", "a") + cw.set_value("include", "path", "../a") with GitConfigParser(osp.relpath(fpa), read_only=True) as cr: assert cr.get_value("a", "value") == "a" assert cr.get_value("b", "value") == "b" - assert cr.get_values("include", "path") == ["b", "a"] + assert cr.get_values("include", "path") == ["subdir/b", "../a"] @with_rw_directory def test_multiple_include_paths_with_same_key(self, rw_dir):