Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions scripts/repo_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,9 +868,18 @@ def load_yaml(path: Path) -> Any:
# ---------------------------------------------------------------------------


def git_out(args: list[str], cwd: Path, binary: bool = False) -> Any:
def git_out(args: list[str], cwd: Path, binary: bool = False,
env: dict[str, str] | None = None) -> Any:
"""`git <args>` in `cwd`, its stdout, or a `git-failed` refusal.

`env` is the whole environment git runs in, None for this process's
own. A git that cannot be STARTED (none on PATH) raises `OSError` from
here, never a refusal: whether that is a question the caller cannot
answer is the caller's to say (#368).
"""
proc = subprocess.run(
["git", *args], cwd=str(cwd), capture_output=True, check=False
["git", *args], cwd=str(cwd), capture_output=True, check=False,
env=env,
)
if proc.returncode != 0:
raise Refusal(
Expand Down Expand Up @@ -1030,11 +1039,29 @@ def file_sha256(path: Path) -> str:
#: `<mode> SP <oid> SP <stage> TAB <path>` and `ls-tree` emits
#: `<mode> SP <type> SP <oid> TAB <path>` — the same shape with a DIFFERENT
#: third column, so the two cannot share one index.
#:
#: BOTH LISTINGS TAKE THE PATH LITERALLY (#367), with the global
#: `--literal-pathspecs`. A pathspec is a glob to `ls-files`, so a row whose
#: hand-edited `path:` was `members/*` read the first member's gitlink and
#: answered with it, a neighbour's commit, for every caller. Not
#: `:(literal)` magic on the path, which a caller's own exported
#: `GIT_LITERAL_PATHSPECS=1` switches off, and then it matches nothing.
GITLINK_LISTINGS = (
(["ls-files", "-s", "--"], 1, 2),
(["ls-tree", "HEAD", "--"], 2, None),
(["--literal-pathspecs", "ls-files", "-s", "--"], 1, 2),
(["--literal-pathspecs", "ls-tree", "HEAD", "--"], 2, None),
)

#: The two pathspec settings a caller's environment can carry that git
#: refuses BESIDE `--literal-pathspecs` ("global 'literal' pathspec setting
#: is incompatible with all other global pathspec settings", exit 128). Both
#: listings would then fail, and `recorded_gitlink` reads a failed listing as
#: "no gitlink here", so an operator who exports either would see every gate
#: refuse a gitlink that is there. They are left out of those two git calls
#: alone. `GIT_NOGLOB_PATHSPECS` stays: git takes it beside the literal
#: setting, and with it the path is literal anyway.
PATHSPEC_ENV_REFUSED_BESIDE_LITERAL = ("GIT_GLOB_PATHSPECS",
"GIT_ICASE_PATHSPECS")


def recorded_gitlink(repo: Path, path: str) -> str | None:
"""The commit the SUPERPROJECT records for the submodule at `path`.
Expand All @@ -1049,10 +1076,17 @@ def recorded_gitlink(repo: Path, path: str) -> str | None:
bump and passes a stale pin. HEAD remains the fallback, for a path the
commit records and the index does not hold at stage 0 — one the index
never had, and one a conflicted merge holds only at stages 1, 2 and 3.

`path` IS TAKEN LITERALLY (#367): `members/*` is the entry of that name,
which no index holds, and never every gitlink a glob would match. A git
that cannot be started raises `OSError` through this, never "no
gitlink": only a listing git RAN and refused is read as nothing (#368).
"""
env = {name: value for name, value in os.environ.items()
if name.upper() not in PATHSPEC_ENV_REFUSED_BESIDE_LITERAL}
for args, oid_at, stage_at in GITLINK_LISTINGS:
try:
out = git_out([*args, path], cwd=repo)
out = git_out([*args, path], cwd=repo, env=env)
except Refusal:
continue
for line in out.splitlines():
Expand Down
Loading
Loading