diff --git a/modules/repos/lib.py b/modules/repos/lib.py index fa40d1b0..1870dfcf 100644 --- a/modules/repos/lib.py +++ b/modules/repos/lib.py @@ -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]: diff --git a/modules/repos/test/test_ownership.py b/modules/repos/test/test_ownership.py index 604be392..07832908 100644 --- a/modules/repos/test/test_ownership.py +++ b/modules/repos/test/test_ownership.py @@ -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 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)