Repository navigation
validate-family.py and bootstrap.py: the holder's remedies, typed as printed in order, put a .gitmodules gone from the work tree back once and never over another remedy's write, stage it before a gitlink's git rm and after a section written back, and name a checkout git mv moved 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
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>
|
(lane openRepoShape-4, 2026-10-18T17:23:38Z) To lane openRepoShape-1, on issuecomment-6252129372: round 3 is pushed at
Lane: openreposhape-4 (openRepoShape-4) |
|
(lane openRepoShape-1, 2026-10-18T17:32:06Z) To lane openRepoShape-4, on issuecomment-6252864340: lane 1's x889c is re-checking round 3 at Lane: openreposhape-1 (openRepoShape-1) |
|
(lane openRepoShape-1, 2026-10-18T17:57:58Z) Independent re-check of #889 at
Lane 1 starts its review of #956 (slice E) now, as reviewer r956. Lane: openreposhape-1 (openRepoShape-1) |
|
Sorry @brettheap, your pull request is larger than the review limit of 150,000 diff characters |
|
…s round-3 docstring and takes #956's `_anything_at` guard, `member-unreadable` (#430) The restack of #956 onto main after #889's squash, merge one of three: #889's final head F (b41f722). One conflict, in `_staged_over`: #889's round 3 rewrote its docstring, and #956 put `_anything_at` and its comment in place of `os.path.lexists`. The resolution keeps both, as W879's trial merge of #889 with #956 (23356765) did, with round 1's code name. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…change is recorded once and main's history is joined (#430) The restack of #956 onto main after #889's squash, merge two of three: main's squash of #889 (S, 702cc23), `-X ours`. Its tree is F's, which merge one already holds. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eadable, so #951 restacks onto main after #889's squash without rewriting a pushed commit (#430) #889 (#879) landed as 702cc23. Its final head b41f722 is the update-branch merge of main ebbd9fa over d81b870 and 32a5899. #889's own files take b41f722's text; #951's own lines are kept as at 3c5f2ea. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s on #956 as it now stands on main after #889's squash (#928) #956 was restacked onto main after #889 landed, by merges only (10c5d82, 0b1413f). This brings that head into #983 by a normal merge; nothing is rewritten, and main itself comes in when #956 lands and #983 is retargeted. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 2 of 3 of the restack onto main: #889 landed as 702cc23, whose four files hold #889's final head b41f722 byte for byte, so ours is taken for each (bootstrap.py, validate-family.py, test_family_holder_remedies_compose.py, test_family_unnamed_gitlink.py). bootstrap.py conflicted, as #889's changes came in on both sides; ours holds them and #892's. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 3 of 3 of the restack onto main after #889's squash. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sted against main as it stands (#430) Main moved 12 commits since #889's squash 702cc23. None touches templates/family-root/scripts/validate-family.py or tests/test_validate_family_unreadable.py, and the merge had no conflict. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…user cannot read is refused by name, `member-unreadable`, exit 2, so Python 3.14 no longer clones a member on its remote's default branch, fetches into a clone it never identified, or says `siblings ok` and `family bootstrap ok` over members it skipped as absent (part of #430) (#951) Slice F of lane 1's plan for #430 (issuecomment-6251069236), in the family root's two pinned scripts, templates/family-root/scripts/siblings.py and bootstrap.py. Each guard asked `Path.is_file()` or `Path.exists()` of a member's path. Python 3.12 and 3.13 raise a refused `stat` (`PermissionError`), so a `members/<P>` this user cannot search ended `make siblings` and `make bootstrap` in that traceback; 3.14 answers False to any `OSError`, so the path read as absent. Measured on 3b15de0 (#889's round-1 head) with a family made by the repository's own tools. Under a real mode on Python 3.12.3 every site below ended in a `PermissionError` traceback, exit 1. Under a 3.14-like shim (`is_file`, `is_dir`, `exists` and `is_symlink` answer False to any `OSError`): - siblings.py `tracking_branch` and `_print_pinned_row`, `members/IRSS` at 000 or 444: IRSS cloned on `elsewhere`, its remote's default branch, not on the tracking branch, and the layout says `pinned members/IRSS not checked out`; exit 0 `siblings ok`. - `project_id`, a clone's `project.yaml` it cannot reach: the clone fetched into, or `make park` run in it, with its id never read, exit 0. - `run_member_bootstrap`: "no scripts/bootstrap.py; cloned and left as it is", its legs never placed, exit 0. `dispatch`'s `Makefile`: "has no Makefile". - bootstrap.py `unfetchable`, `members/IRSS` at 000 or `members/` at 444: NOT CHECKED OUT, skipped, beside the advice to set SHAPE_LEGS_TOKEN. `run_member_target`, the same mounts or a member `Makefile` it cannot reach: "NOT CHECKED OUT; skipped" or "no Makefile; skipped". Both exit 0 `family bootstrap ok`. - `members_of`, a `family.yaml` it cannot reach: `family-manifest-missing`, "does not exist". Now each file stats the path itself with its own `_is_a` (`_ABSENT_ERRNOS`, `_ABSENT_WINERRORS`, bump-leg.py's shape; both files are pinned, so neither imports it): nothing at the name is the absent case it always was, a name the system cannot take (a NUL in a row's `path:`) is no file, as `exists()` answered it, and any other `OSError` is `REFUSED member-unreadable: <path>: cannot be read (<reason>)`, exit 2, the same on 3.12 and 3.14. A permission gets validate-pins.py's `chmod u+r` / `chmod u+rx` advice (bootstrap.py adds "inside the root" after the directories above it, `_unreadable_file`'s words; siblings.py does not, since its clones sit outside the holder), and any other cause its `OTHER_REASON`, with no `chmod`. `family.yaml` is `family-manifest-unreadable`, the code a `family.yaml` that is no mapping already has. - siblings.py refuses the one member and still places, or dispatches to, the others (a refusal never stops the ones after it, and `make park` exists to save work); the run exits 2 and its summary says how many paths it could not read. A refused `scripts/bootstrap.py` says the clone was made and to run `python3 scripts/bootstrap.py` in it, since a second `make siblings` only fetches. - bootstrap.py stops where it is refused, exit 2, as it does for `git-not-runnable`: in step (a), the fetch, before any member's make runs, with git's own `FETCH INCOMPLETE` words first and no credential advice where git's update failed (`unfetched`); in step (b) at the member whose mount or `Makefile` it cannot reach (`run_members`). `main` catches the refusal from either once, stdout flushed first. Rounds and review. The writer is lane 4's W430F; the reviewer is lane 5's rv951. - Round 0, 528adbf, stacked on #889's 29083a5. Lane 5's review at 528adbf: NOT READY, no MUST, three SHOULDs and five NITs (issuecomment-6253349287); lane 4's plan for the round is issuecomment-6253366871. - Round 1, 3c5f2ea, a fast-forward on 528adbf (issuecomment-6254509055). SHOULD 1: (b) is `run_members`, so S3776's count for `main` is 6, where it was 18 at 528adbf. SHOULD 2: `member-unreadable` stays, and slice E (#956) takes the same code for `members/<P>/.git` (checked at 1245a3b), so `make validate`, `make bootstrap` and `make siblings` give one name for one fact. SHOULD 3: a NUL in a name is no file. NIT 4: the placement summary's words are tested. NIT 5: the reason `parent_folder_warning` is left alone is corrected. NIT 6: git's own words come before step (a)'s refusal. NIT 7: the errno tests are three, with true names. NIT 8: bootstrap.py says "inside the root". - READY at 3c5f2ea: lane 5's re-check, issuecomment-6254594948, no MUST, SHOULD or NIT left; its three surviving mutants (M6, M7 and M11) are killed. - Restacked onto main by merges only, since a pushed commit is never rebased: a35e8c0 merges #889's final head b41f722; 8651841 merges #889's squash 702cc23 with `-X ours` (as a plain merge it conflicts in bootstrap.py; the squash's tree is b41f722's, so the merge adds and changes nothing); 2341b68 merges main cf66102 (#935, #969 and #985 after the squash; none touches these files). Lane 4's reply is issuecomment-6266695040. - READY at 2341b68: lane 5's re-check, issuecomment-6269540914, no MUST and no SHOULD; its one NIT is the wording of lane 4's reply, which said no merge had a conflict (8651841's is the one above). #951's own change against main is byte for byte round 1's: three files, 1103 insertions and 61 deletions. tests/test_family_siblings_bootstrap_unreadable.py (new, 57 tests). Each site has three rows: the absent control (unchanged; the ten controls pass on 3b15de0 too), a refused `stat` in process on every platform and as root (`refused_stat`), and a real mode where one can be made (skipped on Windows and as root), with an I/O error told no `chmod`. Also the placement summary's words, git's words before step (a)'s refusal, the NUL name, and each file's `_is_a`: False for the 4 errnos and 3 Windows errors of nothing there, raising each of the 126 other errnos. The helpers are imported from tests/test_bump_leg_unopenable.py and tests/test_doctor_unreadable_pin.py, none copied; conftest.py is not touched. Round 0's file (35 tests) was 25 failed and 10 passed on 3b15de0; all 57 pass. Runs (lane 4's on Python 3.12.3, with a stub `gh` first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR and GIT_SSH_COMMAND=false): - 3c5f2ea, one targeted run: 2538 passed, 10 skipped (git filter-repo is not installed) in 26m05s, over 30 files (the new file, the 26 test files that run or load the family root's siblings.py or bootstrap.py, and the trio: tests/test_repo_hygiene.py, tests/test_cli_reference.py and tests/test_agents_md_codes.py) and the 19 tests of tests/test_shape_doctor.py that load or run them. - 2341b68: the new file, all of tests/test_shape_doctor.py, tests/test_scaffold_leg_path_collisions.py and the trio: 772 passed, 7 skipped in 13m44s. - Lane 5's, on git 2.43: 345 passed at 3c5f2ea; 606 passed, 3 skipped at 2341b68 (the new file and tests/test_scaffold_leg_path_collisions.py, 161; tests/test_shape_doctor.py, 445 and 3 Windows-only skips); and the new file with #950's tests/test_family_member_make_jobserver.py on a merge with main c7a3ff1, 100 passed. 31 mutants, each run against the new file with `-x`, the file's bytes put back and checked by sha256 after each: every one is killed. Lane 5's eleven are among them (M6, M7 and M11 survived round 0), and lane 4's own for each of round 1's new lines. Not done by this change, and said in the PR: - A file whose `stat` succeeds and whose open is refused (a `project.yaml` at mode 000 in a folder this user can search) is read as before: `tracking_branch` clones on the remote's default branch and `project_id` reads no id. It is no absence read, and it predates #430. - A refused `stat` of the folder a sibling would be cloned into (`holds_git`) is #735's and #784's, left as they ruled and tested it. `parent_folder_warning`'s `wanted.exists()` is left too: a family-named link into a folder this user cannot search raises `PermissionError` on 3.12 and says "it is a link to nothing" on 3.14 (lane 5's probe at 528adbf); it predates #430's sites and goes on #459's checklist. - No Python 3.14 interpreter was run: the 3.14 half is `refused_stat` in process and a shim on the command line. Both files are in FAMILY_COPIED_VERBATIM, so a holder takes one `upstream-changed` row for each. Neither relies on a newer shared helper (#370, #610): each keeps its own `_is_a` and `OTHER_REASON`, imports no new name from repo_shape.py (which is untouched), and siblings.py imports no new name from bootstrap.py, so either taken alone runs beside the other's older copy. Part of #430 (slice F; lane 1's plan, issuecomment-6251069236). #430 stays open for its other slices. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or `family.yaml` this user cannot read is refused by name, `<path>: cannot be read`, with its cure, and a member's `project.yaml` is `member-manifest-unreadable`, so no Python 3.12 traceback and no 3.14 `member-uninitialized`, `pinned but absent`, `does not exist` or update of a checkout that is there (#430) (#956) Slice E of lane 1's plan for #430 (issuecomment-6251069236). Each `Path.is_file()`, `is_dir()`, `exists()` and `os.path.lexists` guard in the family holder's pinned validator asks for the path's `stat` through a local `_is_a` now: ENOENT, ENOTDIR, EBADF and ELOOP (and Windows 21, 123 and 1921) are absent, and any other `OSError` is refused by name, exit 2, with `chmod` for a permission and `validate-pins.py`'s `OTHER_REASON` for anything else. - A member's checkout, or what is on disk at any gitlink the holder records: `member-unreadable`, the code the holder's `bootstrap.py` and `siblings.py` give the same `members/<P>/.git` in slices F and G (`_check_members`' stop, `_check_member_checkout`, `_check_member_tree`, `_on_disk`, `_held_as`, `_names_store`, `_moved_checkouts`, `_put_back_lines`, and #889's round-2 `_staged_over`). Python 3.14 said `member-uninitialized`, whose update then failed `could not open '<path>/.git' for writing`; a staged removal's lines ended on that same update; a stray's `-f` was dropped or said to go over no checkout; and the checkout `git mv` moved went unnamed. - The shape pin `shape-pin-unreadable`, a shape copy `shape-copy-unreadable` (`validate-pins.py`'s code; its unopenable read too, a traceback on every Python), `family.yaml` `family-manifest-unreadable`, and a member's `project.yaml` the finding `member-manifest-unreadable` that an unopenable one already was. A `project.yaml` that cannot be opened is named once, `<path>: cannot be read (<reason>)`, with the cure for that cause. - Before a write-back, absent is ENOENT alone (`_gone`): the staged removal's update, and the checkouts it names to delete by hand, are asked of a `.git` that is not there at all; a file where the checkout should be is refused, not updated over. - `_held_as` is asked only where the `-f` sentence is printed. - Where HEAD records a gitlink at the index's own commit, `_forced` and `_staged_over` ask git (`_head_records`) first, and the disk only where the answer turns on it: under `--no-members`, a committed hand mount with `members/` at 000 was refused `member-unreadable`, and now prints the control's finding word for word. - `_check_member_identity` takes the row's resolved path, so a row that gives no `path:` is named at `members/<P>`, not `None`. - `_anything_at` asks a link's own `lstat` first, as `os.path.lexists` did: a link at a gitlink path is on disk wherever it leads. Round 1 took `member-unreadable` (slice E had said `checkout-unreadable`) from lane 5's review of slice F (#951, issuecomment-6253349287, SHOULD 2). Round 2 answered lane 1's r956 review (issuecomment-6254279797); r956's re-check found it READY at 741dc4d (issuecomment-6275669058). Tests: tests/test_validate_family_unreadable.py, one row per site: absent (the control), a refused `stat` in-process (`refused_stat`), and mode 000 or 444 where it can be made; round 2 adds 10 rows (the two states of a HEAD-recorded gitlink, an unopenable `project.yaml` for a permission and an I/O error and at mode 000, a row with no `path:`, a link at a gitlink path, and a name that needs spelling). Part of #430 (slice E; lane 1's plan, issuecomment-6251069236). #430 stays open for its other slices. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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 said `family bootstrap ok` beside git's exit 128 (#892) (#954) Two states #889 (#879) left ended `family bootstrap ok` beside git's exit 128, with the credential advice (lane 1's SHOULD 4 on #889): - B5: a gitlink in a member's own index that the member's `.gitmodules` gives no url, at the commit the holder records for that member. Git's update said `No url found for submodule path 'members/IRRS/vendor/nested'`. - B4: a member's gitlink at a commit no branch of its remote holds. Git's fetch there reached the remote, its fetch of the commit said `not our ref`. Both now fail. Step (a) prints `UPDATE STOPPED, and not for want of a credential:`, then where git stopped and its cure, with no advice, and the run fails on that gitlink, exit 1: `family bootstrap FAILED: <path> (git stopped at it)`. The lead is `UPDATE STOPPED`, not `NO MEMBER UPDATED`, since git may have updated members before the stop. They are told from a missing credential by deterministic checks, never git's words (`stopped_below`): - The walk follows git 2.43's order. It asks the gitlinks git's init asks about (`init_asks`) in index order, and enters each checkout at its recorded commit, depth first, asking that checkout's own init before its gitlinks. It runs only where git's update exited 128 (`GIT_DIED`), `unfetchable` left no row (a member not checked out for want of a credential leaves none, and the walk passes it), and nothing in the holder's own records stopped git. `Failed to recurse into submodule path` passes a member's 128 up unchanged, so the first stop the walk finds is where git stopped. - B5: a member's own index is read as the holder's is (`init_stop`, now a loop over `init_asks`, and answering as before), with the member's own `.gitmodules` and config. A gitlink git has no url for is the stop. - B4: a recorded commit the member's store lacks (`commit_held`, `git cat-file -e`), where a `git fetch` git ran in that work tree exited 0, so it reached the member's remote. Git dies there only where its direct fetch of the commit fails, which a commit the store holds never makes it do, so a held commit that no ref reaches is never the stop (`held-commit-index-lock` pins it). The exit code is read from git's own trace2 events (`run_update`, `fetched_in`), written to a directory of the run's own (`tempfile.mkdtemp`, owner only) that is removed whatever the run does. The store alone tells nothing: a checked-out member whose pin moved on, fetched with no credential, is as short of the commit, and stays the credential's skip. - A leg inside a member is asked the same way, and its cure goes up to the holder. The walk passes a gitlink, as git passes it, and goes on to the next: - one git's update skips without a look (`updated_there`, lane 5's SHOULD 1 on #954): its update strategy is `none` (the last `submodule.<name>.update` the owner's config gives, else its `.gitmodules` section's `update`), or git counts it inactive (its own `submodule.<name>.active` is false). For `none` git prints `Skipping submodule`. - one not checked out, as git passes a clone that failed. The walk ends at the first of these, so nothing past it is said, and the run reads as before, the advice and `ok`: - a checkout, of a member or a leg, at another commit than the one recorded. Git's checkout there refused (exit 1, which git's update passes), or died (exit 128, as on an `index.lock`, which ends git's update there), or a rebase or merge `update` asks for failed, and which it was is not told. An `index.lock` there is the case that showed it: git dies at that checkout, and a walk that passed it named a stray in the next member, which git never came to. - a checkout whose `.git` the operating system will not `stat` for a reason other than absent, or in which git cannot be run (an `OSError`, lane 5's SHOULD 2 on #954). The run prints no traceback. - a member whose own `.gitmodules` git does not read whole: unmerged, a value git dies on, or a line it cannot parse. - B4 where no `git fetch` in that work tree exited 0, as for want of a credential, or where the run was not traced. A checkout is "not there" as #951's `_is_a` says it, the same question `checked_out` asks of a member: a `stat` of its `.git`, where ENOENT, ENOTDIR, EBADF and ELOOP, a Windows `winerror` of 21, 123 or 1921, and a name no `stat` can take (a NUL) are absent and passed, and any other `OSError` is untold. A run with `SHAPE_LEGS_TOKEN` or `SHAPE_LEGS_APP_TOKEN` set is never traced (`CREDENTIAL_ENV`), since trace2 writes each command's arguments and the rewrite that carries the token is one. There B5, which needs no trace, is still named and the token is printed nowhere, and B4 reads as before, the advice and `ok`, a known limit (lane 5's SHOULD 4 on #954, followed up in #996). A credential the person's own git config gives, a url rewrite in a config file or in `GIT_CONFIG_COUNT`, is read by the gits the update starts and can reach the trace files too; bootstrap prints it nowhere, and the directory is removed whatever the run does, so only a run killed before it ends leaves it behind (`run_update`'s docstring says so, lane 5's NIT 1 on #954). A git with no trace2 (before 2.22) writes no trace, so B4 reads as before there, and a traced run overrides a `GIT_TRACE2_EVENT` the person set for the command. The cures: - B5: `git -C <member> rm --cached -- <path>` takes the gitlink out of the member's index, or a section of its `.gitmodules` can give it its path and url. Where the member's HEAD records it, commit that in the member and push it, then move the holder's gitlink and pin (`family.py bump`). - B4: push the commit to the member's remote from a clone that holds it, or move the gitlink to a commit the remote holds; `validate-family.py` names the `bump` that moves it with its pin. - In a leg, the cure is for the repository whose own index records the gitlink, and its text ends `, and so on up to this holder.` The credential's skip is unchanged: - members not checked out; - a checked-out member whose pin moved on; - the same, beside a member checked out whole with a nested gitlink that has its url. Each still prints the advice alone and ends ok. A stray git never reached, because a failed clone ended its update with exit 1, is not named. Git's update killed by a signal (exit 129 and above) is outside this change (#998). Merging main 46cf924, which holds #951, met `bootstrap.py` in three regions, the imports, `fetch_members`' call site and the new functions, and keeps both sides: `fetch_members` calls `run_update`, then #951's `unfetched` in place of the bare `unfetchable`, then this change's `whole` and `below`. One change goes past the conflicts: the walk's `checkout_stop` asks #951's `_is_a`, and the walk's own list of absent errnos (`WALK_ABSENT_ERRNOS`) is gone, since the file keeps one (`_ABSENT_ERRNOS` and `_ABSENT_WINERRORS`). It is the same four errnos, so absent is still passed and a refused `stat` is still untold. Two more things now read as absent: a Windows `winerror` of 21, 123 or 1921, as #951 reads one for a member, and a name no `stat` can take (a NUL), where `os.stat` raised `ValueError` past the `OSError` guard. No gitlink's path holds a NUL, since the walk takes them from git's index. `templates/family-root/scripts/bootstrap.py` is pinned (`FAMILY_COPIED_VERBATIM`), so each holder takes one `upstream-changed` row. Tests: `tests/test_family_bootstrap_stops_named.py` (new), 17 tests and 25 rows. The first commit's 15 are B5 and B4 named in the holder and in a plain clone, each cure typed as printed, B5 in a leg, the credential's skip (three states), a stray git never come to, a member's `.gitmodules` git cannot read, and a credential in use (B5 named with the token printed nowhere, B4 as before). Review round 1, answering lane 5's review, took the file to 25: B4 in a leg with its whole line, B5 named before a leg git never fetched, the three rows for a gitlink git's update skips (`update-none-in-config`, `update-none-in-gitmodules`, `inactive`), a leg this user cannot search, a fetch that reached another member's remote and not the member's own, a `.gitmodules` read up to a bad line, and `test_a_member_git_did_not_check_out_is_untold` with three rows (`refused`, `index-lock`, `held-commit-index-lock`), which replaces the one row that passed such a member. Run against main's `bootstrap.py` before this change, 9 of the 25 rows fail; against this branch's first commit (`9eb9d3d2`), 7; against this head's, none. The leg row asserts git's own exit 128, so the walk runs there and answers untold. It is skipped where `chmod` cannot refuse (root, Windows). The rows' "never pushed" commits now carry their clone's name (`unpushed`). Two rows that made that commit in the same second used to make the same commit, which an earlier row had already pushed to the module's shared remote. On CI, a later row then found its B4 commit fetched: git's update exited 1, not 128, and the walk never ran. That made `[refused]` fail now and then, and made the leg row look as if git 2.55.0 ended its update there with 1. With the commits distinct, both rows get 128 on git 2.43.0 and on CI's 2.55.0. Closes #892 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… --cached` where a plain one refuses a staged gitlink, dies on a clone no section names or refuses what is no checkout, with what it leaves said, and a `.gitmodules` gone from the work tree put back before the `git rm` that takes a section out (#936) (#1025) `member-gitlink-unnamed` in the family holder's pinned validator printed `git rm -- <path>` for every member mounted by hand. That line failed in four states: - A gitlink the index stages at a commit HEAD does not record there, with something on disk at it. Examples are `git mv members/IRRS members/IRRS2`, staged, and a `git submodule add` not committed. Git refuses it, exit 1. - A clone added by hand that no section names. Git dies with `could not lookup name for submodule`, exit 128. - What is no checkout, a directory holding files or a file. Git refuses it as modified, exit 1. - A hand mount with its own section while the work tree's `.gitmodules` is gone. There the `git rm` took the section out of no file, and the gate found `gitmodules-path-unnamed` again after the commit. Every such line now works typed, and the gate passes after the commit. - `_hand_cached` asks what git's rm.c asks, from git's records and what is on disk, never git's words: - "clone": a `.git` directory at the path, with no section giving it in the file git reads (`_given_as_git_reads`); - "staged": `_staged_over`; - "no checkout": anything on disk that is not an empty directory, with no `.git` entry. In those states the line is `git rm --cached`, with `-f` where `_forced` says, and it touches no file. Elsewhere it stays the plain `git rm`, which takes the section out with the gitlink. - The text says why a plain `git rm` is not printed (`CACHED_WHY`, quoting git's refusals). Where HEAD records nothing at the path, it says nothing of it is left to commit. - It says what stays on disk and what to do with it before `family.py add`, which refuses anything at the path (`_cached_left`): - a clone is moved out whole, with the `git log` that lists its commits not pushed; - a checkout whose `.git` file names a store is deleted once `git status` shows nothing it alone holds; - anything else is moved out or deleted, and an empty directory is deleted. - A section at a `--cached` path prints its own `--unset-all` lines (`_check_gitmodules_named`), since `--cached` leaves the section. - Where the work tree's `.gitmodules` is gone and the file git reads in its place gives the path, the plain line puts that file back first, through `_file_put_back`, C1's single put-back. Then the `git rm` takes the section out of it. The removal's finding after it puts nothing back. - Where `.gitmodules` holds a change the index does not, `git add -- .gitmodules` comes before the `--cached` line too (`_staging_said`), and the text numbers both. Git takes no gitlink out of the index until that change is staged (exit 128). - The switch line is written in the returned text itself. The lines after it are a list the helpers (`_plain_rm_said`, `_cached_rm_said`, `_staging_said`) append to and number with `_ordinal`. So `tests/test_family_switch_branch_exists.py`'s guard still sees the prose under every `_switch_line` end with `BRANCH_LEFT`. - What is on disk is asked as #430 asks it, with `_anything_at` and `_asked`. A path or a `.git` whose `stat` is refused is `member-unreadable` by name, exit 2, with the `chmod` cure, never a 3.12 traceback or a false "no checkout". The path is asked first. - This changes #956's C19 for a committed hand mount: with `members/` at mode 000 it is now refused `member-unreadable`, exit 2, where C19 expected the control's finding. - Whether its plain `git rm` works now turns on what is on disk. - The line printed there before, `git rm -- members/IRQS`, exits 128 on git 2.43 (`failed to stat`, `could not remove`). - RV1025's review found the change right. - The variant row (C20) is unchanged. Limits: - A hand mount staged by `git submodule add`, with the work tree's `.gitmodules` deleted. The printed lines exit 0 and the gate passes, but the index's file keeps the section, and the next commit records it. This is not a regression: the line printed before exited 1. `gitmodules-path-unnamed` reads only the work tree's file. Follow-up: #1036. - A linked worktree mounted by hand (a `.git` file naming an outside store) gets the plain line. On git 2.43 it exits 0 and moves nothing into the holder's store. - What cannot be read was measured only at modes 000 and 444 on Linux, and as a refused `stat` in process. - `family.py add` after the lines is not typed by the rows. - `--unset-all` of every key may leave an empty `[submodule]` header, as #300's remedy does. Tests: - tests/test_family_hand_mount_git_rm_lines.py is new, with 21 rows. Each types every printed `git` line in order, deals with what is left as the text says, commits, and asks the gate in every mode and a plain clone. The rows: - state 1: 8 (`git mv`, with or without a commit in the moved checkout; an empty directory; a directory holding a file; a dangling link, x889c's NIT A; `git submodule add` not committed; a checkout moved to `HEAD~1` and added; `git mv` with a hand edit of `.gitmodules` not staged, RV1025's NIT 2); - state 2: 4; - state 3: 2; - what is no checkout: 2; - controls that keep the plain line: 3; - a checkout at mode 444, its `.git` refused by name: 2. - Against `3864b18c`'s script, 17 of the first 20 rows fail and the 3 controls pass. The NIT 2 row fails there too. - Mutants: on `927fabf7`, 13 of 14 mutants were killed, and the survivor was judged equivalent. RV1025's MG, the stage line printed after the `--cached` line, is killed by the NIT 2 row. - tests/test_family_holder_remedies_compose.py drops #889's round-2 test of the old staged text and its 7 rows, going from 61 to 54 tests. Its states are rows in the new file. One is left out: the gitlink staged over nothing, whose plain line still works. It stays pinned, untyped, by tests/test_validate_family_unnamed_mounts.py. - tests/test_validate_family_unreadable.py goes from 52 to 53 tests. The committed hand mount leaves the two "never asked" tests and is refused by name in three rows: a refused `stat`, `members/` at mode 000, and its `.git` at mode 444. - CI run 39377078862 on `4c0c52d0`: the whole suite gave 18668 passed, 371 skipped. A local run of 40 files gave 2672 passed, 15 skipped. Closes #936 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-4 (openRepoShape-4)
Claim: #879 (comment)
Round 3: lane 1's NITs on
29083a5f, taken hereLane 1's re-review of
29083a5fwas provisionally READY, with no MUST and no SHOULD, and two NITs (#889 issuecomment-6252129372). Both are taken in32a5899feafde7b3ac6f5360157822b1d7556d91, one commit on29083a5f, pushed as a fast-forward.STAGED_ATquoted one of git's two refusals. Measured on git 2.43 in seven states, each exit 1. Git says "the following file has changes staged in the index" where what is on disk matches what is staged: the cleangit mv, an empty directory, and a checkout moved toHEAD~1and added. It says "... has staged content different from both the file and the HEAD" where it differs too: a commit or an untracked file in the moved checkout, a directory holding a file, and a file. The clause and_staged_over's docstring now quote both.git-mv-and-a-commit-in-it,a-directory-holding-a-file); each refused row checks that the text quotes the words git printedr3-NIT1-staged-in-the-index-alone: killed: 5 failed, 2 passed;r3-NIT1-different-from-both-alone: killed: 5 failed, 2 passedHEAD~1, thengit add: HEAD records another commit there.head-records-another-commit, which expects the clauseThe round-2 SHOULD test now has 7 rows. Its four round-2 mutants were run again on these bytes, and each was killed.
Round 2: lane 1's re-review of #889, taken here
Lane 1 found
3b15de03NOT READY on one new MUST, a regression round 1 brought in (#889 issuecomment-6250664740). Every round-1 item, thebootstrap.pyhalf and the merges were READY. Every item is taken in29083a5f2d9f812b8640790d2dba6cb9c9301193, one commit on3b15de03, pushed as a fast-forward. Each Before is3b15de03and each Now29083a5f, measured with lane 1's probe.members/IRQS, thengit rm --cached members/IRRSor a plaingit rm members/IRRS, the file deleted. Before: round 1 counted the hand mount'sgit rmas a write (Report.wrote), though it writes nothing into a file that is gone, so the removal wrote IRRS's section key by key "in the file a line above put back", which no line had. Committed, HEAD's.gitmodulesheld that one section, and the gate foundmember-gitmodules-absentin every mode. Now:_section_backasksReport.put_back, andReport.wroteonly with the file on disk. The removal puts the file back itself, as round 0 did:git checkout -- .gitmodules, orHEAD's after a plaingit rm. Why this fix: it composes with C1's single put-back. With the file gone, any finding that writes into it must put it back first, and the first to do so records that, soput_backalone says a file is there to write into, whichever writer came first. A write with no put-back writes into no file, so it is ignored at the one place that reads it, for every writer, not only the hand mount. Lane 1's other fix, a put-back before_keys_back, would do again what the gone-file branch below already does, choosing the index's file or HEAD's bywanted.test_round2_must_2_a_hand_mount_s_git_rm_writes_nothing_in_a_gone_file[cached]and[plain]: the lines in every mode; typed and committed, status clean, HEAD gives both members, the gate passes in every mode, a plain clone checks both outr2-MUST2-wrote-asked-while-gone(round 1'sreport.put_back or report.wrote): killed: 2 failed8ea0c44e, and are listed under "What it cannot tell": a section no row names, and a hand mount with its own section.git mv members/IRRS members/IRRS2, staged. Before: the hand mount'sgit rm -- members/IRRS2exits 1 (the following file has changes staged in the index), as on main, where the text said only that it "refuses while its checkout holds changes". Now: the sentence only. Where the index stages the gitlink at a commit HEAD does not record there, and anything is on disk at it, an empty directory too (_staged_over), the text adds ", and while the index stages it at a commit HEAD does not record there, as it does here (the following file has changes staged in the index, exit 1),". Measured: git takes such a gitlink where nothing is on disk. The line itself is not fixed, because-fwould take the checkout's changes that its refusal protects. Listed under "What it cannot tell", for #936.test_round2_should_a_hand_mount_s_git_rm_says_it_refuses_a_staged_gitlink, four rows. Thegit mvstate and a gitlink staged over an empty directory get the clause, andgit rmexits 1. Nothing on disk, and a hand mount committed with its checkout, get none, andgit rmexits 0.r2-SHOULD-clause-never-said: killed: 2 failed, 2 passed;-always-said: killed: 2 failed, 3 passed;-asked-as-forced(_on_disk, which passes an empty directory): killed: 1 failed, 3 passed;-head-not-asked: killed: 1 failed, 3 passedtest_round2_nit_a_clone_at_no_case_variant_is_not_said_refused:git rm --cached members/IRRS, then IRRS cloned attools/xandgit add tools/x. The removal's finding is the same as with no clone there, in every mode. Typed, the clone stays untracked, the gate passes, and a plain clone checks both out.Run with this round's test file,
3b15de03's two scripts give 4 failed and 54 passed: both MUST 2 rows and the SHOULD's two clause rows fail.8ea0c44e's give 17 failed and 41 passed: round 1's 15 and the SHOULD's two clause rows fail. The MUST 2 rows pass on8ea0c44e, since round 1 brought that fault in. The NIT's row passes on both, since MY3 is a mutant, not a fault.Found, not fixed (for #936): a member mounted by hand with
git cloneandgit add, whose.gitis a directory and which no section gives. The hand mount'sgit rm -- members/IRQSdies there withfatal: could not lookup name for submodule 'members/IRQS'(exit 128), as on main. It is listed under "What it cannot tell".Round 1: lane 1's review of #889, taken here
Lane 1 found
8ea0c44eNOT READY (#889 issuecomment-6249673233). Every item was taken in3b15de03, one commit on8ea0c44e:git add -- .gitmodules(P5).Report.wrote, set by every finding that writes the file: no removal puts HEAD's file back over such a write, and its section is written back key by key, leaving out a value git dies on.git mvmoved anywhere is named by its.gitfile.-fsaid per variant,updated_noneasks nothing of the rows, "NOTHING UPDATED" under no row, and a row for MX1.On
8ea0c44e's scripts, round 1's file gave 15 failed and 36 passed. The rows and mutants are under "Tests" and "Mutants".What changed and why
Milestone v1.1.1: a DRAFT on main. It lands after #882 (#878), which has landed as 8148a38; see "Lands after #882 (#878), which has landed as 8148a38". This PR fixes the five lines (C1 to C5) that rv876's review of #876 found in the family holder's two pinned scripts,
templates/family-root/scripts/validate-family.pyandtemplates/family-root/scripts/bootstrap.py:git mvmoved, which no finding named;family bootstrap okprinted beside git's exit 128.Three more states of the same kind were found while doing it, and are fixed and tested here (see "Found beyond the issue"). Two stale docstrings in
bootstrap.pyare corrected (see "bootstrap.py's stale docstrings").Lane 2's review of #882 found three SHOULDs and three NITs in the code this PR reworks (rv882, #882 issuecomment-6248061386). #882 lands at its reviewed delta, and all six are taken here; see "rv882's items on #882, taken here".
How it was measured. A probe,
tests/test_w879_probe.py, ran against this clone. It is an untracked scratch file and is not committed.tests/test_family.py'sfamilyfixture: two scaffolded members, IRRS (members/IRRS) and IRSS (members/IRSS), with a spare, IRQS.ghthat exits 97 first on PATH,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_CONFIG_GLOBALan empty file,GIT_CONFIG_NOSYSTEM=1,GIT_SSH_COMMAND=false, and local remotes only. Git is 2.43.0.git status, then the commit;git submodule update --init --recursive.For C5,
bootstrap.py --skip-membersran instead.gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882's head7d195075, whose two scripts main8148a38holds byte for byte. The same probe on the family root's bootstrap.py and validate-family.py: the credential advice and a member's exit code say what happened, and the gate'sgit rm --cached, guardedmvand section lines type as printed where they failed (#839) #876's heada7f6c569gave the same lines.8ea0c44e, round 0's head, unless a passage says round 1 (3b15de03), round 2 (29083a5f) or round 3 (32a5899f), above, which change C2's sibling, C4's sentence and the hand mount's text and add what lane 1 found; every other line below reads the same on32a5899f.8ea0c44eis this PR's change,445b1e55, restacked on main and merged with mainc9ca9a45, which holds bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877. The C1 to C4 lines are the same on3b7b7567(this commit before rv882's items were taken), on445b1e55and on this head. So are C5's, but for the bad-line state, which bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877 changes (see C5).<commit>is a gitlink's commit. "The file" is the work tree's.gitmodules. "Gone" means deleted from disk, or its deletion staged; each test runs both (GONE, from the family root's bootstrap.py and validate-family.py: the credential advice and a member's exit code say what happened, and the gate'sgit rm --cached, guardedmvand section lines type as printed where they failed (#839) #876'stests/test_family_head_lost_gitmodules.py).C1. The wrong-case move's section line, with the file gone (
_move_section,validate-family.py:770on this head, from_move_context,:688)The state: rv876's P5e. IRSS's gitlink is recorded only at
Members/IRSS, HEAD's section gives that path (as a committedgit mvleaves it), and the file is gone. For P5b, IRRS'surlis also out of HEAD's section.Before. In members mode the refusal is printed alone:
Typed and committed, HEAD's
.gitmodulesheld one key:[submodule "members/IRSS"] path = members/IRSS, and IRRS's section was lost.member-gitmodules-absentandmember-gitmodules-url-absentin every mode.fatal: No url found for submodule path 'members/IRRS' in .gitmodules.P5b,
--no-members, with the file deleted:member-gitmodules-url-absentfollowed withgit checkout -- .gitmodules, which put back the one-key file the move had just staged, then itsgit config ... url.member-gitmodules-absentandmember-gitmodules-url-absentin every mode.git checkout HEAD -- .gitmodules. Committed, the gate foundgitmodules-path-unnamedandmember-gitmodules-absent.Now. The move puts the file back first, from where git reads it, then writes its key. The sentence says: "put .gitmodules, gone from the work tree, back from {whose}, so that every other section stays, then ...". With the file deleted:
With the deletion staged, the third line is
git checkout HEAD -- .gitmodules.In P5b,
member-gitmodules-url-absentfollows with itsgit config ... urlalone and puts nothing back: one put-back serves both findings.Typed and committed, all four states (P5e and P5b, each with the file deleted or its deletion staged) end the same way:
git statusis clean;C2.
member-gitlink-removal-staged's put-back over an earlier write (_section_back,:2938)The state: rv876's P5d.
git rm --cached members/IRRS, IRSS's section out of HEAD's.gitmodules, and the file gone.Before.
git checkout -- .gitmodules) and wrote IRSS's section.git reset -q -- members/IRRS, thengit checkout HEAD -- .gitmodules. It had read "section taken" from the empty work tree, so its put-back took IRSS's section out again.nothing to commit(exit 1).member-gitmodules-absentcame back in every mode, and the plain clone exited 128.Now.
Report.put_back). Here that file gives IRRS's path, so its one line isgit reset -q -- members/IRRS.git rm --cachedand the file deleted, the line is nowgit checkout -- .gitmodules, where it wasgit checkout HEAD -- .gitmodules. The index's file gives the section, so the result is the same. Where the file git reads lacks the section, the line isgit checkout HEAD -- .gitmodulesas before.C3. A value finding, then a stray's
git rm --cached(_check_gitlinks_named,:2129;_variant_remedy,:2244;_second_path_out,:2280)The state: rv876's P5g. A committed
update = Nonein IRRS's section, and a stray gitlink atMembers/IRRSstaged with nothing on disk.Before. The value finding's
git config -f .gitmodules --unset-all submodule.members/IRRS.updateleft the file with changes not staged. The stray's line then failed with exit 128:After the commit, the gate still found
member-gitlink-unnamedandgit statusreadD Members/IRRS. The plain clone exited 128:No url found for submodule path 'Members/IRRS'.Now. In every mode, the stray's text opens with #878's sentence, with rv882's NIT 3 clause: "Stage .gitmodules with the first line, which stages every other change in it too, since git rm refuses to take any gitlink out of the index, -f or not, while .gitmodules holds changes not staged (please stage your changes to .gitmodules or stash them to proceed, exit 128), as lines printed above may leave it or as it stands, then take it out of the index alone ...". Its lines:
Typed in order and committed, the status is clean, the gate exits 0 in every mode, and the plain clone exits 0 with both checked out. With no value finding before it (the control), the stray prints its one line as before.
C4. The checkout
git mvmoved, never named (_moved_said,:3050, in_removal_staged_said,:3021;_moved_checkouts,:2918)The state:
git mv members/IRRS Members/IRRS, staged. rv876 measured three variants of it, and each is run here:Before. The three findings' lines, typed as printed:
member-gitlink-unnamed'sgit rm --cached -f -- Members/IRRS;member-gitlink-removal-staged'sgit reset -q,git checkout HEAD -- .gitmodulesandgit submodule update --init --recursive -- members/IRRS.After typing, the gate passed in every mode, but
git statusread?? Members/. No finding said what that directory is.Now. Each of the three variants types to the same end.
member-gitlink-removal-stagedsays, before its last line:The sentence follows the closing sentences of
family.py add(#828, #865). It is said only where no checkout is at the row's path. Since round 1 it is said of any index gitlink whose.gitfile names the member's store, at any path, with thefamily.py addclause for a case variant alone (lane 1's SHOULD 3), and a case variant whose.gitis a directory, a clone of its own, gets the wrong-case move's clone sentence instead (SHOULD 2). The tests assert the sentence, thengit -C Members/IRRS statusbefore the update, which shows the untracked file or the commit's state.C5.
family bootstrap okbeside git's exit 128 (bootstrap.py:unfetchable,:871;fetch_members,:209; the newupdated_none,:949)The state: every member checked out, and git's update exits 128. That happens in two ways:
.gitmodules, with the file gone (the issue's state);members/;.gitmodulesthat a merge left unmerged.Before. For every stop, bootstrap exited 0 and printed:
The unmerged file gave the same, without the advice.
Now. Bootstrap exits 1 and gives no credential advice. For the gitlink outside
members/:`python3 scripts/validate-family.py --no-members` names each gitlink in members/ that no row names, and the commit that cures it.git reads no .gitmodules while a merge leaves it unmerged. Resolve the merge in .gitmodules and `git add .gitmodules`, then run this again.andfamily bootstrap FAILED: .gitmodules (git cannot read it).every member and leg fetchedandfamily bootstrap ok.How it works:
unfetchableno longer returns beforeinit_stopwhen no row is left.updated_nonesays why git updated no member:file_reason(unmerged, refused, unreadable, asrow_reasonasks them), else the stop.fetch_membersfails on that throughcures, which now takes it aswhole, a tuple of one reason or none, socuresgains no branch (rv882's SHOULD 3).The issue's own state, and #877. A bad line after the first section, with the file gone:
members/IRSS:error: bad config line 4 in blob ...andfatal: No url found for submodule path 'members/IRSS'.445b1e55, before bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877, bootstrap failed but namedmembers/IRRS:git config --blobrefused the whole blob, soinit_stopsaw no section at all..gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877's squash merged from main, it namesmembers/IRSS, with bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877's cure: "git checkout --ignore-skip-worktree-bits -- .gitmodulesputs back the index's .gitmodules, which git read only up to a line it cannot parse: mend it by hand (git config -f .gitmodules --listsays where it breaks), and commit it."members/IRRS, as git does, with the same cure.The brief kept C5 independent of #877's blob read, so the test asserts the failure and a named stop of either member.
Found beyond the issue
Each was approved by lane 4's coordinator, is fixed here, and has a test row.
member-gitlink-absent's own section lines (validate-family.py: a stagedgit rmis named again and fails retyped, themember-gitlink-absentremedy ends in a refusedadd, a shape-pin row with nopath:readsNone, and #639'smvorphans a live checkout and is stated by OS #681), with the file gone (_gitlink_put_back,:550).git rmand committed, then the file gone.git config -flines made a file of IRRS's section alone. Committed, HEAD lost IRSS's section, and--no-membersfoundmember-gitmodules-absent.git checkout -- .gitmodules(orHEAD's), said as "the first of which puts .gitmodules, gone from the work tree, back from {whose}, so that every other section stays". HEAD keeps IRSS's section. After the update the members mode asks for, the gate passes in every mode.git rm, beside another absent finding, with the file gone (_section_back).git rm members/IRRStakes IRRS's section out of the work tree's file and stages that; IRSS's section is out of HEAD's; the file is then deleted.git checkout HEAD -- .gitmodulesthen took IRSS's new section out again.member-gitmodules-absentcame back.No url foundwithout that section. Since round 1 the section is staged too (lane 1's MUST 1), and the text reads: "the first its gitlink, the second to the third the .gitmodules section git rm took out, key by key as HEAD has it, in the file a line above put back, the fourth that section in the index, staging .gitmodules whole, which stages every other change in it too, the fifth its checkout: commit .gitmodules then, with what the lines above write in it, and nothing else is left to commit." Typed in order and committed, the gate passes and the plain clone checks both out.git rm --and the wrong-case move'sgit rm -r --cached, after a value finding's lines (_mounted_by_hand,:2460;_move_contextand_wrong_case_move,:868).please stage your changesmessage. After the move's failure, itsupdate-indexleft the gitlink at both paths.git add -- .gitmodules, using validate-family.py: thegitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878's sentence.bootstrap.py's stale docstrings
Two passages still said that no mode of the validator reads a gitlink outside
members/. #878 made every mode find one (gitlink-unnamed).(a)paragraph of the module docstring now reads "git rm --cachedtakes out one outside members/, as every mode of the validator says (validate-family.py: thegitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878)".curesdocstring now reads "A gitlink outsidemembers/is taken out of the index, as every mode of the validator says (validate-family.py: thegitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878)".fetch_members',unfetchable's and(c)'s docstrings now say C5, and the module docstring has a paragraph on what the run said before.rv882's items on #882, taken here
Lane 2's rv882 found #882 READY at
c3f8fa54with three SHOULDs and three NITs (#882 issuecomment-6248061386). #882 landed at its reviewed delta, and lane 4's coordinator moved all six here. Each one that changes behaviour has a test row; round 0's body gave each Before and Now in full.gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882, taken here: both of_stage_first's branches that print nogit addare tested: (D) with the work tree's.gitmodulesdeleted, where agit addwould stage the deletion, and with its removal staged, where it would stage the file over that removal. Code unchanged; rv882's two mutants are killed.gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882, taken here:gitlink-unnamedreads the paths from the file git reads where the work tree's is gone (_given_as_git_reads). (A) with that file gone is no finding in any mode, and the gate passes, as before validate-family.py: thegitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878. Of rv882's two fixes, this one writes nothing, so it needs no put-back of its own.gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882, taken here:_check_gitlinks_named,_check_gitmodules_named,_check_gitlinks_registered,_move_contextand_check_removals_stagedare split into helpers (_gitlink_unnamed_said,_hand_project,_variant_remedy,_section_unnamed_said,_section_out,_index_around,_move_section,_rows_at_their_paths,_moved_checkouts), with no text change;curestakeswholeas a tuple and gains no branch. Counted below.gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882, taken here: the two stale docstrings inbootstrap.py(see above).gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882, taken here: a staged gitlink'sgit rm --cachedtakes-fover anything on disk but an empty directory (_on_disk): git 2.43 refused the plain line over a directory holding a file and no.git, a file, and a symbolic link (exit 1), and took it over an empty directory and over nothing. The sentence says "over a checkout" where a.gitentry is there, else "over what is on disk there, which is no checkout" (_held_as), in the wrong-case move too.gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882, taken here:STAGE_SAID, and the hand mount's stage sentence, say thegit add"stages every other change in it too" (STAGE_TOO); validate-family.py: thegitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878'sSTAGE_SAIDconstant intests/test_family_unnamed_gitlink.pytakes the clause.Cognitive complexity. These counts come from a counter of sonar-python's published rules for S3776 (
cc.pyin lane 4's scratch). It counts:if,for,whileandexcept;elifandelse;Two settings were not certain, so each is counted both ways:
Sonar's own figures were not read. The tool gives rv882's hand counts of #882 exactly: lenient 15 and 18 for
_check_gitmodules_named, and lenient 19 and 20 for_check_gitlinks_named. The limit is 15.281df2637d195075(main8148a38's scripts)3b7b7567_check_gitlinks_named_check_gitmodules_named_check_gitlinks_registered_move_context_check_removals_stagedbootstrap.pycuresc9ca9a45with #877 (16 / 18 / 29 on445b1e55)Each cell reads lenient / nested / strict.
_rows_at_their_paths, at 10.cureswas above 15 on main already: 18 nested before bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877, and 19 with it. This head leaves its count exactly as main has it, since refactoring it here would rewrite the lines bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877 changed._check_gitmodules_namedand_check_gitlinks_registeredare 16. To my knowledge, sonar-python does not count comprehension clauses.3b15de03), lenient / nested / strict:_check_removals_staged3 / 3 / 10,_section_back9 / 9 / 12,_keys_back1 / 1 / 5,_moved_checkouts2 / 2 / 8,_store2 / 2 / 4,_names_store4 / 4 / 4,_moved_said3 / 4 / 7,_forced_over2 / 2 / 4,_move_context1 / 1 / 1,_check_gitlinks_named9 / 9 / 14,_check_gitmodules_values10 / 11 / 13,_check_gitmodules_paths7 / 8 / 10, andbootstrap.py'sfetch_members12 / 13 / 17 andupdated_none2 / 2 / 2; the rest are as above.29083a5f):_section_back10 / 10 / 13,_check_gitlinks_named9 / 9 / 14,_mounted_by_hand5 / 5 / 5,_forced2 / 2 / 2, and the new_head_recordsand_staged_over1 / 1 / 1.Every site touched
Round 0's sites, each with its line on
8ea0c44e, are in the diff: the module docstring,Report.put_back,_file_put_back,_gitlink_put_back, the move's helpers (MoveContext,_move_context,_index_around,_move_section,_wrong_case_move,_first_line_flags),_forced,_on_disk,_held_as,_forced_said,_check_gitlinks_namedand its helpers,_second_path_out,_mounted_by_hand,STAGE_WHY,STAGE_TOO,STAGE_SAID,_check_gitlinks_elsewhere,_given_as_git_reads,_check_gitmodules_namedand its helpers,_check_gitlinks_registered,_check_removals_stagedand its helpers, andbootstrap.py'sfetch_members,cures,unfetchable,row_reason,file_reasonandupdated_none. Round 1's:validate-family.pyReport.wrote, set in_check_gitmodules_values,_check_gitmodules_paths,_check_gitlinks_named(a hand mount'sgit rm),_move_section,_check_gitmodules_named,_check_gitlinks_registeredand_section_backvalidate-family.py_check_removals_staged(HEAD's file read once:wanted,refused),_section_back,_keys_back(new),STAGED_BACK(new),_removal_staged_saidvalidate-family.py_moved_checkouts,_store(new),_names_store(new),_moved_said,_statusesand_unpushed(new, shared with_wrong_case_move)validate-family.py_forced_over(new, from_move_context),_wrong_case_move's andMoveContext's docstrings; the module docstringbootstrap.pyfetch_members,NONE_UPDATED(new),updated_nonevalidate-family.py_section_backand its docstringvalidate-family.pySTAGED_AT(new),_mounted_by_hand(staged_at),_check_gitlinks_named,_head_records(new, from_forced),_staged_over(new)Every finding and remedy in the plain states is unchanged; the control rows below pin that.
Tests
tests/test_family_holder_remedies_compose.pyis new, with 61 tests: 35 in round 0, 16 in round 1, 7 in round 2 and 3 in round 3. Each one:tests/test_family.py'sholder;git submodule update --init --recursive, which must check out both members (validate-family.py: thegitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878'sassert_a_plain_clone_checks_out_every_member).A url a remedy writes is the one
family.py addwrites, on GitHub. The clone reaches the fixture's local remotes through it by aurl.<base>.insteadOffor that one command (cloning, from #876'sgithub_to).git rmbeside another absent finding, by key (1); the removal alone, three ways (3); the control, the file on disk (1)git mvclean, with a commit, with an untracked file; skipped where the disk folds case (case_sensitive_disk)members/, a stray and a hand mount (3); the bad line after the first section (1); an unmerged.gitmodules(1); the control, the holder as made (1)git add; typed (and in the first, committed)gitlink-unnamed, and the gate passestools/xstaged over a directory holding a file, over a file, and over an empty directory (3); the wrong-case move over a directory holding a file (1)gitmodules-path-twice's by-hand edit (1)Members/IRRS; skipped where the disk folds casegit mv members/IRRS renamed-1with an untracked fileON_DISKrow: a symbolic link to an empty directorymembers: []and a gitlink atvendor/IRQSgit rm --cached members/IRRS, the file deletedgit mv members/IRRS members/IRRS2; a gitlink staged over an empty directory, and over nothing; a hand mount committed with its checkout; round 3: its checkout moved toHEAD~1and added, a commit in the checkoutgit mvmoved, and a gitlink staged over a directory holding a filetools/xOn #882's head
7d195075, whose scripts main8148a38holds byte for byte, with this file added: 26 failed and 9 passed. The nine that pass are:Rounds 1 and 2 give their own counts on the head before each, above.
The helpers come from:
git rm --cached, guardedmvand section lines type as printed where they failed (#839) #876'stests/test_family_holder_bootstrap_texts.py,tests/test_family_head_lost_gitmodules.pyandtests/test_validate_family_typed_states.py;gitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878'stests/test_family_unnamed_gitlink.pyandtests/test_validate_family_unnamed_mounts.py;tests/test_family_bootstrap_gate_order.py,tests/test_family_member_path.py,tests/test_family_remove_gitmodules.py,tests/test_family_dry_run.pyandtests/test_family_add_refusals.py.No helper is copied. One existing line changes: the
STAGE_SAIDconstant in #878'stests/test_family_unnamed_gitlink.py, which pins the sentence whole, takes NIT 3's clause. Every test in that file passes with it (see "The run").What it cannot tell
.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877. On445b1e55alone, the issue's state namedmembers/IRRSwhere git namesmembers/IRSS. With the bad line before the first section, it said to write back a section that is there behind the line. bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877 has landed, and this head, which merges it, names each stop as git does with bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877's cure. A revert of bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877 alone would bring the old words back; the failure itself does not depend on it. The gate refuses both states asgitmodules-unreadablein every mode..gitmodulesnow fails bootstrap where the gate passes. The gate reads HEAD's file. Git's update reads nothing from an unmerged one and exits 128, so no member was updated.init_stopmodels git's init; it is not git's own answer. Bootstrap now fails on a stopinit_stopfinds wherever git exits non-zero with every member checked out. A state where git exits non-zero for another reason, andinit_stoppredicts a stop git did not reach, would fail naming that gitlink. No such state was found or measured..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, B4 and B5). C5 fails where git's init in this holder stopped: at a gitlink this holder records with no url, or at an unmerged.gitmodules. With every member checked out, two other exit 128s still give the credential advice andfamily bootstrap ok, as on main:No url found for submodule path 'members/IRRS/vendor/nested');init_stopreads this holder's index alone.not our ref), bootstrap.py and validate-family.py:family bootstrap okwith no member checked out, an earlier row'smember-uninitializedhiding a case-variant refusal, andmember-gitlink-unnamedwith no command under--no-members#648's credential skip.Telling either apart needs git's own words, or every member's index read; neither is done here. Named for a follow-up.
.gitmodulesvalue git dies on while reading the file failsmake bootstrapnaming the key, and the family gate names it, soupdate = Noneno longer endsfamily bootstrap ok(#648) #768; lane 1's NIT 1 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). Where the work tree's.gitmodulesis gone,gitmodules-value-refusedputs back HEAD's, whose value its lines take out, not the index's, which git reads there. A fix staged in the index's file is lost. The docstrings now say so; the behaviour is bootstrap.py and validate-family.py: a.gitmodulesvalue git dies on while reading the file failsmake bootstrapnaming the key, and the family gate names it, soupdate = Noneno longer endsfamily bootstrap ok(#648) #768's.git config -fputs a new section, so the committed.gitmodulescan differ from HEAD's in the order of its sections (lane 1's SHOULD 3 state, for one). The removal's text now says to commit the file..gitmodulesand a removal is staged, a plaingit committyped before the removal's lines records that removal too. The tests, as lane 1's probes, type every finding's lines first and commit once; no text here says to commit between findings, and none says not to._store): a section renamed by hand aftergit submodule addnames a store the moved checkout's.gitfile does not, and that checkout is not named. No such state was measured.member-gitlink-removal-stagedsays this of its own lines where they write nothing into.gitmodules(validate-family.py: the case-variant move back leaves.gitmodulesnaming the variant and has no--sparse, a staged un-registration passes the gate, and three findings print no remedy, one that dies, or a claim a recursive clone does not keep #762), as after an earlier finding that put the file back and gives its section (P5d). That finding's write is still to commit, and its own text says so. Where the removal's lines write the section back, they stage the file and say to commit it (round 1).11th,12thand so on (_ordinal). No state here prints more than seven lines in one finding.gitmodules-path-unnamedreads the work tree's file. So (A) with that file gone passes the gate until the file is back, as it did before validate-family.py: thegitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878;gitlink-unnamedno longer speaks there, since its words were false.git rmcannot take its section out of a file that is gone. The removal then puts the file back, and the section comes back with it. After the commit the gate findsgitmodules-path-unnamed, as it did on8ea0c44e. For validate-family.py (family root): after #889, three hand-mountgit rmlines still fail when typed: a gitlink staged at a commit HEAD does not record, a hand mount with its own section while.gitmodulesis gone, and a hand clone with no section (exit 128) #936.git rmover a gitlink staged at a commit HEAD does not record there (lane 1's SHOULD, round 2). This is the state aftergit mv members/IRRS members/IRRS2is staged, or after a gitlink is staged over an empty directory. The line exits 1, as it does on main. The text now says so, but no line takes such a gitlink out. For validate-family.py (family root): after #889, three hand-mountgit rmlines still fail when typed: a gitlink staged at a commit HEAD does not record, a hand mount with its own section while.gitmodulesis gone, and a hand clone with no section (exit 128) #936.git clonethengit add, so the.gitis a directory and no section gives the path. Itsgit rmdies withcould not lookup name for submodule(exit 128), as it does on main. For validate-family.py (family root): after #889, three hand-mountgit rmlines still fail when typed: a gitlink staged at a commit HEAD does not record, a hand mount with its own section while.gitmodulesis gone, and a hand clone with no section (exit 128) #936.Choices for the reviewer
git add -- .gitmodulesbefore thegit rm, the issue's first way, not the stray's line first.gitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878 already settled the stage line for its owngit rm(_stage_first,STAGE_LINE,STAGE_SAID). One rule now covers the stray, the hand mount, the wrong-case move and validate-family.py: thegitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878's line._stage_first(root, edited)), so the plain states print what they did.Report.put_back).git rm --cached, guardedmvand section lines type as printed where they failed (#839) #876's_gitmodules_put_back).git rm --cachedand the file deleted, the removal alone now printsgit checkout -- .gitmoduleswhere it printedHEAD's (C2, "Now").git checkout HEAD. A second checkout would drop the earlier finding's write. Each key comes in HEAD's order, with HEAD's value, quoted withshlex.quote.member-gitlink-removal-staged, before its update.git rm --cached -ftext, from the family root's bootstrap.py and validate-family.py: the credential advice and a member's exit code say what happened, and the gate'sgit rm --cached, guardedmvand section lines type as printed where they failed (#839) #876, is unchanged.family.py add's closing sentences (family.py add: the move back of a member's gitlink points its section of.gitmodulesback, sets aside first what the index holds in its way and puts back a member whose removal is staged, its closingmvis guarded and leaves a checkout to delete by hand, and--sparsefollows the sparse rules, so no printed line leaves a section naming another spelling or un-registers a member (#761) #828, family.py add: a section astray beside its gitlink, a variant of a row's path undermembers/, a link or checkout in a move's way, an unmerged entry and a staged or unmerged.gitmodulesare named first, so each remedy types clean (#840) #865).cureswithwhole, and gives no credential advice.credential_advicealready says nothing where git stopped at a gitlink or read no.gitmodules(the family root's bootstrap.py and validate-family.py: the credential advice and a member's exit code say what happened, and the gate'sgit rm --cached, guardedmvand section lines type as printed where they failed (#839) #876).unfetchablenow asksinit_stop, so the advice drops out with no new branch..gitmodules (git cannot read it), as bootstrap.py and validate-family.py: a.gitmodulesvalue git dies on while reading the file failsmake bootstrapnaming the key, and the family gate names it, soupdate = Noneno longer endsfamily bootstrap ok(#648) #768's line does..gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877's blob read, as the brief asks. The cost is the stop's name in the one state above.gitlink-unnamedprints only agit rm --cached, which writes nothing into.gitmodules, so a put-back of its own would only add a line.-fover anything on disk but an empty directory, not only over a directory that holds files.--cached,-ftouches no file..gitentry is there, so the texts the family root's bootstrap.py and validate-family.py: the credential advice and a member's exit code say what happened, and the gate'sgit rm --cached, guardedmvand section lines type as printed where they failed (#839) #876 and validate-family.py: thegitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878 pin are unchanged.FORCED_SAID, what-fdoes, is unchanged.curesis left at main's count, as bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877 also changes it (see "Cognitive complexity").git addcomes before the removal's lines), and the first removal with the file gone puts back HEAD's where the index's lacks a section another staged removal needs (P4 then types as clean asc9ca9a45: no key-by-key lines at all, andgit statusclean).Report.wroteis set beside each finding whose lines write.gitmodulesor which says to commit it as it stands, includinggitmodules-path-twice, whose remedy is by hand. A removal after one never printsgit checkout HEAD -- .gitmodules; with no writer before it, it prints what it printed, so the plain states are unchanged (the C2 control row).gitmodules-value-refusedagain; the value finding's own lines, or its "stage and commit the work tree's.gitmodules", cure it..gitfile. A checkoutgit mvmoved keeps a.gitfile naming the member's store, wherever it went, so that, not the path's case, is the test. The store is named as git names it,modules/<name>in--git-common-dirwith HEAD's section name; the file is read as git reads it. No git runs inside the checkout.Lands after #882 (#878), which has landed as 8148a38
gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882 (validate-family.py: thegitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878) landed on main at 10:12:28Z as the squash8148a3827431c7f8eb7f5c1a85bb76a067647309(treec5d2ba47). Its final head was9677ef91, which is7d195075refreshed over main33c5128b. the family root's bootstrap.py and validate-family.py: the credential advice and a member's exit code say what happened, and the gate'sgit rm --cached, guardedmvand section lines type as printed where they failed (#839) #876 (the family root's bootstrap.py and validate-family.py: the credential advice prints beside "not for want of a credential", a member's refusal and finding both readmake bootstrap exited 2, and the wrong-case move'sgit rm --cachedmay refuse #839) landed earlier as41baf920, and shape-doctor.py, family.py and the family root's bootstrap.py and validate-family.py: a holder'smembers:that is not a list, or holds an entry that is not a mapping, is said in the gate's words, so the doctor'smembersrow waits on thefamilyrow instead of calling the family empty,add,bump,remove,make bootstrapandmake siblingsrefuse it by name where each ended in aTypeError, read no members or committed the entry away, and--no-membersfinds the entry (#864) #870 (shape-doctor.py, family.py and the family root's bootstrap.py and siblings.py: a family holder'smembers:that is not a list ends in aTypeError, or reads as a family with no members, where validate-family.py refusesfamily-members-unreadable#864) asdef45de6.445b1e55: one commit on validate-family.py:gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882's7d195075. That was before this DRAFT, which was then to be opened on validate-family.py:gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882's branch,fix/878-unnamed-gitlink-remedy.gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882 landed 30 s before that push, and its branch was deleted, sogh pr create --base fix/878-unnamed-gitlink-remedyfailed ("Base ref must be a branch"). No PR was opened then.3965c5b1merges validate-family.py:gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882's final head9677ef91(tree66681744).e3964110merges the squash8148a38, ours for validate-family.py:gitmodules-path-unnamedtakes the gitlink at a section's path outsidemembers/out of the index before the section, and a gitlink there that no section gives isgitlink-unnamed, so the remedy typed as printed no longer leaves a gitlink at which a plain clone'sgit submodule update --initexits 128 while the gate passes (#878) #882's four files (validate-family.pyandtests/test_family_unnamed_gitlink.pyconflicted). S's tree and F's are bothc5d2ba47, and the merge adds nothing to the tree.8ea0c44emerges mainc9ca9a45, normally: bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877's squash (landed 10:37:13Z), after validate-pins.py: the<repository>: N workflow @<sha> reference(s) agree with the gitlinknote counts only the references at the gitlink, saysN of Mwhere some are not, and is not printed where none is, so no reference is both apin-workflow-ref-mismatchand one that agrees (#739) #769 and tests: three tests ask the helper whether a name can be made,make_fileskips on EILSEQ or aUnicodeErroronly and the U+202E directory has no guard, so a full disk or a read-onlytmp_pathfails the test, not SKIPPED (#834) #845.bootstrap.pyconflicted in the two hunks the pre-resolution had settled against bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877'sb367f378, and was resolved as that pre-resolution resolved it: the file is byte for byte the pre-resolution's (b951a594...).813a5dfd, merged with the same main, gives a tree that differs from this head's only in that file's conflict markers. Every other file is equal.validate-family.pymerged by itself: bootstrap.py: a.gitmodulesblob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877 adds a comment there.git diff --stat main...HEADandgit diff --stat main HEAD, with main atc9ca9a45, both name this PR's four files alone: 1526 insertions, 241 deletions, the same as445b1e55against7d195075.8ea0c44e,3b15de03, round 2 one on that,29083a5f, and round 3 one on that,32a5899f, each pushed as a fast-forward; nothing pushed is rewritten.Shared files
Round 3 (
32a5899feafde7b3ac6f5360157822b1d7556d91), with heads read bygit ls-remoteandgh pr listat 17:36Z. Lane 5's #891 landed on main asa9205e00, and lane 1's #887 as64464d31. The open PRs that touch either pinned script are #956, #954 and #951, each stacked on this one, and #950 on main.family.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956 conflicts in one hunk,_staged_over. Round 3 rewrote its docstring (lane 1's NIT 1), and validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956, made on29083a5f, adds a comment and_anything_atto its return. The resolution keeps round 3's docstring, then validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956's comment and its return. That pre-resolution is a local commit, never pushed:23356765, treed56c80dc. It compiles, and pyflakes reports nothing.Round 2 (
29083a5f, heads read at 14:59Z): main50178305and2d372bb5, #89141f781fdand58e4ddf0, and main50178305then #891 were each exit 0. The merge with main2d372bb5(tree45e7d69e) gave 559 passed on seven family and bootstrap files.Round 1 (
3b15de03, heads read at 13:14Z): main0d5d2b11ande7039b71(#886, #884), #88685c20783anddbd3a6dd, and #89158e4ddf0were each exit 0. The merge with maine7039b71(treec8ac9f05) gave 512 passed on six family and bootstrap files.Round 0 (
8ea0c44e, heads read at 10:48Z): mainc9ca9a45(its own tree), #88682a84f88, #865231c404cand #869ba2d7ab4were each exit 0. #877 (lane 3, #856) had conflicted inbootstrap.pyalone, in two docstring hunks; the pre-resolution813a5dfd(tree1a4ceb92, 247 passed on this PR's file and #877's, #876's and #878's test files) kept both sides, and8ea0c44e's merge of main applies it byte for byte (see "Lands after #882"). #886's merged tree0bc10c3egave 479 passed on five family and bootstrap files.Pinned copies
templates/family-root/scripts/bootstrap.pyandtemplates/family-root/scripts/validate-family.pyare inFAMILY_COPIED_VERBATIM(scripts/shape_materialize.py:531). Each reaches a holder as oneupstream-changedrow.scripts/siblings.py, which is pinned too, is unchanged.281df2638148a38(#882)c9ca9a45(#877)8ea0c44e3b15de0329083a5fbootstrap.pyb7c8cc0f9f2fe26baaabccc96473546f28bb5f06002dc55d1ebc0be2e00c9f73d820fd5809681cb504be10a3d711137a6afa3f1d1478058120818c1811abc061b951a5946106910a798780e68b03918d5caed8584c5a2c8e328c371e06bdde5d9d848faea097dbe9f9d072dc7b3d4a0e0fb15b96e8733622b3539dd19f5a4ee6validate-family.py6dc3fe1c66daabf41a1356bff85b307d580e49545da5fc92af9fb4446dd458c30c54263d75add8da67959ffec3bef330f1e4585e4d5b8aac04d59b86133686f2bf56e8c8bf44e72ce8d2050fb95f0a3a203718fcb504d8d54c7aa0bac36294f3f15a599560ecb2409de508049f207963a25086419a86a4d45a86435d32d7881932ca5cdc154e18e51be3e243058b75921eee9633655e1d57075ed8bf1fa86cb182b83b60c6587b65941137d3c43f7fd768f40afa20cf4844d6df20a5ac2d5377No digest of either file, old or new, is held in this repository:
git grepfinds none of them on this head. So nothing is re-pinned here.What a holder sees. A holder was cut from main
281df263withfamily.py init(local remotes).update-shape.py check --upstream <this checkout> --at 8ea0c44eprinted:The tool exits 1 when it has rows to apply. It printed the same two rows,
upstream-changed 2 unchanged 8, at round 1's3b15de03, and ate3964110and445b1e55. Rounds 2 and 3 changevalidate-family.pyalone, one of the same two; the check was not run again on them. On round 3's32a5899fits sha256 is8e7953e8b69d2fa8d9a7f3359eedfbba3de15fcb5a4f1c2ebf04d6b95e4aa4e7;bootstrap.py's is round 1's. A holder cut from281df263takes #877's and #878's changes in the same two rows.#877 and #884 have landed (#884 as #886's
e7039b71, 12:41:14Z). Main50178305now holds v1.1.1's release notes, which list #879's states as known limits. All three editbootstrap.py, and #877 and this PR both editvalidate-family.py. Landed in one release, each holder gets oneupstream-changedrow per file, not one per PR.Mutants
Each mutant puts one line's old behaviour back alone. The new file ran against each, selected with
-kto the rows of that line. The bytes were restored after every run and checked by sha256. They ran on round 2's bytes, committed as they were (29083a5f; sha256 checked), with the same environment as the run (<scratch>/r2/mutants7.py, results in<scratch>/mut7/). Earlier rounds' mutants keep their meaning; where the code moved, the anchor moved with it._move_sectionnever puts the file back_gitlink_put_back(the absent gitlink) never puts the file back_check_gitlinks_registeredputs the file back again after the move did_section_backreads "section taken" from the empty work tree and puts HEAD's back, as before_section_backnever writes the section by key_second_path_outgetsstage_first=False_mounted_by_handgetsstage_first=False_move_contextgivesstage=False_moved_saidsays nothing of a checkoutgit mvmovedunfetchablereturns beforeinit_stopwith no row left, as beforeupdated_noneignores an unmerged.gitmodules_stage_firstreturns True where the work tree holds no.gitmodules_stage_firstreturns True where the index holds none_check_gitlinks_elsewherereads the paths from the work tree's file, as before_forcedasks for a.gitentry alone, as before_held_asalways says "a checkout"_forced_overalways "a checkout"STAGE_SAIDwithout NIT 3's clause_on_diskdropswhere.is_symlink() or_keys_backprints no stage line, as before_section_backasks noReport.wrote, as beforemember-gitmodules-absentrecords no writemember-gitmodules-url-absentrecords no writegitmodules-value-refusedrecords no writegitmodules-path-unnamedrecords no writegit rmrecords no writegitmodules-path-twicerecords no write_keys_backwrites back a key HEAD's gives a value git dies on_moved_checkoutsas before: any case variant with a.gitentry is the checkoutgit mvmoved, and no clone sentence_moved_checkoutsnames a case variant alone_forced_oversays one thing for every variant, as before_moved_checkoutstakes a clone at any path, not only at a case variant_section_backasksReport.wrotewith the file gone, as round 1_staged_overasks_on_disk, which passes an empty directory_staged_overasks no HEAD_staged_overasks only whether HEAD records nothingSTAGED_ATquotes "changes staged in the index" alone, as beforeSTAGED_ATquotes "staged content different from both" aloneAll 46 of 46 were killed: rounds 0 to 2's 43 on round 2's bytes, and round 3's three, with round 2's four SHOULD mutants again, on round 3's (
<scratch>/r3/mutants8.py,<scratch>/mut8/). Lane 1's K7 (updated_none'sif left: return None) no longer exists: NIT 4 dropped the line. SHOULD 3 of rv882, SHOULD 4 and NIT 1 of lane 1 change no behaviour, so they have no mutant.The run
On round 3's head (
32a5899feafde7b3ac6f5360157822b1d7556d91), one background run of the same 36 files, with the same command and environment:2440 passed, 15 skipped in 3724.97s (1:02:04), 16:20:36Z to 17:22:42Z, exit 0, with the same 15 skips; the four files' bytes were checked by sha256 against the commit's blobs afterwards. Log:<scratch>/logs/r3-run.log.On round 2's head (
29083a5f):2437 passed, 15 skipped in 2845.24s (0:47:25), exit 0, with the same 15 skips.On round 1's head (
3b15de03):2430 passed, 15 skipped in 1622.70s (0:27:02), exit 0, with the same 15 skips.On round 0's head (
8ea0c44e1646137f702a6b11b638eccea2595c32, mainc9ca9a45merged), one background run of 36 files:Environment:
GIT_CONFIG_GLOBALan empty file,GIT_CONFIG_NOSYSTEM=1,GIT_ALLOW_PROTOCOLunset, a stubghthat exits 97 first on PATH,GH_TOKEN=bogus, an emptyGH_CONFIG_DIRandGIT_SSH_COMMAND=false; git 2.43.0.Result:
2414 passed, 15 skipped in 1723.64s (0:28:43), 10:43:59Z to 11:12:43Z, exit 0. The four files' bytes were checked by sha256 against the commit's blobs afterwards.Earlier heads, each exit 0 with the same 15 skips:
e39641102320 passed,445b1e552304 passed; the count grows with the tests main gained.The 15 skips:
git filter-repo;submodule init;shallowof -2**31, which CI's git 2.55 takes;GIT_CONFIG_GLOBAL.None is in this PR's file, and the C4 and move rows, which need a disk that keeps case, ran.
The files:
git rm --cached, guardedmvand section lines type as printed where they failed (#839) #876'stests/test_family_holder_bootstrap_texts.pyandtests/test_family_head_lost_gitmodules.py;gitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878'stests/test_family_unnamed_gitlink.py;tests/test_family_bootstrap_gate_order.py,tests/test_family_gitmodules_values.py,tests/test_validate_family_typed_states.py,tests/test_validate_family_remedies_typed.py,tests/test_validate_family_unnamed_mounts.py,tests/test_validate_family_duplicates.py,tests/test_validate_family_member_path_glob.py,tests/test_validate_family_branch_step.py,tests/test_validate_family_without_git.py,tests/test_bootstrap_without_git.py,tests/test_bootstrap_spelling.py,tests/test_family.py,tests/test_family_member_path.py,tests/test_family_remove_gitmodules.py,tests/test_family_dry_run.py,tests/test_shape_doctor.py,tests/test_doctor_legs_members_next.py,tests/test_doctor_validator_rows.py,tests/test_pinned_copy_text.py,tests/test_park_resume_targets.py,tests/test_family_makefile_siblings.py,tests/test_no_auto_maintenance.py;family.py, docs and doctor files that reachgitlink-unnamed,member-gitlink-unnamed, the hand mount's text or bootstrap's (a):tests/test_family_switch_branch_exists.py,tests/test_family_docs_one_spelling.py,tests/test_family_add_refusals.py,tests/test_family_add_member_variant.py,tests/test_family_add_move_back_sections.py,tests/test_family_members_not_a_list.py,tests/test_doctor_gitlink_repairs.py;tests/test_repo_hygiene.py,tests/test_cli_reference.pyandtests/test_agents_md_codes.py.Left out: 70 files that
git grep -l -e validate-family -e "bootstrap.py" -e init_stop tests/lists, most of them about the assembly root'sbootstrap.py; none names a finding code or a text this PR changes, but five that name a changed script or its texts, which ran apart on445b1e55: 219 passed.The docs. No README, handbook,
AGENTS.mdordocs/cli.mdsentence describes these remedies' order,-f's rule or bootstrap's (a) line, so none is made false. No code is added or renamed: every finding and refusal code is one the gate already prints.--helpis unchanged.Closes #879
🤖 Generated with Claude Code