Repository navigation
bump-leg.py: the leg's fetch leaves out every refspec of the leg's own that can bring a tag under origin/, and one onto the branches' names from another namespace, by the rule family.py bump asks, now one function both tools import, so a branch and a tag of one name no longer fail it and a tag no longer answers for a branch (#1037) - #1068
Conversation
…n that can bring a tag under `origin/`, and one onto the branches' names from another namespace, by the rule `family.py bump` asks, now one function both tools import, so a branch and a tag of one name no longer fail it and a tag no longer answers for a branch (#1037) `bump-leg.py` fetches the leg's remote with `+refs/heads/*:refs/remotes/origin/*` and the leg's own refspecs that write under `refs/remotes/origin/` (#898), so that `--prune` keeps what they keep, and then asks `on_a_remote_branch`, which reads every ref there. `_leg_fetch_refspecs` left out only a destination outside `origin/`. #946 and #1027 gave `family.py`'s `_origin_fetch_refspecs` two more clauses, a source that can name a tag and the branches' glob from a source not under `refs/heads/`, and the leg's fetch had neither. Lane 4's RV1027 asked for this issue. THE RULE IS NOW ONE FUNCTION. `shape_materialize.py` gains `origin_fetch_refspecs(configured)`, with `ORIGIN_BRANCHES`, `ORIGIN_BRANCH_NAMES`, `TAGS_PREFIX` and `can_name_a_tag`, moved from `family.py` with their docstrings: skip a negative refspec, force a positive one, leave out by the three clauses in #1027's order, and keep the rest once each. `family.py`'s `_origin_fetch_refspecs` and `bump-leg.py`'s `_leg_fetch_refspecs` each read `remote.origin.fetch` their own way, as before, and return what the shared function returns. - It lives in `shape_materialize.py`, as `readable_branch` and the trailer helpers do: both tools already import it, and it is in no copy list. `repo_shape.py` is a shape copy every root digest-pins, so a function added there would be an `upstream-changed` row for every project. `bump-leg.py` does not import `family.py`. - `family.py` loses `_ORIGIN_BRANCHES`, `_ORIGIN_BRANCH_NAMES`, `_TAGS_PREFIX`, `_can_name_a_tag` and the loop; its docstring points at the shared one for the clauses. Its behaviour does not change: `test_family_bump_fetch_refspecs.py` passes as it did. - `bump-leg.py` loses `LEG_BRANCHES` and the loop. The leg's reading is kept as it was: `git config --list` through `run`, a line at a time. #946's U184 for legs (RV946's N7) is not in scope. - The stale docstring is fixed: "`family.py` shares both; #905 holds them" is gone, with the tags-into-`origin/` case it named, and the docstring says what is left out now and why. `fetch_leg`'s prune and `--refmap=` paragraphs say a refspec left out has its refs pruned until the leg's own next `git fetch origin`. MEASURED ON MAIN, git 2.43, by the new test file, at `1a47ed07e` and again at `eee089cae`, the base this branch is rebased on: - With a branch and a tag of the name a tag-capable refspec writes, the leg's fetch exited 128 ("Cannot fetch both"), where the leg's own `git fetch origin` exits 0, and both runs gave the failed-fetch note. - With no name shared, the tag under `origin/` answered for a branch, and a commit only a tag of the remote reaches was pinned. - A short name gave two answers by turns. A fetch that finds the ref the short name wrote prunes it, as the remote has no ref of that very name, and the fetch after it writes it back. So the dry run pinned the commit and the real run refused it. A probe of three fetches in a row showed the ref written, then deleted, then written. tests/test_bump_leg_fetch_refspecs.py is new, 38 rows: - `TAGS_UNDER_ORIGIN` holds the issue's five refspecs: the tags onto the branches' names and in a folder, a mirror into `origin/` and in a folder, and a short name. Each runs end to end, both runs, through the leg's `origin` and through `--local-remote-dir`, with no ref of the leg moved: - the clash row fetches with no note and pins; - the reachability row refuses `bump-leg-commit-local-only` in the words for a commit only a tag of the remote reaches. - `WHICH_CLAUSE` is `test_family_bump_fetch_refspecs.py`'s own 18-row table, imported and asked of `_leg_fetch_refspecs` in process, so one table holds both tools to one rule. Its comment there follows the rename. On main's three scripts the new file gives 32 failed and 6 passed: all 20 end-to-end rows, and the 12 table rows a tag or the branches' glob leaves out. With the family refspec file beside it, the run is 32 failed and 39 passed on main and 71 passed here. The targeted runs at this head all passed: - `-k bump` over the 11 `tests/test_bump_leg*.py`: 339 passed, 1 skipped; - `-k bump` over the 45 `tests/test_family*.py`: 277 passed; - the new file, `test_family_bump_fetch_refspecs.py`, `test_no_auto_maintenance.py`, `test_repo_hygiene.py`, `test_cli_reference.py`, `test_agents_md_codes.py` and `test_family_dry_run.py`: 362 passed. Fourteen mutants, each run over the new file, the family refspec file and `test_bump_leg_reachability_words.py` (105 tests), then restored and checked by sha256. Eleven are caught: - forcing removed: 3 fail; - clause 1, a destination outside `origin/`, removed: 7; - clause 2, a source that can name a tag, removed: 34; - clause 3, the branches' glob, removed: 2; - `can_name_a_tag`'s `refs/tags/` check removed: 2; - its pattern check removed: 10; - its pattern check made True for every pattern: 10; - its no-pattern check made False: 12; - its no-pattern check made True: 4; - `bump-leg.py` passing none of the leg's own refspecs: 8; - `family.py` passing none of the mount's: 6. Three survive: - The negative-refspec skip removed is equivalent. Git refuses a negative refspec that has a destination, "invalid refspec", exit 128, in the repository's own fetch too, so clause 1 leaves out every one git takes. - Dedupe removed is equivalent at the fetch. Git takes two identical refspecs, exit 0, and writes the same refs. - Clause 3's guard removed survives, and is #1066 (below). Limits: - #1066: clause 3's guard keeps a refspec onto the branches' glob from a folder of `refs/heads/`: `+refs/heads/release/*:refs/remotes/origin/*`. Where the remote has `x` and `release/x`, the fetch exits 128 ("Cannot fetch both refs/heads/x and refs/heads/release/x to refs/remotes/origin/x"), where the repository's own fetch exits 0 (measured, git 2.43). This is #946's rule, unchanged here, and now both tools have it. The mutant that drops the guard survives. - The leg's config is still read a line at a time (#946's U184 for legs). - What #1027 lists of its own rule holds for both tools now. A full name that is no pattern can still bring a tag. Every short name is left out. A non-tag refspec in a folder can still clash with a branch. Closes #1037 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR extracts the origin fetch-refspec filtering rule into Sequence diagram for safe branch-answer fetchingsequenceDiagram
participant Tool as family.py or bump-leg.py
participant Shared as shape_materialize.py
participant Git as git fetch origin
participant Refs as origin tracking refs
Tool->>Tool: Read remote.origin.fetch
Tool->>Shared: origin_fetch_refspecs(configured)
Shared-->>Tool: Branch refspec plus safe configured refspecs
Tool->>Git: Fetch with --prune and filtered refspecs
Git->>Refs: Update branch refs and retained non-tag refs
Git-->>Tool: Branch reachability reflects branches only
Flow diagram for origin fetch-refspec filteringflowchart TD
Start[Configured remote.origin.fetch] --> Normalize[Normalize positive refspecs\nand force them]
Normalize --> Destination{Destination under\nrefs/remotes/origin/?}
Destination -->|No| Drop[Leave out]
Destination -->|Yes| Tag{"can_name_a_tag(source)?"}
Tag -->|Yes| Drop
Tag -->|No| Names{Maps another namespace\nonto branch names?}
Names -->|Yes| Drop
Names -->|No| Keep[Keep refspec]
Keep --> Result[Add to fetch with\nORIGIN_BRANCHES]
Normalize --> Negative{Negative refspec?}
Negative -->|Yes| Drop
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-2, 2026-10-20T17:59:11Z) Independent review of #1068 (#1037) at Findings (no MUST, no SHOULD)
Checked and true
Could not verify
Lane: openreposhape-2 (openRepoShape-2) |
|
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 23 hours by commenting |
|
…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>
…y refspec of the repository's own onto the branches' names, whatever its source, and every one that puts a branch under a name not its own, so `family.py bump` and `bump-leg.py` no longer fail where the remote has `x` and `release/x` and the repository's own `git fetch origin` works (#1066) (#1087) Both tools fetch `+refs/heads/*:refs/remotes/origin/*` and the repository's own refspecs that write under `refs/remotes/origin/`, as `origin_fetch_refspecs` picks them (#1068). Its third clause, from #946, left out a refspec onto `refs/remotes/origin/*` only where its source is not under `refs/heads/`. A plain `refs/heads/*` refspec is the first refspec itself, so that guard only ever carried a folder of branches onto the branches' names, such as `+refs/heads/release/*:refs/remotes/origin/*`. Where the remote has `x` and `release/x`, the fetch exited 128 ("Cannot fetch both refs/heads/x and refs/heads/release/x to refs/remotes/origin/x") where `git fetch origin` exits 0. Both runs then said the fetch had failed and answered from stale refs. THE RULE. A configured refspec rides beside the first one only where git cannot put two refs on one name through it: - THE BRANCHES' GLOB (clause 3) leaves out every refspec onto `refs/remotes/origin/*`, whatever its source. The guard is gone. - A BRANCH UNDER ANOTHER NAME (clause 4, new, `gives_a_branch_another_name`) leaves out a source under `refs/heads/` whose destination is not `refs/remotes/origin/` and the source's own part after `refs/heads/`. That covers one branch onto another's name, every branch into a folder, and a folder into another folder. Each one fails the same way wherever the remote has the two branches. - One that gives each branch its own name, a branch (`+refs/heads/main:refs/remotes/origin/main`), a folder (`+refs/heads/release/*:refs/remotes/origin/release/*`) or a pattern, maps every ref where the first refspec does, so it still rides along. - Clauses 1 and 2 (outside `origin/`; a source that can name a tag) are unchanged, so #1009's and #1037's tag rules hold exactly. - Nothing is lost to the answer. The first refspec brings every branch under its own name, and a left-out refspec only ever named branches. Measured on git 2.43 by a probe of 31 cases, each comparing the repository's own `git fetch origin` with the tools' fetch on main's rule and on this one, through `origin` and through a path: - The issue's case, its unforced form, the same beside a negative refspec, one branch onto another's, `x` onto `release/x`, every branch into a folder and a folder into another: the repository's own fetch exits 0. Main's fetch exits 128 ("Cannot fetch both"). This fetch exits 0, with every branch's remote-tracking ref at that branch. - A folder of branches under its own name, a pattern, one branch under its own name, the glob itself, forced or not, negative refspecs and every tag case #1009 and #1037 settled: the same refspecs carried and the same exits on main and here. - The cost is the one #905 accepted for the tags onto the branches' names. A ref the repository kept under another name holds the branch of that name, or is pruned, until its own next fetch writes it back. A refspec written without `+` is refused there as non-fast-forward where the two commits are not in line, exit 1, as the tags onto the branches' names are on main (measured, both). - A differential over 21,487 single and paired refspecs: this rule never carries a refspec main's does not. Each one it newly leaves out has a source under `refs/heads/` and a destination that is not the branch's own name. Docstrings: `origin_fetch_refspecs` names four clauses and what can still clash. `bump-leg.py`'s `_leg_fetch_refspecs` says how the leg's fetch can still fail, and that #1066's way no longer does (#1068's review, NIT 2). Its module docstring and `fetch_leg`'s prune and `--refmap=` paragraphs follow, as does `family.py`'s `_origin_fetch_refspecs`. Tests: - `BRANCH_UNDER_ANOTHER_NAME` (in tests/test_family_bump_fetch_refspecs.py) holds three refspecs: the issue's, one branch onto another's, and every branch into a folder. Each runs end to end through `family.py bump` (both runs) and through `bump-leg.py` (both runs, through `origin` and through `--local-remote-dir`). Each proves the clash first, then that both runs print no note, pin the commit and move no ref. - `WHICH_CLAUSE`, the table both tools' files ask, goes from 18 to 29 rows. `outside-origin-prefix` (NIT 1) has a pull request for its source, so only clause 1's `/` can leave it out. Four rows are THE BRANCHES' GLOB's, four are the new clause's, one is a negative refspec, and two more are carried. `carried-branches-in-a-folder` (`refs/heads/*:refs/remotes/origin/all/*`) was a #1066 case. It is now `another-name-branches-in-a-folder`, left out. - NIT 3: `only_a_tag_reaches` in tests/test_bump_leg_reachability_words.py holds the premise block both files repeated. Each file's clash premise and the leg's two-run tail are now one helper each (`clashes`, `both_runs_pin`). Before and after, over the three changed test files (136 tests): - main's scripts with these tests: 21 failed, 115 passed. The 21 are the 9 end-to-end rows (3 through `family.py bump`, 6 through `bump-leg.py`) and 6 table rows for each tool; - this head: 136 passed. Gates at this head: - A, `-k bump` over the 11 `tests/test_bump_leg*.py`: 356 passed, 1 skipped; - B, `-k bump` over the 46 `tests/test_family*.py`: 291 passed; - C, the two refspec files, `test_no_auto_maintenance.py`, `test_repo_hygiene.py`, `test_cli_reference.py`, `test_agents_md_codes.py` and `test_family_dry_run.py`: 393 passed. Mutants, run one at a time over the three changed files, each restored and checked by sha256: - clause 1's `/` removed: 2 fail (`outside-origin-prefix`, both tools); - clause 2 removed: 34; - clause 3 removed: 2; - #946's guard put back on clause 3: 0. This one is equivalent: clause 4 then leaves the same refspecs out, and the only branch source onto the glob it lets through is the first refspec itself, which is deduped; - clause 4 removed: 14; - clause 4's `refs/heads/` test removed: 9; - that test inverted: 23; - its own-name test negated: 21; - every branch source left out: 7; - the own-name test off by one: 7. Four were not run, stopped at a usage checkpoint: clause 4 reduced to the glob alone, the negative skip removed, forcing removed, and dedupe removed. Limit: a refspec carried into a folder of its own from another namespace, such as pull requests in `origin/pr/`, still fails the fetch where the remote has a branch whose name falls in that folder (`pr/7` beside pull request 7). The repository's own fetch fails there too wherever it has the branches' glob, as a clone's default refspec is. Measured: with `main` alone plus pull requests, its own fetch exits 0 and this one exits 128, on main and here. Leaving such a refspec out would prune what it keeps on every run, which #686 carries it to keep. #1027 already listed this limit. Closes #1066 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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>
… holds without its commit is one it cannot answer for, `check` says a commit it cannot ask was not asked, and a pinned product YAML 1.1 or 1.2 reads as no text is quoted (#912) (#1090) Each pin question is answered from the commit actually pinned, with words that say why, and a pinned product is written so that any YAML reader reads it back as text. This is #912, which groups three lines of #457's release-commit triage: L1-81, L1-82's second case, and L2-14. One commit, on main `5e539a87`. It changes `scripts/shape_materialize.py`, `adopt-project.py` and `scripts/bump-leg.py`, and six test files. The issue's line numbers are at `5017830`. Each claim was measured again by name, on main `6c861635` first and again on `5e539a87`: - **L1-81 held.** `commit_in_clone` (`scripts/shape_materialize.py:3650` at `6c861635`, `:3809` at `5e539a87`) asks `rev-parse -q --verify <id>^{}` of an annotated tag. In a clone that holds the tag but not its commit, git answers `error: missing object referenced by '<tag>'` (git 2.43 and 2.47.1), and the id was refused as `is an annotated tag that names no commit`. `bump-leg.py`'s `peeled_target` (`:972`, now `:977`) did the same. - **L1-82's second case held.** `_recomputed_pin_finding` (`adopt-project.py:872`, now `:871`) asks a recorded `commit:` what it names only of a recorded clone that is still at its path and is no partial clone. Where the clone is gone or partial, or `gh api` read the digest, `check` exits 0 on a tag's id with a NOTE that names only the digest. - **L2-14 held, in a new place.** `_neutral_pin_entry` is no longer `adopt-project.py`'s own. Since #924 both tools call `shape_materialize.neutral_pin_entry` (`:524`), and it still asked only the project's reader. Its docstring even said that a bare `yes`, `on`, `.5` or `.inf` stays bare, naming this issue. - **Main moved under the change** while it was being tested. The commit was rebased from `6c861635` onto `5e539a87` with no conflict, and its patch is the same (`git patch-id --stable` `427e9e35fd2464cb` both times): - #1068 (`6797ca70`) changes `bump-leg.py`'s `_leg_fetch_refspecs` and `fetch_leg`'s docstring, and moves a refspec helper into `shape_materialize.py`. It does not touch `peeled_target`, and tag following under `refs/tags/`, which the new bump row relies on, is unchanged. - #1064 (`5e539a87`) changes `shape_materialize.py`'s manifest readers and `adopt-project.py`'s `_manifest_findings` and one import. It does not touch `commit_in_clone`, `neutral_pin_entry` or `_pin_digest_findings`. - Every row and every mutant below was run again on the rebased head, and the new rows were run again on `5e539a87`'s code. ### L1-81: a tag whose commit the clone lacks is a clone that cannot answer - `commit_in_clone` returns `(oid, "")` where a tag's peel cannot be read (`names is None`), as it already does for an id the clone does not hold. `plan` and the scaffold then read the digest there. `git ls-tree` finds no tree at the id, so they refuse it `pin-source-unreadable`, whose remediation says to point `--pin-source` at a clone that HAS the pinned commit, "`git fetch --all` first". That is what main said before #651. `check` says `plan-pin-source-unreadable`. - `peeled_target` returns the id as typed there. The reachability verdict answers it, as it answers an id the leg does not hold: `bump-leg-commit-not-on-remote`, "<repository> has no branch containing <id>", from both runs. - A tag of a tree or a blob, which the clone can peel, is still `pin-not-a-commit` or `bump-leg-target-not-a-commit` by name. ### L1-82: `check` says it did not ask a recorded commit it could not ask - The forge is not asked. `check` and `execute` read no network for a pin (#273's review S3, #449), `execute --local-remote-dir` needs no `gh`, and the rows that hold that (`gh` never run) stay. In a partial clone, git would fetch an id it lacks from the promisor remote. - So the NOTE says it, in the words the issue's second option asks for. It was: `NOTE <entry>: digest NOT rechecked here: <why>. The root's own `validate-pins.py` recomputes it wherever it can read the product.` It is now: `NOTE <entry>: digest NOT rechecked here, nor its `commit:` asked whether it names a commit: <why>. The root's own `validate-pins.py` recomputes the digest wherever it can read the product; `plan` asks what the commit names where it reads the digest.` - `<why>` is unchanged for each of the four cases: gone, partial, `gh api`, and a `digest_source:` that names no clone. No refusal code changes, so `AGENTS.md`'s table is unchanged. README, `docs/` and the handbook quote neither sentence. ### L2-14: a product YAML 1.1 or 1.2 reads as no text is double-quoted - New `shape_materialize.YAML_NOT_TEXT_RE`. Of the plain scalars `PLAIN_SCALAR_RE` takes, which begin with a letter, `_` or `.`, it matches the ones YAML 1.1 or YAML 1.2's core schema resolves to a bool, a null or a float: - YAML 1.1's bools `y`, `n`, `yes`, `no`, `on` and `off`, and both versions' `true` and `false`, in their three cases; - `null` in its three cases; - `.inf` and `.nan` in their three cases; - a point before digits, as YAML 1.1's pattern is written (`\.[0-9.]*` with a signed exponent, so `.`, `..` and `.5.5` too); - and as its own example `685.230_15e+03` and its readers read it, with `_` among the digits (PyYAML takes `.5_` for 0.5, ruamel.yaml takes `._` for a float), with YAML 1.2's unsigned exponent `.5e3`. - Nothing else either version resolves to no text is a plain scalar `PLAIN_SCALAR_RE` takes: `~`, the empty null, `<<`, `=`, and ints, timestamps and floats that begin with a digit or a sign. - `neutral_pin_entry` writes a name bare only where it is a plain scalar, the new rule does not match it, and the project's reader reads it back. So both tools double-quote these names. Every other name is the line it was, `openGlass` byte for byte. - The plan's `product:`, which `y` writes and only this tool reads, is unchanged. The pin file's `product:` was always double-quoted. The probe is `probe912.py` (not committed). It runs with a stub `gh` that exits 97 first on PATH, a bogus token, an empty `GH_CONFIG_DIR`, `GIT_CONFIG_GLOBAL=/dev/null` and `GIT_SSH_COMMAND=false`. The `lacking` clone holds the tags `v1` and `v1-again` but not their commit. | Case | main `6c861635` and `5e539a87` | This head | | --- | --- | --- | | `commit_in_clone(lacking, <tag>)` | `(<tag>, 'is an annotated tag that names no commit')` | `(<tag>, '')` | | `peeled_target(lacking, <tag>)` | `REFUSED bump-leg-target-not-a-commit: --to <tag> is an annotated tag that names no commit in the leg` | returns `<tag>` | | scaffold `--dry-run` and `plan`, `--pin openGlass@<tag> --pin-source lacking` | exit 2 `REFUSED pin-not-a-commit: ... is an annotated tag that names no commit in the clone at <lacking>, not a commit` | exit 2 `REFUSED pin-source-unreadable: <lacking> could not answer for opensoft/openGlass @ <tag>: ... exited 128: fatal: not a tree object`, remediation "`git fetch --all` first" | | `check`, recorded clone `lacking` | exit 1 `FINDING plan-pin-not-a-commit: ... which is an annotated tag that names no commit` | exit 1 `FINDING plan-pin-source-unreadable: ... cannot answer for it` | | `check`, a tag's id, clone whole (control) | exit 1 `plan-pin-not-a-commit`, "an annotated tag of commit <c>" | the same | | `check`, a tag's id, clone partial / gone / `gh api` / none named | exit 0, NOTE names only the digest | exit 0, the NOTE above; `gh` never ran | | names `--pin` admits of up to three characters (278915) | 231714 written bare; PyYAML misreads 132 of them, ruamel.yaml 0.19 (YAML 1.2) 132 | 231543 bare; 0 misread by either | #457 counted 138 at `3dd806f`. On `6c861635`, PyYAML misread `yes`, `no`, `on` and `off` in three cases each, `.0` to `.99`, and `.0_` to `.9_`. ruamel.yaml misread the dot forms, `._`, `._0` to `._9` and `.__`, and its YAML 1.1 mode also reads `y` and `n` as bools. **Found while testing.** git 2.43's `fetch` follows a remote tag whose object is already in the store, and brings the tag's commit with it. Its tag-following check counts the tag object itself, not only the commit. So in `bump-leg.py` a tag that the leg holds without its commit is a tag the leg's remote does not advertise, or one met after a fetch that failed. The bump row uses a tag that was never pushed. New rows, in each file's style and with its helpers: - `tests/test_pin_annotated_tag.py`: - a `lacking` fixture (`git hash-object -t tag -w` of the tags' bytes); - `commit_in_clone` answers `(id, "")` (2 rows); - `plan` and the scaffold's `--dry-run` refuse `pin-source-unreadable` with no "annotated tag" in the output (4); - `check` finds `plan-pin-source-unreadable` and `execute` refuses before any leg (1); - the NOTE for a tag's id, clone gone, partial or `gh api`, with `gh` never run (3). - `tests/test_bump_leg_dry_run_truth.py`: a tag the leg holds without its commit is `bump-leg-commit-not-on-remote` in both runs, with nothing written, through `--local-remote-dir` and `origin` (2). - `tests/test_neutral_pin_entry_one_spelling.py`: - each of 25 YAML 1.1 and 1.2 words and 20 dot floats is double-quoted and reads back (45); - the same 45, with the project's reader stubbed to read everything back, are double-quoted by the rule alone (45); - 20 look-alikes (`openGlass`, `openYes`, `yEs`, `.e5`, `._.` ...) stay bare (20); - the whole enumeration, read by PyYAML, ruamel.yaml 1.2 and ruamel.yaml 1.1 where they are installed (1; skipped without either reader, so CI holds the rule by the rows above). - Changed rows, whose old expectation was the bug: - `SPELLED["yes"]`, and the entry-equals-adopt's-old-spelling row, which now expects exactly `.`, `..`, `.0`, `.9`, `._` and `yes` to differ; - `.5`, `.inf`, `yes` and `off` move out of the bare groups in `test_scaffold_pin_and_reuse.py` and `test_adopt_pins.py` into `STOCK_MISREAD`; - the NOT-rechecked constants in `test_adopt_pins.py`, and one line in `test_pin_source_top_level.py`. No assert joins conditions with `and` or `or`. **The rows fail on main.** The six test files were run on main's code, the scripts as on main with the new tests beside them: - on `6c861635`: 96 failed, 98 passed, and 16 failed, 232 passed; - on `5e539a87`: 112 failed, 330 passed. Every failure is a new or changed row: - spelling 85; - tag file 10; - top-level partial-clone NOTE 1; - `test_adopt_pins.py` 10; - bump 2; - scaffold entry 4. Each mutant was applied alone to a scratch clone of the head and its rows were run. The results were the same on `6c861635` and on the rebased head. | Mutant | Result | | --- | --- | | M1 `commit_in_clone` without the `names is None` return | killed, 3 failed | | M2 `peeled_target` without it | killed, 2 failed | | M3 `if not peeled` in place of `if names is None` | survives: equivalent. The two differ only where `rev-parse <id>^{}` prints an id and `cat-file -t` of that id fails, and rev-parse parsed that object to print it. | | M4 the NOTE's old words | killed, 3 failed | | M5 `neutral_pin_entry` without the rule | killed, 85 failed | | M6 rule without `y`/`n` | killed, 9 | | M7 without `yes`/`no`/`on`/`off` | killed, 28 | | M8 without `true`/`false`/`null` | killed, 9 (the reader-stubbed rows) | | M9 without `.inf`/`.nan` | killed, 12 | | M10 without YAML 1.1's pattern as written | killed, 9 | | M11 without the `_`-digits pattern | killed, 14 | | M12 that pattern's exponent signed only | killed, 4 | | M13 no end anchor | killed, 15 | All runs go through lane 5's `run9.sh`: a stub `gh` first on PATH, a bogus token, every `LANES_*` variable cleared, `EMAIL=service@opensoft.dev`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, and one basetemp per run. - **The six test files on the head `d5389eb6a71e`:** - git 2.43: 442 passed; - git 2.47.1 (first on PATH): 442 passed. - They also passed on `6c861635` before the rebase: 442 on 2.47.1, and on 2.43 in groups of 115, 31, 24, 149 and 123. - **git 2.43, every other test file that runs a changed script.** These are the files that name `adopt-project`, `ADOPT`, `shape_materialize`, `bump-leg` or `BUMP`, plus `test_shape_doctor.py`, `test_scaffold_leg_path_collisions.py` and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`), less the six. That is 177 test files in two shards, 179 paths with `tests/conftest.py` and `tests/gh_guard.py`: - shard a, 88 files: 6371 passed, 33 skipped (`END 2026-10-20T23:52:07Z rc=0`); - shard b, 91 files: 6296 passed, 3 skipped (`END 2026-10-20T23:51:58Z rc=0`). `git merge-tree --write-tree` of the head against origin/main and against each open PR that touches a file this changes. Each merge is clean, and no PR's added lines quote the changed words or name the changed functions: | Against | Head | Result | | --- | --- | --- | | origin/main | `5e539a878fc9` | clean, tree `95f9b53262fc` (the head's own tree) | | #1073 `l3-914-adopt-record-and-doctor` | `b58a4e2ccae0` | clean, `70f85d401ede` | | #1072 `l3-918-source-and-locale` | `2ffd82356c7b` | clean, `2e341d250ea1` | | #1071 `l3-916-adopt-test-strength` | `24273dd47e42` | clean, `0d183517f958` | | #1070 `fix/979-named-link-one-finding` | `20d2d4ba67f6` | clean, `a5beb7be1408` | | #1031 `fix/910-check-throwaway-repos` | `74c17705edc2` | clean, `324855e1983d` | | #1081 `fix/1060-pin-lane-in-process` | `7d4f2d82df4b` | clean, `fc9fe407b03b` (it touches none of these files) | Main moved after the runs above, which were made on `5e539a87`: #1071 (`bccae280`), #1076 (`c13c3985`), #1081 (`06cbd478`) and #1072 (`1e7971cd`) have landed. #1071 and #1072 touch two of this change's files, `tests/test_adopt_pins.py` and `adopt-project.py`. The head merges clean with origin/main `1e7971cd` (tree `ffb8249e`, from `git merge-tree --write-tree`), and the landing gates the predicted squash tree over the main of the day. #1068 and #1064 have landed and are under this commit. `fix/904-kept-links` was not on origin when this was pushed; it is now #1091, open at `8f5fd0fd`, and merges with this head clean (tree `0153f3e7e3c9`, measured by rv1090). #1062 touches none of these files. - rv1090, lane 5's independent reviewer, who wrote none of this: READY at `d5389eb6` (issuecomment-6316009648), with no MUST. - Its SHOULD is a missing row: no row has an exponent of two or more digits, and the head quotes those names correctly. - Its two code NITs: the no-`origin` bump refusal for a tag the leg holds, and an `else` nothing reaches. With the SHOULD, they are filed as #1103, to land after the v1.2.0 tag. - Its body NITs are fixed above: 177 test files, one `Claim:` line, and #1091's state. Review: rv1090, lane 5's independent reviewer, not the writer (w912): READY at `d5389eb6` (#1090 issuecomment-6316009648) with no MUST; its SHOULD (a missing row for exponents of two or more digits, which the head quotes correctly) and its two code NITs are filed as #1103 for after the v1.2.0 tag, and its body NITs are fixed. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 d5389eb passed tests, tests-git-bash and SonarCloud; the head 1ad7605 is a clean update-branch merge of main 7e00834, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree e182fde over main 7e00834: the adopt_pins, bump_leg_dry_run_truth, neutral_pin_entry_one_spelling, pin_annotated_tag, pin_source_top_level, scaffold_pin_and_reuse, repo_hygiene, agents_md_codes, cli_reference sets, 613 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 #912 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



What
bump-leg.py's fetch of a leg now leaves out every refspec of the leg's own that can bring a tag underrefs/remotes/origin/, and one onto the branches' names from another namespace.family.py bump's fetch of a member has done this since #946 and #1027.The rule is now ONE function,
origin_fetch_refspecsinscripts/shape_materialize.py, and both tools ask it:family.py's_origin_fetch_refspecsasks it of a member's mount.bump-leg.py's_leg_fetch_refspecsasks it of a leg.Each tool still reads
remote.origin.fetchits own way.Why one function, and why there
shape_materialize.pyalready holds what both tools share (readable_branch, the trailer helpers,run). Both import it, and it is in no copy list.repo_shape.pyis a shape copy every root digest-pins. A function added there would be anupstream-changedrow for every project, and no root fetches for a pin.bump-leg.pydoes not importfamily.py, a command-line tool of more than 10,000 lines, for one rule.bump's fetch no longer moves the mount's own branches, works where the mount maps tags intoorigin/instead of telling the person to fix the network, reads its refspecs withgit config --list -z, and asksgit remote get-url originfirst, so a mount with nooriginis not fetched from a path of that name (#905) #946 and family.py:bump's fetch leaves out every refspec of the mount's whose source can name a tag, a pattern such asrefs/*and a short name too, so a mirror in a folder,+refs/*:refs/remotes/origin/all/*, no longer fails the fetch on a branch and a tag of one name or pins a commit only a tag reaches (#1009) #1027 changed only one of them.Changes
scripts/shape_materialize.pygains a new section:ORIGIN_BRANCHES,ORIGIN_BRANCH_NAMESandTAGS_PREFIX;can_name_a_tagandorigin_fetch_refspecs, moved fromfamily.pywith their docstrings and made general.scripts/family.pyloses its private copies and asks the shared function. Its behaviour does not change: its 33-row file passes as before.scripts/bump-leg.py:LEG_BRANCHESand the loop, and asks the shared function;git config --listthroughrun);grep -n "shares both" scripts/bump-leg.pyfinds nothing;fetch_leg's prune and--refmap=paragraphs.tests/test_bump_leg_fetch_refspecs.pyis new, with 38 rows:TAGS_UNDER_ORIGINholds the issue's five refspecs, each run end to end in both runs, throughoriginand through--local-remote-dir, with no ref of the leg moved;bump-leg-commit-local-onlyin the tag words;WHICH_CLAUSEis the family file's 18-row table, imported and asked of_leg_fetch_refspecs.tests/test_family_bump_fetch_refspecs.py: the table's comment follows the rename.Measured on main (git 2.43), at
1a47ed07eand again ateee089cae, this branch's baseThe new file on main's three scripts gives 32 failed and 6 passed:
On this branch, all 38 pass. With the family refspec file beside it, the run is 32 failed and 39 passed on main, and 71 passed here.
The short-name rows also show something else. On main the dry run pinned the commit and the real run refused it. A fetch that finds the ref a short name wrote prunes it, because the remote has no ref of that very name. The next fetch writes it back. A probe of three fetches in a row showed the ref written, then deleted, then written.
Targeted runs at this head (all passed)
The pytest environment had the stub
ghfirst on PATH,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR, and everyLANES_*unset.-k bumpover the 11tests/test_bump_leg*.py-k bumpover the 45tests/test_family*.pytest_family_bump_fetch_refspecs.py,test_no_auto_maintenance.py,test_repo_hygiene.py,test_cli_reference.py,test_agents_md_codes.py,test_family_dry_run.pyMutants
Each mutant ran over the new file, the family refspec file and
test_bump_leg_reachability_words.py(105 tests), then was restored and checked by sha256.origin/) removedcan_name_a_tag) removedrefs/heads/) removedcan_name_a_tag:refs/tags/check removedcan_name_a_tag: pattern check removedcan_name_a_tag: every pattern taken as one that can name a tagcan_name_a_tag: no-pattern check made Falsecan_name_a_tag: no-pattern check made Truebump-leg.pypasses none of the leg's own refspecsfamily.pypasses none of the mount's own refspecsLimits
+refs/heads/release/*:refs/remotes/origin/*. Where the remote hasxandrelease/x, the fetch exits 128 ("Cannot fetch both refs/heads/x and refs/heads/release/x to refs/remotes/origin/x"), where the repository's own fetch exits 0 (measured, git 2.43). This is family.py:bump's fetch no longer moves the mount's own branches, works where the mount maps tags intoorigin/instead of telling the person to fix the network, reads its refspecs withgit config --list -z, and asksgit remote get-url originfirst, so a mount with nooriginis not fetched from a path of that name (#905) #946's rule, unchanged here and now shared by both tools. The mutant that drops the guard (M6) survives.bump's fetch no longer moves the mount's own branches, works where the mount maps tags intoorigin/instead of telling the person to fix the network, reads its refspecs withgit config --list -z, and asksgit remote get-url originfirst, so a mount with nooriginis not fetched from a path of that name (#905) #946's U184 for legs, RV946's N7). It is out of scope.bump's fetch leaves out every refspec of the mount's whose source can name a tag, a pattern such asrefs/*and a short name too, so a mirror in a folder,+refs/*:refs/remotes/origin/all/*, no longer fails the fetch on a branch and a tag of one name or pins a commit only a tag reaches (#1009) #1027's own limits hold for both tools: a full name that is no pattern can still bring a tag, every short name is left out, and a non-tag refspec in a folder can still clash with a branch.Closes #1037
Lane: openreposhape-4 (openRepoShape-4)
🤖 Generated with Claude Code
Summary by Sourcery
Unify origin fetch refspec filtering across family and leg bumps so tags cannot answer for branches.
Bug Fixes:
Enhancements:
Tests: