Skip to content

Commit 5e4b33f

Browse files
committed
fix: recognize git worktree root in find_vcs_root()
In a git worktree (or a submodule) the ".git" entry is a file containing a "gitdir: <path>" pointer rather than a directory. `_is_valid_vcs_dir()` only accepted directories, so `find_vcs_root()` walked past the worktree root and failed to locate the project root, breaking `molecule.yml` discovery for users working inside a worktree. Treat a ".git" file as a valid VCS root marker when it contains a "gitdir:" pointer, while still rejecting unrelated stray ".git" files. Refs: #4142
1 parent ac6bba5 commit 5e4b33f

3 files changed

Lines changed: 109 additions & 8 deletions

File tree

.config/dictionary.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Ulimits
2222
Unittests
2323
Virt
2424
Virt's
25+
Worktree
2526
XDIST
2627
antsibull
2728
anyconfig
@@ -63,6 +64,7 @@ followlinks
6364
fooo
6465
frob
6566
getfixturevalue
67+
gitdir
6668
goss
6769
hookwrapper
6870
hostvars
@@ -144,6 +146,7 @@ vfoo
144146
virtenv
145147
virtualmachines
146148
webservers
149+
worktree
147150
worktrees
148151
yourcollection
149152
yournamespace

src/molecule/util.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -525,23 +525,30 @@ def _filter_platforms(
525525

526526

527527
def _is_valid_vcs_dir(path: Path, name: str) -> bool:
528-
"""Check if a VCS directory is a genuine repository root.
528+
"""Check if a VCS entry marks a genuine repository root.
529529
530530
Args:
531531
path: Parent directory to check.
532-
name: VCS directory name (e.g. ".git", ".hg", ".svn").
532+
name: VCS entry name (e.g. ".git", ".hg", ".svn").
533533
534534
Returns:
535-
Whether the VCS directory is a genuine repository root.
536-
"""
537-
vcs_dir = path / name
538-
if not vcs_dir.is_dir():
535+
Whether the VCS entry marks a genuine repository root.
536+
"""
537+
vcs_entry = path / name
538+
# In a git worktree (or a submodule) the ".git" entry is a file
539+
# containing a "gitdir: <path>" pointer rather than a directory.
540+
if name == ".git" and vcs_entry.is_file():
541+
try:
542+
return vcs_entry.read_text(encoding="utf-8").startswith("gitdir:")
543+
except OSError:
544+
return False
545+
if not vcs_entry.is_dir():
539546
return False
540547
# A real .git directory always contains a HEAD file.
541-
# Bare repos, worktrees, and regular repos all have it.
548+
# Bare repos and regular repos all have it.
542549
# Spurious .git dirs (e.g. created by GitKraken) do not.
543550
if name == ".git":
544-
return (vcs_dir / "HEAD").exists()
551+
return (vcs_entry / "HEAD").exists()
545552
return True
546553

547554

tests/unit/test_util.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,97 @@ def test_find_vcs_root_skips_fake_git_dir(tmp_path: Path) -> None:
11311131
assert result == str(repo)
11321132

11331133

1134+
def test_find_vcs_root_in_git_worktree(tmp_path: Path) -> None:
1135+
"""Ensure find_vcs_root recognizes a worktree where .git is a file.
1136+
1137+
In a git worktree the ".git" entry is a file containing a
1138+
"gitdir: <path>" pointer rather than a directory.
1139+
1140+
Args:
1141+
tmp_path: pytest fixture for temporary directory.
1142+
"""
1143+
# Worktree root: .git is a file pointing at the main repo's gitdir.
1144+
worktree = tmp_path / "worktree"
1145+
worktree.mkdir()
1146+
git_file = worktree / ".git"
1147+
git_file.write_text(f"gitdir: {tmp_path / 'main' / '.git' / 'worktrees' / 'wt'}\n")
1148+
1149+
subdir = worktree / "infra"
1150+
subdir.mkdir()
1151+
1152+
util.find_vcs_root.cache_clear()
1153+
result = util.find_vcs_root(location=str(subdir))
1154+
assert result == str(worktree)
1155+
1156+
1157+
def test_find_vcs_root_skips_bogus_git_file(tmp_path: Path) -> None:
1158+
"""Ensure find_vcs_root ignores a .git file without a gitdir pointer.
1159+
1160+
Args:
1161+
tmp_path: pytest fixture for temporary directory.
1162+
"""
1163+
# Real repo at top level.
1164+
repo = tmp_path / "repo"
1165+
repo.mkdir()
1166+
git_dir = repo / ".git"
1167+
git_dir.mkdir()
1168+
(git_dir / "HEAD").write_text("ref: refs/heads/main\n")
1169+
1170+
# Subdirectory with a spurious .git file that is not a worktree pointer.
1171+
subdir = repo / "infra"
1172+
subdir.mkdir()
1173+
(subdir / ".git").write_text("not a gitdir pointer\n")
1174+
1175+
util.find_vcs_root.cache_clear()
1176+
result = util.find_vcs_root(location=str(subdir))
1177+
assert result == str(repo)
1178+
1179+
1180+
def test_find_vcs_root_skips_unreadable_git_file(
1181+
tmp_path: Path,
1182+
monkeypatch: pytest.MonkeyPatch,
1183+
) -> None:
1184+
"""Ensure find_vcs_root ignores a .git file that cannot be read.
1185+
1186+
If reading the ".git" file raises OSError (e.g. permission denied),
1187+
the entry should be skipped rather than crash.
1188+
1189+
Args:
1190+
tmp_path: pytest fixture for temporary directory.
1191+
monkeypatch: pytest fixture for patching attributes.
1192+
"""
1193+
# Real repo at top level.
1194+
repo = tmp_path / "repo"
1195+
repo.mkdir()
1196+
git_dir = repo / ".git"
1197+
git_dir.mkdir()
1198+
(git_dir / "HEAD").write_text("ref: refs/heads/main\n")
1199+
1200+
# Subdirectory with a .git file that will fail to read.
1201+
subdir = repo / "infra"
1202+
subdir.mkdir()
1203+
unreadable = subdir / ".git"
1204+
unreadable.write_text("gitdir: /somewhere\n")
1205+
1206+
original_read_text = Path.read_text
1207+
1208+
def raise_for_target(
1209+
self: Path,
1210+
encoding: str | None = None,
1211+
errors: str | None = None,
1212+
) -> str:
1213+
if self == unreadable:
1214+
msg = "simulated permission error"
1215+
raise PermissionError(msg)
1216+
return original_read_text(self, encoding=encoding, errors=errors)
1217+
1218+
monkeypatch.setattr(Path, "read_text", raise_for_target)
1219+
1220+
util.find_vcs_root.cache_clear()
1221+
result = util.find_vcs_root(location=str(subdir))
1222+
assert result == str(repo)
1223+
1224+
11341225
@pytest.mark.parametrize(
11351226
("input_value", "expected"),
11361227
(

0 commit comments

Comments
 (0)