Repository navigation
validate-family.py, validate-pins.py and both bootstrap.py copies: a git Windows cannot execute ([WinError 193], a nameless OSError) is git-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1, and one test holds the four copies to one answer and one text (#1001) - #1028
Conversation
…HEAD's `.gitmodules` gives is `member-gitmodules-absent`, so a holder whose HEAD lost the file no longer says `family ok`, `remove` refuses it before `deinit` instead of leaving the mount empty, and the gitlink remedies name a case variant and the commit `add` needs (#611) 571(b). In a holder whose HEAD lost `.gitmodules` (the commit #407 describes), `family.py remove --dry-run` exited 0, and the real run did `git submodule deinit` and `git rm`, then died at its commit's `git --literal-pathspecs add -f -- .gitmodules family.yaml` (`pathspec '.gitmodules' did not match any files`); the rollback left `members/<P>` empty. `_checked_gitmodules_held` now refuses that holder, before any git command that writes and under `--dry-run` alike: no `.gitmodules` in the work tree, the index or HEAD while the index records the member's gitlink. Refused, not committed without the file, as #407 refuses a `.gitmodules` the work tree lacks. The remedy writes back, with `git config -f .gitmodules`, the section `family.py add` writes for each member whose gitlink the index records, then `git add -f .gitmodules`; `remove` then runs as it always has. 571(c), #407's decision 3. `validate-family.py` reads HEAD's `.gitmodules` (`_head_gitmodules`, HEAD's root tree listed with no pathspec) and makes `member-gitmodules-absent` for each row at `members/<project>` whose gitlink no section gives, in every mode, `--no-members` included. Its two lines write the section back as `family.py add` writes one, keeping the url of a section of that name that has one. A member it names that is not checked out is not refused `member-uninitialized`: that refusal named a `git submodule update` that exits 0 there and dropped the finding, which is what a fresh clone and CI met. It reads HEAD, not the work tree, so a deletion nobody committed, an unstaged edit and a sparse checkout that leaves the file out are no finding, and a holder with no members is asked nothing. 574/2 (#363's item 3). A row whose gitlink HEAD records only at a case variant of its path (`Members/<P>`) is refused `member-gitlink-absent` naming each variant, and the move that puts it back in the index alone: `git rm -r --cached` of the variants and `git update-index --add --cacheinfo 160000,<pin>,members/<P>`, then a commit. `family.py add`, the plain refusal's remedy, cannot help there: the row exists. 574/3. A hand mount's `member-gitlink-unnamed` remedy says to commit the `git rm` before the `add`: since #574, `add` refuses a path under another spelling of `members/` that HEAD holds and the index no longer does. `templates/family-root/scripts/validate-family.py` is a pinned copy: each holder sees it as `upstream-changed` on its next `update-shape.py check`. Tests: tests/test_family_head_lost_gitmodules.py (new, 24); the pinned remedy text and the case-variant states in tests/test_validate_family_unnamed_mounts.py; a docstring in tests/test_family_remove_gitmodules.py. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s-absent` reads and the members `remove`'s refusal is for are pinned, so a finding for a row with no gitlink or a hand-edited path, or the refusal over a member with no gitlink, no longer passes (#611) Two tests. In a holder whose HEAD lost `.gitmodules`: a row with no gitlink (`member-gitlink-absent`'s) and a row whose `path:` was edited by hand (`member-path`'s, its mount `member-gitlink-unnamed`'s) get no `member-gitmodules-absent`; and `remove` of a member with no gitlink is not that refusal, but the dry run's `did not match any file(s) known to git`, as before. Each holds a condition of the first commit that its other tests did not: both pass on BASE 396c0ff and on that commit. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….gitmodules` over an unmerged gitlink too, and the case-variant move prints `--ignore-unmatch` and the `mv` a case-sensitive disk needs, so the mount is no longer emptied there and the printed lines work half typed or with both directories on disk (#611) Fix round 1, from the review of 6f938ed. `_checked_gitmodules_held` (scripts/family.py) took the member's gitlink only at stage 0. `git submodule deinit` and `git rm` run on an unmerged gitlink as on a merged one, so with `members/<P>` held only at stages 1, 2 and 3 in a holder whose HEAD lost `.gitmodules`, the dry run exited 0 and the real run emptied the mount and died at its commit. It now takes a gitlink at any stage, and both runs refuse `member-gitmodules-absent` before `deinit`, with the holder byte for byte as it was. The docstring says so. The case-variant refusal of `member-gitlink-absent` (templates/family-root/scripts/validate-family.py, pinned) now prints `git rm -r --cached --ignore-unmatch -- <variants>`: a variant whose removal is staged and not committed is still named from HEAD, and the plain line failed there (`pathspec ... did not match`). Its remediation also says what is left on disk: where nothing is checked out at the row's path, as in a fresh clone, `git submodule update --init --recursive` then checks the member out; on a case-sensitive disk a directory left at a variant stays, untracked, and `family.py add` refuses it as `family-members-dir-variant`, so it gives an `mv` of each out of the holder, to `../<variant with / as ->`, and says to leave it on macOS and Windows, where it is the row's own directory. The validator's module docstring names `member-gitmodules-absent`, says what a staged, uncommitted `.gitmodules` does (HEAD's file is what counts, as #407's decision 3 says: an uncommitted `add` is the finding for its member, a staged section removal is none, a staged restore is still the finding), and gives EXIT CODES' one exception, an unregistered member that is not checked out (exit 1, not 2). Tests (tests/test_family_head_lost_gitmodules.py, 12 more, 38 in all): the unmerged gitlink refused alike and repaired as printed; one test per condition no test held (a checked-out member still gets its tree check beside the finding; HEAD's unreadable file refused in every mode; the `<url>` placeholder; `remove`'s remedy names only members with a gitlink in the index; one finding per path for a duplicate row); the variant move half typed (both disks); and, on a case-sensitive disk, every printed line typed with both directories present, after which the gate passes and another member is added. tests/test_validate_family_unnamed_mounts.py pins the `--ignore-unmatch` line. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…modules: the gitlink remedy keeps #611's commit-the-removal clause and ends with BRANCH_LEFT (#611 x #607) This merges origin/main at 95780dd, which holds #637's squash (95780dd, "family.py and validate-family.py: where an earlier act left the branch ...", #607). It adds `BRANCH_LEFT` to scripts/family.py and templates/family-root/scripts/validate-family.py, and the guard `test_every_remedy_that_names_a_switch_line_ends_with_the_sentence` in tests/test_family_switch_branch_exists.py. Lane 2's #634 (#610) is not in main yet, so `_check_member_gitlink` is untouched here. Two hand resolutions, one sentence in two places, as #639's body named it: - templates/family-root/scripts/validate-family.py, the end of `member-gitlink-unnamed`'s remedy: #611's "and commit that removal, since family.py add refuses a path under another spelling of members/ that HEAD holds and the index no longer does, then mount it with the third, ..." clause is kept, and #607's `BRANCH_LEFT` ends the prose, as "... if one is still there. {BRANCH_LEFT}\n". - tests/test_validate_family_unnamed_mounts.py, `GITLINK_REMEDY`: the same clause, ending ".git/modules if one is still there. " + BRANCH_LEFT. No other file conflicted. #637's guard test passes on this merge without a further change: the remedies #611 added name no `_switch_line`, so no BRANCH_LEFT is appended beyond the one resolved above. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ck out fails the family bootstrap, every row's gitlink is asked before any row's checkout, and `--no-members` prints the case-variant move, so a clone of a holder whose HEAD lost `.gitmodules` or of #363's state no longer ends `family bootstrap ok` or hides the variant behind `member-uninitialized` (#648) (a) `templates/family-root/scripts/bootstrap.py` said "every member and leg fetched" and ended `family bootstrap ok`, exit 0, over members it had not checked out: in a fresh recursive clone of a holder whose HEAD lost `.gitmodules` (git exits 0), and in a plain clone of that holder or of #363's state, where git dies `No url found for submodule path` (exit 128). After the fetch it now asks this holder's own records of each row whose member is not checked out: no gitlink at the row's path (`recorded_gitlink`, as `member-gitlink-absent` reads it), or no section of `.gitmodules` giving that path, or, where git exited 0, any such row. Each is named on one `NOT CHECKED OUT, and not for want of a credential` line with why, which names `validate-family.py`, and the run ends `family bootstrap FAILED: <P> (not checked out)`, exit 1. "every member and leg fetched" is said only where git exited 0 and every row is checked out. A row git could not fetch for want of a credential is still the skip it was (`test_bootstrap_degrades_when_a_member_cannot_be_fetched`), and a good holder's run prints what it printed. (b) `validate-family.py` asked each row whole, gitlink then checkout, before the next. In a plain clone of #363's state plus `git submodule update --init --recursive`, which dies at `Members/<P>` before it checks out any member, an earlier row was refused `member-uninitialized` (naming that same update) and the later row's variant refusal was never reached. `_check_member` now stops at the gitlink and returns the member (`Gitlinked`); `_check_member_checkout` asks the digest and identity of each once every row's gitlink has been asked. (c) Under `--no-members`, `member-gitlink-unnamed` for a member's only gitlink, in the wrong case, printed no command. Where the row's path is its canonical one and its pin a commit, it now prints the index move the row's members-mode refusal prints, word for word (`_wrong_case_move`, shared by both), to the pinned commit, every variant on one line. Docs: the validator's WHAT IS CHECKED docstring (the order; the move under --no-members) and bootstrap's (a) and (c). README.md's "A member it cannot fetch is reported and skipped, never a failure" is no longer true, so it says "One it lacks a credential for is reported and skipped, not a failure", in the same line count (README is at its 1449-line cap), and docs/handbook.html says the same, with its § Families marker set to the segment's new blob. Pinned copies changed: templates/family-root/scripts/bootstrap.py and templates/family-root/scripts/validate-family.py. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tstrap-and-gate-order, as lane 5's coordinator asked (#648 x #611) This merges origin/fix/611-validate-family-head-gitmodules at 08b6245, #639's head after its own merge of main 95780dd (#637's squash, #607), keeping that merge's one hand resolution: `member-gitlink-unnamed`'s hand-mount remedy ends with #611's commit-the-removal clause and then `BRANCH_LEFT`. No file conflicted. The remedy #648 adds under `--no-members` (`_wrong_case_move`) names no `_switch_line`, so #637's guard asks no `BRANCH_LEFT` of it. README.md's § Families segment, which #648 edits, is the same in both parents, so docs/handbook.html's marker for it stays the blob #648 set. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…member THE LOCKSTEP RULE rides on `Gitlinked`, so the two-pass gate (#648) passes it to the digest finding (#648 x #610) One file conflicted, `templates/family-root/scripts/validate-family.py`, in two regions, both where this branch split `_check_member` in two (the gitlink first, the checkout once every row's gitlink is asked) and #634 (#610) made THE LOCKSTEP RULE a per-member `_lockstep(...)` value passed to the gitlink and tree checks: 1. `_check_member`'s gitlink call. #634 computes `rule = _lockstep(root, project, str(row.get("id") or project))` and passes it to `_check_member_gitlink`; this branch returns a `Gitlinked` there. Resolved: the call passes `rule`, as #634 has it, and `Gitlinked` gains a sixth field, `rule`, which the return carries. 2. `_check_member`'s tail, which this branch moved into `_check_member_checkout`. #634 passes `rule` to `_check_member_tree`. Resolved: `_check_member_checkout` passes `asked.rule`. So each member's gitlink-mismatch and digest-mismatch findings end at the same rule #634 gives them, naming that member, and the closing count's `_lockstep(root)` is #634's, merged clean. `Gitlinked`'s docstring names the new field. No other file conflicted. The resolution is the one rv648 built and tested in its scratch clone (the same blob of #634's file, 519b71f), plus that docstring. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n gives a url fails the run naming every row, and a member git exited 0 over is given the update that checks it out, so a stray case variant, a hand mount or a url-less section no longer ends `family bootstrap ok` (#648, rv648) The fix round for rv648's review of 8b39cd1 (1 MUST, 2 SHOULD, 5 NIT). M1. In a plain clone, `git submodule update --init` dies in its init step at the first gitlink, in index order, that no section of `.gitmodules` gives a url, and checks out no member. `unfetchable` asked only each row's own gitlink and section, so a stray `Members/<P>` beside `members/<P>` (#361's STRAY), a member mounted by hand with no section, or a row's section that lost its url (rv648's S1, H1 and U1) passed every row as the credential skip, and the run ended ok. `init_stop` now asks the holder's records which gitlink git's init stops at: the index's gitlinks in order, each against the work tree's sections (`gitmodules_sections`, which replaces `gitmodules_paths`) and the git config. Measured before it was taken, rv648's prototype failed three states that are a missing credential: a recursive clone of the stray or of the hand mount with no credential, and a url-less section an earlier `git submodule init` registered. So two of git's rules are kept: where the config sets `submodule.active` (a recursive clone sets `.`), init asks only the gitlinks a section gives, and a section with no url does not stop it where the config holds a url under its name. Where git exited non-zero and init stops at a gitlink, every row not checked out is named `git stopped at <gitlink>, a gitlink no section of .gitmodules gives a url`, and the run fails. What cures it: `validate-family.py --no-members` names a gitlink no row names and the commit that cures it (members checked, it refuses `member-uninitialized` for the first row in these states, a checklist candidate). Where a row names the gitlink, nothing in the validator does, so the line says to write that section back as `family.py add` writes one. Each why now carries its own sentence. S1. For "git exited 0 and checked nothing out there" (a subset clone, an `update = none`), the line printed the validator sentence, whose `member-uninitialized` remedy exits 0 and checks nothing out. It prints `git submodule update --init --checkout --recursive -- <path>` instead; typed, it checks the member out in both states and the run ends ok. S2. `test_a_missing_credential_is_still_the_skip_it_was` asserts that " every member and leg fetched" is not printed (rv648's mutant R2). NITs. README and the handbook say `make bootstrap` skips a member it lacks a credential for, and the § Families marker is the new segment's blob, b3d19092; README stays at 1459 lines, its cap. The docstrings say "an empty mapping" and "a missing credential exits non-zero too", and `validate-family.py`'s WHAT IS CHECKED says the move under `--no-members` is printed only for a canonical path and a 40-hex pin. Tests: the variant-plain case and the round trip name MEMBER too; new tests for S1 and H1 (typed through `--no-members` and committed, the run checks both out), U1 (the section written back), the two recursive clones with no credential and the registered url (still the skip), and the update that exits 0, by `update = none` and by a subset clone, with its printed line typed. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing in #414's url-less member state now fails, as 1a69505 makes it, so the test pins exit 1 and `git stopped at members/Ghost` where it pinned the old `ok` (#648, rv648's U1) `test_414_3_there_the_bootstrap_cures_nothing_and_the_git_line_cures_one` builds rv648's U1 state: a member, Ghost, whose `.gitmodules` section gives its path and no url. Git's init stops the holder's update there, before it checks out the other member. The test asserted bootstrap's exit 0, the false `ok` rv648's M1 asked to end. It asserts exit 1 now, and the line naming where git stopped. Its point is unchanged: the bootstrap cures nothing, and the doctor's git line cures the other member. Found by the targeted run of every test file that drives the family bootstrap (1 failed, 3797 passed, 32 skipped); the file now passes alone, 124 passed. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ember git has no url for is its own reason, and a gitlink outside members/ gets a cure true for it, so a later duplicate section, a section under a name git ignores or a url git drops no longer ends `family bootstrap ok`, and a subset clone's printed update no longer dies `No url found` (#648, rv648's verify) rv648's verify of 45c512e (1 MUST, 1 SHOULD, 3 NIT). M1. `init_stop` read `.gitmodules` with `git config --file --list`, which keeps entries git's submodule parser drops or overrides. In three plain clones git's init died (`No url found`, exit 128) and the run still said ok: a second section for a member's path, with no url, after its own (D1); the member's section under the name `../Nova` (Q1); and its url `-nowhere` (O1). `gitmodules_sections` now returns a `Gitmodules` with git 2.43's own reading beside the one the validator's findings use: - a section whose name is empty or has a `..` component, split on `/` and on a backslash, is ignored whole (`check_submodule_name`); - a `path` or `url` that begins with `-` is dropped, and the value before it stands (`looks_like_command_line_option`); - where two sections give one path, the later decides, and a section that gives a path again lets go of the one it gave, even where a later section gave that one too (`cache_put_path`, `cache_remove_path`). `has_url` asks the section git reads for a path, then the config under that section's name. `init_stop` and the reasons ask it. Where more than one section gives the path git has no url for, the line names `validate-family.py --no-members`, whose `gitmodules-path-twice` says to take the others out: a section written back would be one more (D1). S1. In a subset clone of a member whose section has no url (A1), git exits 0 and the line printed the exit-0 skip's update, which dies `No url found` when typed. "git has no url for that path" is now its own reason, asked before "git exited 0", with the write-it-back cure. The stop's sentence says "a gitlink it has no url for", which is true in D1 too, where a section does give the path a url. NIT 1. A gitlink outside members/ that git stopped at (H2) was sent to `--no-members`, which reads only members/ and says `family ok` there. It now gets `git rm --cached -- <path>` and a commit: measured, that ends ok and the gate passes in every mode, while a section written for it leaves `gitmodules-path-unnamed` in every mode. NIT 2. The `cures` docstring says what members mode refuses for #363's variant (`member-gitlink-absent`, with the move) as well as for a stray or a hand mount (`member-uninitialized`). Tests in tests/test_family_bootstrap_gate_order.py: U1, Q1 (three names) and O1 in one parametrized test, each written back; D1, typed through `gitmodules-path-twice`; D2, a name with dots and a url `-x` after a url, each still the credential's skip and then a true ok; A1, typed through both rounds; and H2, typed and the gate passing in every mode. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts own reason, since git reads no section from it, so a plain clone mid-merge whose update dies `No url found` no longer ends `family bootstrap ok` (#648, rv648's MM1) rv648's verify of 23e1105 (1 MUST). In a plain clone, a merge that conflicts in `.gitmodules`, resolved in the work tree and not added, leaves the index holding the file only at its three conflict stages. Git then reads no `.gitmodules` at all (`repo_read_gitmodules` asks `is_gitmodules_unmerged` first), so its init dies at the first gitlink, exit 128. `gitmodules_sections` read the work tree's file, found every section with its url, and the run ended ok. `gitmodules_sections` now asks `git ls-files -u -z -- .gitmodules` first. Where the index holds the file at a conflict stage, it returns nothing, with `Gitmodules.unmerged` set. `unfetchable` asks that first: every row not checked out gets "git reads no .gitmodules while a merge leaves it unmerged", and the line says to resolve the merge in .gitmodules and `git add .gitmodules`, then run this again. Added, the same run checks both members out. Test: rv648's MM1 in tests/test_family_bootstrap_gate_order.py, the two sides of the merge each giving OTHER's section another `branch`; the cure typed, and the run ends ok. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n as it reads the file fails `make bootstrap` naming the key, and the family gate names it in every mode, so `update = None` no longer ends `family bootstrap ok` (#768) Git 2.43 dies reading a `.gitmodules` that gives a submodule a `path`, `url`, `update`, `ignore` or `branch` with no value (a `branch` with none crashes it, exit 139), an `update` other than `none`, `checkout`, `rebase` or `merge`, a `shallow` that is no boolean, or a `fetchRecurseSubmodules` that is neither a boolean nor `on-demand`, before `git submodule update` checks out any member. The family holder's bootstrap listed the file with `git config --list`, which reads all of them, and ended `family bootstrap ok`; the gate refused `member-uninitialized` members checked, naming the update that dies, and said `family ok` under `--no-members`. bootstrap.py (the family root's, pinned): - `gitmodules_sections` checks each entry against git's rules (`refused_value`; a boolean is read by git itself, `git config --type=bool`). Where one would make git die it returns those entries and no sections, and `unfetchable` gives every row not checked out the reason "git cannot read .gitmodules: `<key>` is `<value>`" (or "has no value"). The cure names each key and what git takes for it. Where every member was checked out, the run fails on `.gitmodules` all the same, since git updated none of them. - Where the work tree has no `.gitmodules`, it reads the index's, then HEAD's, with `git config --blob`, as git does. A key with no value ends the reading of such a blob, where git calls the line bad and goes on. - `init_stop` reads a section's own `submodule.<name>.active` before `submodule.active`'s patterns, and matches a plain path pattern, as `git clone --recurse-submodules=<path>` writes one, to that path and what is under it. A pattern with pathspec magic or a glob keeps the old reading. validate-family.py (the family root's, pinned): - A new finding, `gitmodules-value-refused`, for each such key in the work tree's `.gitmodules` and in HEAD's, asked before any member check and in every mode. Its remedy prints `git config -f .gitmodules --unset-all submodule.<name>.<key>`, one command to a line, writes a member's `path` or `url` back where taking it out would leave none, says which values git takes, and ends with the commit. It says which file gives the value; where HEAD's alone does, it says to commit the work tree's, and where the work tree has no `.gitmodules`, it puts HEAD's back first. - Where the work tree's file gives such a value, `gitmodules-path-twice` and `gitmodules-path-unnamed` read nothing from it, as git reads nothing, so no command they print meets one of its lines. Where either file gives one, a member that is not checked out is not refused `member-uninitialized`: the finding fails the run. tests/test_family_gitmodules_values.py: every row of the issue's table and `branch` with no value, in a plain clone, through bootstrap and the gate in three modes, the printed commands typed; the no-misfire values; the index's and HEAD's file; `submodule.<name>.active` and the subset's patterns; and which file gives the value. tests/test_validate_family_without_git.py: with no git and no work tree `.gitmodules`, the first check to run git is now the reading of HEAD's `.gitmodules`, so the `git-not-runnable` refusal names that read, where it named the gitlink listing. Exit 2 and the remedy are as they were. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ues.py: a `branch` with no value is read as the git that runs reads it, a work tree `.gitmodules` git cannot parse fails the run with every member checked out, and the boolean probe reads no config of the person's, so CI's git 2.55 passes the row (#768, rv819) CI's run 39031235254 (git 2.55.0) failed `test_a_value_git_dies_on_fails_bootstrap_and_every_mode_of_the_gate[branch-no-value]`, and lane 2's review rv819 of c12b5f8 (NOT READY) found one MUST, four SHOULDs and five NITs. This commit answers them. MUST 1. The row pinned git's own update at exit 139, the crash w768 measured on git 2.43.0. Git 2.55 refuses the line, `error: missing value for 'submodule.members/IRSS.branch'` and `fatal: bad config line 7`, exit 128; the same assertion fails on git 2.47.1 (`assert 128 == 139`). The table's exit column is gone: the premise is that git's update died and checked nothing out, and every other assertion is the tools' own. SHOULD 2. Measured on git 2.43.0 and 2.47.1, a `branch` with no value after a submodule's url, then `git submodule update --init --recursive` in a plain clone: from the work tree's file, 2.43.0 crashes (exit 139) and 2.47.1 dies (exit 128); from the index's file or HEAD's, a blob, 2.43.0 crashes and 2.47.1 says `error: bad config line 4 in blob`, reads every section before it, checks the submodule out and exits 0, as for a `url` with no value. git's submodule-config.c gained `if (!value) ret = config_error_nonbool(var);` for `branch` in v2.43.1 and v2.44.0; v2.43.0, v2.42.4 and v2.39.5 lack it. Bootstrap kept 2.43.0's rule for every git, so on 2.43.1 and later such a blob had every member not checked out named "git cannot read .gitmodules" and the run failed, where git failed only for want of a credential (measured on 2.47.1 at c12b5f8). `crashes_on_branch` now reads `git version`: before 2.43.1, or a version it cannot read, the key is refused as before; from 2.43.1 on, the line ends the reading of a blob (`ends_blob_reading`). The gate is unchanged: it reads the work tree's file and HEAD's, which a clone checks out as its work tree's, and every git dies on the key there. The comments that said git crashes say which git does. SHOULD 3. Where every member was checked out, a work tree `.gitmodules` `git config --list` cannot parse (a conflict marker, an open quote) ended `family bootstrap ok` over git's `fatal: bad config line`. `Gitmodules` now says `unreadable`, and the run says NO MEMBER UPDATED and fails on `.gitmodules`, as for a refused value; a row not checked out is said to be that (`git cannot parse .gitmodules`), where it was "no section gives that path", and the cure is the file mended by hand. SHOULD 4. New tests catch each survivor rv819 named: the index's file staged apart from HEAD's; `update = rebase` and `update = merge`; a key with no value inside an ignored section of a blob; a `submodule.active` pattern that is only the start of a path; `submodule.<name>.active` set twice; a url beginning with `-` before the one with no value; the `<url>` placeholder; a section name with a backslash; and HEAD's value alone leaving the work tree's file to `gitmodules-path-twice`. SHOULD 5. `gitmodules_sections` is split into `gitmodules_source`, `gitmodules_entries`, `ends_blob_reading` and `read_sections`; `unfetchable` gives each row's reason to `row_reason`; `cures` gives the `.gitmodules` sentences to `gitmodules_cures`; `_check_gitmodules_values` gives its text to `_value_said` and `_value_taken_out`. NIT 6: the gate-in-three-modes block and the good-run block are helpers. NIT 8: where HEAD's file alone gives the value, the finding says git dies on it in a clone. NIT 9: both boolean probes ask git for the `--default` of a key in an empty file (`git config --file <null>`), so no `probe.value` in the person's config is read; one in it made both tools refuse `shallow = true`. NIT 7 (one copy of the rules) and NIT 10 (the body's count) are not code changes here. tests/test_family_gitmodules_values.py: 52 tests, from 28. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…762) landed, so validate-family.py keeps both sides, `stop` is asked only where `not refused`, and `_check_removals_staged` is handed the sections as read #820 (#762, `b9b70692`) landed on main after #819's last head `e1379a9d`. The two PRs conflict in `templates/family-root/scripts/validate-family.py` only; `templates/family-root/scripts/bootstrap.py` merges by itself. The resolution is the one lane 2 measured on #819 and #820 (rv819, rv820): keep both sides, with two changes that its notes for the second lander ask for. Conflicts, all in `templates/family-root/scripts/validate-family.py`: 1. The module docstring's EXIT CODES sentence. #819 added "and any while `.gitmodules` gives a value git dies on" with `gitmodules-value-refused`; #820 added "or gives a url, or whose removal is staged" with `member-gitmodules-url-absent` and `member-gitlink-removal-staged`. Now one sentence names all four exit-1 codes. 2. The `_check_members` docstring. Both paragraphs stay: #820's sentence about `_init_stop_said` ends the #648 paragraph, and #819's paragraph about a value git dies on follows it, with a clause that `_init_stop_said` is not asked there. 3. The `_check_member_checkout` call: `set(gitlinks) if refused else unregistered` (#819) and `stop` (#820), as `_check_member_checkout(root, asked, report, set(gitlinks) if refused else unregistered, stop)`. Two lines outside the blocks, which lane 2's notes ask for: - `stop` is computed only where `not refused`. Given #819's emptied `sections`, `_init_stop_said` names the first gitlink as a stop git never reaches when the work tree's file gives a value git dies on. Today `set(gitlinks)` keeps that text from being printed, but only by ordering. - `_check_removals_staged` is handed `as_read`, the sections before #819 empties them. Given `{}`, a staged `git rm --cached` printed `git checkout HEAD -- .gitmodules` as putting back "the .gitmodules section git rm took out", and `git rm --cached` took none (rv820). Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…side a value git dies on in the work tree's `.gitmodules` is a finding that prints the reset alone, so `_check_removals_staged` handed the emptied sections no longer puts `git checkout HEAD -- .gitmodules` in it (#768, point 3 of the merge with `b9b70692`) The merge of main `b9b70692` (#820, #762) into this branch hands `_check_removals_staged` the work tree's sections as read (`as_read`), not as #768 empties them where the work tree's file gives a value git dies on (rv820's note for whichever of #819 and #820 landed second). No test held that line: a mutant giving it `sections` again passed all 191 tests of `test_validate_family_typed_states.py`, `test_family_gitmodules_values.py`, `test_family_head_lost_gitmodules.py`, `test_validate_family_remedies_typed.py` and `test_validate_family_unnamed_mounts.py`. `test_a_staged_removal_beside_a_refused_value_puts_no_section_back` holds the state: the work tree's file gives OTHER's section `update = None`, and `git rm --cached -- members/IRRS` is staged. In all three modes the gate finds `gitmodules-value-refused` and `member-gitlink-removal-staged`, whole (`taken_out` and `removal_staged`), the second's commands are `git reset -q -- members/IRRS` alone, and `git checkout HEAD -- .gitmodules` is not in it. The test passes on `e32ed55a`'s validator. With the call changed back to `sections` it fails at the first mode: the finding's last line is the `git checkout HEAD -- .gitmodules` it should not print. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… advice and a member's exit code say what happened, and the gate's `git rm --cached`, guarded `mv` and section lines type as printed where they failed (#839) Seven lines of #839, in the holder's two pinned scripts, on #819's head (233a334): 1. `fetch_members` printed "If the members are PRIVATE, set SHAPE_LEGS_TOKEN ..." for every failed `git submodule update`, then "NOT CHECKED OUT, and not for want of a credential" for every row `unfetchable` names. `credential_advice` prints it only where git asked for a credential: not where its init died at a gitlink it has no url for, nor where it read nothing from `.gitmodules` (unmerged, a value git dies on, a file it cannot parse). Beside a denial it says it is for the members that line does not name. A leg left unfetched in a checked-out member keeps it. 2. `run_member_target` said `make <target> exited 2` for a member's finding (1), its refusal (2) and make's own failure alike: GNU make exits 2 for each (measured, GNU Make 4.3). `run_make` passes make's stderr on as it comes and reads the code from its last `Error <code>` line: `make bootstrap: the member's command exited N`, or `make bootstrap exited 2 and printed no ... line` where make printed none. Make is still what runs, so `--members-make validate` gets it too. 3. In the gate's state after `git mv members/<P> Members/<P>`, staged, with a commit or an untracked file in the moved checkout, the printed `git rm --cached -- Members/<P>` (the STRAY finding) refused, exit 1. `_forced` reads where the index stages a variant at a commit HEAD does not record there over a checkout; the STRAY line and the wrong-case move's `git rm` then carry `-f` and say what it does, as family.py's move back says it (#828). 4. The wrong-case move's guard followed a link (`-ef`, `-e`), so a link at the variant to the row's own directory or to a clone was kept, and `family.py add` refused it; with nothing there the `mv` failed `cannot stat`. `_guarded_move` prints `[ ! -L V ] && { [ ! -e V ] || [ V -ef R ] || [ -e V/.git ]; } || mv -- V ../<V>`, run in dash and bash on seven layouts. Comment 6237599648 is the same fault. 5. Comment 6237354577 (a): bootstrap already reads the work tree's, the index's, then HEAD's `.gitmodules` at 233a334 (#819); a test asks it. 6. Comment 6237354577 (b): `member-gitmodules-absent`'s `git config -f` lines made a one-section file where the work tree's `.gitmodules` is gone. The first finding that writes the file puts it back first (`_gitmodules_put_back`, `_back_first`), and no other, nor any after a `gitmodules-value-refused` that put HEAD's back; a second `member-gitmodules-url-absent` put the file back over the first's url. Tests: tests/test_family_holder_bootstrap_texts.py (48), and `variant_refused` in tests/test_family_head_lost_gitmodules.py and the make line in tests/test_bootstrap_spelling.py follow the new texts. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as `_check_gitmodules_values` returns it, so `_check_members` gains no condition for the put-back `gitmodules-value-refused` already printed (#839) Where the work tree holds `.gitmodules`, `_gitmodules_put_back` names no line to put it back either way, so `refused` alone says the same as `refused and not os.path.lexists(root / GITMODULES)`, and the cognitive complexity of `_check_members` stays at its base's. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…section's path outside `members/` out of the index before the section, and a gitlink there that no section gives is `gitlink-unnamed`, so the remedy typed as printed no longer leaves a gitlink at which a plain clone's `git submodule update --init` exits 128 while the gate passes (#878) `_member_gitlinks` listed gitlinks in `members/` alone, so no mode asked about one elsewhere. `git submodule add <IRSS> tools/x`, committed, or a member added at `Members/IRRS` and moved with `git mv` to `renamed-1`: the gate printed only the section's `--unset-all` lines, and typed as printed they left the gitlink. The gate then passed in every mode while `git submodule status` and a plain clone's update exited 128 with no member checked out, which is how the holder's CI checks its members out. - `_member_gitlinks` lists every gitlink (`_in_members` says which are in `members/`); `member-gitlink-unnamed` still asks only those. - `gitmodules-path-unnamed` for a section at such a gitlink prints `git rm --cached -- <path>` and its commit first, then its own lines, once per path (`_gitlink_out_first`). Where HEAD's `.gitmodules` holds no section of its name, it says to stage the file and that nothing of it is left to commit. - `gitlink-unnamed` (`_check_gitlinks_elsewhere`): a gitlink outside `members/` that no section of the work tree's `.gitmodules` gives, with bootstrap's cure, `git rm --cached` and the commit, or the commit alone where its removal is staged (`_second_path_out`). - Each of those `git rm --cached` lines takes `-f`, with #828's clause, where the index stages the gitlink at a commit HEAD does not record and a checkout is on disk at it (`_forced`, #876), and comes after `git add -- .gitmodules` where lines printed before it may leave that file unstaged, or it is (`_stage_first`): git takes no gitlink out of the index then (`please stage your changes to .gitmodules`, exit 128), measured after a value finding's lines (#879's C3 order), an earlier unnamed section's, and a hand edit. Tests: tests/test_family_unnamed_gitlink.py (new, 17 cases), and two tests that pinned the old scope: `test_only_members_is_asked_about` and rv648's H2 test, whose `--no-members` now names bootstrap's own line. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…printed in order, put a `.gitmodules` gone from the work tree back once, stage it before a gitlink's `git rm`, and name the checkout `git mv` moved before the update, and bootstrap fails where git's init stopped with every member checked out, so no remedy undoes another or exits 128 and no `family bootstrap ok` follows git's exit 128 (#879) - C1: where the work tree's `.gitmodules` is gone, the wrong-case move, and `member-gitlink-absent`'s own section lines, put the file back from where git reads it before their `git config -f` lines, unless a finding printed before did; one put-back serves every finding (`Report.put_back`). - C2: `member-gitlink-removal-staged` reads "section taken" from the file git reads, puts nothing back after an earlier finding did, and writes a section a plain `git rm` took out back key by key into that file. - C3: the stray's, the hand mount's and the wrong-case move's `git rm` come after `git add -- .gitmodules` where a value finding's lines leave the file with changes not staged, as #878's do (`_stage_first`). - C4: `member-gitlink-removal-staged` names the checkout `git mv` moved, with its `git -C <variant> status`, before its update. - C5: `unfetchable` asks `init_stop` with no row left, and `fetch_members` fails on the stop, or on an unmerged `.gitmodules`, through `cures`. - bootstrap.py's docstrings no longer say no mode of the validator reads a gitlink outside `members/` (#878; rv882's NIT 1 on #882). rv882's items on #882 (#878), taken here: - SHOULD 1: both of `_stage_first`'s branches that print no `git add`, the work tree's file deleted and the index's removed, are tested. - SHOULD 2: `gitlink-unnamed` reads the paths `.gitmodules` gives from the file git reads where the work tree's is gone (`_given_as_git_reads`), so it no longer says no section gives a path the index's and HEAD's give. - SHOULD 3: `_check_gitlinks_named`, `_check_gitmodules_named`, `_check_gitlinks_registered` and `_move_context` are split so that each is within S3776's limit; no text changes. - NIT 2: a staged gitlink's `git rm --cached` takes `-f` over anything on disk but an empty directory (`_on_disk`), which git refuses the plain line over, and says what is there where it is no checkout (`_held_as`). - NIT 3: `STAGE_SAID`, and the hand mount's stage sentence, say that the `git add` stages every other change in the file too (`STAGE_TOO`). Tests: tests/test_family_holder_remedies_compose.py; the `STAGE_SAID` constant in tests/test_family_unnamed_gitlink.py. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…983's two files: the second of three merges that bring #1001 onto main (#1001) #983 landed as 5b41d44, whose tree for validate-family.py and tests/test_validate_family_git_not_runnable.py is ba96867's, merged in the first merge. Both conflicts were #1001's own lines against #983's text; each file keeps this branch's side, which is 5b41d44's plus #1001's hunks and nothing else. The merge's tree is its first parent's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-4 RV1028, 2026-10-20T05:24:44Z) RV1028, lane 4's independent review of #1028 (for #1001), at head d2eda8b, reviewed as its own commit against its parent 840d2a0. #983 landed at 05:14:37Z as 5b41d44 while I worked, and #1028 now targets main. Verdict: READY. No MUST. One SHOULD (open an issue, no code change here) and three NITs. The ruleThe four copies hold one text: validate-family.py:352-355, validate-pins.py:816-819, the holder's bootstrap.py:470-473 ( I loaded each copy in one process and handed it errors that the parity does not list:
All four copies give the same answer to every row, at head and at base. Only the nameless 193 changed. I read CPython 3.12's
TestsPython 3.12.3, git 2.43.0, a stub gh that exits 97, and one basetemp.
My mutantsEach was run against the three rule files (150 tests). Each was restored with
The five flagged points1.
2. The upstream notice: probed, and refusing is right.
The notice is best-effort for an ABSENT answer. Its docstring (#694) already says that a git that cannot be started is not one, and the no-git row asserts that. With one git for the whole run, the validators have already refused with exit 2, so the exit code changes for nobody, and the last line now names git. I scanned all four files by AST, transitively: every 3. 4. Not run on Windows. See SHOULD 1. 5. #954. Verified.
PinsI made an assembly root and a holder with the standard at 840d2a0, then ran this clone's
No blob sha or sha256 of the four files, at base or head, appears anywhere in the tree. The body
ItemsSHOULD 1: open a follow-up issue for the other nameless
NIT 1 (tests/test_bootstrap_without_git.py:95): no row holds the notice's new answer to a 193, and R3 survives.
NIT 2: refresh the body. Three sections are out of date:
NIT 3: when #954 lands, whichever PR lands second resolves its hunk by taking #954's side.
Lane: openreposhape-4 (openRepoShape-4) |
…ice is held to refuse a git Windows cannot execute (`[WinError 193]`) as it refuses a missing git, so a notice that swallowed it again fails a row (RV1028's NIT 1 on #1028) (#1001) #1001 made the notice's `_git_runs` count a nameless `OSError` whose `winerror` is 193, so `_clone_tip`'s `except OSError` no longer swallows it and the run refuses `git-not-runnable`, exit 2. No row held that: RV1028's mutant R3, `_clone_tip` returning None for a refusal caused by 193, passed every test. The no-git notice row is now parametrized over the two ways git cannot start: no git on PATH, and every start of git raising Windows' `[WinError 193]` through `GIT_NOT_WIN32`, the `sitecustomize` stand-in. `GIT_NOT_WIN32` moves here from tests/test_validate_family_git_not_runnable.py, which imports it, since that file already imports this one's remedies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… holding U+202E or another character the launcher refuses, by code point and in the launcher's words, before it prints the value or runs `git clone` (#953) (#1043) `setup-project.py`'s self-bootstrap refuses an `$OPENREPOSHAPE_REPO` holding a character the launcher refuses (U+202E and the rest of `MISDRAWN_CHARACTERS`). It refuses by code point, in the launcher's words, before it prints the value or hands it to git. Head `c42afa30`, one commit on origin/main `5b41d44f`. Only `setup-project.py` and a new `tests/test_setup_project_repo_misdrawn.py` change. No flag, no `--help` line and no doc changes. **`setup-project.py`** (section 0, just above `self_bootstrap`): - `MISDRAWN_CHARACTERS` is the launcher's list (`openRepoShape:502` at `5b41d44f`), entry for entry, in the package's tuple form. It is a copy, for `checked_value`'s reason: self-bootstrap mode runs before there is a checkout to read the launcher from. - `misdrawn_characters(value)` names the characters a value holds as `U+XXXX`, in code point order. It is the package's `standard.misdrawn_characters`, and it looks for characters, as `CONTROL_RE` does. - `checked_repo(value)` asks that first. It refuses with the launcher's `checked_repo` sentence, which names the code points and nothing else of the value. Then it calls `checked_value(..., PATH_RE)`, unchanged, for a leading `-` and a control character. - `self_bootstrap` reads a non-empty `$OPENREPOSHAPE_REPO` through `checked_repo`. Unset or empty still means the default. The check stays where `--org` is asked, so the order is the same: `--org` first, then the repository, then `openRepoShape setup` is printed, then `mkdtemp` and `git clone`. - The value's raw bytes are never printed. The refusal goes through `die`, so it is `REFUSED: ...` on stderr with exit 2. **`tests/test_setup_project_repo_misdrawn.py`**, 22 tests: - **Parity:** `test_the_misdrawn_table_is_the_launchers_list` reads `MISDRAWN_CHARACTERS=(...)` out of `openRepoShape` and compares the tuple. It also checks that `misdrawn_characters` names all of the launcher tests' `MISDRAWN`, each once and in order. `test_misdrawn_characters_names_the_launchers_and_nothing_else` runs the launcher's `SHARING_ITS_BYTES`. These follow #947's two tests for the package. - **Direct path, 12 tests:** two runs (`Atlas --org acme`, which is the issue's run, and `--org acme` alone) times six values: U+202E, U+00AD, U+200B, U+E0001 (two, three and four UTF-8 bytes), two characters at once, and a tab before U+202E (the launcher asks for the code point before the control character). - Each test runs a lone copy of the file under `call_loggers`' logging git, gh and curl. - Each checks the launcher tests' `assert_refused_saying`: exit 2, the launcher's sentence, an empty stdout, and no listed character, escape or `forged` in either stream. So no `E2 80 AE` gets out. - The git log must hold exactly one call: the checkout probe. - **Values that still go on, 7 tests:** the launcher's `NOT_CONTROLS` after `acme/`: e-acute, a no-break space, U+200C and U+200D where a script or an emoji needs them, U+2026, U+FF21 and a subdivision flag. Each is printed in the clone line and handed to `git clone` as before. - **Package path, 1 test:** a tree with no `.git` (the package's modules under `src/openreposhape/`, plus `setup-project.py`, `scaffold-project.py`, `shape-doctor.py` and `contracts/repository-naming.yaml`). `openRepoShape Atlas --org acme` is run from that tree's `src` with the issue's value. The package prints its one line (`the standard is the checkout this package sits in, <tree>.`) and hands over. The tree's `setup-project.py` refuses: exit 2, the launcher's sentence, no U+202E and no `forged` in either stream, and only the checkout probe in the git log. - Tests that use the logging stub are skipped on Windows. There `subprocess` starts `git.exe` and never the stub's `.cmd`, so the stub would log nothing. CI is Linux only (#597). - `setup.sh` has no twin of these tests. Its guard covers control characters and the two separators and no other listed character, by #940's design: the launcher refuses the rest first where it is the way in. **"No git call", stated exactly.** The refused run makes one git call, `git -C <dir> rev-parse --show-toplevel`. That is `is_shape_checkout`, which runs before `self_bootstrap` on every run past `--help` and `--preflight`. It asks whether the file stands in a checkout, which is how the run knows it is self-bootstrapping at all, and it names nothing of the value. No git call names the value, and there is no clone. Moving the check above the probe would also refuse the value on the developer path, which never reads it. It would also leave `setup-project.py` ordered differently from `setup.sh`, which probes first and guards only on its self-bootstrap path. These run `$Q/v118/w953/probe.py`. Each case uses a fresh scratch folder, a logging git stub that then runs the real git, a gh that fails, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false` and `GIT_CONFIG_GLOBAL=/dev/null`. "Before" is `git archive 5b41d44`; "after" is the worktree. Results are the same under git 2.43.0 and 2.47.1. | Case | Before (`5b41d44f`) | After | |---|---|---| | package, ZIP tree, `Atlas --org acme`, `acme/r[U+202E]forged` | exit 128; `E2 80 AE` on stdout and stderr; git: probe, then `clone --quiet -- acme/r[U+202E]forged /tmp/openreposhape-shape-...` | exit 2; no `E2 80 AE`; git: probe only; stdout is the package's one line | | direct, `Atlas --org acme`, same value | exit 128; raw bytes in the clone line; probe + clone | exit 2; refusal names `U+202E`; probe only | | direct, `--org acme`, same value | exit 128; probe + clone | exit 2; probe only | | direct, `acme/r[U+200B]forged` | exit 128; probe + clone | exit 2; names `U+200B` | | direct, `acme/r[U+E0001]forged` | exit 128; probe + clone | exit 2; names `U+E0001` | | direct, `acme/r[U+2026]b` (not listed) | exit 128; probe + clone | the same: probe + clone, exit 128 | The "after" refusal, from the probe: ``` REFUSED: OPENREPOSHAPE_REPO holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it. This tool will not put it on a `git clone` command line. Set it to a URL or a path. ``` The new file was run on main's `setup-project.py` (`git show 5b41d44:setup-project.py`, put back byte for byte afterwards): **15 failed, 7 passed**. - Failed: both parity tests (`AttributeError`: no `MISDRAWN_CHARACTERS`, no `misdrawn_characters`), all 12 direct tests, and the package test. - Passed: the 7 `NOT_CONTROLS` tests. They pass at main by design, because they guard against a check that is too broad. Each mutant was applied alone (`$Q/v118/w953/mutate.py`), the new file was run (22 tests), and both files were put back. **All 18 are killed.** | Mutant | Failed | |---|---| | m01 check removed (main's read) | 13 | | m02a check moved after the `(0) self-bootstrap` line | 13 | | m02b check moved after the clone line | 13 | | m03a only the value's first character checked | 15 | | m03b only the table's first entry checked | 13 | | m04 only U+202E checked | 8 (U+00AD, U+200B, U+E0001, two-at-once) | | m05 refusal exits 1 | 13 | | m06a table drops `E0001` | 4 | | m06b parity compares the first five entries + table drops `E0001` | 4 (parity's second assert, `SHARING_ITS_BYTES`, both U+E0001 tests) | | m06c parity compares the first five + table adds `200C` | 2 (`SHARING_ITS_BYTES`, the Persian/U+200C test) | | m06d parity compares the first five + `2060-206F` narrowed to `206E` | 2 | | m06e parity uses set inclusion + table drops `E0001` | 4 | | m07 a range read without its last code point | 15 | | m08 control check asked before the code points | 2 (both tab-then-U+202E tests) | | m09 characters named in the value's order | 3 | | m10 every character past ASCII refused | 7 (every `NOT_CONTROLS` test) | | m11 refusal quotes the value back | 13 | | m12 check skipped on a hand-over (`OPENREPOSHAPE_SELF_BOOTSTRAP=1`) | 1 (the package test alone) | All at `c42afa30`, one pytest process at a time: `/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed`, one reused basetemp, `EMAIL=service@opensoft.dev`, a stub gh first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`. | Run | git | Files | Result | |---|---|---|---| | h1 | 2.43.0 | `test_setup_project_repo_misdrawn.py` (22), `test_setup_project_ref_parts.py` (27), `test_openreposhape_package.py` (213), `test_readme_windows_quoting_sentence.py` (44), `test_release_install_check_children.py` (30), `test_repo_hygiene.py` (100), `test_agents_md_codes.py` (47), `test_cli_reference.py` (23) | 506 passed in 94.06s | | h2 | 2.43.0 | `test_setup_project_py.py` | 340 passed in 122.68s | | h3 | 2.43.0 | `test_setup_sh.py` | 191 passed in 96.62s | | h4 | 2.47.1 | `test_setup_project_repo_misdrawn.py`, `test_setup_project_ref_parts.py`, `test_setup_project_py.py` | 389 passed in 128.13s | | h5 | 2.43.0 | `test_no_auto_maintenance.py`, `test_setup_project_dry_run_offers.py`, `test_validator_child_encoding.py`, `test_setup_project_name_edges.py`, `test_setup_project_typed_name.py` | 353 passed, 2 skipped in 73.92s (the two skips: `GIT_CONFIG_GLOBAL` was set) | No `shell_quoted` call was added (count 0), so the README's and the handbook's Windows-quoting sentences and `NAMED` are unchanged. `setup-project.py` is not a pinned copy. The wheel takes it at build time (`pyproject.toml` `force-include`), and the tests that read its source (`test_setup_project_py.py`, `test_release_install_check_children.py`, `test_repo_hygiene.py`) pass. - `git merge-tree --write-tree` of `c42afa30` with origin/main `5b41d44f` (fetched with `-c gc.auto=0 -c maintenance.auto=false`) is clean, tree `59de4c261c228274a5cff18aff953211de7002e3`. - No open PR touches `setup-project.py` or the new test file. Every open PR's head was still merged in, and all ten are clean: #1041 `4a23c6e2`, #1039 `5f924500`, #1038 `fd1de5ab`, #1031 `eea70be1`, #1030 `9b4845bb`, #1028 `0c9eac6c`, #1027 `052f1e24`, #1025 `f83ca618`, #1023 `4d0aa1cf`, #954 `dc7fd7ea`. - **RV947's companion is left out.** That companion is the package's scaffold run asking the repository even beside a checkout. - Once this lands, no package run prints the value or hands it to git. Beside a git checkout, the hand-over takes `setup-project.py`'s developer path, which never reads `$OPENREPOSHAPE_REPO`. `self_bootstrap` and `standard.py` are the only shipped Python that read it. Beside a tree with no `.git`, this check refuses it. - The companion would change `standard.prepare`, which `--doctor` and the four tools share. They would then refuse a value they never use. That is the launcher's "checked once, before every run but `--help`" policy, and it is a decision about the package of its own. - `src/openreposhape/standard.py` is also open in #1038. - **Found while measuring, not changed here:** `CONTROL_RE` and `PATH_RE` hold `\x00-\x1f\x7f` only, so a C1 control passes `checked_value`. The launcher and the package refuse C1 (U+0080 to U+009F) in a repository. With `acme/r[U+009B]31mforged`, the clone line prints the raw `C2 9B`, before and after this change. `--into` and `--local-remote-dir` go through the same `PATH_RE` (read in the code, not measured). That would be a follow-up for `checked_value`, which every flag shares. Review: rv953, lane 5's independent reviewer, not the writer (w953): READY at `c42afa30`, the head that lands (issuecomment-6284626889), with no MUST. It rules #953's Verify met in substance: the refused run's only git call is the checkout probe every run makes, which names nothing of the value, and there is no clone. Its SHOULD is #459's documented C1 known limit (U031, L4-24), left with its test NIT for a follow-up on a ruling. CI (Brett Heap's ruling 27): H0 is the head c42afa3 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 5b41d44; and the local gate passed on its predicted squash tree e146bb5 over main e656199: the setup_project_repo_misdrawn, repo_hygiene, agents_md_codes, cli_reference sets, 192 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Closes #953. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-4 RV1028, 2026-10-20T09:40:43Z) RV1028's re-check of #1028 at head 411e0d6, the delta d2eda8b..411e0d6. My first review is issuecomment-6284060655, and W1001's note is issuecomment-6285342529. Verdict at 411e0d6: READY. No MUST, no SHOULD and no NIT outstanding. Each earlier item is answered (see below), and nothing new turned up. The three merges411e0d6 is a fast-forward of d2eda8b ( 3fb918e merges #983's final head ba96867. Its tree is 9aab7ae merges the squash 5b41d44. Its tree is also
The delta 9aab7ae..411e0d6 (NIT 1)The delta is 2 test files, +47/-35:
My mutants, rerun at 411e0d6 against the three rule files (151 tests). Each was restored, its sha256 matched the pristine copy, and
RunsPython 3.12.3, git 2.43.0, a stub gh, one basetemp.
The 15 failures are the 14 from my first review plus the new notice row. That row fails there on the validator's traceback, exit 1. Main and the shared filesMain is now dc7cd49.
Two of the open PRs have moved since the body's table: #954 is at fe907c8 and #1025 at 4c0c52d. The conclusions still hold:
The other items
CI on 411e0d6 (
|
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 1 day and 9 hours by commenting |
|
…hain link before an `=` that leaves a path after it, so the offer the gate prints for a link holding `=` verifies that link when typed as printed (#980) (#1044) For a declared-unverified chain link whose name holds `=`, the naming gate offers only sources that verify that link's tree when typed as printed. The gate offered `open=y` the source `--link-source open=y=<path>`, and read every `--link-source` key at the value's first `=`: the key was `open`, which no chain names, so the value was a bare PATH for the first link, `open=y` stayed declared-unverified under the same offer, exit 0. **The issue's direction (a)**, in `scripts/validate-repository-naming.py`. `LinkSources._read_source` reads a `--link-source` key the way `_read_remedy` reads a `--link-remedy` link since #949: the longest text before an `=` that names a link of a chain this run reads, by `pin_product_key`, and leaves a path after it. `__init__` and `no_path` both take the key and the path from it, so the path refused as empty is the one after the key's own `=`. For a chain whose links hold no `=`, every value is read exactly as before: a text holding `=` can only name a link that holds one. **What (a) keeps from the documented rules, and how each case was checked** (probes below, and one test row or more each): - A link holding `=`: `open=y=<tree>` verifies `open=y`, as printed and in capitals (`OPEN=Y=<tree>`, by `pin_product_key` as every key is). - Two links where one is a prefix of the other (`open` and `open=y` in one chain): `open=y=<tree>` is `open=y`'s and `open=<tree>` is `open`'s, in either order. On main both were `open`'s and the later replaced the earlier, so `open=y` read none. - A path that itself holds `=`: `open=y=<dir>/a=b/tree` is `open=y`'s with that whole path. A text holding `/` names no link, since a link's name is `repo_basename`'d, so an `=` after a `/` never ends the key. A path that begins like a longer link's name is given after `./`: `open=./y=dir` is `open`'s in a chain naming `open=y` too. - A key that names a link but leaves an empty path: `open=y=` is `PRODUCT=` for `open=y`, refused as `link-source-no-path`, exit 2, as `openXdox=` is (#442). On main it was a bare PATH asked for `openXdox`, a link the person did not name. Where a shorter link leaves a path (`open=y=` with `open` in the chain too), that link is the key, `open` with the path `y=`, as `--link-remedy openXdox=y=` gives `openXdox` the words `y=` where a chain names `openXdox=y`: the longest link that leaves a path wins, and `open=y` leaves none. - A value that is a real directory spelled like the whole value: `open=y=x` beside a directory `open=y=x` is still `open=y`'s, decided by the chain and not the disk, with `_also_a_directory`'s line; `./open=y=x` asks for the directory, as a bare PATH for the first link. - The key is read whole, where a remedy's link is stripped and cut at its last `/`: ` open=y=<tree>` and `opensoft/open=y=<tree>` name no link and stay bare PATHs, as on main. **One difference from `--link-remedy`, deliberately.** `--link-remedy open=y=` is a NOTE (its link falls back to the first `=`, `open`). `--link-source open=y=` is refused, because the #442 rule refuses `PRODUCT=` for every link, and `--link-remedy`'s own `openXdox=` refusal is made by argparse before the chains are known, which is why it cannot catch `open=y=`. `_read_source`'s docstring says so. **Not (b).** (a) makes the offer the gate already prints true, so the person who types it gets the verification; (b) would only stop offering it. Setup's step (4) is unchanged: it passes the gate its `--pin-source` as an absolute path, with a separator before every `=`, so no text before one names a link. **Docs.** The module docstring (a new paragraph, "A KEY IS THE LONGEST LINK A VALUE NAMES"), the `LinkSources`, `_link_remedy` and `_read_remedy` docstrings, the `--link-source` help, and `docs/cli.md` regenerated from it. `setup-project.py`'s `as_the_plan_reads_it` docstring stated the first-`=` rule, and now states this one. README, the handbook and AGENTS.md state no rule for how a `--link-source` key is read (README names `--link-source PRODUCT=<path>` only), so they are unchanged. The class docstring records that `validate-pins.py` still ends a `--pin-source` key at the first `=`; no scaffold writes a pin holding one (`--pin` names are `[A-Za-z0-9_.-]`), and it is left as its own change. Measured from a root scaffolded at main `5b41d44f` (local remotes, no network): - `update-shape.py check --upstream <this checkout> --at c64e85f` lists `upstream-changed scripts/validate-repository-naming.py`, with `upstream-changed 1 unchanged 10`. - `update-shape.py apply --yes --branch ...` exits 0, commits `contracts/shape-pin.yaml`, `project.yaml` and the copy, and the project's validators print `pins ok` and `manifest ok`. A fresh `check` prints `unchanged 11`, and the root's own naming gate exits 0. - The new sha256 of `scripts/validate-repository-naming.py`: `ec167fb0e259f90705db895a1a37f4adebd4cbf03ca2d5ae18f6d9a8df896548` (78664 bytes). No other pinned copy and no template changes. Run from empty folders, `<D>` a fresh folder, trees as named (`trees/y` declares `openInk`, `trees/x` declares `open=y`; for the chain with `open`, `trees/x2` declares `open` and `trees/o` declares `open=y`). The gate prints an arrow between links; it is written `->` here. | Probe | At main `5b41d44f` | At head `c64e85fe` | |---|---|---| | P1, the issue's row: `--referent-chain openXdox,open=y,openInk --link-source open=y=<D>/trees/y` | `link-source-unreadable ... could not answer for openXdox`, "It was read as a PATH: `open` is not a link", and `open=y` declared-unverified with the same offer; exit 0 | only `openXdox` declared-unverified (no source named it); no `link-source-unreadable`; exit 0 | | P2, P1 plus `openXdox=<D>/trees/x`, `--explain` | NOTE: the value not used, read as a PATH; `CHAIN openXdox -> open=y -> openInk [declared-unverified]` | `[verified]`, nothing warned or noted | | P3, chain `openXdox,open,open=y,openInk`, `open=<o>` then `open=y=<y>` | `link-source-unreadable: y=<D>/trees/y (--link-source open=...)`, NOTE `open=<o>` replaced; 2 links declared-unverified | `[verified]`, nothing warned or noted | | P4, `open=y=` (chain without `open`) | bare PATH `open=y=` asked for `openXdox`; exit 0 | `REFUSED link-source-no-path: --link-source open=y= names no path for open=y`; exit 2 | | P5, `open=y=` (chain with `open`) | `y= (--link-source open=...) could not answer for open` | the same | | P6, `open=y=<D>/trees/a=b/y` | read as a PATH, `[declared-unverified]` | `[verified]` | | P7, `open=y=x` beside a directory `open=y=x` | bare PATH, read as the directory for `openXdox` | `link-source-unreadable: x (--link-source open=y=...) could not answer for open=y`, then "`open=y=x` is also a directory; `--link-source ./open=y=x` reads it as one." | | P7b, `./open=y=x` | the directory, for `openXdox` | the same | | P8, chain with `open`: `open=./y=dir`, `open=y=<y>` | `open=y=<y>` read for `open`, replacing `open=./y=dir` | `[verified]` | Eleven new rows in `tests/test_chain_remedy_callers.py`, section "#980", reusing its helpers (`naming`, `tree`, `gate_warnings`, `UNREACHED`, `GATE_OFFER`, `EQUALS`), with no assert that joins conditions with `and`: - `test_the_offer_for_a_link_holding_an_equals_verifies_it[as-printed, in-capitals, a-path-holding-an-equals]`: reads the offer the gate prints for `open=y`, types it with a tree, and asserts the one WARNING left is `openXdox`'s, with no `link-source-unreadable` and no NOTE. - `test_each_link_reads_its_own_source_where_one_name_begins_another[open-first, open=y-first, a-path-that-begins-like-the-longer-link]` - `test_a_value_that_ends_at_a_link_holding_an_equals_names_no_path` - `test_a_shorter_link_that_leaves_a_path_is_the_key` - `test_a_link_holding_an_equals_wins_over_a_directory_spelled_like_it` - `test_a_key_is_read_whole[a-blank, an-owner]` **Failing at main.** The test file copied onto a `git archive` of `5b41d44f`: 8 of the 11 fail, each on the defect (the offer row's `open=y` WARNING and `link-source-unreadable`, the prefix rows' `[declared-unverified]`, `open=y=` exiting 0, the keyed reading not winning over the directory). The other 3 pass at main by design: they pin behaviour main already had (`test_a_shorter_link_that_leaves_a_path_is_the_key`, `test_a_key_is_read_whole[a-blank]` and `[an-owner]`), and each kills a mutant below. **Mutants**, each applied alone to the fixed gate in a scratch repository, then `test_chain_remedy_callers.py`, `test_link_source_chain_defaults.py`, `test_naming_policy.py`, `test_chain_link_fold.py` and `test_chain_pin_sources.py` (400 passed, 4 skipped with no mutant). All 11 killed: | Mutant | Failed | Killed by (new rows unless named) | |---|---|---| | M1 back to `typed.partition("=")` | 8 | the offer rows, the prefix rows, `open=y=`, the directory row | | M2 the shortest match instead of the longest | 3 | the prefix rows | | M3 the "leaves a path after it" check dropped | 1 | `test_a_shorter_link_that_leaves_a_path_is_the_key` | | M4a the `./PATH` rule dropped (`./` stripped from the key) | 3 | the directory row; `test_a_labelled_source_holding_an_equals_is_not_said_to_be_a_path`; `test_naming_policy.py::test_a_link_named_before_the_equals_is_the_key_even_over_a_directory` | | M4b the key cut at its last `/`, as a remedy's link is | 4 | as M4a, and `test_a_key_is_read_whole[an-owner]` | | M5 the empty-path fallback dropped | 6 | `open=y=`; `test_a_value_that_names_no_path_is_spelled`; 4 rows of `test_link_source_chain_defaults.py::test_a_link_source_that_names_no_path_is_refused` | | M6 the fallback at the first `=`, as a remedy's | 1 | `open=y=` | | M7 `no_path` taken after the first `=` | 1 | `open=y=` | | M8 the keyed path taken after the first `=` | 7 | the offer rows, the prefix rows, the directory row | | M10 the key stripped, as a remedy's link is | 1 | `test_a_key_is_read_whole[a-blank]` | | M12 the key not folded by `pin_product_key` | 56 | `in-capitals` and the prefix rows, and 52 existing rows | **Runs**, all on head `c64e85fe`, `/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed`, one reused basetemp per run, with `EMAIL=service@opensoft.dev`, a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false` and `GIT_CONFIG_GLOBAL=/dev/null`: - git 2.43.0 (`/usr/bin/git`), 31 files: the 25 that run the naming gate (`grep -l validate-repository-naming tests/`, the pinned-copy files `test_pinned_copy_text.py`, `test_update_shape_kept_helper.py`, `test_update_shape_unreadable.py` and `test_adopt_reencoded_files.py` among them, and `test_repo_hygiene.py`), the files that scan every script's source (`test_git_output_decoding.py`, `test_shape_advisory.py`, `test_no_auto_maintenance.py`, `test_non_utf8_git_failures.py`), and the trio's other two, `test_agents_md_codes.py` and `test_cli_reference.py`: **2964 passed, 5 skipped** in 1749 s, exit 0. - git 2.47.1 first on PATH, the 25 naming-gate files: **2680 passed, 3 skipped** in 1181 s, exit 0. No template changes. `grep` of `tests/` found no fixed digest or `^id:`-style pattern over the gate: the tests that read a root's copy of it (`test_shape_doctor.py`, `test_doctor_validator_verdicts.py`) compare it at run time, and both are in the first run, as are the source scanners above and `test_cli_reference.py`, which holds `docs/cli.md` to the help. - `git merge-tree --write-tree origin/main HEAD`, origin/main `5b41d44f` (fetched with `-c gc.auto=0 -c maintenance.auto=false`, and `git ls-remote` agrees): clean, tree `82780dc63d7f`, the head's own tree. - Open pull requests: none of the 9 open (#954, #1023, #1025, #1027, #1028, #1030, #1031, #1038, #1039) touches `scripts/validate-repository-naming.py`, `tests/test_chain_remedy_callers.py`, `setup-project.py` or `docs/cli.md`, so there is no merge-tree to run against them. Review: rv980, lane 5's independent reviewer, not the writer (w980): READY at `c64e85fe` (issuecomment-6288391970), with no MUST or SHOULD; the landing head only merges main over it. Its NIT-1 (three test rows for the corners of the old first-`=` split) and NIT-2 (a sentence in `validate-pins.py`) go to a follow-up. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 c64e85f passed tests, tests-git-bash and SonarCloud; the head 0f5845b is a clean update-branch merge of main b0babe1, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree ac9c882 over main b0babe1: the chain_remedy_callers, repo_hygiene, agents_md_codes, cli_reference sets, 233 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Closes #980. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the run by name, and B4 with a token in use is told from fetches redone without a trace, so the token reaches no file (#998, #996) (#1045) Two stops of git's update in the family root's `scripts/bootstrap.py` still ended `family bootstrap ok` with the credential advice. Both were found by lane 5's review of #954 (issuecomment-6253384652): - **#998** (checklist candidate 1): git's update ended by a signal. On git 2.43.0, a member's own `.gitmodules` that gives its `code` leg a `branch` with no value crashes `submodule--helper` (signal 11), and the update exits 139. - **#996** (SHOULD 4): B4 (a member's commit that no branch of its remote holds) with `SHAPE_LEGS_TOKEN` or `SHAPE_LEGS_APP_TOKEN` set. #954 reads B4 from git's trace2 events, and it never traces a run with a token in use. Both now fail by name, with no advice. Each closes in its own commit: - `da7b14f8` closes #998; - `b2bbb31c` closes #996. **Stacked on #954** (#892, lane 4's), at its head `fe907c84`, and **lands after it**. The branch is `fe907c84` plus these two commits. Until #954 lands, GitHub's diff against main shows #954's changes too. ### #998: a signal (`da7b14f8`) - **`ended_on(returncode)`** reads the exit code alone, never git's words, as #892 reads 128: - `128 + N`, up to `128 + signal.NSIG`: a git that git's update ran died of signal N, and the update went on and passed the code up; - Python's `-N`: git itself was killed. - **Read as before:** - 128 is git's `die`. It goes to #892's walk, as it did. - A code from `128 + signal.NSIG` up (255, which git exits with where a command of its returns -1) numbers no signal. It reads as before. - **Where the run then ends:** - Where `unfetchable` names rows, or why git updated none, that line stands. This is the case where git 2.43.0 crashes on the holder's own `.gitmodules`, and #768's rows hold it (mutant S7). - Otherwise (a) prints: ``` UPDATE KILLED, and not for want of a credential: git's update ended on signal 11 (SIGSEGV, exit 139), which a git it ran died of. Git's own words above say which git and where. family bootstrap FAILED: git's update (ended on signal 11) ``` For `-N` the line ends `(SIGSEGV, exit -11), which killed it.` instead. - **No credential advice.** `credential_advice` gives none where a signal ended the update. A fetch git cannot make dies with 128, which ends git's update there. So an update that ends with `128 + N` met no failed fetch. ### #996: B4 with a token in use (`b2bbb31c`) This follows the issue's "Direction (no trace2)". - **When it runs:** only after git's update exits 128 with a token in use, and only at the first checkout the walk reaches whose store lacks its recorded commit (`commit_held`). The walk ends there either way, so this costs two fetches at most. - **What it runs** (`refetched`), the two fetches git's update makes there (`run_update_procedure`): - `git fetch origin`; - then, where the commit is still absent, `git fetch origin <commit>`. - **How each fetch runs:** - with the same `-c url...insteadOf` rewrite in argv as the update; - with every `GIT_TRACE*` variable removed; - with `GIT_PROTOCOL_FROM_USER=0`. This matches what `git submodule` gives every git it starts. Without it, a plain fetch took the `file` transport that git's update refused (`protocol.file.allow=user`), and it brought a commit git's update could not: the token control row found this. - **How it is read:** - B4 is where the first fetch exits 0 and the commit is still absent after the second. - A first fetch that fails is the credential's skip, as now. - Where either fetch brings the commit, nothing is said (untold). - **The token appears in no output and in no file:** - git's update and both fetches run in `untraced()`'s environment: every `GIT_TRACE*` variable removed, and trace2's three targets (`GIT_TRACE2`, `GIT_TRACE2_EVENT`, `GIT_TRACE2_PERF`) set to `0`. - The `0` also turns off a target that the system or global config names (`trace2.normalTarget`, `trace2.perfTarget`, `trace2.eventTarget`). Git reads those config targets wherever the variable is absent or empty. - Before this, with `GIT_TRACE2_EVENT` set by the caller, git's update wrote the token's rewrite into that file. This was measured at #954's head: the probes below, and the control rows failing at base. - The token still travels in argv only. No file is written, and what the two fetches print is neither read nor shown. - **The walk's interface:** it now takes a `Reached` predicate (`fetch_reached`) in place of the trace's work-tree set. With no token, the predicate is the trace's answer, as before. With a token, it is `refetched`. - **Not done: the issue's optional last paragraph** (advice that names the token in use). It is neither small nor clear: - the App token and the PAT need different cures (an installation that does not include the repository, against a PAT without `contents:read`); - every token run's advice text would change, while the control rows keep the advice as it is. - It is better as its own issue. **Setup:** - git 2.43.0 and Python 3.12.3; - the family fixture of `tests/test_family.py`, in the holder with every member checked out; - `bootstrap.py --skip-members`; - a stub `gh`, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`; - the token is the fake `w892-a-token-no-forge-takes`. | state | before | after | |---|---|---| | P3: IRRS's own `.gitmodules` gives `code` a bare `branch` | `FETCH INCOMPLETE (git exit 139)`, `error: submodule--helper died of signal 11`, the advice, `family bootstrap ok`, exit 0 | the `UPDATE KILLED ... signal 11 (SIGSEGV, exit 139)` line, `FAILED: git's update (ended on signal 11)`, no advice, exit 1 | | wrapper `git` answers (a)'s update with 139 | advice, ok, exit 0 | `UPDATE KILLED ... (SIGSEGV, exit 139) ...`, exit 1 | | wrapper `git` kills itself with SIGSEGV | `(git exit -11)`, advice, ok, exit 0 | `UPDATE KILLED ... (SIGSEGV, exit -11), which killed it.`, exit 1 | | wrapper `git` answers 128, every member at its pin | advice, ok | advice, ok (unchanged) | | B4 at IRSS with `SHAPE_LEGS_TOKEN`, then with `SHAPE_LEGS_APP_TOKEN`; the caller sets `GIT_TRACE2_EVENT` and `GIT_TRACE` under TMPDIR | git's own `not our ref` lines, the advice to set the token, ok, exit 0; **the token is in `trace2.json`** | `UPDATE STOPPED, and not for want of a credential: git stopped at members/IRSS, whose fetch reached its remote and did not bring <commit>, ...`, `FAILED: members/IRSS (git stopped at it)`, exit 1; the token is in no file and no output | | control: token set, the file transport refused, IRSS's pin moved on | advice, ok; **the token is in `trace2.json`** | advice, ok; the token is in no file | Git's own `git submodule update --init --recursive` exits 139 in P3 and 128 in each B4 and control state. All rows are in `tests/test_family_bootstrap_stops_named.py`, #954's file. They are written in its style and reuse its helpers (`b4`, `pin_moved_on`, `moved_by_hand`, `unpushed`, `cloning`, `isolated`, `said_after_the_fetch`), and no assert joins conditions with `and`. The file now has 24 tests and 37 rows (25 before). **#998:** - `test_p3_a_member_s_gitmodules_git_crashes_on_fails_naming_the_signal`: P3's state. It is skipped where git's own update does not exit 139: 2.43.1 and later refuse the line and die with 128, as `tests/test_doctor_legs_members_next.py` skips a crash. - `test_an_update_ended_on_a_signal_fails_naming_it[exit-139]` and `[sigsegv]`: the portable row. A wrapper `git` first on PATH runs the real git by its absolute path for everything but (a)'s update, and for that one it exits 139, or kills itself with SIGSEGV (core dumps off). - `test_a_code_no_signal_gives_is_read_as_before[exit-128]` and `[exit-255]`: controls; the advice and ok. - The credential control (no credential, pin moved on, exit 128) is #954's `test_the_credential_s_skip_is_as_it_was[pin-moved-on]`, unchanged. It keeps the advice and ok, and it kills S1. **#996:** - `test_with_a_credential_in_use_b4_is_named_and_the_token_is_not[SHAPE_LEGS_APP_TOKEN]` and `[SHAPE_LEGS_TOKEN]`: this is `test_with_a_credential_in_use_b4_is_read_as_before` turned round. Each asserts: - exit 1, the B4 line and the FAILED line, with no advice; - the token absent from stdout, stderr and every file under TMPDIR; - every trace the person may ask for is on and is written by the walk's own gits: `GIT_TRACE`, `GIT_TRACE2_EVENT` and `GIT_TRACE_CURL` in the environment, and trace2's normal and perf targets in the global config. - `test_with_a_credential_in_use_the_credential_s_skip_is_as_it_was[pin-moved-on]` and `[b4]`: a token set, the file transport refused. Each gives the advice and ok, the token is in no file, and git's own update exits 128. - `test_with_a_credential_in_use_the_two_fetches_are_made_again_untraced`: a wrapper `git` logs the commands bootstrap runs itself, with the token masked. The log reads exactly: - the update, with only trace2's three `=0`; - `fetch origin`, then `fetch origin <commit>`, each with the token's two `-c`, `GIT_PROTOCOL_FROM_USER=0` and only trace2's three `=0`. Every caller trace is on during this row too. - `test_with_no_credential_the_walk_makes_no_fetch_of_its_own`: with no token, the update runs traced into `TMPDIR/bootstrap-trace-*`, and no fetch is made. - `test_with_a_credential_in_use_a_commit_a_fetch_brings_is_untold[first-fetch]` and `[second-fetch]`: the wrapper answers the update with 128 having fetched nothing. Each gives the advice and ok. - `[first-fetch]`: IRSS's pin is moved to a commit on a branch of its remote, so the first fetch brings it and no second fetch is made. - `[second-fetch]`: the commit is at `refs/pins/<commit>`, so only the second fetch brings it. Wrapper rows are skipped on Windows, where a `#!` script is not run. **A fix to #954's `unpushed` helper.** It now numbers each commit it makes (`MADE`, a counter for the session). Lane 4's `fe907c84` names the commit after the clone's path. But under `tmp_path_retention_policy=failed`, pytest removes a passed row's directory, and it gives the next row whose name begins with the same 30 characters the same `tmp_path`. - In the first 22-file run, `[first-fetch]` and `[second-fetch]` both ran in `test_with_a_credential_in_use_0` and made one commit in the same second. - `[first-fetch]` had pushed that commit to a branch, so `[second-fetch]` failed. - The same can befall #954's own rows under that policy, but not under CI's default retention, where each row's directory number is new. **Fails at #954's head.** The new file was run against `bootstrap.py` at `4f97fedb` (sha256 `8282da7a...`, the same bytes at `fe907c84`): | `bootstrap.py` at | result | |---|---| | `4f97fedb` / `fe907c84` (#954) | **10 failed, 27 passed** | | `da7b14f8` (#998 alone) | **7 failed, 30 passed** (the #996 rows) | | H0 `b2bbb31c` | **37 passed** (git 2.43.0, three runs); **36 passed, 1 skipped** (git 2.47.1: P3) | - **The 10 rows that fail at #954's head:** - P3; - `exit-139` and `sigsegv`; - both B4-with-token rows; - both token controls, on the token in the caller's trace files; - the two-fetches row; - both fetch-brings rows. - **The 3 controls that pass there:** `exit-128`, `exit-255`, and the no-credential row. Each mutant was applied alone to `b2bbb31c`'s `bootstrap.py` (sha256 `e7372f13...`; the landing head's file, sha256 `50a16751...`, is that change plus #1023's hunks, which change none of the functions the mutants change) and run against the stops-named file, and the bytes were restored and checked by sha256 after each. All are **killed**. | mutant | result | |---|---| | S1 `> 128` made `>= 128` | killed, 23 failed (the 128 controls and every #954 row) | | S2 the negative `returncode` dropped | killed, 1 failed (`sigsegv`) | | S3 the advice still printed | killed, 3 failed | | S4 `ok` still printed (no failure line) | killed, 3 failed | | S5 the `128 + signal.NSIG` bound dropped | killed, 1 failed (`exit-255`) | | S6 the `UPDATE KILLED` line dropped | killed, 3 failed | | S7 the signal said before `unfetchable`'s rows | killed by #768's rows in `test_family_gitmodules_values.py` (3 failed: `branch-no-value`, `index-read`, `head-read`) | | T1 `GIT_TRACE*` left in the refetch's environment | killed, 6 failed | | T2 the second fetch skipped | killed, 2 failed | | T3 a failed first fetch read as B4 | killed, 2 failed (both token controls) | | T4a the token condition dropped (refetch with no token) | killed, 1 failed (the no-credential row) | | T4b traced with the token (#954's R10) | killed, 6 failed | | T5 the `-c` rewrite dropped from the refetch | killed, 2 failed | | T6 git's update with a token not untraced | killed, 6 failed | | T7 trace2's targets not set to `0` | killed, 6 failed (the global config's targets) | | T8 `GIT_PROTOCOL_FROM_USER=0` dropped | killed, 4 failed | | T9 the store not asked after the first fetch | killed, 1 failed (`[first-fetch]`) | | T10 the prefix narrowed to `GIT_TRACE2` | killed, 1 failed (the two-fetches row) | | T11 the store not asked after the second fetch | killed, 1 failed (`[second-fetch]`) | | R7 (lane 5's, on the refactored predicate) `same_place(checkout) in fetched` made `bool(fetched)` | killed, 1 failed | All runs used `/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed`, with one reused basetemp, and this environment: - `EMAIL=service@opensoft.dev`; - a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`; - `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`; - `LANES_*` and `GIT_ALLOW_PROTOCOL` unset. | git | files | result | |---|---|---| | 2.43.0 | the 22 files below, at `2a0681ef` (this head before the `unpushed` fix, same `bootstrap.py`) | 1 failed, 1638 passed, 2 skipped, in 11:07; the one failure is `[second-fetch]`, fixed by the `unpushed` change | | 2.43.0 | H0 `b2bbb31c`: the stops-named file + `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_no_auto_maintenance.py` | 265 passed, 2 skipped | | 2.47.1 | this head: `test_family_bootstrap_stops_named.py` | 36 passed, 1 skipped (P3) | | 2.43.0 | after #998 alone: the pinned-copy files and the trio (8 files) | 822 passed, 2 skipped | **The 22 files:** - the stops-named file; - the two that `grep -l 'family-root/scripts/bootstrap' tests/` names: `test_family.py` and `test_update_shape_kept_helper.py`; - every other file that runs the family root's `bootstrap.py`: `test_family_holder_remedies_compose.py`, `test_family_holder_bootstrap_texts.py`, `test_family_bootstrap_gate_order.py`, `test_family_gitmodules_values.py`, `test_bootstrap_without_git.py`, `test_family_unnamed_gitlink.py`, `test_family_config_appends.py`, `test_family_member_make_jobserver.py`, `test_family_siblings_bootstrap_unreadable.py`, `test_family_head_lost_gitmodules.py`, `test_family_members_not_a_list.py` and `test_family_makefile_siblings.py`; - the files that pin the file's text: `test_pinned_copy_text.py`, `test_bootstrap_spelling.py`, `test_template_text_placeholders.py` and `test_no_auto_maintenance.py`; - the trio: `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`. **Skips and scope:** - The skips are the environment's: `test_no_auto_maintenance.py` skips 2 rows because the session began with `GIT_CONFIG_GLOBAL` set. - The full suite was not run. **Static checks:** - flake8 finds in `bootstrap.py` only the three lines it found at base: line 7, the E127 in `reason`, and the `--members-make` line in `main`. - In the test file it finds only the F811s every fixture-importing file has. - Both files are ASCII apart from the em dash already in `bootstrap.py`, with no raw control characters. The landing head is `3272d533`: the restack `1fb2fbd4` (`b2bbb31c`'s change on #954's squash `8a057215`) merged with main `eee089ca` by `gh pr update-branch`. The merge is clean, and its tree is `git merge-tree --write-tree`'s own (`642527df`). - Of main's commits since `8a057215`, only #1023 (`9e154d87`) touches `templates/family-root/scripts/`. The merge brings exactly #1023's `bootstrap.py` hunks into this file (patch-id `5563fd8e` on both sides). This PR's own change is the same before and after it (patch-id `ca48f8b2`), and so is the whole PR against main (`a2dbbd28`). - #1028 landed as `b0babe18` and is in main, so the conflict with #954's hunk measured at `b2bbb31c` no longer arises. - rv1045m, lane 5's independent reviewer, delta-checked the merged file READY (#1045 issuecomment-6295632295). The two patches share no function, and neither changes an environment, argv or exit code that the other reads. `templates/family-root/scripts/bootstrap.py` is in `FAMILY_COPIED_VERBATIM`, so each holder gets one `upstream-changed` row for it, as for #954. No Windows or macOS run was made, since CI is Linux only (#597). Review: rv996, lane 5's independent reviewer, not the writer (w996): READY at `b2bbb31c` (issuecomment-6288783192), with no MUST or SHOULD; the landing head restacks that change unchanged onto #954's squash (restack3.sh's changed-lines check). Its five NITs and two checklist candidates go to follow-ups. CI (Brett Heap's ruling 27): H0 b2bbb31 passed tests, tests-git-bash and SonarCloud; the head 3272d53 merges #954's final head, then #954's squash 8a05721 (the branch's own side for #954's files), then main, with tests, tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree fb7a8df over main 6476c17: the family_bootstrap_stops_named, repo_hygiene, agents_md_codes, cli_reference sets, 207 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Closes #996 Closes #998 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…`git` Windows cannot start for a reason other than 193 that names no file (`winerror` 216, 1260, 11, or 188-202) is `git-not-runnable`, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1 (#1040) (#1062) #1001 (#1028) made the four copies of the `git-not-runnable` rule count a nameless `OSError` whose `winerror` is 193, a `git.exe` Windows cannot execute. Windows has other `CreateProcess` failures that mean git itself could not be started, and each names no file and is neither `FileNotFoundError` nor `PermissionError`, so all four copies passed it through and the run ended in a traceback, exit 1, as 193 did before #1001. RV1028 raised this as its SHOULD 1. Read from CPython 3.12's `PC/errmap.h` (main's is the same for these codes), with each code's words from Microsoft's System Error Codes: - 216 (`ERROR_EXE_MACHINE_TYPE_MISMATCH`), a `git.exe` built for another architecture, and 1260 (`ERROR_ACCESS_DISABLED_BY_POLICY`), one that AppLocker or a software restriction policy blocks: no case, so `EINVAL`, a plain `OSError`. - 11 (`ERROR_BAD_FORMAT`) and 188 to 202 other than 193: the rest of the `ENOEXEC` family, a program Windows cannot load, a plain `OSError`. - 267 (`ERROR_DIRECTORY`), a `cwd` that is not a folder: `ENOTDIR`, a nameless `NotADirectoryError`. It is not git's, and still passes through. The rule, the same text in all four (`_git_runs` in both validators and in the assembly root's bootstrap.py, `git_not_started` in the holder's): nameless = (exc.filename is None and (isinstance(exc, (FileNotFoundError, PermissionError)) or getattr(exc, "winerror", None) in (11, *range(188, 203), 216, 1260))) if exc.filename != "git" and not nameless: The code list is written in the expression, not in a module constant, so `test_the_four_copies_are_one_text`, which compares the rule's AST in the four, holds the list itself to one text. Only the list changed: a named error still counts only where it names `git`, and a nameless `EIO`, `ENOMEM`, an `ENOEXEC` with no `winerror`, and a 267 pass through. Each copy's docstring says which codes count and why, and that 267 does not. Tests: - tests/test_validate_pins_without_git.py: `win_error(winerror, number, words)` builds what CPython raises on Windows; `WIN_ERROR_193` is built by it, beside `WIN_ERROR_216` and `WIN_ERROR_1260`, which the no-git run takes in every run; `NOT_GIT` gains a nameless `winerror-267`, passed through at each call. - tests/test_validate_family_git_not_runnable.py: parity rows `winerror-216` and `winerror-1260` (which the per-call rows take too), `winerror-11` and `winerror-188` to `winerror-202` (`NOT_LOADABLE`, asked of the four copies alone: each is 193 but for its `winerror` and words), all `git-not-runnable`, and `winerror-267`, passed through by all four; each copy's own script is run where every start of git raises 216 as well as 193: exit 2, the refusal, the copy's remedy, no traceback. - tests/test_bootstrap_without_git.py: the `sitecustomize` shim is built by `git_raising(error)`; `GIT_NOT_WIN32` raises 193 as before, and `GIT_FOR_ANOTHER_MACHINE` raises 216; the upstream notice's no-git row takes 216 too; each root's rule counts 216 and 1260 and passes a nameless 267 through. #1028's NIT 3: `answer`'s docstring in tests/test_validate_family_git_not_runnable.py named `fetch_members` as the call site of the holder's `git_not_started`. #954 moved that call into `started()`, with the same three lines `answer` copies, so only the name was stale; it now says `started`. All four files are pinned copies, as in #1028: an assembly root takes `upstream-changed` rows for scripts/validate-pins.py and scripts/bootstrap.py, a holder for scripts/validate-family.py and scripts/bootstrap.py. Nothing new is imported from repo_shape.py. Not run on Windows: the codes, their classes and their words are read from CPython and Microsoft's documentation, and the errors are built by hand, as #1001's 193 is. Round 2, for RV1062's review: validate-family.py, validate-pins.py and both bootstrap.py copies: a `git.exe` App Control for Business blocks (`[WinError 4551]`) is `git-not-runnable` too, and two rows hold the code list exact (#1040) RV1062's S1: 4551 (`ERROR_SYSTEM_INTEGRITY_POLICY_VIOLATION`) is App Control for Business (WDAC) or Smart App Control refusing to start an image, the kind of block 1260 is for AppLocker and software restriction policies. CPython 3.12.10 raised it from `_winapi.CreateProcess` as a plain `OSError` naming no file (ROCm/TheRock issue 2607: `OSError: [WinError 4551] An Application Control policy has blocked this file`), and `PC/errmap.h` has no case for it, so it is `EINVAL`, as 216 and 1260 are. Every copy passed it through, and the run ended in a traceback, exit 1. The four copies, still one text: in (11, *range(188, 203), 216, 1260, 4551) Each copy's docstring names 4551 beside 1260, and says any other `winerror` passes through. RV1062's S2: nothing held the list exact beyond 267, so a rule counting any nameless `winerror` but 267 (RV1062's mutant A, `not in (None, 267)`) passed every row. `NOT_GIT` gains two nameless `CreateProcess` failures that say nothing of git, `winerror-87` (`ERROR_INVALID_PARAMETER`, `EINVAL`, the class 216, 1260 and 4551 arrive in) and `winerror-8` (`ERROR_NOT_ENOUGH_MEMORY`, `ENOMEM`), each passed through: by the parity in all four copies, at each of validate-pins.py's and validate-family.py's three calls, and by each root's own rule. Tests: - tests/test_validate_pins_without_git.py: `WIN_ERROR_4551`, which the no-git run takes in every run; `NOT_GIT` gains `winerror-87` and `winerror-8`. - tests/test_validate_family_git_not_runnable.py: `winerror-4551` in `WILL_NOT_START`, so a parity row and the per-call rows, as 1260 has; `winerror-87` and `winerror-8` arrive through `NOT_GIT`. - tests/test_bootstrap_without_git.py: each root's rule counts 4551 and passes a nameless 87 and 8 through. Words from Microsoft's System Error Codes (87, 8) and from the CPython traceback in ROCm/TheRock issue 2607 (4551); the name 4551 from mingw-w64's winerror.h. Not run on Windows. Closes #1040 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



(lane openRepoShape-4, DRAFT)
Stacked, now on main: this PR was stacked on #983, the change for #928. #983 landed as squash
5b41d44(its final headba968677), and GitHub retargeted this PR to main. The branch came onto main by three merges, and nothing pushed is rewritten:3fb918eamerges validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983's final headba968677. Clean.9aab7ae1merges the squash5b41d44, keeping this branch's side of validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983's two files.ba968677's.d2eda8b4's, and differ from the squash only by this PR's own lines (15 and 214).5b41d44itself.git diff main...9aab7ae1isd2eda8b4's change line for line, 7 files, +234/-80. Only one hunk header moves, by 17 lines, in the assembly root'sbootstrap.py.Then
411e0d6atakes RV1028's NIT 1 (below). Head:411e0d6a.On Windows, a
giton PATH that Windows cannot execute is nowgit-not-runnable, exit 2, in all four copies of the rule. Before, each run ended in a traceback, exit 1.OSErrorthere:winerror193,errnoENOEXEC, and no file name ([WinError 193] %1 is not a valid Win32 application).OSErrornamedgit, or a namelessFileNotFoundErrororPermissionError, so this error passed through.git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983 gavevalidate-family.pythe rulevalidate-pins.pyalready had, and disclosed this gap. RV983 (validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983 issuecomment-6269371615) asked for this follow-up as its SHOULD 1, and asked for the bootstraps inPARITYas its NIT 1.Closes #1001
Changes
The rule, one text in four copies
templates/family-root/scripts/validate-family.py,_git_runs: itsnamelesstakes thewinerror193 clause. Its docstring no longer says 193 passes through.templates/assembly-root/scripts/validate-pins.py,_git_runs: the same clause. Its docstring names the three nameless errors that count.templates/family-root/scripts/bootstrap.py,git_not_started(bootstrap.py (the family root's and the assembly root's):make bootstrapwith no git on PATH ends in aFileNotFoundErrortraceback, exit 1, where the validators refusegit-not-runnable, exit 2 #694): the same clause, and the docstring says so.templates/assembly-root/scripts/bootstrap.py,_git_runs(bootstrap.py (the family root's and the assembly root's):make bootstrapwith no git on PATH ends in aFileNotFoundErrortraceback, exit 1, where the validators refusegit-not-runnable, exit 2 #694): this copy spelled the same rule in other words (if exc.filename != "git" and (exc.filename is not None or not (...))). It now uses the text above, with the clause, and the docstring says so.What stays the same:
REFUSED git-not-runnable: <what>: git could not be run (%1 is not a valid Win32 application), then the copy's "put git on PATH" remedy, then exit 2.winerror.One behaviour change outside the refusal itself
bootstrap.py,_clone_tip) holds a_git_runsblock insidetry ... except OSError: return None. A nameless 193 used to pass through_git_runsand be swallowed there, and the notice said nothing.git-not-runnable, exit 2, as it already did for a missing git.sitecustomizeraising 193.411e0d6a, a row holds it (RV1028's NIT 1):test_the_offline_upstream_notice_with_no_git_refuses_too[winerror-193], end to end, underGIT_NOT_WIN32._git_runsblock inside a handler that catchesOSError.The tests
tests/test_validate_pins_without_git.py(58 -> 61 rows):WIN_ERROR_193moves here from validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983'stests/test_validate_family_git_not_runnable.py, where it was the helperwin_error_193. It now sits besideNO_GITand the rest, in the module the other two files import their errors from.test_no_git_at_all_is_one_refusal_in_every_runtakes it in each of its three runs: exit 2, the refusal,REMEDIATION, no traceback.tests/test_validate_family_git_not_runnable.py(validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983's file, 59 -> 68 rows):winerror-193now expectsgit-not-runnable. It moves fromNOT_GITStoWILL_NOT_START, so the three per-call rows (gitlink, commit, digest) take it too, and its three pass-through rows go.PARITYloads all four copies. The holder'sgit_not_startedis asked the way its one call site asks it.git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983's test is renamed fromtest_both_validators_give_one_answer_to_each_oserrortotest_the_four_copies_give_one_answer_to_each_oserror. A counted answer now also requires the missing git's words, and a remedy that starts "put git on PATH".winerror-193-naming-the-member. It holds the half of the rule that counts 193 only when nothing is named.test_a_git_windows_cannot_execute_is_refused_by_each_copy, one row per copy: validate-family.py, validate-pins.py and both bootstrap.py copies: agitthat is not a Win32 application ([WinError 193]) ends in a traceback, exit 1, notgit-not-runnable#1001's "Verify" row in each copy. It runs each copy's own script in its own root, and the child gets theGIT_NOT_WIN32sitecustomize, which makessubprocess.Popenraise Windows' error forgit, astests/test_update_shape_landing.py'sGH_ON_PATHstands in forgh. Each row asserts exit 2, the refusal, the copy's remedy, and no traceback.test_the_four_copies_are_one_text. validate-family.py, validate-pins.py and both bootstrap.py copies: agitthat is not a Win32 application ([WinError 193]) ends in a traceback, exit 1, notgit-not-runnable#1001 asks for the four copies to stay one text. Until now no test held them to it:CONTRACTS_DIRsentence is false for every scaffolded root, and the chain WARNING offers remedies the scaffold lacks (one shape bump before the v1.1.0 tag) #610's skew test,tests/test_pinned_copy_text.py'stest_no_pinned_script_imports_what_an_older_library_lacks, checks only IMPORTS: that no pinned script imports a name newer than a keptrepo_shape.py. That is why the rule is copied rather than shared. It says nothing about the copies' text.namelessexpression and theifthat reads it in all four copies, so layout and indentation do not count.tests/test_bootstrap_without_git.py(19 -> 22 rows):WIN_ERROR_193, and the remedy is now checked on every counted row.411e0d6a, the no-git notice row is parametrized over the two ways git cannot start: no git on PATH, andGIT_NOT_WIN32.GIT_NOT_WIN32moves here fromtests/test_validate_family_git_not_runnable.py, which imports it, since that file already imports this one's remedies.Results
Before and after
The new tests at
d2eda8b4, run on840d2a07's templates: 14 of 150 fail. They are the one-text test, and the 13 rows that hand a copy a nameless 193 and expect the refusal:validate-family.py's three per-call rows;validate-pins.py's three runs;Every other row passes there, the named-193 row included. At
d2eda8b4, all 150 of 150 pass.The four copies, run by hand. Each copy's own script in its own root, under the same
sitecustomize. Run with Python 3.12.3, aghstub,GIT_CONFIG_GLOBAL=/dev/null,GIT_CONFIG_NOSYSTEM=1andGIT_SSH_COMMAND=false:840d2a07validate-family.pyOSError: [Errno 8] %1 is not a valid Win32 application, exit 1REFUSED git-not-runnable: .gitmodules could not be read as git reads it: git could not be run (%1 is not a valid Win32 application), its remedy, exit 2validate-pins.pyREFUSED git-not-runnable: the spec leg's gitlink recorded at 'spec' could not be read: ..., its remedy, exit 2bootstrap.pyREFUSED git-not-runnable: the members could not be fetched: ..., its remedy, exit 2bootstrap.pyREFUSED git-not-runnable: the spec leg at spec could not be placed on its tracking branch: ..., its remedy, exit 2The runs
All on Python 3.12.3, git 2.43.0 and the same environment:
d2eda8b4, 23 files: 1460 passed, 0 failed, in 9:51.9aab7ae1, the same 23 files plustest_assembly_root_unreadable_pathsandtest_bootstrap_after_bump(the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so ascripts/nobody may search no longer SKIPs every validator intobootstrap ok(#430, slice D) #958's and bootstrap.py: the paste words it prints quote a leading=, are made of the bytes git is handed (UTF-8 on Windows), and the NOT INITIALIZED command names a leg's path as one word after a--, with./before a:(#961) #973's, which reach the assembly root'sbootstrap.py): 1696 passed, 0 failed, in 12:29.411e0d6a, the same 25 files: 1697 passed, 0 failed, in 13:29.The files:
test_bootstrap_refusal_exit,test_bootstrap_upstream_notice,test_family_add_refusals,test_family_siblings_bootstrap_unreadable,test_validate_family_without_git,test_validate_pins_named_and_spelledandtest_validate_pins_unreadable_workflows_dir;test_pinned_copy_text,test_bootstrap_pasteable_lines,test_chain_remedy_callers,test_chain_pin_sources,test_template_text_placeholdersandtest_refusal_default_remediation;test_validate_family_unreadable,test_family_holder_remedies_compose,test_family_holder_bootstrap_textsandtest_no_auto_maintenance;test_repo_hygiene,test_agents_md_codesandtest_cli_reference.The rest of the suite is left to CI.
Mutants
13 mutants at
d2eda8b4, each run against the three rule files (150 tests). All 13 were killed. The fixed bytes were put back after each one and checked by sha256 andgit status.parity[winerror-193-naming-the-member]and its three per-call rowsvalidate-family.pyonlyvalidate-pins.pyonlybootstrap.pyonlybootstrap.pyonlyvalidate-family.pyonlyvalidate-pins.pyonlybootstrap.pyonlybootstrap.pyonlytest_the_four_copies_are_one_textalone, by designerrno8 forwinerror193), all fourexec-format-error-naming-no-filerows (parity, per call,validate-pins.py's per call)In M4 to M11, a behavioural row fails apart from the one-text test, so no single-copy mutant is caught by the text test alone. M12 is the exception, by design.
RV1028's mutants R3 and R2, at
411e0d6a, against the three rule files (151 tests), restored the same way:_clone_tipreturns None for a refusal whose cause haswinerror193 (it survived atd2eda8b4): killed, 1 failed,test_the_offline_upstream_notice_with_no_git_refuses_too[winerror-193]._clone_tipcatches(OSError, Refusal): killed, 2 failed, both notice rows.Shared files
git merge-tree --write-treeof this head411e0d6aagainst main and the open PRs that touch these files. #958 and #973 merged asaf0e60f2and5957f8c3; both came in throughba968677, with no conflict.5b41d44f, predicted0f94ba5e(#1027:scripts/family.pyand its test only)f207e982; this head holds main up to5b41d44fbootstrap.py)4f97fedb7fd75e91bootstrap.py)5cf603651dd48bb91dd48bb9validate-family.py)927fabf77c08db197c08db19#954's conflict is not this PR's.
bootstrap.py. It is the same hunk this PR's base840d2a07met with the family root's bootstrap.py: a gitlink a member's own index gives no url, or a member's commit that a fetch reaching its remote did not bring, fails the run by name with its cure and no credential advice, where it saidfamily bootstrap okbeside git's exit 128 (#892) #954, and the results differ only in this PR's own lines, which merge cleanly outside the hunk.family bootstrap okbeside git's exit 128 (#892) #954 are051176ceandb41f722b.b41f722bis a merge on validate-family.py and bootstrap.py: the holder's remedies, typed as printed in order, put a.gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889's unsquashed branch (fix/879-holder-remedies-compose) that both this branch's history (through validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983's) and the family root's bootstrap.py: a gitlink a member's own index gives no url, or a member's commit that a fetch reaching its remote did not bring, fails the run by name with its cure and no credential advice, where it saidfamily bootstrap okbeside git's exit 128 (#892) #954's contain, and main does not.family bootstrap okbeside git's exit 128 (#892) #954 cleanly, and so does a squash of this PR on main. In that merge, this PR's 193 clause is ingit_not_started.RV1028's NIT 3, for later: when #954 lands, whichever of #954 and this PR lands second resolves the hunk by taking #954's side.
family bootstrap okbeside git's exit 128 (#892) #954's side gives exactly the family root's bootstrap.py: a gitlink a member's own index gives no url, or a member's commit that a fetch reaching its remote did not bring, fails the run by name with its cure and no credential advice, where it saidfamily bootstrap okbeside git's exit 128 (#892) #954's file plus this PR's lines (RV1028 checked it by diff).answer's docstring intests/test_validate_family_git_not_runnable.pynamesfetch_membersas the call site. the family root's bootstrap.py: a gitlink a member's own index gives no url, or a member's commit that a fetch reaching its remote did not bring, fails the run by name with its cure and no credential advice, where it saidfamily bootstrap okbeside git's exit 128 (#892) #954 moves that call intostarted()with the same three lines, so the emulation stays right and only the name goes stale.Kept away from the others.
scripts/nobody may search no longer SKIPs every validator intobootstrap ok(#430, slice D) #958 inserted lines right after the assembly root's_git_runs. This PR's hunk stays inside_git_runs.family bootstrap okbeside git's exit 128 (#892) #954 moves the holder's fetch call site intostarted()and keepsgit_not_starteditself. This PR's hunk stays insidegit_not_started.Pinned copies
COPIED_VERBATIM,FAMILY_COPIED_VERBATIM).upstream-changedrows forscripts/validate-pins.pyandscripts/bootstrap.py.scripts/validate-family.pyandscripts/bootstrap.py.applytakes each root's rows, as bootstrap.py: the name the rename line gives a twin skips one with a branch stored under it,Main-keptoverMain-kept/x, which git refuses, so the pasted line works and the next run makes the tracking branch (#1000) #1006 and validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983 handled theirs. RV1028'supdate-shape.py checkfrom 840d2a0 to d2eda8b gave exactly those rows.tests/or anywhere else.scripts/repo_shape.py.tests/test_pinned_copy_text.py's skew test passes.Not run on Windows
The 193 error is built by hand, as validate-family.py:
git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983 built it, from CPython'sPC/errmap.hand_winapi.CreateProcess.The end-to-end rows stand in for Windows with a
sitecustomize, not a realgit.exe. Everything afterPopenis real: the script, its handlers, its exit code and both streams.Limit: only 193 is counted, as validate-family.py, validate-pins.py and both bootstrap.py copies: a
gitthat is not a Win32 application ([WinError 193]) ends in a traceback, exit 1, notgit-not-runnable#1001 asks. The other namelessCreateProcessfailures that mean git could not start still pass through, ending in a traceback, exit 1:git.exefor another architecture (EINVAL);EINVAL);ENOEXECcodes 11 and 188 to 202 other than 193.validate-family.py, validate-pins.py and both bootstrap.py copies: a
gitWindows cannot start for a reason other than 193 that names no file (winerror216, 1260, 11, or 188-202) ends in a traceback, exit 1, notgit-not-runnable#1040 is the follow-up for those (RV1028's SHOULD 1). It names the exact values, so that 267, a working directory that is not a folder, keeps passing through.CI
members:line is refused by the reader as a key written twice since #974, which turned main's tests red (#1024) #1026 landed as051176ce. It fixes the two rows oftests/test_family_add_gitlink_and_null_members.pythat had kept main's push run RED since scripts/repo_shape.py: the YAML reader refuses a key written twice in one mapping, block or flow, by the line of the second, and a block scalar line indented less than its first by its line, where it kept the last key and cut the line's first characters; such a line opening#ends the block as a comment (#917) #974.5b41d44and this branch both hold051176ce.d2eda8b4,tests,tests-git-bash,packageand SonarCloud all passed.411e0d6a, at 07:01Z,tests-git-bash,packageand SonarCloud had passed andtestswas still running.🤖 Generated with Claude Code
Summary by Sourcery
Handle unexecutable Git binaries on Windows consistently across validators and bootstrap scripts.
New Features:
[WinError 193]failures when starting Git asgit-not-runnableacross both validators and both bootstrap scripts.Bug Fixes:
Enhancements:
Tests:
[WinError 193]coverage for validators, bootstraps, and the offline upstream notice.