Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c2f2884
validate-family.py and family.py: a row's gitlink that no section of …
Oct 15, 2026
6f938ed
tests/test_family_head_lost_gitmodules.py: the rows `member-gitmodule…
Oct 15, 2026
6aace42
family.py and validate-family.py: `remove` refuses a holder with no `…
Oct 16, 2026
08b6245
Merge main 95780dd0 (#637, #607) into fix/611-validate-family-head-gi…
Oct 16, 2026
d660ec5
bootstrap.py and validate-family.py: a member no credential would che…
Oct 16, 2026
852c5c4
Merge #639's head 08b6245b (main 95780dd0, #637, #607) into fix/648-b…
Oct 16, 2026
159a08f
Merge branch 'main' into fix/611-validate-family-head-gitmodules
brettheap Oct 16, 2026
431aaf3
Merge #639's final head 159a08f0 (main 24769920 merged into it) into …
Oct 16, 2026
8b39cd1
Merge main c6122e8e, #639's squash, into fix/648-bootstrap-and-gate-o…
Oct 16, 2026
bad6c00
Merge main 2fac9790 into fix/648-bootstrap-and-gate-order: #634's per…
Oct 16, 2026
1a69505
bootstrap.py: a plain clone's update that dies at a gitlink no sectio…
Oct 16, 2026
45c512e
tests/test_doctor_legs_members_next.py: the bootstrap that cures noth…
Oct 16, 2026
23e1105
bootstrap.py: `.gitmodules` is read as git's own parser reads it, a m…
Oct 17, 2026
f74f640
bootstrap.py: a `.gitmodules` a merge left unmerged in the index is i…
Oct 17, 2026
5afb24b
Merge branch 'main' into fix/648-bootstrap-and-gate-order
brettheap Oct 17, 2026
fa69ead
bootstrap.py and validate-family.py: a `.gitmodules` value git dies o…
Oct 17, 2026
3c6a859
Merge #682's final head 5afb24b9 (landed as dc7d3fb)
Oct 17, 2026
c12b5f8
Merge main dc7d3fbb.. after #682 landed as dc7d3fb
Oct 17, 2026
e1379a9
bootstrap.py, validate-family.py and tests/test_family_gitmodules_val…
Oct 17, 2026
e32ed55
Merge origin/main (`b9b70692`) into fix/768-gitmodules-values: #820 (…
Oct 18, 2026
233a334
tests/test_family_gitmodules_values.py: a staged `git rm --cached` be…
Oct 18, 2026
20f74cb
the family root's bootstrap.py and validate-family.py: the credential…
Oct 18, 2026
77f8a6c
validate-family.py: `_check_gitlinks_registered` is handed `refused` …
Oct 18, 2026
a7f6c56
Merge #819's squash 1eff1fd after it landed, no conflicts
Oct 18, 2026
c3f8fa5
validate-family.py: `gitmodules-path-unnamed` takes the gitlink at a …
Oct 18, 2026
ad351e4
Merge branch 'main' into fix/839-holder-bootstrap-texts
brettheap Oct 18, 2026
fd0f8d9
Merge #876's final head ad351e4f (a7f6c569 and main def45de), before …
Oct 18, 2026
7d19507
Merge #876's squash 41baf92, ours for its files (its tree is F's)
Oct 18, 2026
445b1e5
validate-family.py and bootstrap.py: the holder's remedies, typed as …
Oct 18, 2026
9677ef9
Merge branch 'main' into fix/878-unnamed-gitlink-remedy
brettheap Oct 18, 2026
3965c5b
Merge #882's final head 9677ef91 (7d195075 refreshed over main 33c512…
Oct 18, 2026
e396411
Merge #882's squash 8148a38, ours for its files (its tree is F's)
Oct 18, 2026
8ea0c44
Merge main c9ca9a45 (#877's squash, after #769 and #845), bootstrap.p…
Oct 18, 2026
3b15de0
validate-family.py and bootstrap.py: a staged removal's section writt…
Oct 18, 2026
29083a5
validate-family.py: a staged removal writes no section into a `.gitmo…
Oct 18, 2026
32a5899
validate-family.py: a hand mount's text quotes both of git's refusals…
Oct 18, 2026
86ce601
validate-family.py: a member's checkout, a shape copy, the shape pin …
Oct 18, 2026
1245a3b
validate-family.py: a member's checkout, or what is on disk at a gitl…
Oct 18, 2026
4f831a9
validate-family.py: `git-not-runnable` counts only an `OSError` that …
Oct 18, 2026
d81b870
Merge branch 'main' into fix/879-holder-remedies-compose
brettheap Oct 18, 2026
b41f722
Merge branch 'main' into fix/879-holder-remedies-compose
brettheap Oct 18, 2026
10c5d82
Merge #889's final head b41f722b into #956: `_staged_over` keeps #889…
Oct 18, 2026
0b1413f
Merge #889's squash 702cc23d into #956, ours for its files, so #889's…
Oct 18, 2026
a2e818e
Merge #956's restacked head 0b1413f2 into #983, so #928's change stan…
Oct 19, 2026
dcec55e
validate-family.py: where HEAD records a gitlink at the index's own c…
Oct 19, 2026
741dc4d
Merge main dd2cf2e8 into #956 round 2, with no conflict, so #956 is t…
Oct 19, 2026
f3a2c74
Merge branch 'main' into fix/430-e-validate-family-unreadable
brettheap Oct 19, 2026
23cdb8a
Merge #956's round-2 head 741dc4d9 into #983: `_check_member_checkout…
Oct 19, 2026
d1ac2b2
Merge #956's final head f3a2c746 into #983: the first of three merges…
Oct 19, 2026
840d2a0
Merge #956's squash 3864b18 into #983, keeping #983's side of validat…
Oct 19, 2026
fc752a7
Merge branch 'main' into fix/928-git-not-runnable-member-oserror
brettheap Oct 20, 2026
ba96867
Merge branch 'main' into fix/928-git-not-runnable-member-oserror
brettheap Oct 20, 2026
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
106 changes: 73 additions & 33 deletions templates/family-root/scripts/validate-family.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,19 +314,45 @@ def _git_runs(unread: str) -> Iterator[None]:

`git_out` raises `OSError` when there is no git to run, and nothing here
caught it: the run ended in a `FileNotFoundError` traceback, which exits
1, a FINDING's code, though nothing was found. So every check that runs
git, in every mode, runs it inside this, and `unread` says which check
it was and what it could not read. ONE code for the whole validator,
1, a FINDING's code, though nothing was found. So every call that runs
git, in every mode, runs inside this, and `unread` says which check it
was and what it could not read. ONE code for the whole validator,
whichever check met the missing git first: it is one fact about the
machine, and `gitmodules-unreadable` keeps meaning a file git ran and
could not read.

Around the git calls ALONE, and never around a whole check: a
`project.yaml` nobody may read is an `OSError` too, and it is not this.
ONLY AN ERROR FROM STARTING GIT COUNTS, told apart two ways, as
`validate-pins.py` tells it (#928). Around the git calls ALONE, and
never around a whole check: a `project.yaml` nobody may read is an
`OSError` too, and it is not this. A member's gitlink check and its
tree check each ran whole inside one, so whatever else either met was
told to put git on PATH. And inside a git call the class is not enough:
`subprocess` raises the same `FileNotFoundError` or `PermissionError`
when the DIRECTORY it was to run git in, a member's checkout, cannot be
entered, and on POSIX names that directory in `filename`, where a
program that will not start names `git`. So any `OSError` whose
`filename` is `git` counts, `ENOEXEC`'s included (a binary for another
architecture, a script with no shebang), and so does a
`FileNotFoundError` or a `PermissionError` that names no file, as
Windows' `CreateProcess` raises one for a missing git. Every other
`OSError` passes through as it came, and so does Windows' `[WinError
193]`, a `git.exe` it cannot execute, which names no file and is
neither class: `validate-pins.py` passes it through as well.

`validate-pins.py`'s RULE, COPIED, NOT SHARED. This file is a pinned
copy, and a holder re-copies its pinned files one row at a time, so it
may run beside a `repo_shape.py` older than itself: a rule moved there
would end this run in an `ImportError` (#610's skew rule).
`tests/test_validate_family_git_not_runnable.py` holds the two copies to
one answer.
"""
try:
yield
except OSError as exc:
nameless = (exc.filename is None
and isinstance(exc, (FileNotFoundError, PermissionError)))
if exc.filename != "git" and not nameless:
raise
raise Refusal(
"git-not-runnable",
f"{unread}: git could not be run ({exc.strerror or exc})",
Expand Down Expand Up @@ -737,8 +763,13 @@ def _check_member_gitlink(root: Path, path: str, commit: str, project: str,
variant of `path`, the refusal names that instead (#611). A gitlink
whose removal is staged (`removed`) is HEAD's, which the next commit
drops: `member-gitlink-removal-staged` names it, and nothing is said of
it here (#762)."""
gitlink = recorded_gitlink(root, path)
it here (#762).

The gitlink is read inside `_git_runs`, and nothing else here is: the
remedy a refusal builds reads the disk and HEAD's `.gitmodules`, and
the git it runs is in a `_git_runs` of its own (#928)."""
with _git_runs(f"the gitlink recorded at {path} could not be read"):
gitlink = recorded_gitlink(root, path)
if gitlink is None:
variants = sorted(other for other in gitlinks if other != path
and other.casefold() == path.casefold())
Expand Down Expand Up @@ -1351,6 +1382,11 @@ def _check_member_tree(member: Path, pin: dict, path: str, commit: str,
Python 3.13, and on 3.14 was `member-uninitialized`, whose update then
failed (`could not open '<path>/.git' for writing`) on the checkout
that is there.

The pinned commit's lookup and the digest are read inside `_git_runs`,
and nothing else here is (#928). All of this ran inside one, the `stat`
of the checkout's `.git` too: on Python 3.12, a refused one met here
was `git-not-runnable`, which says to put git on PATH.
"""
if not _checked_out(member, path):
raise Refusal(
Expand All @@ -1362,21 +1398,28 @@ def _check_member_tree(member: Path, pin: dict, path: str, commit: str,
f"Remediation: {stop}`git submodule update --init --recursive`, "
"or `make bootstrap`, which resolves a credential for private "
"members first.")
try:
git_out(["rev-parse", "--verify", "--quiet", f"{commit}^{{commit}}"],
cwd=member)
except Refusal as exc:
fetch = shlex.join(["git", "-C", path, "fetch", "origin", commit])
raise Refusal(
"member-commit-unresolvable",
f"{path}: the pinned commit {commit} is not in that member's "
f"object store ({exc.detail})",
f"Remediation: where a remote of the member holds it, `{fetch}` "
"fetches it into that store, and this then reads it. Where none "
"does, that fetch fails `not our ref`, exit 128, as `git "
"submodule update` does, and the pin names a commit no clone can "
"fetch: move the gitlink and the pin to one a remote holds. "
+ rule) from exc
unread = (f"{project}'s pinned commit and its tree digest could not be "
f"read in {path}")
# `_git_runs` OUTSIDE the `try`, as `validate-pins.py` has it: the
# refusal it raises is a `Refusal`, and inside, the `except Refusal`
# below would turn "no git to run" into `member-commit-unresolvable`.
with _git_runs(unread):
try:
git_out(["rev-parse", "--verify", "--quiet",
f"{commit}^{{commit}}"], cwd=member)
except Refusal as exc:
fetch = shlex.join(["git", "-C", path, "fetch", "origin",
commit])
raise Refusal(
"member-commit-unresolvable",
f"{path}: the pinned commit {commit} is not in that member's "
f"object store ({exc.detail})",
f"Remediation: where a remote of the member holds it, "
f"`{fetch}` fetches it into that store, and this then reads "
"it. Where none does, that fetch fails `not our ref`, exit "
"128, as `git submodule update` does, and the pin names a "
"commit no clone can fetch: move the gitlink and the pin to "
"one a remote holds. " + rule) from exc

recorded = pin.get("tree_sha256")
if not isinstance(recorded, str) or not SHA256_RE.match(recorded):
Expand All @@ -1391,7 +1434,8 @@ def _check_member_tree(member: Path, pin: dict, path: str, commit: str,
f"{TREE_DIGEST_DEFINITION!r}. A digest whose definition is "
"unstated is a number, not an identity.")
else:
actual = tree_digest(member, commit)
with _git_runs(unread):
actual = tree_digest(member, commit)
if actual.lower() != recorded.lower():
report.finding(
"member-digest-mismatch",
Expand Down Expand Up @@ -1433,8 +1477,8 @@ def _check_member(root: Path, row: dict, seen: dict, report: Report,
dropped both findings unprinted.

The git each check runs is run inside `_git_runs`, which names the check
when there is no git to run (#368); the identity check reads a file and
runs none.
when there is no git to run (#368), around the git calls alone (#928);
the identity check reads a file and runs none.
"""
project = _member_project_name(row, seen, report)
if project is None:
Expand All @@ -1454,10 +1498,8 @@ def _check_member(root: Path, row: dict, seen: dict, report: Report,
# ---- the gitlink -------------------------------------------------------
# THE LOCKSTEP RULE each finding below ends at names this member (#610).
rule = _lockstep(root, project, str(row.get("id") or project))
with _git_runs(f"the gitlink recorded at {path} could not be read"):
_check_member_gitlink(root, path, commit, project,
row.get("repository"), gitlinks, report, rule,
removed)
_check_member_gitlink(root, path, commit, project, row.get("repository"),
gitlinks, report, rule, removed)
return Gitlinked(row, project, path, pin, commit, rule)


Expand All @@ -1483,10 +1525,8 @@ def _check_member_checkout(root: Path, asked: Gitlinked, report: Report,
member = root / asked.path
if asked.path in unregistered and not _checked_out(member, asked.path):
return
with _git_runs(f"{asked.project}'s pinned commit and its tree digest "
f"could not be read in {asked.path}"):
_check_member_tree(member, asked.pin, asked.path, asked.commit,
asked.project, report, asked.rule, stop)
_check_member_tree(member, asked.pin, asked.path, asked.commit,
asked.project, report, asked.rule, stop)
_check_member_identity(member, asked.row, asked.project, asked.path,
report)

Expand Down
Loading
Loading