|
| 1 | +# This module is part of GitPython and is released under the |
| 2 | +# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ |
| 3 | + |
| 4 | +"""Verify that fixture directories are usable by git. |
| 5 | +
|
| 6 | +If a fixture directory is missing, isn't an initialized git repository, |
| 7 | +or is rejected by git for "dubious ownership", dependent tests |
| 8 | +elsewhere in the suite fail in opaque ways. The checks here name the |
| 9 | +preconditions directly so a misconfigured environment is recognizable |
| 10 | +from the test output rather than from a cascade of unrelated-seeming |
| 11 | +failures. |
| 12 | +
|
| 13 | +These tests do not exercise GitPython's production code. They verify |
| 14 | +the conditions under which production code is exercised are valid. |
| 15 | +""" |
| 16 | + |
| 17 | +import subprocess |
| 18 | +from pathlib import Path |
| 19 | + |
| 20 | +import pytest |
| 21 | + |
| 22 | +REPO_ROOT = Path(__file__).resolve().parent.parent |
| 23 | + |
| 24 | +# Directories git must trust for the test suite to operate normally. The |
| 25 | +# current set is the GitPython working tree plus the working trees of its |
| 26 | +# gitdb submodule and the smmap submodule nested inside gitdb. New entries |
| 27 | +# should be added here whenever the test suite gains a dependency on git |
| 28 | +# accepting another directory. |
| 29 | +FIXTURE_DIRS = [ |
| 30 | + pytest.param(REPO_ROOT, id="repo_root"), |
| 31 | + pytest.param(REPO_ROOT / "git" / "ext" / "gitdb", id="gitdb"), |
| 32 | + pytest.param( |
| 33 | + REPO_ROOT / "git" / "ext" / "gitdb" / "gitdb" / "ext" / "smmap", |
| 34 | + id="smmap", |
| 35 | + ), |
| 36 | +] |
| 37 | + |
| 38 | +# Submodule working trees that must be present and initialized for the |
| 39 | +# test suite to operate normally: gitdb at `git/ext/gitdb`, and smmap |
| 40 | +# nested inside gitdb at `git/ext/gitdb/gitdb/ext/smmap`. The paths |
| 41 | +# below are anchored at REPO_ROOT (the GitPython source tree), not at |
| 42 | +# any rorepo redirection target. |
| 43 | +SUBMODULE_DIRS = [ |
| 44 | + pytest.param(REPO_ROOT / "git" / "ext" / "gitdb", id="gitdb"), |
| 45 | + pytest.param( |
| 46 | + REPO_ROOT / "git" / "ext" / "gitdb" / "gitdb" / "ext" / "smmap", |
| 47 | + id="smmap", |
| 48 | + ), |
| 49 | +] |
| 50 | + |
| 51 | + |
| 52 | +@pytest.mark.parametrize("fixture_dir", FIXTURE_DIRS) |
| 53 | +def test_fixture_dir_is_trusted_by_git(fixture_dir: Path) -> None: |
| 54 | + """git accepts ``fixture_dir`` as its own repository owned by a trusted user. |
| 55 | +
|
| 56 | + Run ``git -C <fixture_dir> rev-parse --show-toplevel`` and assert it |
| 57 | + succeeds and reports ``fixture_dir`` itself as the toplevel. Failure |
| 58 | + typically means the directory's on-disk ownership doesn't match the |
| 59 | + running user and the CI workflow's ``safe.directory`` list is missing |
| 60 | + an entry that would override the check. |
| 61 | + """ |
| 62 | + if not fixture_dir.exists(): |
| 63 | + pytest.skip(f"{fixture_dir} not present (run `git submodule update --init --recursive` from the repo root)") |
| 64 | + if not (fixture_dir / ".git").exists(): |
| 65 | + pytest.skip( |
| 66 | + f"{fixture_dir} has no .git marker " |
| 67 | + "(submodule not initialized; run " |
| 68 | + "`git submodule update --init --recursive` from the repo root)" |
| 69 | + ) |
| 70 | + try: |
| 71 | + result = subprocess.run( |
| 72 | + ["git", "-C", str(fixture_dir), "rev-parse", "--show-toplevel"], |
| 73 | + capture_output=True, |
| 74 | + text=True, |
| 75 | + check=False, |
| 76 | + ) |
| 77 | + except FileNotFoundError: |
| 78 | + pytest.skip("git is not installed or not on PATH") |
| 79 | + assert result.returncode == 0, ( |
| 80 | + f"git refuses to operate in {fixture_dir}.\n" |
| 81 | + f"stderr: {result.stderr.strip()}\n" |
| 82 | + "The directory's owner doesn't match the running user and no " |
| 83 | + "`safe.directory` entry overrides the check. On CI, the " |
| 84 | + "workflow's `safe.directory` list typically needs an entry for " |
| 85 | + "this path. Locally, this is unexpected and usually indicates " |
| 86 | + "an ownership problem worth investigating." |
| 87 | + ) |
| 88 | + reported = Path(result.stdout.strip()) |
| 89 | + assert reported.samefile(fixture_dir), ( |
| 90 | + f"git reports the toplevel as {reported}, " |
| 91 | + f"not as {fixture_dir} itself. " |
| 92 | + "This usually means the directory is not an initialized git " |
| 93 | + "repository (its `.git` marker may be stale or pointing elsewhere)." |
| 94 | + ) |
| 95 | + |
| 96 | + |
| 97 | +@pytest.mark.parametrize("submodule_dir", SUBMODULE_DIRS) |
| 98 | +def test_required_submodule_is_initialized(submodule_dir: Path) -> None: |
| 99 | + """The submodule's working tree is present and initialized. |
| 100 | +
|
| 101 | + Failure means the source tree is a git clone but the submodule's |
| 102 | + working tree hasn't been populated. Skipped when the source tree |
| 103 | + itself isn't a git clone (e.g. an extracted release tarball), since |
| 104 | + ``git submodule update`` cannot operate there; setups that handle |
| 105 | + submodules in a separately-prepared tree (via |
| 106 | + ``GIT_PYTHON_TEST_GIT_REPO_BASE``) are exempted from this check. |
| 107 | + """ |
| 108 | + if not (REPO_ROOT / ".git").exists(): |
| 109 | + pytest.skip( |
| 110 | + "Source tree is not a git clone (no .git in REPO_ROOT); submodules " |
| 111 | + "cannot be initialized via `git submodule update` here. Setups " |
| 112 | + "that prepare submodules in a separately-pointed tree (via " |
| 113 | + "GIT_PYTHON_TEST_GIT_REPO_BASE) are exempted from this check." |
| 114 | + ) |
| 115 | + # The assertion messages below recommend `git submodule update --init |
| 116 | + # --recursive` rather than `init-tests-after-clone.sh`, even though the |
| 117 | + # latter is the documented entry point for first-time test setup. Two |
| 118 | + # reasons: the script performs `git reset --hard` operations that can |
| 119 | + # destroy local work, and #1713 showed the script itself can carry |
| 120 | + # submodule-init regressions, in which case recommending it would lead |
| 121 | + # developers in a circle. The direct git command is a safe minimal fix |
| 122 | + # for this test's specific failure mode and bypasses any such regression. |
| 123 | + assert submodule_dir.is_dir(), ( |
| 124 | + f"Submodule working tree missing: {submodule_dir}.\n" |
| 125 | + "Run `git submodule update --init --recursive` from the repo root." |
| 126 | + ) |
| 127 | + assert (submodule_dir / ".git").exists(), ( |
| 128 | + f"Submodule directory exists but has no .git marker: {submodule_dir}.\n" |
| 129 | + "The submodule hasn't been initialized. " |
| 130 | + "Run `git submodule update --init --recursive` from the repo root." |
| 131 | + ) |
0 commit comments