From f5335a329bcafaefa28990071ad95a0e7810afb0 Mon Sep 17 00:00:00 2001 From: Gaven Lan Ren Date: Wed, 28 Oct 2026 13:31:50 -0400 Subject: [PATCH] fix(util): preserve separators before relative path tokens The public `join_path` helper drops the slash in `join_path("parent", "/child")`, yielding `parentchild`. Strip leading slashes from later tokens before the normal joining logic so they remain relative to the accumulated prefix and receive a separator. Document this retained relative-token contract and cover string/PathLike inputs, chained tokens, roots, empty tokens and repeated leading separators. Five of eleven regression cases fail before this change. Afterward the complete utility module passes 128 tests with 39 skips on each CLI/Gix backend. Tree/reference/remote caller modules pass 230 tests with seven stalled-remote timeout failures per backend; the exact same seven failures reproduce on unchanged main. Repository pre-commit hooks pass. Tested on Linux with Python 3.12.14, Git 2.52.0 and GixPython 0.3.0; no other platform execution is claimed. Assisted-by: OpenAI Codex (model: unknown, autonomous) --- git/util.py | 11 ++++++----- test/test_util.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/git/util.py b/git/util.py index 2d4cc7c23..5170592ab 100644 --- a/git/util.py +++ b/git/util.py @@ -235,15 +235,16 @@ def stream_copy(source: SupportsRead[AnyStr], destination: SupportsWrite[AnyStr] def join_path(a: PathLike, *p: PathLike) -> PathLike: R"""Join path tokens together similar to osp.join, but always use ``/`` instead of - possibly ``\`` on Windows.""" + possibly ``\`` on Windows. + + Later tokens are treated as relative, even when they start with ``/``. + """ path = os.fspath(a) for b in p: - b = os.fspath(b) + b = os.fspath(b).lstrip("/") if not b: continue - if b.startswith("/"): - path += b[1:] - elif path == "" or path.endswith("/"): + if path == "" or path.endswith("/"): path += b else: path += "/" + b diff --git a/test/test_util.py b/test/test_util.py index acbff17b7..4ec6ba5f8 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -38,6 +38,7 @@ decygpath, get_user_id, is_cygwin_git, + join_path, remove_password_if_present, rmtree, ) @@ -46,6 +47,26 @@ from test.lib import TestBase, requires_symlinks, with_rw_repo +@pytest.mark.parametrize( + "parts, expected", + [ + (("parent", "/child"), "parent/child"), + (("parent", "/child", "/leaf"), "parent/child/leaf"), + ((pathlib.Path("parent"), "/child"), "parent/child"), + (("parent/", "/child"), "parent/child"), + (("parent", "child"), "parent/child"), + (("", "/child"), "child"), + (("/", "/child"), "/child"), + (("parent", "", "child"), "parent/child"), + (("parent", "//child"), "parent/child"), + (("/parent/", "///child"), "/parent/child"), + (("parent", "///"), "parent"), + ], +) +def test_join_path_keeps_later_tokens_relative(parts, expected): + assert join_path(*parts) == expected + + @pytest.fixture def tmp_path(request): # Unreadable files and symlinks cannot be copied by native fixture tooling.