Skip to content

Commit 464da19

Browse files
Byroncodex
andcommitted
various fixes for windows paths
- Preserve POSIX backslashes in include patterns - Accept UNC share roots as rooted paths - treat includeIf patterns correctly on Windows, expecting forward slashes - Recognize UNC roots on older Python versions Assisted-by: GPT 5.6 Co-authored-by: GPT 5.6 <codex@openai.com>
1 parent f3391b0 commit 464da19

4 files changed

Lines changed: 47 additions & 12 deletions

File tree

git/config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -576,8 +576,10 @@ def _all_items(section: str) -> List[Tuple[str, str]]:
576576
value = match.group(2).strip()
577577

578578
if keyword in ["gitdir", "gitdir/i"]:
579-
value = osp.expanduser(value).replace("\\", "/")
580-
git_dir = os.fspath(self._repo.git_dir).replace("\\", "/") if self._repo.git_dir else None
579+
value = osp.expanduser(value)
580+
git_dir = os.fspath(self._repo.git_dir) if self._repo.git_dir else None
581+
if sys.platform == "win32":
582+
git_dir = git_dir.replace("\\", "/") if git_dir else None
581583

582584
drive, _tail = osp.splitdrive(value)
583585
if not drive and not any(value.startswith(s) for s in ["./", "/"]):

git/util.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,17 +316,20 @@ def join_path_native(a: PathLike, *p: PathLike) -> PathLike:
316316

317317

318318
def _is_path_rooted(path: PathLike) -> bool:
319-
r"""Whether ``path`` has a root component after any drive.
319+
r"""Whether ``path`` has a root, including one encoded in a UNC drive.
320320
321321
On Windows, ``\directory`` is rooted on the current drive without being
322322
absolute, while ``C:\directory`` has both a drive and a root. In contrast,
323323
``directory`` and the drive-relative ``C:directory`` have no root.
324+
UNC paths are rooted: ``\\server\share`` stores the share in the drive
325+
returned by :func:`os.path.splitdrive`, while ``\\server\share\directory``
326+
additionally has a rooted tail.
324327
On POSIX, which has no drive concept, this simply distinguishes absolute
325328
paths such as ``/directory`` from relative paths such as ``directory``.
326329
"""
327-
_drive, tail = osp.splitdrive(os.fspath(path))
330+
drive, tail = osp.splitdrive(os.fspath(path))
328331
separators = (os.sep,) if os.altsep is None else (os.sep, os.altsep)
329-
return tail.startswith(separators)
332+
return tail.startswith(separators) or drive.startswith(separators)
330333

331334

332335
def _to_relative_path(root: PathLike, path: PathLike) -> str:

test/test_config.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io
88
import os
99
import os.path as osp
10+
import sys
1011
from unittest import mock
1112

1213
import pytest
@@ -430,6 +431,7 @@ def test_multiple_include_paths_with_same_key(self, rw_dir):
430431
def test_conditional_includes_from_git_dir(self, rw_dir):
431432
# Initiate repository path.
432433
git_dir = osp.join(rw_dir, "target1", "repo1")
434+
git_dir_pattern = git_dir.replace("\\", "/")
433435
os.makedirs(git_dir)
434436

435437
# Initiate mocked repository.
@@ -441,37 +443,42 @@ def test_conditional_includes_from_git_dir(self, rw_dir):
441443
template = '[includeIf "{}:{}"]\n path={}\n'
442444

443445
with open(path1, "w") as stream:
446+
# on Windows, this writes a backslash pattern.
444447
stream.write(template.format("gitdir", git_dir, path2))
445448

446449
# Ensure that config is ignored if no repo is set.
447450
with GitConfigParser(path1) as config:
448451
assert not config._has_includes()
449452
assert config._included_paths() == []
450453

451-
# Ensure that config is included if path is matching git_dir.
452-
with GitConfigParser(path1, repo=repo) as config:
453-
assert config._has_includes()
454-
assert config._included_paths() == [("path", path2)]
454+
# Git uses forward slashes in gitdir patterns on every platform:
455+
# backslashes escape the next pattern character rather than separate
456+
# path components. On Windows, GitPython therefore normalizes git_dir
457+
# to forward slashes but leaves this backslash pattern unchanged, so
458+
# the two do not match and no path is included.
459+
with GitConfigParser(path1, repo=repo, merge_includes=False) as config:
460+
expected_paths = [] if sys.platform == "win32" else [("path", path2)]
461+
assert config._included_paths() == expected_paths
455462

456463
# Ensure that Git's forward-slash syntax matches native Windows paths.
457464
with open(path1, "w") as stream:
458-
stream.write(template.format("gitdir", git_dir.replace("\\", "/"), path2))
465+
stream.write(template.format("gitdir", git_dir_pattern, path2))
459466

460467
with GitConfigParser(path1, repo=repo) as config:
461468
assert config._has_includes()
462469
assert config._included_paths() == [("path", path2)]
463470

464471
# Ensure that config is ignored if case is incorrect.
465472
with open(path1, "w") as stream:
466-
stream.write(template.format("gitdir", git_dir.upper(), path2))
473+
stream.write(template.format("gitdir", git_dir_pattern.upper(), path2))
467474

468475
with GitConfigParser(path1, repo=repo) as config:
469476
assert not config._has_includes()
470477
assert config._included_paths() == []
471478

472479
# Ensure that config is included if case is ignored.
473480
with open(path1, "w") as stream:
474-
stream.write(template.format("gitdir/i", git_dir.upper(), path2))
481+
stream.write(template.format("gitdir/i", git_dir_pattern.upper(), path2))
475482

476483
with GitConfigParser(path1, repo=repo) as config:
477484
assert config._has_includes()
@@ -501,6 +508,20 @@ def test_conditional_includes_from_git_dir(self, rw_dir):
501508
assert config._has_includes()
502509
assert config._included_paths() == [("path", path2)]
503510

511+
@with_rw_directory
512+
def test_conditional_includes_do_not_treat_backslashes_as_separators(self, rw_dir):
513+
git_dir = osp.join(rw_dir, "target", "repo")
514+
repo = mock.Mock(git_dir=git_dir)
515+
config_path = osp.join(rw_dir, "config")
516+
included_path = osp.join(rw_dir, "included")
517+
pattern = git_dir.replace("\\", "/").replace("/target/repo", R"/target\repo")
518+
519+
with open(config_path, "w") as stream:
520+
stream.write(f'[includeIf "gitdir:{pattern}"]\n path={included_path}\n')
521+
522+
with GitConfigParser(config_path, repo=repo, merge_includes=False) as config:
523+
assert config._included_paths() == []
524+
504525
@with_rw_directory
505526
def test_conditional_includes_from_branch_name(self, rw_dir):
506527
# Initiate mocked branch.

test/test_index.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,15 @@ class Mocked:
10901090
actual_path = index._to_relative_path(path)
10911091
self.assertEqual(expected_path, actual_path)
10921092

1093+
@pytest.mark.skipif(sys.platform != "win32", reason="Specifically for Windows.")
1094+
def test__to_relative_path_windows_unc_share_root(self):
1095+
for repo_root in [R"\\server\share", R"\\?\UNC\server\share"]:
1096+
with self.subTest(repo_root=repo_root):
1097+
repo = mock.Mock(bare=False, git_dir=repo_root, working_tree_dir=repo_root)
1098+
index = IndexFile(repo)
1099+
1100+
self.assertEqual(index._to_relative_path(repo_root), ".")
1101+
10931102
@pytest.mark.skipif(sys.platform != "win32", reason="Specifically for Windows.")
10941103
@with_rw_directory
10951104
def test__to_relative_path_windows_path_kinds(self, rw_dir):

0 commit comments

Comments
 (0)