Skip to content

Commit b83fcb8

Browse files
b84330808claude
andcommitted
fix(config): match ~/ gitdir includes on Windows
On Windows, `GitConfigParser` never applied an `[includeIf "gitdir:~/..."]` section, although `git config` does. `os.path.expanduser` returns the home directory with backslashes, so `gitdir:~/work/` became the pattern `C:\Users\me/work/**`. GitPython compares gitdir patterns with the repository's `git_dir` written with forward slashes, and in a gitdir pattern a backslash escapes the next character instead of separating path components, so the pattern could never match. Git converts the home directory to forward slashes when it expands `~` on Windows (`interpolate_path()` in `path.c`, which `prepare_include_condition_pattern()` in `config.c` uses for gitdir conditions). A usual setup such as [includeIf "gitdir:~/work/"] path = ~/.gitconfig-work therefore made GitPython read `user.email` and other settings for a repository under `~/work` from the global configuration only, while Git read them from the included file. On Windows, a pattern that is `~` or starts with `~/` now gets the home directory with forward slashes. The rest of the pattern is unchanged, so a backslash written in the pattern still escapes the next character, as `test_conditional_includes_do_not_treat_backslashes_as_separators` expects. Other platforms and `~user` patterns keep the previous `os.path.expanduser` call. `test_conditional_includes_expand_home_in_git_dir_pattern` points the home directory at a temporary directory and checks that `gitdir:~/target/` matches a repository below it. On Windows it fails without this change and passes with it. On other platforms it passes either way. Validated on Windows 11 with Git for Windows 2.51.2 and Python 3.11.9: `test/test_config.py` passes (55 passed, 3 skipped); `ruff check`, `ruff format --check`, `mypy` and `basedpyright --warnings` report no issues. For a repository whose `.git/config` includes a file through `gitdir:~/...`, `git config --get user.email` returns the included value, and `Repo.config_reader("repository").get_value("user", "email")` now returns it too instead of finding no value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ccc4811 commit b83fcb8

2 files changed

Lines changed: 22 additions & 1 deletion

File tree

‎git/config.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,12 @@ def _all_items(section: str) -> List[Tuple[str, str]]:
730730
value = match.group(2).strip()
731731

732732
if keyword in ["gitdir", "gitdir/i"]:
733-
value = osp.expanduser(value)
733+
if sys.platform == "win32" and (value == "~" or value.startswith("~/")):
734+
# Like Git, substitute the home directory with forward slashes, as a
735+
# backslash in the pattern would escape the next character.
736+
value = osp.expanduser("~").replace("\\", "/") + value[1:]
737+
else:
738+
value = osp.expanduser(value)
734739
git_dir = os.fspath(self._repo.git_dir) if self._repo.git_dir else None
735740
if sys.platform == "win32":
736741
git_dir = git_dir.replace("\\", "/") if git_dir else None

‎test/test_config.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,22 @@ def test_conditional_includes_do_not_treat_backslashes_as_separators(self, rw_di
989989
with GitConfigParser(config_path, repo=repo, merge_includes=False) as config:
990990
assert config._included_paths() == []
991991

992+
@with_rw_directory
993+
def test_conditional_includes_expand_home_in_git_dir_pattern(self, rw_dir):
994+
git_dir = osp.join(rw_dir, "target", "repo", ".git")
995+
repo = mock.Mock(git_dir=git_dir)
996+
config_path = osp.join(rw_dir, "config")
997+
included_path = osp.join(rw_dir, "included")
998+
999+
with open(config_path, "w") as stream:
1000+
stream.write(f'[includeIf "gitdir:~/target/"]\n path={included_path}\n')
1001+
1002+
# On Windows, the home directory has backslashes, but Git matches it as if it
1003+
# were written with forward slashes.
1004+
with mock.patch.dict(os.environ, {"HOME": rw_dir, "USERPROFILE": rw_dir}):
1005+
with GitConfigParser(config_path, repo=repo, merge_includes=False) as config:
1006+
assert config._included_paths() == [("path", included_path)]
1007+
9921008
@with_rw_directory
9931009
def test_conditional_includes_from_branch_name(self, rw_dir):
9941010
# Initiate mocked branch.

0 commit comments

Comments
 (0)