Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 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
d2eda8b
validate-family.py, validate-pins.py and both bootstrap.py copies: a …
Oct 20, 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
3fb918e
Merge #983's final head ba968677 into #1001: the first of three merge…
Oct 20, 2026
9aab7ae
Merge #983's squash 5b41d44 into #1001, keeping this branch's side of…
Oct 20, 2026
411e0d6
tests/test_bootstrap_without_git.py: the assembly root's upstream not…
Oct 20, 2026
aa7f31e
Merge branch 'main' into fix/1001-winerror-193-git-not-runnable
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
13 changes: 8 additions & 5 deletions templates/assembly-root/scripts/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,15 +328,18 @@ def _git_runs(unread: str) -> Iterator[None]:
words are the validators'. Which `OSError` is git's is read as
`validate-pins.py` reads it, by NAME: one whose `filename` is `git`, or
a nameless `FileNotFoundError` or `PermissionError`, which is what
Windows raises for a program it cannot start. An error that names a
directory, which `subprocess` raises for a `cwd` it cannot enter, is
not git's, and goes on as it did.
Windows raises for a program it cannot start, or a nameless `OSError`
whose `winerror` is 193, which it raises for a `git.exe` it cannot
execute (#1001). An error that names a directory, which `subprocess`
raises for a `cwd` it cannot enter, is not git's, and goes on as it did.
"""
try:
yield
except OSError as exc:
if exc.filename != "git" and (exc.filename is not None or not (
isinstance(exc, (FileNotFoundError, PermissionError)))):
nameless = (exc.filename is None
and (isinstance(exc, (FileNotFoundError, PermissionError))
or getattr(exc, "winerror", None) == 193))
if exc.filename != "git" and not nameless:
raise
raise Refusal("git-not-runnable",
f"{unread}: git could not be run "
Expand Down
14 changes: 8 additions & 6 deletions templates/assembly-root/scripts/validate-pins.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,11 +799,12 @@ def _git_runs(unread: str) -> Iterator[None]:
`filename` where a program that will not start names `git`. So the rule
is the NAME: any `OSError` whose `filename` is `git` counts, `ENOEXEC`'s
included, and an error that names a directory or any other file passes
through unchanged, and so does any `OSError` that names none. The one
nameless error that counts is a `FileNotFoundError` or a
`PermissionError`: Windows' `CreateProcess` failures carry no filename,
so a missing git there is `[WinError 2]` with `filename` None, and
nothing says it was the directory.
through unchanged. Of the errors that name none, only a
`FileNotFoundError` or a `PermissionError` counts, and an `OSError`
whose `winerror` is 193: Windows' `CreateProcess` failures carry no
filename, so a missing git there is `[WinError 2]` with `filename` None,
and nothing says it was the directory, and a `git.exe` it cannot
execute is `[WinError 193]`, which is neither class (#1001).

`recorded_gitlink`'s `except Refusal` is not in the way: an `OSError` is
no `Refusal`, so a missing git reaches here instead of reading as "no
Expand All @@ -813,7 +814,8 @@ def _git_runs(unread: str) -> Iterator[None]:
yield
except OSError as exc:
nameless = (exc.filename is None
and isinstance(exc, (FileNotFoundError, PermissionError)))
and (isinstance(exc, (FileNotFoundError, PermissionError))
or getattr(exc, "winerror", None) == 193))
if exc.filename != "git" and not nameless:
raise
raise Refusal(
Expand Down
13 changes: 8 additions & 5 deletions templates/family-root/scripts/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,11 +462,14 @@ def git_not_started(exc: OSError) -> Refusal | None:
`FileNotFoundError` traceback, exit 1, the code of `family bootstrap
FAILED`, though nothing had failed. Git's is told by its NAME, as
`validate-pins.py` tells it: a `filename` of `git`, or none at all on a
`FileNotFoundError` or a `PermissionError`, as Windows raises them; one
naming a directory is not git's. The code, and the remedy's first
sentence, are `validate-family.py`'s."""
nameless = exc.filename is None and isinstance(
exc, (FileNotFoundError, PermissionError))
`FileNotFoundError` or a `PermissionError`, as Windows raises them, or
on an `OSError` whose `winerror` is 193, Windows' `[WinError 193]` for a
`git.exe` it cannot execute (#1001); one naming a directory is not
git's. The code, and the remedy's first sentence, are
`validate-family.py`'s."""
nameless = (exc.filename is None
and (isinstance(exc, (FileNotFoundError, PermissionError))
or getattr(exc, "winerror", None) == 193))
if exc.filename != "git" and not nameless:
return None
return Refusal(
Expand Down
15 changes: 8 additions & 7 deletions templates/family-root/scripts/validate-family.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,23 +334,24 @@ def _git_runs(unread: str) -> Iterator[None]:
`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.
Windows' `CreateProcess` raises one for a missing git, and so does an
`OSError` that names none whose `winerror` is 193, Windows' `[WinError
193]` for a `git.exe` it cannot execute, which is neither class (#1001).
Every other `OSError` passes through as it came.

`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.
`tests/test_validate_family_git_not_runnable.py` holds this copy, that
one and both `bootstrap.py`s' to one answer and one text.
"""
try:
yield
except OSError as exc:
nameless = (exc.filename is None
and isinstance(exc, (FileNotFoundError, PermissionError)))
and (isinstance(exc, (FileNotFoundError, PermissionError))
or getattr(exc, "winerror", None) == 193))
if exc.filename != "git" and not nameless:
raise
raise Refusal(
Expand Down
62 changes: 54 additions & 8 deletions tests/test_bootstrap_without_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
going on would only say it again. Which `OSError` is git's is read by NAME,
as `validate-pins.py` reads it: `filename` `git`, or none on a
`FileNotFoundError` or a `PermissionError`, which is how Windows raises
them; an error naming a directory goes on as it did.
them, or on an `OSError` whose `winerror` is 193, Windows' `[WinError 193]`
for a `git.exe` it cannot execute (#1001); an error naming a directory goes
on as it did.

The end-to-end runs are a person's: a scaffold's recursive clone, a plain
clone, and a family holder, each run with no git on `PATH`. The rule's other
Expand All @@ -37,6 +39,7 @@
from conftest import (PROJECT, REPO, clone_just_pushed, load_without_bytecode,
run_script)
from test_family import family, holder # noqa: F401
from test_validate_pins_without_git import WIN_ERROR_193

#: What `subprocess` says when there is no git to start, here.
NO_GIT = ("The system cannot find the file specified" if os.name == "nt"
Expand All @@ -53,6 +56,33 @@
"through git too, so without it nothing here can be fetched or "
"bootstrapped.")

#: What stands where a `git.exe` Windows cannot execute would. Python imports
#: `sitecustomize` at start-up from `PYTHONPATH`, so the script, and anything
#: it starts, load this first, as `tests/test_update_shape_landing.py`'s
#: `GH_ON_PATH` stands in for `gh`. Starting a program named `git` raises
#: what CPython raises on Windows there, `WIN_ERROR_193`; nothing else
#: changes.
GIT_NOT_WIN32 = """\
import errno
import os
import subprocess


class _Popen(subprocess.Popen):
def __init__(self, args, *rest, **kwargs):
if (isinstance(args, (list, tuple)) and args
and os.path.basename(str(args[0])).lower()
in ("git", "git.exe")):
error = OSError(errno.ENOEXEC,
"%1 is not a valid Win32 application")
error.winerror = 193
raise error
super().__init__(args, *rest, **kwargs)


subprocess.Popen = _Popen
"""


def without_git(root: Path, tmp_path: Path, *extra: str,
env: dict | None = None):
Expand Down Expand Up @@ -89,25 +119,37 @@ def test_an_assembly_root_with_no_git_refuses_in_step_a(project, tmp_path,
"\n(a) legs on tracking branches, pins untouched\n"), result.stdout


@pytest.mark.parametrize("shim, said", [
(None, NO_GIT), (GIT_NOT_WIN32, WIN_ERROR_193.strerror)],
ids=["no-git", "winerror-193"])
def test_the_offline_upstream_notice_with_no_git_refuses_too(scaffolded,
tmp_path):
tmp_path, shim,
said):
"""A PLAIN clone: both legs NOT INITIALIZED, so step (a) runs no git, and
`SHAPE_UPSTREAM_PATH` names a directory, so the notice after the
validators runs one. `validate-pins.py` refuses `git-not-runnable`
itself; the notice then ended the run in a traceback. It refuses, and
the review-authority step is not reached."""
the review-authority step is not reached. So it does where every start
of git raises Windows' `[WinError 193]` (`GIT_NOT_WIN32`; #1001,
RV1028's NIT 1), which the notice's `except OSError` used to swallow
and say nothing of."""
clone = tmp_path / PROJECT
made = clone_just_pushed(scaffolded["remotes"] / f"{PROJECT}.git", clone,
recurse=False)
assert made.returncode == 0, made.stderr
upstream = tmp_path / "upstream"
upstream.mkdir()

result = without_git(clone, tmp_path,
env={"SHAPE_UPSTREAM_PATH": str(upstream)})
env = {"SHAPE_UPSTREAM_PATH": str(upstream)}
if shim is not None:
sitecustomize = tmp_path / "git-not-win32"
sitecustomize.mkdir()
(sitecustomize / "sitecustomize.py").write_text(shim, encoding="utf-8")
env["PYTHONPATH"] = str(sitecustomize)

result = without_git(clone, tmp_path, env=env)
refused(result,
"REFUSED git-not-runnable: the clone SHAPE_UPSTREAM_PATH names, "
f"{upstream}, could not be read: git could not be run ({NO_GIT})",
f"{upstream}, could not be read: git could not be run ({said})",
ASSEMBLY_REMEDY)
assert " [spec] spec: NOT INITIALIZED." in result.stdout, result.stdout
assert "(c) review authority" not in result.stdout
Expand Down Expand Up @@ -157,12 +199,16 @@ def made_of(module, error: OSError):
PermissionError(errno.EACCES, "Permission denied", "git"),
OSError(errno.ENOEXEC, "Exec format error", "git"),
FileNotFoundError(2, "The system cannot find the file specified"),
], ids=["no git", "git not executable", "git no program", "windows"])
WIN_ERROR_193,
], ids=["no git", "git not executable", "git no program", "windows",
"windows winerror 193"])
def test_each_way_git_cannot_start_is_git_not_runnable(root_module, error):
made = made_of(root_module, error)
assert made.code == "git-not-runnable", made
assert made.detail.endswith(
f": git could not be run ({error.strerror})"), made.detail
assert made.remediation.startswith(
"Remediation: put git on PATH and run this again. "), made


@pytest.mark.parametrize("error", [
Expand Down
Loading
Loading