repos: find submodule git directories the way git does

A submodule git directory which borrows another (commondir) has no
objects of its own, and was skipped instead of refused, because
"has objects/" decided what a git directory was.  The walk now follows
git's layout: under .git/modules every directory holding HEAD is a git
directory (whatever the submodule's name, even `refs`), which must not
borrow, must have its own objects and a regular config, and is
sanitized; nested submodules are found in its own `modules`, and its
refs and logs are never mistaken for git directories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Roland 2026-10-06 18:28:31 -06:00
parent 9e8947767b
commit a1d4c42eee
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 60 additions and 6 deletions

View file

@ -443,12 +443,26 @@ def sanitize_git_config(directory: pathlib.Path):
if path.is_symlink():
# git would follow it out of the repo.
raise ValueError(f'{path} is a link')
for submodule_git_dir in sorted({i.parent for i in modules.rglob('HEAD')}):
if not (submodule_git_dir / 'objects').is_dir():
continue # e.g. refs/remotes/origin/HEAD, a ref, not a git directory.
if borrowed := _borrowing_git_file(submodule_git_dir):
raise ValueError(f'{submodule_git_dir} uses another git directory ({borrowed})')
_sanitize_config_file(submodule_git_dir / 'config', worktree_root=root)
_sanitize_submodule_git_dirs(modules, root)
def _sanitize_submodule_git_dirs(modules: pathlib.Path, root: pathlib.Path):
"""Check and sanitize every git directory in a `modules` directory, as git finds them: a directory holding HEAD
is a submodule's git directory (a submodule's name may contain `/`), and its nested submodules are in its own
`modules`. Its refs and logs (which also hold files named HEAD) are not git directories."""
for child in sorted(modules.iterdir()):
if not child.is_dir():
continue
if not (child / 'HEAD').exists():
_sanitize_submodule_git_dirs(child, root)
continue
if borrowed := _borrowing_git_file(child):
raise ValueError(f'{child} uses another git directory ({borrowed})')
if not (child / 'objects').is_dir():
raise ValueError(f'{child} has no objects of its own')
_sanitize_config_file(child / 'config', worktree_root=root)
if (child / 'modules').is_dir():
_sanitize_submodule_git_dirs(child / 'modules', root)
def find_readme(directory: pathlib.Path) -> Optional[str]:

View file

@ -308,3 +308,43 @@ def test_submodule_git_directories_pass(tmp_path):
(git_dir / 'config').write_text('[core]\n\tbare = false\n')
lib.sanitize_git_config(repo)
assert 'proxy' not in (module / 'config').read_text()
@pytest.mark.parametrize('name', ['library', 'refs', 'vendor/library'])
def test_submodule_git_directory_without_objects_is_refused(tmp_path, name):
"""A git directory which borrows another (commondir) has no objects of its own; it is refused, not skipped,
whatever the submodule is named."""
repo = tmp_path / 'repo'
git_dir = repo / '.git'
module = git_dir / 'modules' / name
module.mkdir(parents=True)
(git_dir / 'config').write_text('[core]\n\tbare = false\n')
(module / 'HEAD').write_text('ref: refs/heads/main\n')
(module / 'commondir').write_text(f'{tmp_path}\n')
with pytest.raises(ValueError):
lib.sanitize_git_config(repo)
def test_nested_submodule_git_directories(tmp_path):
"""A nested submodule's git directory is checked too; refs and logs named HEAD are not git directories."""
repo = tmp_path / 'repo'
git_dir = repo / '.git'
(git_dir).mkdir(parents=True)
(git_dir / 'config').write_text('[core]\n\tbare = false\n')
outer = git_dir / 'modules/library'
inner = outer / 'modules/nested'
for gitdir in (outer, inner):
(gitdir / 'objects').mkdir(parents=True)
(gitdir / 'HEAD').write_text('ref: refs/heads/main\n')
(gitdir / 'config').write_text('[core]\n\tbare = false\n[http]\n\tproxy = evil\n')
(gitdir / 'refs/remotes/origin').mkdir(parents=True)
(gitdir / 'refs/remotes/origin/HEAD').write_text('ref: refs/remotes/origin/main\n')
(gitdir / 'logs').mkdir()
(gitdir / 'logs/HEAD').write_text('0000 1111 t <t@t> 0 +0000\tclone\n')
lib.sanitize_git_config(repo)
assert 'proxy' not in (outer / 'config').read_text()
assert 'proxy' not in (inner / 'config').read_text()
(inner / 'commondir').write_text(f'{tmp_path}\n')
with pytest.raises(ValueError):
lib.sanitize_git_config(repo)