Repository navigation
validate-family.py: gitmodules-path-unnamed reads the file git reads where the work tree's .gitmodules is gone, the index's then HEAD's, and its lines put that file back first, so a section git submodule add staged is no longer committed past a passing gate (#1036) - #1069
Conversation
… --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) Every `git rm` line `member-gitlink-unnamed` prints for a hand mount now works typed, and the gate passes after the commit. Based on main, where #889 landed as 702cc23. - `_hand_cached`: asked of git's records and what is on disk, as git's rm.c asks it, never of git's words. The line is `git rm --cached`, with `-f` where `_forced` says, which touches no file, where a plain `git rm` would - refuse a gitlink the index stages at a commit HEAD does not record there, over anything on disk (exit 1, either of git's two refusals, both quoted: `STAGED_AT`); - die on a clone whose `.git` is a directory that no section of the file git reads names (could not lookup name for submodule, exit 128); - refuse what is on disk and is no checkout (exit 1). `_cached_left` says what stays on disk and what to do with it before `family.py add`: move a clone out whole, delete a checkout once nothing only it holds is needed. Where HEAD records nothing at the path, nothing of it is left to commit. - `_check_gitmodules_named`: a section at such a gitlink's path prints its own `--unset-all` lines, which `--cached` leaves to it. - `_plain_rm_said`: where the work tree's `.gitmodules` is gone and the file git reads gives the hand mount's path, the file is put back first, from where git reads it, unless a finding before did, so the `git rm` takes the section out and a staged removal's put-back after it no longer brings it back. - The controls print the plain `git rm`, as before. Tests: tests/test_family_hand_mount_git_rm_lines.py (17 rows); #889's round-2 SHOULD test in tests/test_family_holder_remedies_compose.py is dropped, its states being rows here, and its helpers stay there. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… dangling link is taken out cached, so `_staged_over` reading `exists` in place of `lexists` fails a row (#936) x889c's NIT A on #889 (issuecomment-6253217474): nothing tested `_staged_over`'s `os.path.lexists` against a dangling link. Under `(root / path).exists()` the reason was lost while git still refuses the plain `git rm` (exit 1, "has staged content different from both the file and the HEAD"). The row expects `git rm --cached -f` with the staged reason; the mutant is killed (1 failed, 6 passed). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dy's own text, so the guard that every remedy naming `_switch_line` ends its prose with `BRANCH_LEFT` sees the sentence (#936) The 37-file run on 40e6e27b failed two tests in tests/test_family_switch_branch_exists.py: the guard, and the test that shows the guard is not vacuous. `_mounted_by_hand` built its lines as a list that began with `_switch_line(...)`. The guard reads the string expression a `_switch_line` call stands in, and for this call that was the list. The printed text was the same, and its prose ended with `BRANCH_LEFT`, but the guard could not see it. - `lines` now holds the lines after the switch line. The returned text writes the switch line itself, as main's did. - The helpers number their lines from the second (`_ordinal(len(lines))`), and the add is the `_ordinal(len(lines) + 1)`. - The printed text is unchanged. The 18 rows of tests/test_family_hand_mount_git_rm_lines.py and the two guard tests pass (20 passed). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…asks it, so a path or a `.git` this user cannot read is `member-unreadable` by name, never a 3.12 traceback or a false "no checkout", and a hand mount HEAD records is refused under a folder where its plain `git rm` exits 128 (#936) Moved onto main 3864b18, where #956 (slice E of #430) landed. The rebase had no textual conflict, because #956's hunks and this branch's are apart. Its rule met #936's new reads, though: tests/test_validate_family_unreadable.py failed 3 of 52 on 02bd9e1, the third commit moved: - hand-mount-folder-000 and hand-mount-committed, members/ at mode 000: `_hand_cached`'s `git.is_dir()` ended the gate in a PermissionError traceback on Python 3.12. - hand-mount-committed, a refused `stat` of members/IRQS in process: `_on_disk` refused it `member-unreadable`, exit 2, where the row expected the control's finding, exit 1. Resolved with #936's intent, every hand mount's line works typed, and #430's, a path this user cannot read is never absent: - `_hand_cached` asks the path first (`_anything_at`). Nothing there is the plain line, and a folder above it that cannot be searched is refused at the path, as `_staged_over` refuses it. "clone" asks the file git reads before the `.git`, and the `.git` with `_asked`; "no checkout" asks the `.git` with `_anything_at`. `_cached_left` asks its `.git` the same way. - A hand mount HEAD records is asked of the disk too, since whether its plain `git rm` works turns on what is there. With members/ at mode 000, git 2.43's `git rm -- members/IRQS`, the line main prints there, exits 128: "warning: failed to stat 'members/IRQS': Permission denied", "fatal: could not remove 'members/IRQS'". So r956's C19 row for it moves. `test_a_gitlink_head_records_as_staged_asks_nothing_of_the_disk` and `test_a_folder_under_a_gitlink_git_takes_is_never_asked` keep their variant row (C20). The hand mount is refused by name in `test_a_path_whose_stat_is_refused_is_refused_by_name` (hand-mount-committed) and in `test_a_folder_this_user_cannot_search_is_refused_by_name` (hand-mount-committed-folder-000, and hand-mount-committed-444 for its `.git`). - tests/test_family_hand_mount_git_rm_lines.py: two rows, a checkout and its section committed, and `git submodule add` not committed, each at mode 444. Every mode is refused by name at `<path>/.git`, and with the mode back the gate says what it said. tests/test_validate_family_unreadable.py: 53 passed. The hand mount file and tests/test_family_switch_branch_exists.py: 45 passed. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lict, so #1025 is tested against main as it stands, with #983's change to validate-family.py and #1026's fix of main's two red rows (#936) Main took twelve squashes since e076e3e: #1026, #958, #986, #1002, #1016, #1011, #1021, #1022, #973, #975, #1015 and #983. Only #983 touches validate-family.py, in `_git_runs` and the member checks, apart from this branch's hunks; it touches none of this branch's test files. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ules` not staged puts `git add -- .gitmodules` before a hand mount's `git rm --cached`, so a stage line printed after it, where git's rm exits 128, fails a row (#936) RV1025's NIT 2 on #1025: no row reached `_staging_said` in `_cached_rm_said`, so its mutant MG, which prints the stage line after the `--cached` line, passed every row of the new file and the compose file. The state is RV1025's probe: `git mv members/IRRS members/IRRS2`, staged, then a line appended to the work tree's `.gitmodules` by hand and not staged. The row pins the order (`git add -- .gitmodules`, then `git rm --cached -f -- members/IRRS2`), the text that numbers both, and what is left, then types every line in order, deletes the moved checkout as the text says, commits, and asks the gate in every mode and a plain clone. In MG's order the `git rm --cached -f` exits 128 (`please stage your changes to .gitmodules or stash them to proceed`). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s where the work tree's `.gitmodules` is gone, the index's then HEAD's, and its lines put that file back first, so a section `git submodule add` staged is no longer committed past a passing gate (#1036) RV1025's SHOULD 1 on #1025. In its state, `git submodule add` at members/IRQS, not committed, then the work tree's `.gitmodules` deleted (`MD .gitmodules`), the finding read the work tree's file alone and named nothing: every printed line exited 0, the gate passed in every mode, and the commit of what was staged recorded IRQS's section in HEAD's file. Measured on git 2.43.0, isolated config: with the work tree's file gone, `git submodule status members/IRQS` maps the path, which HEAD's file does not give, so git reads the index's; `git config -f .gitmodules --unset-all ...` on no file exits 5 and makes none; the index's file put back and both keys unset leave it byte-identical to HEAD's. - `_sections_as_git_reads`: the work tree's sections where the file is on disk, else the file git reads in its place (`_gitmodules_put_back`), through `_blob_sections`. `_given_as_git_reads` and `_blob_paths` derive from it, unchanged in what they answer, so #936's hand-mount put-back and this finding agree on one file. - `_check_members` hands it to `_check_gitmodules_named`, but where `.gitmodules` gives a value git dies on: that finding's lines put HEAD's file back and stage it, which drops a section the index's file alone gave. Every other reader of `.gitmodules` is as it was. - `_section_out` takes the put-back (`_file_put_back`, one for every finding) and prints it right before the keys' lines, saying so; the case-variant branch and `_gitlink_out_first` pass it. A gitlink outside `members/` keeps its `git rm --cached` first, which needs no file. Rows at the end of tests/test_family_hand_mount_git_rm_lines.py: the issue's state, the same at tools/x, a stray section, two sections served by one put-back, a hand mount whose plain `git rm` takes its section (no command), and the control where a value git dies on keeps what it read. Two rows of tests/test_family_holder_remedies_compose.py move with the fix: C1's `--no-members` run names the section giving `Members/IRRS` (no command), as it does with the file on disk, and rv882's SHOULD 2 row, which said the gate passed, now expects the `gitmodules-path-unnamed` the same state is with the file on disk, typed through to a passing gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d-index, the first of the three merges after its squash
…, the third of the three merges
Reviewer's GuideFix Sequence diagram for Git-aware .gitmodules validation and section removalsequenceDiagram
participant Validator
participant Worktree
participant Git
participant Index
participant HEAD
Validator->>Worktree: _gitmodules_sections()
alt .gitmodules exists
Worktree-->>Validator: sections from work tree
else .gitmodules is absent
Validator->>Git: _gitmodules_put_back()
Git->>Index: read .gitmodules
alt index has .gitmodules
Index-->>Git: index blob
else index lacks .gitmodules
Git->>HEAD: read .gitmodules
HEAD-->>Git: HEAD blob
end
Git-->>Validator: _blob_sections()
end
Validator->>Validator: _check_gitmodules_named()
Validator->>Validator: _file_put_back()
Validator-->>Validator: emit put-back before --unset-all
Validator-->>Validator: emit section removal commands
Flow diagram for restoring .gitmodules before section removalflowchart TD
A[.gitmodules is absent from work tree] --> B{Source Git reads}
B -->|Index| C[Read index .gitmodules]
B -->|No index file| D[Read HEAD .gitmodules]
C --> E[Identify unnamed sections]
D --> E
E --> F{Gitlink outside members?}
F -->|Yes| G[Emit git rm --cached first]
F -->|No| H[Continue]
G --> H
H --> I[Emit _file_put_back line once]
I --> J[Emit git config --unset-all lines]
J --> K[Commit updated .gitmodules]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-4, 2026-10-20T21:14:38Z) RV1069 review of #1069 at ab8ed9e: Verdict: NOT READY - one SHOULD: in #1036's own state, its own Verify step still records the section past a passing gate. No MUST. Everything else holds: git's order, the put-back's place, both moved rows, the merges, the tests, the mutants, CI and Sonar. SHOULD 1 - #1036's state ends "then commit .gitmodules" where only staging cures it, so the issue's Verify ("commit what is staged") still records
|
…th a printed line where HEAD's file holds no section of the name it takes out, in `members/` and in another case as at a gitlink outside it, so `git commit` after the lines records nothing of a section `git submodule add` staged (#1036) RV1069's SHOULD 1 on #1069. In #1036's state the section's text ended "then commit .gitmodules" and printed no stage line: after the put-back and the `--unset-all` lines the work tree's file was HEAD's, the index's still held the section (`MM .gitmodules`), so `git commit` recorded it and `git commit -- .gitmodules` had nothing to commit. `_section_then` gives every branch one ending, from HEAD's file: the commit where it holds a section of that name, else "then stage .gitmodules: HEAD's holds no section of that name, so nothing of it is left to commit", the words the branch for a gitlink outside `members/` already said, now with `git add -- .gitmodules` printed after the keys' lines (`_section_out`), as the removal's written-back section and the wrong-case move print theirs. Rows: R1 and its tools/x sibling type only the printed lines, then `git commit` of what is staged; both fail on ab8ed9e (HEAD keeps `members/IRQS`; the stage line is missing) and pass here. `tests/test_family_unnamed_gitlink.py`'s `section_first` expects the stage line where the text ends "then stage". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-4, 2026-10-20T22:45:37Z) W1036 round 2 at 8d14a20: RV1069's SHOULD 1 taken. What changed, in templates/family-root/scripts/validate-family.py:
Rows:
Evidence:
Main is at 5e539a8. Nothing since round 1's merge of 00f6d7f touches these four files, so there is no merge this round, and the push fast-forwarded the PR branch from ab8ed9e. The PR body's counts are updated and closingIssuesReferences reads [1036]. Your NIT 1 and NIT 2 behave as on main and are left for the follow-ups the coordinator files. Lane: openreposhape-4 (openRepoShape-4) |
…a signal fails the run by name wherever the update met it, read from git's trace, not only where its code is the update's last; with a token in use that stays a limit (#1056) (#1076) Where a git that git's update ran dies of a signal, `make bootstrap` in a family holder now fails naming it wherever the update met it, not only where the signal's code is the update's last. This is #1056, found by rv996, lane 5's independent review of #1045 (issuecomment-6288783192, checklist candidate 1). Git keeps only the last code that is not 0 (`update_submodules` in git 2.47.1's `builtin/submodule--helper.c`), and only 128 stops it. So a member's crash or kill before a later gitlink's 128 or 1 left no signal in the update's exit code, and #1045's `ended_on` saw none: the run said the credential advice and `family bootstrap ok`, exit 0, with IRRS's legs never updated. One commit, on main `00f6d7f7` (which holds #1045 and #1023). It changes two files: the family root's `templates/family-root/scripts/bootstrap.py` and `tests/test_family_bootstrap_stops_named.py`. ### The signal is read from git's own trace (no token in use) - With no token in use, git's update is already traced into a directory of the run's own (#892). `traced_in` now reads that trace once and returns a `Trace`: `fetched` (#892's work trees, as before) and `died`, read by the new `died_in`. - **`died_in`** reads the events by the code, never git's words, as #892 reads a fetch: - a git writes a `child_start` event (with the argv) and a `child_exit` event (with the code) for each child it starts; - `child_of` pairs the two by the writing git's `sid` and the `child_id`; - a `child_exit` whose code is `128 + N`, N below `signal.NSIG` (`died_of`, now shared with `ended_on`), for a child whose argv begins `git` (`git_child`), is a git dead of signal N; - a hook or another program is not counted. Its code is its own: a `post-checkout` hook that exits 137 is no signal, and `git checkout` exits 1 over it (measured, both gits). - **The first signal** is the first by the time each event gives, which is the order git met them. Where trace2 gives no time (its brief form), it is the order of the files and their lines. - File order alone would name the wrong one. The files go in the order their gits started, so a member's own update comes after git's update's file. - Measured on both gits: SIGKILL at IRRS's `code` leg, then SIGTERM at its `spec` leg. That ended IRRS's own update 143, and git's update's `child_exit` 143 for it came first in the files. - **`signal_met`** chooses what (a) tells: - where git's update exited 0, nothing; - else the trace's first signal, where there is one; - else the update's own exit code, through `ended_on` as #998 reads it (`128 + N`, or `-N` where git itself was killed). - **Nothing where the update exited 0.** It did all it set out to do then, whatever a git it ran died of on the way. Measured on both gits: a `reference-transaction` hook killed IRSS's `git fetch` after it wrote the branch, git found the commit reached, checked it out and exited 0. The run says every member and leg was fetched, as before. ### What the run says - Where the signal's code is the update's own, the line is #998's, word for word, so the controls read as before: ``` UPDATE KILLED, and not for want of a credential: git's update ended on signal 9 (SIGKILL, exit 137), which a git it ran died of. Git's own words above say which git and where. family bootstrap FAILED: git's update (ended on signal 9) ``` - Where the trace gave it and the update ended with another code: ``` UPDATE KILLED, and not for want of a credential: a git that git's update ran died of signal 9 (SIGKILL, exit 137), and git's update went on and ended with exit 128. Git's own words above say which git and where. family bootstrap FAILED: git's update (a git it ran died of signal 9) ``` - No credential advice in either case (`credential_advice`). ### A signal and B4 both told: the signal first - Before, `fetch_members` returned one failure line, the signal's or the walk's stop's. Now `told_past` says each that is there, each with its own failure line. - Where both are told, **the signal is said first**. Why: - It is git's order. A death past the holder's records (128) ends git's update, so git met the signal at an earlier gitlink and went on past it. - Git's own words, printed just above, say them in that order too. - The two cures do not depend on each other. - Measured: the killed checkout in IRRS, then B4 at IRSS, with the credential. Before, the B4 line alone was said. Now: ``` UPDATE KILLED, and not for want of a credential: a git that git's update ran died of signal 9 (SIGKILL, exit 137), and git's update went on and ended with exit 128. Git's own words above say which git and where. UPDATE STOPPED, and not for want of a credential: git stopped at members/IRSS, whose fetch reached its remote and did not bring <commit>, ... family bootstrap FAILED: git's update (a git it ran died of signal 9), members/IRSS (git stopped at it) ``` - A `NOT CHECKED OUT` row or a whole-holder stop (`unfetchable`, `updated_none`) still comes first and alone, as for #998's signal (mutant S7's order is unchanged). ### With a token in use: the limit, said and pinned The issue offered two ways. I measured both, and chose to say the limit and pin it with a row. | measured (killed checkout, then IRSS's pin moved on, the token's fake value) | git 2.43.0 | git 2.47.1 | |---|---|---| | a directory trace target with the token's `-c` in argv: events holding the token | 1 (the top git's `start` argv) | 1 (the same) | | the rewrite given in the environment instead, IRSS's origin an https url through a refused local proxy: events holding the token | 8 (the remote helper's `start` and `child_start` events give the url) | 0 | | an `af_unix:dgram:` socket target read by a thread, in memory | 189 datagrams, 1 holding the token, the `child_exit` 137 for `git checkout` there; no file | 194, 1, the same; no file | So: - **Any file target writes the token**: in argv on both gits, and, moved to the environment, in git 2.43.0's remote helper url. - **Git's one target that is no file, a Unix socket, works**, but this PR does not take it: - this process would have to read it for the whole update, and every git the update starts waits on it once Linux has queued 10 events (`net.unix.max_dgram_qlen` is 10 here), so a stalled reader hangs `make bootstrap`; - the socket's path must fit in the 107 bytes Linux takes. This harness's TMPDIR is 151 bytes, and a socket path under this session's scratchpad (114 bytes) was refused (`AF_UNIX path too long`); - git for Windows builds without it (read, not run). It would be a second trace mechanism, and is better as its own issue if wanted. - **What the docstrings now say:** - `ended_on`: with a token in use the exit code is all there is, and a signal before a later failure is not told. - `credential_advice`: the limit and the measurement. - `run_update`: points at both. - **The row:** `test_with_a_credential_in_use_a_signal_is_told_only_from_git_s_code`. - `[alone]`: the killed checkout alone is named, from git's own 137. - `[then-pin-moved-on]`: the killed checkout, then IRSS's pin moved on with the transport refused, is the credential's skip, the advice and ok. - Both: the token is in no output and no file under TMPDIR, with every caller trace on. ### Two docstrings corrected - `ended_on`: git's update ends with a signal's code only where no gitlink after it fails, since it keeps the last code that is not 0. (It said the update "ends with that code".) - `credential_advice`: a failed fetch does not always end the update with 128. A clone that fails gives 1 and the update goes on (rv996's NIT 2 on #1045), so the advice is dropped beside a signal even where a credential was also wanting. The run fails on the signal, and a run made again says the advice. (It said an update ending `128 + N` "met none".) - The module docstring's (a) and (c), `fetch_members`, `run_update`, `fetched_in`, `GIT_DIED`, `UPDATE_KILLED` and `TRACE_ENV` say the same. The probe is a pytest file kept out of the commit. It builds each state with `tests/test_family_bootstrap_stops_named.py`'s own helpers, runs the family root's `bootstrap.py --skip-members` (offline), and records the exit code, whether the credential advice is said, and whether the fake token is in any output or any file under TMPDIR. It also runs git's own update, traced, on a copy of the same state with no token. It was run at main `00f6d7f7` and at this head, on git 2.43.0 (`/usr/bin/git`) and on git 2.47.1 (a local build, first on PATH). Each cell is the verdict, the signal said, and the exit code: | state (in IRRS, then in IRSS) | main 2.43.0 | head 2.43.0 | main 2.47.1 | head 2.47.1 | |---|---|---|---|---| | killed checkout, then IRSS's pin moved on, no credential | ok + advice (0) | FAILED signal 9 (1) | ok + advice (0) | FAILED signal 9 (1) | | killed checkout, then IRSS's checkout refused, with the credential | ok + advice (0) | FAILED signal 9 (1) | ok + advice (0) | FAILED signal 9 (1) | | killed checkout, then B4 at IRSS, with the credential | FAILED + B4 stop (1) | FAILED signal 9 + B4 stop (1) | FAILED + B4 stop (1) | FAILED signal 9 + B4 stop (1) | | control: killed checkout alone, with the credential | FAILED signal 9 (1) | FAILED signal 9 (1) | FAILED signal 9 (1) | FAILED signal 9 (1) | | P3, then IRSS's pin moved on, no credential | ok + advice (0) | FAILED signal 11 (1) | ok + advice (0) | ok + advice (0) | | P3, then B4, with the credential | ok + advice (0) | FAILED signal 11 (1) | ok + advice (0) | ok + advice (0) | | control: P3 alone, with the credential | FAILED signal 11 (1) | FAILED signal 11 (1) | ok + advice (0) | ok + advice (0) | | a hook that exits 137 (no signal), then IRSS's pin moved on, no credential | ok + advice (0) | ok + advice (0) | ok + advice (0) | ok + advice (0) | | a hook that exits 137, alone, with the credential | ok + advice (0) | ok + advice (0) | ok + advice (0) | ok + advice (0) | | SIGKILL at IRRS, then SIGTERM at IRSS, with the credential | FAILED signal 15 (1) | FAILED signal 9 (1) | FAILED signal 15 (1) | FAILED signal 9 (1) | | SIGKILL at IRRS's `code` leg, then SIGTERM at its `spec` leg, with the credential | FAILED signal 15 (1) | FAILED signal 9 (1) | FAILED signal 15 (1) | FAILED signal 9 (1) | | the same two legs, then IRSS's pin moved on, no credential | ok + advice (0) | FAILED signal 9 (1) | ok + advice (0) | FAILED signal 9 (1) | | token in use: killed checkout, then IRSS's pin moved on | ok + advice (0) | ok + advice (0) | ok + advice (0) | ok + advice (0) | | token in use: killed checkout alone | FAILED signal 9 (1) | FAILED signal 9 (1) | FAILED signal 9 (1) | FAILED signal 9 (1) | | token in use: killed checkout, then B4 | FAILED + B4 stop (1) | FAILED + B4 stop (1) | FAILED + B4 stop (1) | FAILED + B4 stop (1) | | token in use: killed checkout, then IRSS's checkout refused | ok + advice (0) | ok + advice (0) | ok + advice (0) | ok + advice (0) | Read across the table: - **Every no-token row of the probe that holds a signal now fails naming it, with no advice** (the rows that said `ok` with the advice, and the one that named SIGTERM): both killed-checkout rows (pin moved on, checkout refused) on both gits, and P3 (then IRSS's pin moved on, then B4) on git 2.43.0. P3 on git 2.47.1 is not a crash there (git refuses the line and exits 128, so nothing is killed), so its rows read as before. - **Killed checkout, then B4:** the B4 line alone was said at main. Now both are said, the signal first. - **A hook's own code is no signal:** a `post-checkout` hook that exits 137 reads the same at main and at this head: `ok`, with the advice. - **The first signal is the first git met:** SIGKILL at IRRS, then SIGTERM at IRSS (and the two legs of IRRS) named SIGTERM at main, from git's update's own last code, and name SIGKILL now. - **A signal in an update that exits 0 is not said.** A `reference-transaction` hook that kills IRSS's `git fetch` once, after it wrote the ref (git then finds the commit reached, checks it out and exits 0): measured at this head on both gits (main reads no trace, so it says the same), the run says `every member and leg fetched` and `family bootstrap ok`, exit 0. The trace there holds a `git fetch` `child_exit` of 137, and the row `test_a_git_killed_in_an_update_that_exits_0_is_not_said` pins that nothing is said (mutant M7). - **With a token in use nothing changed**, on purpose (the four `token in use` rows): the signal is told only where its code is git's last. - **The fake token** was in no output and in no file under TMPDIR in any run of any row (probe lines `token in output: False`, `token in temp files: []`). All in `tests/test_family_bootstrap_stops_named.py` (#954's and #1045's file), reusing its helpers and its style, with no assert that joins conditions with `and`. The module docstring gains the new rows' list, and the P3 state moved into a `p3()` helper so two rows build it. 11 new rows (37 before, 48 after). The issue's Verify rows (fail at main): - `test_p3_before_a_later_failure_fails_naming_the_signal`: P3, then IRSS's pin moved on, no credential, git 2.43.0: exit 1, signal 11 (SIGSEGV) named, no advice, FAILED. Skipped where git does not crash, as #1045's P3 row is. - `test_a_killed_checkout_before_a_later_failure_fails_naming_it[pin-moved-on-no-credential]` and `[checkout-refused-with-the-credential]`: the killed checkout (a `post-checkout` hook sending SIGKILL to its parent, `kill -s KILL $PPID`), then IRSS's pin moved on with no credential, and then IRSS's checkout refused with the credential. Each exits 1, names signal 9 (SIGKILL), gives no advice. Every POSIX git. The choice rows (fail at main): - `test_a_signal_and_b4_after_it_are_both_named_the_signal_first`: the killed checkout, then B4 at IRSS, with the credential: both lines, the signal first, and the failure line names both. - `test_the_first_signal_is_named[two-members]` and `[two-legs]`: SIGKILL, then SIGTERM, at two members and at two legs of one member. The first by time is named (SIGKILL), where git's update's own last code, and the files' order, name SIGTERM. The controls and the limit (pass at main, as intended, so they hold the rest as it was): - `test_a_killed_checkout_alone_is_named_as_before`: #998's line, word for word. - `test_a_hook_s_own_code_is_no_signal`: a hook that exits 137 is not a signal. - `test_a_git_killed_in_an_update_that_exits_0_is_not_said`: the `reference-transaction` row above. - `test_with_a_credential_in_use_a_signal_is_told_only_from_git_s_code[alone]` and `[then-pin-moved-on]`: the token's limit, pinned. Alone, the killed checkout is named from git's own 137. Then IRSS's pin moved on, the run is the credential's skip, the advice and `ok`, as before. Both: the fake token is in no output and in no file under TMPDIR, with every caller trace variable on. The controls the issue names that already existed are untouched and pass: the signal alone (`test_an_update_ended_on_a_signal_fails_naming_it`, the P3 row), the credential's skip (`test_the_credential_s_skip_is_as_it_was`) and `test_a_code_no_signal_gives_is_read_as_before[exit-128]` and `[exit-255]`. **Failing at main:** main's `bootstrap.py` bytes (sha256 `50a16751...`) against this branch's 48-row file, git 2.43.0: **6 failed, 42 passed**. The six are exactly the rows under "fail at main" above (the P3 row, the two killed-checkout rows, the both-named row and the two first-signal rows). They pass with the change. Each mutant applied alone to the change's `bootstrap.py` (a script replaces one exact string and asserts it occurs once), the stops-named file run on git 2.43.0 (`/usr/bin/git`) through the lane's runner, the file restored from the head's bytes and its sha256 checked after each. All on the final 48-row file (M1 to M3 were run once on the first 47 rows and again on the 48, with the same rows failing). | | mutant | result | |---|---|---| | M1 | the trace read dropped (`Trace(fetched_in(events))`) | killed: 6 failed, 42 passed | | M2 | `128 + N` read as `>= 128` (`GIT_DIED <= code`) | killed: 31 failed, 17 passed | | M3 | a `child_exit` of a child that is not `git` taken (`git_child` accepts any `child_start`) | killed: 1 failed (the hook row) | | M4 | the last signal for the first | killed: 2 failed (`two-members`, `two-legs`) | | M5 | the advice still printed beside a signal | killed: 10 failed | | M6 | the sort by time dropped (file order) | killed: 1 failed (`two-legs`) | | M7 | the exit-0 gate dropped (`signal_met` tells a signal where git exited 0) | killed: 1 failed (the `reference-transaction` row) | | M8 | a child paired by `child_id` alone, without the writing git's `sid` | killed: 1 failed (the hook row) | | M9 | B4 dropped beside a signal | killed: 1 failed (the both-named row) | | M10 | B4 said before the signal | killed: 1 failed (the both-named row) | | M11 | the "went on and ended with exit R" text never said | killed: 6 failed | | M12 | that text always said | killed: 5 failed | | M13 | git's update's own exit code asked before the trace | killed: 2 failed | | M14 | the failure line always "ended on signal N" | killed: 6 failed | | M15 | the `child_exit` kind check dropped from `died_in` | **survives**: 48 passed | **M15 is equivalent, and the check stays as a reader's help.** `died_in` only takes an event whose `(sid, child_id)` is one `child_start` named for a git, and only a `child_exit` event carries a `code`. A `child_start` event has none, so `died_of(None)` is None whether or not the kind is asked. No input reaches a different answer. All through the lane's runner (the real `gh` replaced by a stub, every `LANES_*` variable cleared, `EMAIL=service@opensoft.dev`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`), at most two at once, never the full suite. Git 2.43.0 is `/usr/bin/git`; git 2.47.1 is a local build, first on PATH. `bootstrap.py`'s sha256 began `f1c156e29d07e149`, which the runs recorded and the committed file has (the test file's begins `1d558af665c3`). | run | git | result | |---|---|---| | `tests/test_family_bootstrap_stops_named.py` (48 rows) | 2.43.0 | **48 passed** | | the same | 2.47.1 | **46 passed, 2 skipped** (both P3 rows: that git exits 128 there, not 139) | | main's `bootstrap.py` bytes, this branch's 48 rows | 2.43.0 | 6 failed, 42 passed (the rows listed under Tests) | | the probe, after, 17 states | 2.43.0 and 2.47.1 | 17 passed each | | group A: the stops-named file, `test_family.py`, `test_update_shape_kept_helper.py`, `test_family_holder_remedies_compose.py`, `test_family_gitmodules_values.py`, `test_pinned_copy_text.py`, `test_repo_hygiene.py` | 2.43.0 | **707 passed** | | group B: `test_family_holder_bootstrap_texts.py`, `test_family_bootstrap_gate_order.py`, `test_bootstrap_without_git.py`, `test_family_unnamed_gitlink.py`, `test_family_config_appends.py`, `test_family_member_make_jobserver.py` (#1023's), `test_family_siblings_bootstrap_unreadable.py`, `test_family_head_lost_gitmodules.py`, `test_family_members_not_a_list.py`, `test_family_makefile_siblings.py`, `test_bootstrap_spelling.py`, `test_template_text_placeholders.py`, `test_no_auto_maintenance.py`, `test_agents_md_codes.py`, `test_cli_reference.py` | 2.43.0 | **1010 passed, 3 skipped** (GNU make's jobserver spelling here, and two rows that need a session without `GIT_CONFIG_GLOBAL`) | | the 15 mutants | 2.43.0 | see Mutants | Both groups and every file that names the family root's `bootstrap.py` (`grep -l 'family-root/scripts/bootstrap' tests/*.py` gives two files, both in the groups), `test_pinned_copy_text.py` and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`) are in those two groups. No other test pins the functions or lines this changes (a grep of `tests/`, `docs/` and `scripts/` for them finds only a docstring's mention in `test_family_holder_bootstrap_texts.py`, in group B, and an unrelated workflow-validator row). The runs were made at main `00f6d7f7`'s tree plus this change, before main moved (see Merges). `git merge-tree --write-tree` of this head `73906948` (parent main `00f6d7f7`), after fetching with `gc.auto=0` and `maintenance.auto=false`: | against | result | |---|---| | `origin/main` `4a6b9afde` (3 commits past `00f6d7f7`) | clean (tree `ad8dab73`); none of the 3 touches `bootstrap.py` or the stops-named file | | #1062 `fix/1040-windows-start-errors` `957ba038` (touches the family root's `bootstrap.py`, in `git_not_started` only) | clean (tree `70c5a715`); the merged `bootstrap.py` parses, and holds `died_in` and #1062's `4551` | | #1075 `fix/1055-siblings-untraced` `c3cf5154` (lane 5's: `siblings.py`, `tests/test_family_siblings_untraced.py`, `tests/test_update_shape_kept_helper.py`) | clean (tree `6770fa61`); its `siblings.py` imports `untraced` from `bootstrap.py`, which this does not touch | The other open PRs (#1073, #1072, #1071, #1070, #1069, #1068, #1064, #1031) touch neither file. `templates/family-root/scripts/bootstrap.py` is in `FAMILY_COPIED_VERBATIM`, so each holder gets one `upstream-changed` row for it, as for #1045. The only import is unchanged (`signal` came with #998), and `siblings.py`'s import list from it is unchanged. No Windows or macOS run was made, since CI is Linux only (#597). Review: rv1076, lane 5's independent reviewer, not the writers (w1056 and f1056): READY at `73906948` (issuecomment-6302104583) with no MUST or SHOULD; of its three NITs, the body's is fixed, and the other two (a docstring clause on a member's failed clone, and a text no row reaches) go to a follow-up. CI (Brett Heap's ruling 27): H0 is the head 7390694 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 00f6d7f; and the local gate passed on its predicted squash tree 9dae534 over main bccae28: the family_bootstrap_stops_named, repo_hygiene, agents_md_codes, cli_reference sets, 218 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Closes #1056 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-4, 2026-10-20T23:01:55Z) RV1069 review of #1069 at 8d14a20: Verdict: READY - provided CI SHOULD 1 (round 1) - resolved
I typed only the printed
After each, I read HEAD, the gate in all three modes, and a plain clone (
The
|
| State | Commit exit | HEAD | Local gate | Clean clone's gate |
|---|---|---|---|---|
Composed: git rm --cached, file deleted |
1 ("no changes added") | keeps members/Old |
passes | gitmodules-path-unnamed |
Composed: git rm, deletion staged |
1 | same | passes | same |
Composed: git rm --cached, deletion staged |
1 | same | passes | same |
Mixed: members/New (not in HEAD) before members/Old (in HEAD); New's stage line comes before Old's keys |
1 | same | passes | same |
tools/x committed (rv882's state), gone or on disk: its git rm --cached is staged beside the lines |
0, silent | keeps the tools/x section |
passes | gitmodules-path-unnamed |
Main (6c86163) gives the same outcome for tools/x committed with the file on disk. In the mixed state, main records members/New silently (exit 0). So this is pre-existing, and round 2 improves the mixed case.
Follow-up: print git add -- .gitmodules after the keys in both branches and say "then stage and commit .gitmodules", as the value and url findings already say. Then "type every line, then git commit" is right in every state.
NIT 4 (test gap) - no row covers the case-variant branch's stage line
My mutant RV-G passes staged=False in the case-variant branch (:3206-3209). It survives: 29 of 29 cases pass (the 1036 rows, the two moved rows and test_family_unnamed_gitlink.py). R3 and R4 cover only sections HEAD holds. The code is right: my ghost/Members/IRRS-staged-in-the-index probe passes under (a), (b) and (c), gone and on disk. A row of that shape would guard it.
Also optional: drop the hand git add at tests/test_family_unnamed_gitlink.py:374-375, so the -f rows prove that the printed line stages.
NIT 1 and NIT 2 (round 1, follow-ups) - unchanged
gitmodules-path-twice and the value check still do not read the index's file. Both behave as on main.
Evidence
- Mutants. Each ran over the 29 cases, and the worktree was restored by sha256 (4a0faa39):
- RV-E (the HEAD check inverted): 18 failed;
- RV-F (the name looked up in the index's file, not HEAD's): 6 failed;
- RV-G (no stage line in the case-variant branch): survives, NIT 4;
- RV-H (no stage line in the
tools/xbranch): 3 failed.
- Tests at 8d14a20, all passing:
- the PR's set of 12 files: 567 passed;
- 9 more files that touch this check,
test_family_unnamed_gitlink.pyamong them: 473 passed; - my probes: 36 + 6 cases passed.
- Merges. No merge this round; the branch fast-forwarded from ab8ed9e (one commit, 8d14a20). Main since 00f6d7f touches none of the PR's four files.
git merge-tree --write-treeagainst main bccae28 is clean, and the merged tree differs from main in the four files only. It is also clean against every open PR: validate-family.py, validate-pins.py and both bootstrap.py copies: agitWindows cannot start for a reason other than 193 that names no file (winerror216, 1260, 11, or 188-202) isgit-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1 (#1040) #1062 957ba03 (the only other one touchingvalidate-family.py), adopt-project.py:check's throwaway repositories are made in the work directory with the source's object format and no hook of this machine's; a path git refuses isplan-path-git-refuses, a repository git cannot make thereadopt-git-unasked(#910) #1031, adopt-project.py: a bad link thatplan-empty-symlinkorplan-bad-symlink-targetnames is no longer named again byplan-root-blocks-shape-copyorplan-root-holds-pin, so it gets one finding, and the #908 comments say Linux's bound (#979) #1070, adopt-project.py and family.py: a source this user cannot enter issource-unreadable, a UTF-8 plan reads its source under the C locale, and a member on a branch that is not UTF-8 is put back on it (#918) #1072, adopt-project.py and shape-doctor.py: a.gitmoduleswith no section asks nothing and says how to remove it, a submodule beside one is named, so is a file'sanswered_paths:row with a/, and an identical drop counts asdrop(#914) #1073, the family root's siblings.py: with a token in use, the clone and the fetch that carry it run in bootstrap.py'suntraced()environment, somake siblingsandfamily.py siblingswrite the token to no trace the caller asks git for (#1055) #1075, the family root's bootstrap.py: a git that git's update ran dying of a signal fails the run by name wherever the update met it, read from git's trace, not only where its code is the update's last; with a token in use that stays a limit (#1056) #1076, test_doctor_gitlink_repairs.py: the row that names both bump-leg commands pinsLANES_LANEblank in its own process, asrun_scriptdoes for the doctor it runs, so a shell that exports a lane no longer puts a--traileron the expectation alone (#1060) #1081 and family.py: the by-hand checkout line a failed rollback prints names a branch that is not UTF-8 by its exact bytes, as"$(printf 'caf\351')", not as'caf\udce9', which typed names no branch (#1079) #1084. Main + validate-family.py, validate-pins.py and both bootstrap.py copies: agitWindows cannot start for a reason other than 193 that names no file (winerror216, 1260, 11, or 188-202) isgit-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1 (#1040) #1062 + validate-family.py:gitmodules-path-unnamedreads the file git reads where the work tree's.gitmodulesis gone, the index's then HEAD's, and its lines put that file back first, so a sectiongit submodule addstaged is no longer committed past a passing gate (#1036) #1069 built as a scratch commit: the hand-mount, compose, unnamed_gitlink, git_not_runnable, pinned_copy_text, repo_hygiene, unreadable and bootstrap_without_git files give 486 passed. - CI at 8d14a20.
- package: pass.
- tests-git-bash: pass.
- SonarCloud: Quality Gate passed, with 0 new issues, 0 hotspots and 0.0% duplication.
tests: in progress at 23:01Z. This READY holds only once it passes.
Lane: openreposhape-4 (openRepoShape-4)
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 2 days and 11 hours by commenting |
|
… fetch that carry it run in bootstrap.py's `untraced()` environment, so `make siblings` and `family.py siblings` write the token to no trace the caller asks git for (#1055) (#1075) `make siblings` and `family.py siblings` now write the token in `SHAPE_LEGS_TOKEN` or `SHAPE_LEGS_APP_TOKEN` to no trace the caller asked git for. #996 (#1045) did the same for `make bootstrap`. Lane 5 found this in its independent review of #1045 (rv996, #1045 issuecomment-6288783192) and measured it in #1055. `templates/family-root/scripts/siblings.py` gives the credential's `-c url.https://x-access-token:<token>@github.com/.insteadOf=...` rewrite, in argv, to two gits: - the clone in `place`, for a member with no clone beside the holder; - the fetch in `place`, for a member whose clone is there. Both ran in the caller's own environment. So every trace2 target the caller named wrote the token in the `start` event of each, one line per member. `scrub` kept it out of the output, and only that part held. Now: - **`rewrite_environment(token)`**, a new function in `siblings.py`: - with a token in use, it returns `untraced()` from the `bootstrap.py` beside it. That is every `GIT_TRACE*` variable taken out, and `GIT_TRACE2`, `GIT_TRACE2_EVENT` and `GIT_TRACE2_PERF` set to `0`, which also turns off a target the global or the system config names; - with no token, it returns None, so the two gits keep the caller's environment and stay traced where the caller asked, as before. - **The clone and the fetch** each run with `env=rewrite_environment(token)`. Nothing else in the file changes environment. - **The member's own `scripts/bootstrap.py`**, which `place` runs after a clone, is given no token in its argv, and no `env`, so it keeps the caller's environment and its traces. That environment holds `SHAPE_LEGS_TOKEN` and `SHAPE_LEGS_APP_TOKEN` as before; the shipped member bootstrap (`templates/assembly-root/scripts/bootstrap.py`) reads neither. A test row holds this, and so does `tests/test_validator_child_encoding.py`, which reads that call as handing no `env`. - **#1023's jobserver handling is untouched**: `dispatch` still runs each member's make with `member_make_environment(make)`, and no `MAKEFLAGS` reading changed. - **The docstrings** now say the promise in full: `git_prefix` says the token is "never written into `.git/config`, never persisted, never printed, and never traced". `place`'s ABSENT paragraph in the module docstring says the same, and `rewrite_environment` says what was measured. **One thing the issue did not ask for.** `GIT_TRACE_PERFORMANCE` writes a git's whole command line, `-c` included, on git 2.43.0 and 2.47.1 alike. So at main it held the token too, two lines per run, and the rows below set it. `untraced()` already takes it out: every `GIT_TRACE*` variable goes. This follows #1045's `untraced()` (#996): the clone and the fetch run in the very environment `make bootstrap` gives every git it hands the token. `siblings.py` imports `untraced` from the `bootstrap.py` beside it, next to `REWRITE_TARGETS`, `credential`, `member_make_environment`, `member_path` and `members_of`. The issue left this to the writer, because a holder may keep a `bootstrap.py` older than its `siblings.py` (`update-shape.py apply --accept-local`), and an import of a name that copy lacks would stop `make siblings`, `park` and `resume` with an `ImportError`. The issue's text was written against main `1a47ed07`, where `siblings.py` still kept its own copy of the `MAKEFLAGS` readings. Since then #1023 (`9e154d87`, lane 4's) has decided the question for this pair of files: - it replaced that copy with an import of `member_make_environment`, because a copy in each script failed SonarCloud's duplication gate; - it named the price, in the import's comment and its commit: a holder that keeps its own `bootstrap.py` while `apply` writes `siblings.py` is refused, `update-accept-local-missing-name` (#536, #604), before a byte is written, and merges `bootstrap.py` by hand first. `untraced` takes the same road for the same reasons: - **One rule.** Which traces a command carrying the token is kept out of has one answer, for `make bootstrap` and `make siblings` alike. A copy here could drift from `bootstrap.py`'s, and the drift would be a token in a file. - **A kept old `bootstrap.py` is refused in the one-step case, and not in a two-step one.** The guard reads names. `untraced` is new with #996 (#1045), so a kept `bootstrap.py` from before it is refused by that name, where `--accept-local` names it in the same `apply`. Measured with `update-shape.py`'s own readers, in process: - `siblings.py` at this head takes `REWRITE_TARGETS`, `credential`, `member_make_environment`, `member_path`, `members_of` and `untraced` from `bootstrap`; - the holder's `bootstrap.py` at main `00f6d7f7` lacks none of them; - the holder's `bootstrap.py` at `6476c170` (main just before #1045) lacks `untraced` alone, which is the line the refusal names. - **The one-step case, measured end to end (rv1075).** A holder cut at `6476c170`, and `apply --at c3cf515 --accept-local scripts/bootstrap.py`: exit 2, `scripts/bootstrap.py (kept) lacks untraced, which scripts/siblings.py imports from it`, and the holder's files, HEAD and status unchanged. - **Two applies in a row are not refused (rv1075's SHOULD 1, filed as #1082).** A holder whose earlier apply kept a `bootstrap.py` without `untraced` (rv1075 applied `--at 00f6d7f --accept-local scripts/bootstrap.py`: that target's `siblings.py` imports no `untraced`, so there was nothing to refuse), and then an apply at this head without `--accept-local`, gets `siblings.py` copied. `scripts/siblings.py` then fails with `ImportError: cannot import name 'untraced' from 'bootstrap'`, and so do `make siblings`, `park` and `resume`. The cause is the guard in `update-shape.py` (`:4288-4289`): it checks only the files `--accept-local` names in the same run. That is the gap of #536's guard, and it is on main already for #1023's `member_make_environment`; this change puts `untraced` into it and does not widen it. The text above this paragraph said no `ImportError` could reach a holder, which was too strong; #1082 is the follow-up, and it is not done here. - **No copy, so no test to hold two copies to one answer.** `tests/test_update_shape_kept_helper.py`'s `test_what_siblings_takes_from_the_holders_bootstrap_is_found_defined` already holds that every name `siblings.py` takes is defined in the holder's `bootstrap.py`. Its docstring now says six names, not five. **Setup:** - git 2.43.0 (`/usr/bin/git`) and git 2.47.1, Python 3.12.3; - the family of `tests/test_family.py` (two members, local bare remotes, the file transport allowed through `GIT_CONFIG_COUNT`), laid out as `<tmp>/InkRouter/InkRouter`; - offline: a stub `gh` first on PATH, a bogus `GH_TOKEN`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`; - the holder's own `scripts/siblings.py` was main's (sha256 `86bf57e0...`) or this change's; - the fake token `w1055-a-token-no-forge-takes`. **Each case was run three ways:** - `make siblings`, which clones both members; - `make siblings` again, which fetches both; - `family.py siblings --family-root <holder>` in a fresh layout, which clones both. **Traces were asked for in one of three ways:** - the seven variables `GIT_TRACE`, `GIT_TRACE2`, `GIT_TRACE2_EVENT`, `GIT_TRACE2_PERF`, `GIT_TRACE_PACKET`, `GIT_TRACE_CURL` and `GIT_TRACE_PERFORMANCE`, each set to a file under the run's TMPDIR; - `trace2.eventTarget`, `normalTarget` and `perfTarget` in a scratch `GIT_CONFIG_GLOBAL`; - the same three in a scratch `GIT_CONFIG_SYSTEM`. Each way was run with `SHAPE_LEGS_TOKEN`, with `SHAPE_LEGS_APP_TOKEN`, and with no token. That is 108 runs at main and at the change, 54 for each git. Each run's TMPDIR and the whole family folder (the clones' `.git/config` included) were searched for the token. | traces asked by | main, a token set (both variables, both gits) | this change, a token set | no token, either | |---|---|---|---| | the seven variables | `trace2-event.json`, `trace2-normal.txt`, `trace2-perf.txt`, `trace-performance.txt`: 2 lines each, in the clone, the fetch and `family.py` alike | none | no token anywhere; the same files written | | the global config | `cfg-event.json`, `cfg-normal.txt`, `cfg-perf.txt`: 2 lines each | none | the same | | the system config | `sys-event.json`, `sys-normal.txt`, `sys-perf.txt`: 2 lines each | none | the same | **What the table rests on:** - Every one of the 108 runs (and the 54 runs again on this head's final bytes, sha256 `b1e7bb21...`) exited 0 and ended `siblings ok: 2 member(s) beside the holder in ...`, with the token in neither stdout nor stderr. - The 2 lines in each file are the `start` (or `performance:`) line of the git given the `-c`, one for each member. Each carries the token twice, once for each spelling the rewrite covers. - With every member a local path, `GIT_TRACE`'s file was written and held no token, and `GIT_TRACE_CURL` wrote nothing, with no http remote. An https member is a different case, measured below. - With the change and a token set: - every trace file but `GIT_TRACE_PACKET`'s was still written, by the run's other gits and the member's own bootstrap; - the packet trace was not written, since only the clone and the fetch write one here. **An https remote, measured offline (rv1075, then the row of Round 2).** The rewrite reaches only `github.com`, and the probes above are offline, with local paths. A member whose `.gitmodules` url is `https://github.com/<org>/<repo>.git` is reached by it, and can be tried offline behind a dead proxy, `http.proxy=http://127.0.0.1:9`, so that nothing leaves the host: - rv1075 set one member's url so, with `make siblings`, a fake `SHAPE_LEGS_TOKEN`, and the seven variables set to files, on git 2.43.0 and 2.47.1. Main's `siblings.py` (`86bf57e0`) held the token in `trace.txt`, `trace2-event.json`, `trace2-normal.txt`, `trace2-perf.txt` and `trace-performance.txt`; this head held it in no file and no output. The clone failed cleanly, `git clone exited 128`, and `make siblings` exited 2. - `GIT_TRACE`'s file holds it too, in the `run_command: git remote-https origin https://x-access-token:<token>@github.com/...` lines: 3 lines on git 2.43.0, 4 on 2.47.1 (rv1075). - Round 2 below holds this in a test row. **Still not measured:** a github.com clone or fetch that succeeds, since no run here reaches the network. `tests/test_family_siblings_untraced.py` is new. It uses `tests/test_family.py`'s `family` and `estate`, `offline` from `tests/test_family_add_refusals.py`, `files_holding` and `CREDENTIALS` from `tests/test_family_bootstrap_stops_named.py`, and `NO_CREDENTIAL` and `needs_make` from `tests/test_siblings_family_name.py`. No assert joins conditions with `and`. The token is the fake `w1055-a-token-no-forge-takes`. **The rows.** Each entry point is crossed with each way traces are asked for: - entry points: `make siblings` cloning, `make siblings` again fetching (after a first run with no token and no trace), and `family.py siblings` cloning; - ways: the seven variables, the three targets in a scratch global config (a copy of the session's, which keeps git's automatic maintenance off), and the three targets in a scratch system config. **Three tests** (the third is round 2's): - **`test_with_a_token_in_use_the_token_is_in_no_trace`**: 18 rows (3 entries x 3 ways x 2 variables). Each asserts: - exit 0 and `siblings ok: 2 member(s) ...`; - the token in neither stdout nor stderr; - the token in no file under the test's directory: TMPDIR, every trace and config file, and the clones and their `.git/config`; - every trace file the way asks for written: 5 for the variables, 3 for each config; - the member's own bootstrap traced where a member was cloned: a git traced in a leg of each clone, which only that bootstrap runs. Where nothing was cloned, nothing ran there. - **`test_with_no_token_the_clone_and_the_fetch_are_traced_as_asked`**: 9 controls (3 entries x 3 ways). Each asserts exit 0, `siblings ok` and the same trace files written. It also asserts the trace2 event file holds `siblings.py`'s own git at each member, read from the events: `git clone -q --no-local ...` to each member's slot, or `git fetch --quiet origin` in each member's clone. **Fails on main.** The file was run against main's `siblings.py` (sha256 `86bf57e0...`, put in place and then restored, sha256 checked): - **18 failed, 9 passed**: every row with a token failed, and the 9 controls passed; - the variables rows found the token in `trace-performance.txt`, `trace2-event.json`, `trace2-normal.txt` and `trace2-perf.txt`; - the global-config rows found it in `cfg-event.json`, `cfg-normal.txt` and `cfg-perf.txt`; - the system-config rows found it in `sys-event.json`, `sys-normal.txt` and `sys-perf.txt`. - **`test_an_https_member_with_a_token_in_use_is_in_no_trace`**: 3 rows, one for each of `CALLERS_CONFIG` (round 2 and round 3, below). Each mutant was applied alone to the worktree's file and the new test file run (git 2.43.0). The bytes were restored and checked by sha256 after each: - `siblings.py` sha256 `b776c69b...`: this head's code; it differs from the head only in two docstring lines; - `bootstrap.py` sha256 `50a16751...`, main's, unchanged here. All are **killed**. | mutant | result | |---|---| | M1 the token condition dropped: `untraced()`'s environment always | killed, 9 failed (every control) | | M1b the condition inverted | killed, 27 failed | | M1c the clone untraced always, the fetch as it is | killed, 6 failed (the clone controls) | | M2 the clone left in the caller's environment | killed, 12 failed (every clone row with a token) | | M3 the fetch left in the caller's environment | killed, 6 failed (every fetch row with a token) | | M4a `GIT_TRACE2_EVENT` not set to `0` (`TRACE2_OFF`) | killed, 12 failed (the global- and system-config rows) | | M4b `GIT_TRACE2` not set to `0` | killed, 12 failed (the same) | | M4c `GIT_TRACE2_PERF` not set to `0` | killed, 12 failed (the same) | | M5 the `GIT_TRACE*` removal dropped (`env = dict(os.environ)`) | killed, 6 failed (the variables rows, by `GIT_TRACE_PERFORMANCE`) | | M6 the prefix narrowed to `GIT_TRACE2` | killed, 6 failed (the same) | | M7 the member's own bootstrap run untraced too | killed, 12 failed (the clone rows' bootstrap check) | M5 and M6 are killed only because the variables rows set `GIT_TRACE_PERFORMANCE`. With a local-path remote, of the issue's six variables none but trace2's writes the `-c`, and trace2's three are set to `0` whatever the prefix does. With an https remote `GIT_TRACE` writes it too, in the `run_command: git remote-https` lines; that is what the row of Round 2 holds, and the mutant R4 below, which keeps the caller's `GIT_TRACE`, passed all 27 earlier rows. **How every run was made:** - through lane 5's runner: `/usr/bin/python3 -m pytest -q -rs -p no:cacheprovider -o tmp_path_retention_policy=failed`, one basetemp per run; - `LANES_*` cleared, a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`; - `EMAIL=service@opensoft.dev`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null` at the start of the session. | git | files | result | |---|---|---| | 2.43.0 | `test_family_siblings_untraced.py`, on this change before the docstring fix | 27 passed | | 2.43.0 | the same file against main's `siblings.py` | 18 failed, 9 passed | | 2.43.0 | gate shard A: 19 of the 41 files below | 1623 passed, 4 skipped, in 19:51 | | 2.43.0 | gate shard B: the other 22 of the 41 files | 2415 passed, 8 skipped, in 15:03 | | 2.47.1 | `test_family_siblings_untraced.py` | 27 passed, in 1:14 | **What the 41 gate files are:** - the new file; - `test_family_member_make_jobserver.py`; - every file `grep -l siblings tests/` names; - the pinned-copy and upgrade-row files: `test_pinned_copy_text.py`, `test_update_shape.py`, `test_update_shape_add.py`, `test_update_shape_kept_helper.py`, `test_no_auto_maintenance.py`, `test_template_text_placeholders.py`, `test_bootstrap_spelling.py`; - the trio: `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`. **The gate runs** (both shards at once, then the 2.47.1 run) were made on this change as it is committed: `siblings.py` sha256 `b1e7bb21...` and the new test file sha256 `7ba7a416...`, at main `00f6d7f7` (this branch's base; no rebase was needed, see Merges). The runner's `START` line read `dirty=2`, the two tracked files this commit changes, and a sha256 check after the runs found `siblings.py` unchanged (neither it nor the test files were modified after the runs started). The first pair of shards was stopped part way and re-run whole; only the re-runs count here. **The 41 files, by shard:** - shard A (19): `test_shape_doctor.py`, `test_validator_child_encoding.py`, `test_family_py_unreadable.py`, `test_pinned_copy_text.py`, `test_siblings_inputs.py`, `test_siblings_family_name.py`, `test_family_member_make_jobserver.py`, `test_update_shape_add.py`, `test_adopt_printed_sentences.py`, `test_family_printed_quoting.py`, `test_family_makefile_siblings.py`, `test_template_text_placeholders.py`, `test_park_resume_targets.py`, `test_windows_paths.py`, `test_doctor_gitmodules_not_utf8.py`, `test_local_remote_dir_urls.py`, `test_family_siblings_untraced.py`, `test_setup_family_folder.py`, `test_bootstrap_refusal_exit.py`; - shard B (22): `test_repo_hygiene.py`, `test_doctor_legs_members_next.py`, `test_update_shape.py`, `test_family.py`, `test_update_shape_kept_helper.py`, `test_yaml_reader_spaces.py`, `test_bootstrap_spelling.py`, `test_agents_md_codes.py`, `test_family_siblings_placement.py`, `test_no_auto_maintenance.py`, `test_cli_reference.py`, `test_family_siblings_bootstrap_unreadable.py`, `test_bootstrap_branch_bytes.py`, `test_family_init_asks_every_value.py`, `test_validate_workflow_gitmodules_urls.py`, `test_adopt_contracts_dir.py`, `test_doctor_unfinished_merge.py`, `test_family_members_not_a_list.py`, `test_family_init_landing.py`, `test_family_docs_one_spelling.py`, `test_doctor_members_spelled.py`, `test_pins_child_kind_and_siblings_root_spelling.py`. **The skips are the environment's, each named by its reason:** - shard A: 3 in `test_shape_doctor.py` (the PowerShell line is printed, and `powershell.exe` exists, only on a Windows host) and 1 in `test_family_member_make_jobserver.py` (`make -j2` here writes `--jobserver-auth=3,4`, not GNU make 4.4's named pipe); - shard B: 2 in `test_doctor_legs_members_next.py` (this git crashes on `submodule init`, exit 139, as git 2.43 does on a bare `branch`), 4 in the same file (this git dies on a `shallow` of -2**31, as git 2.43 does), and 2 in `test_no_auto_maintenance.py` (the session began with `GIT_CONFIG_GLOBAL` set, and its file includes that one); - the 2.47.1 run of the new file skipped none. The full suite was not run. **Static checks:** - flake8 finds in `siblings.py` only the three lines it finds at main (two E501, one E127, each moved down by the added lines); - in the new test file it finds only the F811s every fixture-importing file has; - both files are ASCII, apart from the dashes and the CJK example already in `siblings.py`. Fetched with `-c gc.auto=0 -c maintenance.auto=false`, on 2026-10-20 at about 21:03Z. Round 1, `git merge-tree --write-tree` of the first head `c3cf5154` against (round 2's are in its own section): - **origin/main `6c861635`**: clean, exit 0, tree `225b2a99`. - Main has moved two commits past this branch's base `00f6d7f7`: `9cbba9d5` (#1063: `scripts/family.py`, `scripts/shape_materialize.py`, `docs/cli.md` and two tests) and `6c861635` (#1065: `adopt-project.py` and three tests). - Neither touches `siblings.py`, `bootstrap.py`, `tests/test_update_shape_kept_helper.py` or the new test file, so there was no rebase and the gate runs above stand. - **#1062** (`fix/1040-windows-start-errors`, head `957ba038`), the one open PR that touches a family-root script this change reads (`templates/family-root/scripts/bootstrap.py`, with the assembly root's and `validate-family.py`, `validate-pins.py` and three tests): clean, exit 0, tree `84d4b54a`. - Its diff does not touch `untraced()`. - In the merged tree `bootstrap.py` still defines `untraced` (and the five other names `siblings.py` takes from it), and `siblings.py` is byte-identical to this head's (sha256 `b1e7bb21...`). - **The other 9 open PRs** (#1073, #1072, #1071, #1070, #1069, #1068, #1067, #1064, #1031): none touches `siblings.py`, `tests/test_update_shape_kept_helper.py`, or a file the new test imports from (`conftest.py`, `test_family.py`, `test_family_add_refusals.py`, `test_family_bootstrap_stops_named.py`, `test_siblings_family_name.py`), read from `gh pr list --state open --json number,headRefName,headRefOid,files`. `templates/family-root/scripts/siblings.py` is in `FAMILY_COPIED_VERBATIM`, so each holder gets one `upstream-changed` row for it, as #1045 gave one for `bootstrap.py`: - a holder that takes it with `bootstrap.py` from #1045 or later needs nothing more; - a holder that keeps an older `bootstrap.py` with `--accept-local` in the same apply is refused, `update-accept-local-missing-name`, naming `untraced`, before anything is written; - a holder whose earlier apply kept a `bootstrap.py` without `untraced`, and that then applies this change without `--accept-local`, is not refused: `siblings.py` is copied, and `make siblings`, `park` and `resume` meet an `ImportError`. That is the gap of #536's guard, which `member_make_environment` has on main already, and it is #1082's. No Windows or macOS run was made, since CI is Linux only (#597). rv1075, lane 5's independent reviewer, found `c3cf5154` READY (#1075 issuecomment-6302090874): no MUST, two SHOULDs and a NIT. One commit on top of it, `041f7831`, with no rebase: main has touched none of this PR's files. **The three findings:** - **SHOULD 1** (the body said no `ImportError` reaches a holder, and two applies in a row make one): answered in the body above, in "Where the helper comes from" and "The pinned copy". The gap is #536's guard's, it is on main for `member_make_environment` already, and it is #1082's; this PR does not change `update-shape.py`. - **SHOULD 2** (every row used a local file remote, so a leak through `GIT_TRACE` on an https member had no test, and rv1075's mutant R4 passed all 27 rows): answered by the new row below. - **NIT** (three places said the member's own bootstrap "is given no token"): `siblings.py`'s module docstring, `rewrite_environment`'s docstring and the test file's docstring now say it is given no token in its argv. It inherits `SHAPE_LEGS_TOKEN` and `SHAPE_LEGS_APP_TOKEN` in its environment, because `run_member_bootstrap` passes no `env`, and the shipped one reads neither. `rewrite_environment`'s docstring also names the https measurement. **The code of `siblings.py` is unchanged.** Its syntax tree with the docstrings taken out is the same at `c3cf5154` and at `041f7831` (compared with `ast`); the file's sha256 moved from `b1e7bb21...` to `19107ad4...` over comments and docstrings only. So round 1's two gate shards (1623 passed, 4 skipped; 2415 passed, 8 skipped) were not re-run; the targeted runs below are. **The new row,** `test_an_https_member_with_a_token_in_use_is_in_no_trace` (the file has 28 rows now). Its setup: - the estate of `tests/test_family.py`, with `IRRS`'s url in the holder's `.gitmodules` given as `https://github.com/InkRouter/IRRS.git`, and `IRSS` still a local path; - `http.proxy=http://127.0.0.1:9`, a dead proxy, in git's command scope after the entries the session holds, and `NO_PROXY` and `no_proxy` empty, so nothing leaves the host; - a fake `SHAPE_LEGS_TOKEN` and the seven trace variables set to files; - `make siblings`, cloning. It asserts: - the proxy is set where the row sets it: `http.proxy=http://127.0.0.1:9` is in the command scope the run's git reads (`command_scope_at`, which runs `git config --show-origin --list` and writes nothing); - exit 2 and `IRRS: git clone exited 128:` on stderr, which is how the head fails (rv1075 measured the same); - `IRSS` was cloned and `IRRS` was not; - the token is in neither stdout nor stderr, and in no file under the test's directory; - the five trace files the variables ask for are written, by the run's other gits. No assert names the proxy in git's message. The git 2.47.1 this lane builds has no `git-remote-https` (`git: 'remote-https' is not a git command`, `fatal: remote helper 'https' aborted session`), so there the clone fails at once without reaching the proxy, still with 128. `GIT_TRACE` has written the `run_command: git remote-https origin https://x-access-token:<token>@github.com/...` line before that, so the row finds the leak on both gits. A first local version asserted the proxy's message and failed on 2.47.1 for that reason; the assert was dropped, and the commit amended, before anything was pushed. **It fails on main.** The file was run against main's `siblings.py` (sha256 `86bf57e0...`, put in place and then restored, sha256 checked each time): - git 2.43.0, the whole file: **19 failed, 9 passed**, the 18 rows of round 1 and the new row. The new row fails at `assert files_holding(tmp_path, TOKEN) == []`, with 5 files holding the token: `temp/trace-performance.txt`, `temp/trace.txt`, `temp/trace2-event.json`, `temp/trace2-normal.txt` and `temp/trace2-perf.txt`. The round 1 rows find 4 of those, since a local path leaves `temp/trace.txt` clean; - git 2.47.1, the row alone: **1 failed**, the same 5 files. **It kills R4.** R4 is rv1075's mutant, in `rewrite_environment`: `return {**untraced(), **{k: v for k, v in os.environ.items() if k == "GIT_TRACE"}} if token else None`, which keeps the caller's `GIT_TRACE` for the clone and the fetch. Applied alone, then restored and sha256-checked: - git 2.43.0, the whole file: **1 failed, 27 passed**: the new row alone, with `temp/trace.txt` the one file holding the token; - git 2.47.1, the row alone: **1 failed**, the same one file. **The runs on the pushed head `041f7831`** (the runner's `START` line read `dirty=0`), through the same runner as round 1: | git | files | result | |---|---|---| | 2.43.0 | the new file, `test_update_shape_kept_helper.py`, `test_family_makefile_siblings.py`, `test_family_member_make_jobserver.py`, `test_pinned_copy_text.py`, and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`) | 589 passed, 1 skipped (the jobserver file's named-pipe row, as in round 1), in 5:09 | | 2.47.1 (`git version 2.47.1`, printed from the exact path first on PATH) | the new file | 28 passed, in 2:13 | | 2.43.0 | the new file against main's `siblings.py` | 19 failed, 9 passed | | 2.47.1 | the new row against main's `siblings.py` | 1 failed | | 2.43.0 | the new file with R4 | 1 failed, 27 passed | | 2.47.1 | the new row with R4 | 1 failed | | 2.43.0 | on a predicted merge with main (below): the new file, `test_update_shape_kept_helper.py`, `test_pinned_copy_text.py`, `test_family_makefile_siblings.py` | 305 passed, in 2:50 | The full suite was not run. flake8 finds in `siblings.py` the same three lines as at round 1, and in the test file only F811s, from the fixtures it imports. Both files are ASCII, apart from the dashes and the CJK example already in `siblings.py`. **The merges of round 2.** Fetched with `-c gc.auto=0 -c maintenance.auto=false` at about 23:00Z; `git merge-tree --write-tree` of `041f7831` against: - **origin/main `c13c3985`** (#1076's squash, which touches `templates/family-root/scripts/bootstrap.py` and `tests/test_family_bootstrap_stops_named.py`, among others): clean, exit 0, tree `8ae10d0c`. None of the three files this PR changes is among the files main changed since the base `00f6d7f7`, so there is no rebase. In that tree `siblings.py` is byte-identical to this head's, `untraced()` has the same syntax tree as at the base, and `CREDENTIALS` and `files_holding`, which the new file imports from `test_family_bootstrap_stops_named.py`, are still defined. The 305-passed run above is on that tree; - **#1062** (`fix/1040-windows-start-errors`, head `957ba038`): clean, exit 0, tree `e6ed1ba2`. Its `untraced()` has the same syntax tree as main's; - **the other open PRs** (#1031, #1069, #1070, #1072, #1073, #1081, #1084): none touches `siblings.py`, `tests/test_update_shape_kept_helper.py`, `bootstrap.py`, or a file the new test imports from, read from `gh pr list --state open --json number,headRefName,headRefOid,files` at about 23:00Z. #1076 (`fix/1056-signal-anywhere`, head `73906948`) was open when the runs were made, and merged clean with this head then (tree `6ae3e65f`); it has landed since as `c13c3985`, above. **The failure.** Lane 5's landing gate (log `c1075.log`, #1075 issuecomment-6304830183) failed the https row of round 2 at `041f7831`: 1 failed, 356 passed. The change in `siblings.py` was not at fault; the row depended on the caller's git config. - The gate ran with neither `GIT_CONFIG_GLOBAL=/dev/null` nor `GIT_SSH_COMMAND=false`, which the writers' runs and round 2's runs had. - `conftest.py`'s session global file includes the caller's global config (or `~/.gitconfig`, where `GIT_CONFIG_GLOBAL` is unset). The operator's `~/.gitconfig` has `url.git@github.com:.insteadOf=https://github.com/`. - Git keeps the first of two `insteadOf` prefixes of the same length, and the config files are read before the command line, so that rewrite beat the token's `-c` rewrite of the same prefix. Measured with scratch files and a stand-in token, on git 2.43.0: under a global file with that rewrite, `git -c url.https://x-access-token:T@github.com/.insteadOf=https://github.com/ ls-remote --get-url https://github.com/InkRouter/IRRS.git` printed `git@github.com:InkRouter/IRRS.git`. - So the row's `https://github.com/InkRouter/IRRS.git` went over ssh to `InkRouter/IRRS`, a real private repository. The dead proxy never applied, the clone succeeded, `make siblings` exited 0 and the placed members were both there, and the row failed `assert result.returncode == 2`. **The fix** is in `tests/test_family_siblings_untraced.py` only. No other file changed in this round, and `siblings.py` is byte-identical to `041f7831`'s (sha256 `19107ad4...`). - **`hermetic_config`** gives the https row's git a global file of its own, `session_global_config("")` (`conftest.py`'s own function: automatic maintenance off, and no include of the caller's file), `GIT_CONFIG_NOSYSTEM=1` and `GIT_SSH_COMMAND=false`. - It is the least invasive way: only the one row with an https url sets it, `conftest.py` and every other row stay as they were, and the other rows' remotes are local paths, which no rewrite of GitHub's url reaches. - `GIT_CONFIG_GLOBAL` set takes git's two user-level files out of the reading, and `GIT_CONFIG_NOSYSTEM` the system file whatever `GIT_CONFIG_SYSTEM` says (checked on git 2.43.0 and 2.47.1 with scratch files). - `GIT_SSH_COMMAND=false` is a second wall: a url somehow sent over ssh runs `false`. No mutant can show it needed, since the runner exports it too. - **`url_after_config`** is a new assert before the clone: `git ls-remote --get-url <url>` (it applies the config's `insteadOf` and asks nothing of the network) must give the url unrewritten under the row's environment. A config that reaches the row now fails by name, before any clone. - **The row is parametrized by `CALLERS_CONFIG`**: `none` (round 2's row), and `global` and `system`, each a stand-in for a caller's config that would send the url somewhere reachable. `as_the_callers_config` writes a scratch file of the test's own, `url.<file URI of the family's own IRRS remote>.insteadOf=https://github.com/InkRouter/IRRS.git`, into this process's `GIT_CONFIG_GLOBAL` (the session file includes it) or `GIT_CONFIG_SYSTEM`. It is built from tmp files only: no network, and never the real `~/.gitconfig`. The file now has 30 rows. - With the fix, in each of the three, the clone fails at the proxy with git's 128, `make siblings` exits 2, `IRSS` is placed and `IRRS` is not, and the token is in no output and in no file under tmp_path. **The variants fail without the fix.** Each was applied alone to the worktree's test file (or `siblings.py`) and run through the runner, with the file's sha256 checked after it was restored: - **`041f7831`'s file as pushed, under a caller's global config.** Lane 5's runner now honours `L5_GIT_CONFIG_GLOBAL=<scratch file>` as the caller's global config, with ssh impossible. The scratch file rewrote `https://github.com/InkRouter/IRRS.git` to the run's own IRRS remote (a `file://` url under the run's basetemp). The whole file, git 2.43.0: **1 failed, 27 passed**, the https row at `assert 0 == 2`, the gate's failure reproduced with no network. - **T2, `041f7831`'s row plus the variants:** the new file with `hermetic_config` and the `url_after_config` assert taken out. The https rows, git 2.43.0: **2 failed, 1 passed**, `[global]` and `[system]` at `assert 0 == 2` (the clone went to the local remote and exited 0), and `[none]` passes. On git 2.47.1 the same. - **T1,** the row's global file dropped and the assert kept: `[global]` fails at the assert, which sees `file:///...IRRS.git` for the url; the other two pass. - **T3,** `GIT_CONFIG_NOSYSTEM` dropped and the assert kept: `[system]` fails at the assert; the other two pass. **R4 stays killed.** rv1075's R4 (`rewrite_environment` keeps the caller's `GIT_TRACE`), git 2.43.0, the whole file: **3 failed, 27 passed**, the three https rows. **The runs on the pushed head `962ae43c`** (`START` read `dirty=0`), through the same runner as before: | git | files | result | |---|---|---| | 2.43.0 | the new file, `test_update_shape_kept_helper.py`, and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`) | 359 passed, in 3:05 | | 2.47.1 (`git version 2.47.1`, printed from the exact path first on PATH) | the new file | 30 passed, in 1:51 | | 2.43.0 | the new file, with `L5_GIT_CONFIG_GLOBAL` naming that scratch file | 30 passed, in 2:21 | | 2.43.0 | `041f7831`'s file, with the same | 1 failed, 27 passed | | 2.43.0 / 2.47.1 | T2, the https rows | 2 failed, 1 passed / 2 failed, 1 passed | | 2.43.0 | T1, T3, the https rows | 1 failed, 2 passed each | | 2.43.0 | R4, the whole file | 3 failed, 27 passed | The full suite was not run. flake8 finds in the test file only F811s from the fixtures it imports, and nothing over 79 columns; the file is ASCII. **The merge-tree of round 3.** Fetched with `-c gc.auto=0 -c maintenance.auto=false` at about 10:53Z; `git merge-tree --write-tree` of `962ae43c` against: - **origin/main `2a119f28`** (#1092): clean, exit 0, tree `b5ea0114`. Main has landed #1076 (`c13c3985`) and #1062 (`ff0c72eb`) since the last round, and has changed `templates/family-root/scripts/bootstrap.py` and `tests/test_family_bootstrap_stops_named.py` among others, but none of this PR's three files. In that tree `siblings.py` is byte-identical to this head's, `untraced()` has the same syntax tree as at the base `00f6d7f7`, and `CREDENTIALS` and `files_holding` are still defined where the new file imports them from. - **#1088** (`fix/1085-nit-texts`, head `88af1b7b`), which touches the same two files of main's: clean, exit 0, tree `365df092`, with the same findings. - **The other open PRs** (#1087, #1090, #1091, #1097): none touches `siblings.py`, `bootstrap.py`, `tests/test_update_shape_kept_helper.py` or a file the new test imports from, read from `gh pr list --state open --json number,headRefName,headRefOid,files` at about 10:53Z. Lane: openreposhape-5 (openRepoShape-5) Review: rv1075, lane 5's independent reviewer, not the writers (w1055 and f1055): READY at `c3cf5154` (issuecomment-6302090874) with no MUST; its two SHOULDs and its NIT were taken in one new commit, and the re-check is READY at `041f7831` (issuecomment-6303900180), with no MUST or SHOULD; its one NIT, a merge-tree number in the body, is fixed in the body. CI (Brett Heap's ruling 27): H0 is the head 962ae43 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 00f6d7f; and the local gate passed on its predicted squash tree fc2afb3 over main 16f9a0e: the family_siblings_untraced, update_shape_kept_helper, repo_hygiene, agents_md_codes, cli_reference sets, 359 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Closes #1055 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…git dies on read the `.gitmodules` git reads, the index's where the work tree's is gone, and every section's lines end `git add -- .gitmodules`, so a plain `git commit` after them takes the section out of HEAD (#1077) (#1124) ## What this does RV1069's review of #1069 (the PR for #1036) left four follow-ups, recorded in #1077's body (NIT 1, NIT 2) and in issuecomment-6303768948 (NIT 3, NIT 4). Each was measured on main before this change. The only script changed is `templates/family-root/scripts/validate-family.py`, which has no pinned copy. - **NIT 1, `gitmodules-path-twice` read the work tree's `.gitmodules` alone.** A second section for `members/IRRS`, staged in the index's file with the work tree's deleted, was `family ok` in every mode, and a clean clone of the commit after it failed its update and its gate. - It reads the sections as git reads them now (`_sections_as_git_reads`), as `gitmodules-path-unnamed` does since #1036. - Where the work tree's file is gone, it names the line that puts back the file git reads first. That put-back is recorded, so no later finding puts the file back over the hand edit. - It ends "then stage and commit .gitmodules". - **NIT 2, the value check read the work tree's file and HEAD's, never the index's.** An `update = !true` staged in the index's file alone, in a named member's section, was `family ok` while git's own `submodule status` exited 128. - It reads the file git reads (`_listed_as_git_reads`), and says "the index's file, which git reads here" where that is the one read. - Its lines put that file back, not HEAD's. They end "then stage", or "then stage and commit" where HEAD's gives the value too. - Where HEAD's file alone gives the value and the work tree's is gone, it says "Commit the index's .gitmodules, which git reads here and does not give it", with no line, so a fix staged there is kept. - `member-gitmodules-absent` then puts the file back itself: `_check_gitlinks_registered` asks `Report.put_back` alone. - Git is asked in the order it was before (the work tree's file, then HEAD's, then the one read in its place), so with no git to run, `git-not-runnable` names what it named. - **NIT 3, a plain `git commit` after a section's lines.** Where HEAD's file held the section, `gitmodules-path-unnamed`'s lines ended "then commit .gitmodules" and staged nothing. A plain `git commit` after them left the section in HEAD, and a clone's gate failed. Every branch (in `members/`, outside it, and in another case) now prints `git add -- .gitmodules` after the keys, and says "then stage and commit" where HEAD holds the section. - **NIT 4, a test gap.** No row covered the case-variant branch's stage line where HEAD holds no section of the name. RV1069's probe `test_r2_stray_not_in_head` is that row now. ## New rows: `tests/test_validate_family_index_gitmodules.py` Every row types only the printed lines and does by hand only what the text says in words, then makes a plain `git commit`. The gate and a plain clone then pass. - N1: a second section in the index's file alone. - N2: a value in a named member's section, in the index's file alone. - N2b: HEAD's file gives the value, and the index's is mended. - N2c: as N2b, with OTHER's section out of both files, so `member-gitmodules-absent` puts the index's file back first. - N3: a section HEAD holds (a stale one, or `tools/x` with its gitlink), with the file on disk, deleted, or its deletion staged. - N4: the stray `ghost` (RV1069's probe), on disk and deleted. - Fix round 1: path-twice's put-back is recorded, so `gitmodules-path-unnamed` after it prints none (RV1124's P7, on `deleted` and `deletion-staged`); and the value's url is written back from the file git reads, not HEAD's (RV1124's P10). See the fix-round section below. **Before and after.** On main's script the file gives 10 failed and 2 passed. The 2 are N4's rows, which pass there by design (NIT 4 is a test gap) and kill the case-variant mutant M8 below. On this head it gives 12 passed. After fix round 1 the file has 15 rows: on main's script it gives 13 failed and 2 passed (the 2 are N4's), and on this head 15 passed. ## Older rows changed, and why Each one pinned the behaviour #1077 names as the defect. - **"then commit" with no stage line (NIT 3).** - `SECTION_REMEDY` and a new `STAGE` constant (`test_validate_family_unnamed_mounts.py`), and its rows (b) and (c), plus the two-sections row. - `THEN_COMMIT` and `section_first`, which always ends with the stage line now (`test_family_unnamed_gitlink.py`), and its two-sections and control rows. - Two `removal(...)` assertions in `test_validate_family_remedies_typed.py`. - `ghost_out`, R1's replace anchor and R6 in `test_family_hand_mount_git_rm_lines.py`. - Each section in these rows is committed, so HEAD holds it: a plain commit after the old lines left it there. - **Path-twice's ending (NIT 1 and NIT 3).** `test_family_add_refusals.py`'s path-twice row ends "then stage and commit": the commit records the index's file, not the work tree's hand edit. - **HEAD's file put back where git reads the index's (NIT 2).** - `taken_out(put_back=...)` and the two gone-file value rows in `test_family_gitmodules_values.py`. These now read `HERE_AND_HEAD`; one is renamed `..._put_it_back_once`, since its old name said HEAD's file is put back. - The first line of `test_round1_nit_6` in `test_family_holder_remedies_compose.py`. - The value row in `test_family_holder_bootstrap_texts.py`, renamed `test_a_value_finding_that_put_the_file_back_is_not_put_back_over`. - These are now `git checkout -- .gitmodules`, the index's file. - **#1036's R5 control (`test_1036_control_a_value_git_dies_on_keeps_what_it_read`).** - Round 1 put back HEAD's file, which dropped IRQS's section that only the index's file gave. - The value's lines put back the index's file now, so the commit after round 1 records that section, as it does with the file on disk. The next run names it, in every mode. - The row now types round 2 as well, then a plain `git commit`; nothing is left, and the gate and a plain clone pass. - What the control pins is unchanged: where git reads a file that gives a value git dies on, `gitmodules-path-unnamed` reads nothing from it. ## Gates Every run used `/usr/bin/python3 -m pytest -p no:cacheprovider` with: - `EMAIL` set; - `GIT_ALLOW_PROTOCOL`, `GIT_SSH_COMMAND` and the `LANES_*` variables unset; - an empty `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_NOSYSTEM=1`; - test author and committer identities; - a stub `gh` first on PATH, `GH_TOKEN=bogus` and an empty `GH_CONFIG_DIR`. - **Targeted set: 78 files.** - The files that name `gitmodules-path-twice`, `gitmodules-value-refused`, `gitmodules-path-unnamed` or `validate-family` in the head's tree, plus every test file that imports from a test module this changes. - This includes `test_pinned_copy_text`, `test_repo_hygiene`, `test_cli_reference` and `test_agents_md_codes`. - Result: 0 failed in all 78 files, 4390 passed and 34 skipped. - Shards 0 to 4 and 6 to 9 (70 files) were run at `0cd1c3f5`, the three WIP commits this head squashes (the same tree, and the same sha256 of the script): 3379 passed, 25 skipped. - Shard 5 (8 files) was run last, at this head (`672845cc`): 1011 passed, 9 skipped, 0 failed. Two earlier runs of it were cut by checkpoints, at 49% and at 28%, with 0 failures both times. Its files are `test_doctor_legs_members_next.py`, `test_family_add_spelling_leftovers.py`, `test_family_docs_one_spelling.py`, `test_family_makefile_siblings.py`, `test_family_remove_gitmodules.py`, `test_shape_doctor.py`, `test_update_shape_kept_helper.py` and `test_validate_family_unreadable.py`. - The PR's CI `tests` job runs the full suite. - **Older rows.** The 33 rows the change moved, which failed on the new script before their update, now pass with the new rows file (45 passed). ## Mutants Each mutant was run against the new rows file and the older rows that pin the changed paths. The file was restored after each one, and its sha256 checked against the head's. | Mutant | Rows that failed | |---|---| | M1: the value check reads the work tree's file alone | 8 (N2, N2b, N2c, two gitmodules_values rows, round1 NIT 6, the bootstrap_texts row, R5) | | M2: path-twice reads the work tree's file alone | 1 (N1) | | M3: no "commit the index's" branch where HEAD's alone gives the value | 2 (N2b, N2c) | | M4: the value's put-back is HEAD's, as before | 6 | | M5: the value's text does not say to stage the file | 1 (N2) | | M6: no stage line in `members/` | 7 | | M7: no stage line outside `members/` | 3 | | M8: no stage line in the case-variant branch (NIT 4) | 2 (N4 on disk, deleted) | | M9: "then commit" where HEAD holds the section | 10 | | M10: path-twice names no put-back | 1 (N1) | | M11: path-twice ends "then commit" | 3 | | M12: `member-gitmodules-absent` puts nothing back after any value finding | 1 (N2c) | | M13: the file git reads is asked before HEAD's | 3 (the without_git rows) | ## Merge-trees `git merge-tree --write-tree` against `origin/main` 823624d is clean, and was clean against 6da757c, the base the gates ran on. Main has since taken #1101 and #1102; neither touches a file of this PR. No open PR touches these files. #1109, the closest in subject, merges clean too. ## Follow-up, not in this PR RV1124 measured the suspected counterpart of NIT 3 (its N1) and two more older defects (N2 and N6). All three are older than this PR and self-correcting or known limits; the lane files them as the follow-up #1150, and none is fixed here. - N1: the work tree's `.gitmodules` is on disk, and both it and the index's give a value HEAD's does not. The value's lines say nothing of staging, so a plain `git commit` after them records the index's value; the next run names it in HEAD. Main prints the same text and gives the same result. - N2: an index blob with a line git cannot parse after a value is `family ok` (`_blob_listed` returns nothing on git's exit 128). - N6: an unmerged `.gitmodules` (stages 1 to 3), which v1.1.1's release notes list as a known limit. ## Fix round 1: RV1124's review of `672845cc` (S1, N3, N4, N5) RV1124's review of this PR at `672845cc` found one SHOULD and six NITs. This round fixes S1, N3, N4 and N5, in one commit on top of it (`8dfa32bd`), with no amend and no force. N1, N2 and N6 are left to the follow-up #1150. - **S1 (SHOULD): #1111's `test_path_guard_hygiene.py` failed the new guard.** `_listed_as_git_reads` opened with a bare `os.path.lexists(root / GITMODULES)`. Its one caller, `_check_gitmodules_values`, asks it only where the work tree's file is gone (`if present else _listed_as_git_reads(root)`), and `_gitmodules_put_back`, which the function calls next, asks the same `lexists` and is on the test's allowlist. The guard is dropped; the function is `spec = _gitmodules_put_back(root)[0]` and the blob read. On the scratch merge of main `038dafba9` and this head, `test_path_guard_hygiene.py` gives 49 passed, with no failure for this file and no stale `BENIGN` entry. - **N3: two behaviours had no row.** Mutants A and B survived all 439 rows of the PR's nine files. Two rows, written from RV1124's probes P10 and P7: - `test_1077_a_url_git_dies_on_is_written_back_as_the_file_git_reads_gave_it` (P10): the index's file gives a url that is not HEAD's, then a `url` with no value; the line that writes the url back takes it from the index's file. - `test_1077_path_twice_records_its_put_back_for_the_next_finding` (P7, on `deleted` and `deletion-staged`): HEAD's file holds `second` and a stale `members/Old`; path-twice's put-back is recorded, so `gitmodules-path-unnamed` after it prints none, and typed in order then a plain `git commit`, HEAD loses both. - **N4: path-twice's wording.** "back from HEAD's, as the index holds none first" put "first" on the wrong phrase. It says "back first from {whose}, `{put_back}`" now. Only `TWICE_PUT_BACK` in the new rows file pinned the old phrase. - **N5: a stale docstring.** `_gitlink_out_first` said its text ends "then commit .gitmodules"; it now says it ends as `_section_then` does, "then stage and commit" or "then stage". **Rows.** The new rows file has 15 rows. On main's script (`6da757cb`) it gives 13 failed and 2 passed (the 2 are N4's, by design). On `672845cc` it gives 14 passed and 1 failed: the first row (`test_1077_a_second_section_the_index_alone_gives_is_path_twice`) fails on N4's old wording. The P7 and P10 rows pass on `672845cc`, whose code was right; they kill the mutants below. On this head, 15 passed. **Mutants.** Each ran in its own worktree over the new rows file, and the script was restored after each and its sha256 checked. | Mutant | Rows that failed | |---|---| | A: the value's write-back from HEAD's where the work tree's file is gone | 1 (the P10 row: the last line wrote the HEAD url, not the staged one) | | B: path-twice names its put-back without recording it (`_file_put_back(root, None)`) | 2 (the P7 rows: `gitmodules-path-unnamed` printed the put-back again) | **Gate.** On a scratch merge of main `038dafba9` and `2d4cb64b`, whose tree is this head's: the usual environment (`EMAIL`, an empty `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_NOSYSTEM=1`, test identities, a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`). 0 failed, 3458 passed and 27 skipped in all. - `test_path_guard_hygiene.py`: 49 passed. - The 64 files of every `tests/test_family*.py` and `tests/test_validate_family*.py` (the PR's nine among them), plus `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_pinned_copy_text.py` and `test_doctor_validator_rows.py`: 2964 passed and 24 skipped, in four shards (685; 997 and 20 skipped; 557 and 4 skipped; 725). Those four shards were cut by a checkpoint and re-run on the same merge with the same lists. - `test_shape_doctor.py`, which runs `validate-family.py`, in 8 shards by test id, each list non-empty: 445 passed and 3 skipped. **Merge-trees.** `git merge-tree --write-tree origin/main HEAD` at `8dfa32bd` against main `038dafba9` is clean (tree `23c7194f0b6a08f56b93bdc80a05924e90e703a4`). Closes #1077 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Closes #1036
The fault
gitmodules-path-unnamedread its sections from the work tree's.gitmodulesalone. Where that file is gone from disk, git's submodulecommands read the index's, then HEAD's, so a section only the index's file
gives was never named. In RV1025's state on #1025 (
git submodule addatmembers/IRQS, not committed, then the work tree's.gitmodulesdeleted,MD .gitmodules), every printed line exited 0, the gate passed in everymode, and the commit of what was staged recorded IRQS's section in HEAD's
.gitmodules. A clone of that commit then failed the gate.Measured on git 2.43.0, isolated config:
git submodule status members/IRQSmaps the path with the work tree'sfile gone, though HEAD's file does not give it: git reads the index's.
git config -f .gitmodules --unset-all ...with no file on disk exits 5and makes no file, so a section's lines need the file put back first.
git checkout -- .gitmodules, then both--unset-alllines, leave thefile byte-identical to HEAD's; staged, nothing is left to commit.
The change (the issue's first option)
templates/family-root/scripts/validate-family.py:gitmodules-path-unnamedreads_sections_as_git_reads: the work tree'sfile where it is on disk, else the one git reads in its place, the
index's then HEAD's (
_gitmodules_put_back)._given_as_git_reads,which validate-family.py (family root): after #889, three hand-mount
git 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's hand-mount put-back asks, now derives from the same reading(
_blob_sectionsunder_blob_paths), unchanged in what it answers, sothe two agree on one file. Every other reader of
.gitmodules(
gitmodules-path-twice, the value check, the staged removal,_init_stop_said) is as it was..gitmodulesgives a value git dies on: that finding's linesput HEAD's file back and stage it, which drops a section only the index's
file gave, so the section's
--unset-allmet a key the file did not hold(exit 5). It reads what it read before there.
_file_put_back, so one put-back serves every finding and none is putback over what the lines between wrote. The line comes right before the
keys' lines and the text says so: "after the line that puts .gitmodules,
gone from the work tree, back from the index's, so that every other
section stays". For a gitlink outside
members/, itsgit rm --cached,which needs no file on disk, stays first.
name taken out, as after a
git submodule addnot committed, everybranch ends "then stage .gitmodules: HEAD's holds no section of that
name, so nothing of it is left to commit" (
_section_then), the wordsthe branch outside
members/already said, and printsgit add -- .gitmodulesafter the keys' lines. Before, themembers/andcase-variant branches said "then commit .gitmodules": the index's file
kept the section, a
git commitrecorded it, andgit commit -- .gitmoduleshad nothing to commit. Where HEAD's holds the section, thetext still ends "then commit .gitmodules".
Rows
At the end of
tests/test_family_hand_mount_git_rm_lines.py, each typedin order, then committed, ending with the gate passing in every mode and a
plain clone checking both members out:
test_1036_a_section_the_index_alone_gives_comes_out_of_the_file_put_back:the issue's state. Only the printed lines typed, then
git commitofwhat is staged, the issue's Verify: HEAD's
.gitmodulesgives the twomembers alone. It fails on 4c0c52d and on round 1's head ab8ed9e, both
leaving
members/IRQSin HEAD's file.test_1036_outside_members_the_gitlink_goes_first_then_the_put_back: thesame at tools/x, typed the same way; the gate passed on 4c0c52d.
test_1036_a_stray_section_comes_out_of_the_file_put_back, deleted anddeletion staged: a section giving
Members/IRRS; the gate passed on4c0c52d.
test_1036_one_put_back_serves_two_sections: two sections, one put-back.test_1036_a_hand_mount_s_section_under_its_plain_git_rm_prints_nothing,deleted and deletion staged: the section is named, with no command, beside
validate-family.py (family root): after #889, three hand-mount
git 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's put-back and plaingit rm.test_1036_control_a_value_git_dies_on_keeps_what_it_read: the control,the same on 4c0c52d and here.
Two older rows move with the change, for the reviewer
Both in
tests/test_family_holder_remedies_compose.py, each still typedthrough to a passing gate:
test_c1_one_put_back_serves_the_move_and_a_url_finding_after_it, bothcases. With the work tree's file gone, HEAD's section "members/IRRS"
gives
Members/IRRS. The--no-membersrun now names that sectionbetween the move and the url finding, with the existing "recorded in the
wrong case, so no command is printed" advice, which the same state
already prints with the file on disk. No line is added.
test_rv882_should_2_a_gitlink_the_file_git_reads_gives_is_no_finding,both cases.
git submodule addat tools/x, committed, then the filegone: the row said the gate passes. With the file on disk the same state
is
gitmodules-path-unnamed(thetools-xform oftests/test_family_unnamed_gitlink.py), so that pass was this issue'sfault outside
members/. SHOULD 2's own point stands, nogitlink-unnamed; the row now expects the section's finding, itsgitlink out first, then the put-back, then the keys.
Gates
Targeted set and trio (
test_family_hand_mount_git_rm_lines,test_validate_family_unnamed_mounts,test_family_head_lost_gitmodules,test_validate_family_typed_states,test_validate_family_remedies_typed,test_validate_family_unreadable,test_family_holder_remedies_compose,test_family_remove_gitmodules,test_pinned_copy_text,test_repo_hygiene,test_cli_reference,test_agents_md_codes), everyrun with
LANES_*,GIT_SSH_COMMANDandGIT_ALLOW_PROTOCOLunset, astub
ghandEMAILset:tests/test_family_unnamed_gitlink.pytooThe branch was stacked on #1025 and brought onto main by three merges:
#1025's final head 73a1d29, its squash 6476c17 (ours for #1025's own
files), then main 00f6d7f. Against main, round 1 changed the three
files above alone, each with the same added and removed lines, and the
same patch-id, as the change on 4c0c52d. Round 2 (8d14a20) adds
tests/test_family_unnamed_gitlink.py, whosesection_firstexpects thestage line. Main has since moved to 5e539a8, which touches none of the
four files.
Mutants
One at a time, each run over the twelve cases of the rows above and the
two moved rows, each restored and checked by sha256. All killed:
members/git rm --cachedoutsidemembers/Round 2, over the four #1036 rows that print a put-back or a stage line
and the tools/x
-frows oftests/test_family_unnamed_gitlink.py:section-added-not-committed-frows; R1 leavesmembers/IRQSin HEAD's filegit addfirst stages the file's deletion, and the put-back then fails (pathspec '.gitmodules' did not match)What this leaves
what it read before, so a stale section HEAD's file gives is named on
the run after the value's lines are committed.
gitmodules-path-twicereads the work tree's filealone) and NIT 2 (the value check never reads the index's file) behave
as on main and are left to follow-ups.
Lane: openRepoShape-4
🤖 Generated with Claude Code
Summary by Sourcery
Fix family validation so missing work-tree
.gitmodulesfiles are handled like Git handles them and staged submodule sections cannot bypass validation.Bug Fixes:
gitmodules-path-unnamedvalidates the.gitmodulescontent Git actually reads, including the index or HEAD when the work-tree file is missing..gitmodulesfile before removing section keys and stage it when necessary, preventing staged submodule sections from being accidentally committed.Enhancements:
.gitmodulesrestoration consistent across findings and preserve the correct remediation order for gitlinks and section cleanup.Tests:
.gitmodulesfiles, index-only sections, stray and case-variant sections, multiple findings sharing one restoration, and related remediation scenarios.