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.