Repository navigation
validate-family.py: a hand mount's git rm line works typed: git rm --cached where a plain one refuses a staged gitlink, dies on a clone no section names or refuses what is no checkout, with what it leaves said, and a .gitmodules gone from the work tree put back before the git rm that takes a section out (#936) - #1025
Conversation
… --cached` where a plain one refuses a staged gitlink, dies on a clone no section names or refuses what is no checkout, with what it leaves said, and a `.gitmodules` gone from the work tree put back before the `git rm` that takes a section out (#936) Every `git rm` line `member-gitlink-unnamed` prints for a hand mount now works typed, and the gate passes after the commit. Based on main, where #889 landed as 702cc23. - `_hand_cached`: asked of git's records and what is on disk, as git's rm.c asks it, never of git's words. The line is `git rm --cached`, with `-f` where `_forced` says, which touches no file, where a plain `git rm` would - refuse a gitlink the index stages at a commit HEAD does not record there, over anything on disk (exit 1, either of git's two refusals, both quoted: `STAGED_AT`); - die on a clone whose `.git` is a directory that no section of the file git reads names (could not lookup name for submodule, exit 128); - refuse what is on disk and is no checkout (exit 1). `_cached_left` says what stays on disk and what to do with it before `family.py add`: move a clone out whole, delete a checkout once nothing only it holds is needed. Where HEAD records nothing at the path, nothing of it is left to commit. - `_check_gitmodules_named`: a section at such a gitlink's path prints its own `--unset-all` lines, which `--cached` leaves to it. - `_plain_rm_said`: where the work tree's `.gitmodules` is gone and the file git reads gives the hand mount's path, the file is put back first, from where git reads it, unless a finding before did, so the `git rm` takes the section out and a staged removal's put-back after it no longer brings it back. - The controls print the plain `git rm`, as before. Tests: tests/test_family_hand_mount_git_rm_lines.py (17 rows); #889's round-2 SHOULD test in tests/test_family_holder_remedies_compose.py is dropped, its states being rows here, and its helpers stay there. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… dangling link is taken out cached, so `_staged_over` reading `exists` in place of `lexists` fails a row (#936) x889c's NIT A on #889 (issuecomment-6253217474): nothing tested `_staged_over`'s `os.path.lexists` against a dangling link. Under `(root / path).exists()` the reason was lost while git still refuses the plain `git rm` (exit 1, "has staged content different from both the file and the HEAD"). The row expects `git rm --cached -f` with the staged reason; the mutant is killed (1 failed, 6 passed). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dy's own text, so the guard that every remedy naming `_switch_line` ends its prose with `BRANCH_LEFT` sees the sentence (#936) The 37-file run on 40e6e27b failed two tests in tests/test_family_switch_branch_exists.py: the guard, and the test that shows the guard is not vacuous. `_mounted_by_hand` built its lines as a list that began with `_switch_line(...)`. The guard reads the string expression a `_switch_line` call stands in, and for this call that was the list. The printed text was the same, and its prose ended with `BRANCH_LEFT`, but the guard could not see it. - `lines` now holds the lines after the switch line. The returned text writes the switch line itself, as main's did. - The helpers number their lines from the second (`_ordinal(len(lines))`), and the add is the `_ordinal(len(lines) + 1)`. - The printed text is unchanged. The 18 rows of tests/test_family_hand_mount_git_rm_lines.py and the two guard tests pass (20 passed). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…asks it, so a path or a `.git` this user cannot read is `member-unreadable` by name, never a 3.12 traceback or a false "no checkout", and a hand mount HEAD records is refused under a folder where its plain `git rm` exits 128 (#936) Moved onto main 3864b18, where #956 (slice E of #430) landed. The rebase had no textual conflict, because #956's hunks and this branch's are apart. Its rule met #936's new reads, though: tests/test_validate_family_unreadable.py failed 3 of 52 on 02bd9e1, the third commit moved: - hand-mount-folder-000 and hand-mount-committed, members/ at mode 000: `_hand_cached`'s `git.is_dir()` ended the gate in a PermissionError traceback on Python 3.12. - hand-mount-committed, a refused `stat` of members/IRQS in process: `_on_disk` refused it `member-unreadable`, exit 2, where the row expected the control's finding, exit 1. Resolved with #936's intent, every hand mount's line works typed, and #430's, a path this user cannot read is never absent: - `_hand_cached` asks the path first (`_anything_at`). Nothing there is the plain line, and a folder above it that cannot be searched is refused at the path, as `_staged_over` refuses it. "clone" asks the file git reads before the `.git`, and the `.git` with `_asked`; "no checkout" asks the `.git` with `_anything_at`. `_cached_left` asks its `.git` the same way. - A hand mount HEAD records is asked of the disk too, since whether its plain `git rm` works turns on what is there. With members/ at mode 000, git 2.43's `git rm -- members/IRQS`, the line main prints there, exits 128: "warning: failed to stat 'members/IRQS': Permission denied", "fatal: could not remove 'members/IRQS'". So r956's C19 row for it moves. `test_a_gitlink_head_records_as_staged_asks_nothing_of_the_disk` and `test_a_folder_under_a_gitlink_git_takes_is_never_asked` keep their variant row (C20). The hand mount is refused by name in `test_a_path_whose_stat_is_refused_is_refused_by_name` (hand-mount-committed) and in `test_a_folder_this_user_cannot_search_is_refused_by_name` (hand-mount-committed-folder-000, and hand-mount-committed-444 for its `.git`). - tests/test_family_hand_mount_git_rm_lines.py: two rows, a checkout and its section committed, and `git submodule add` not committed, each at mode 444. Every mode is refused by name at `<path>/.git`, and with the mode back the gate says what it said. tests/test_validate_family_unreadable.py: 53 passed. The hand mount file and tests/test_family_switch_branch_exists.py: 45 passed. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR makes every printed Sequence diagram for restoring .gitmodules before hand-mount removalsequenceDiagram
participant Validator
participant WorkTree
participant Git
participant Report
Validator->>WorkTree: _file_put_back
WorkTree-->>Validator: Restore .gitmodules from index or HEAD
Validator->>Report: Record restoration line
Validator->>Git: git rm -- members/IRQS
Git-->>WorkTree: Remove gitlink and its section
Validator->>Git: Commit removal
Validator-->>WorkTree: family.py add can remount path
State diagram for hand-mounted gitlink remediation outcomesstateDiagram-v2
[*] --> Inspect
Inspect --> PlainRemoval: _hand_cached returns None
Inspect --> CachedRemoval: clone, staged, or no checkout
Inspect --> Refused: _anything_at or _asked reports member-unreadable
PlainRemoval --> SectionRemoved: git rm -- path
CachedRemoval --> IndexOnly: git rm --cached [ -f ] -- path
IndexOnly --> DiskExplained: _cached_left
SectionRemoved --> Committed
DiskExplained --> ManualCleanup
ManualCleanup --> Committed
Committed --> [*]
Flow diagram for executable hand-mount git rm remediationflowchart TD
A[Unnamed hand-mounted gitlink] --> B{_hand_cached}
B -->|Nothing on disk| C[git rm -- path]
B -->|Clone without named section| D[git rm --cached path]
B -->|Staged over HEAD| D
B -->|No checkout on disk| D
B -->|Readable checkout with safe plain removal| C
D --> E{_forced}
E -->|Required| F[git rm --cached -f -- path]
E -->|Not required| G[git rm --cached -- path]
C --> H[Section removed from .gitmodules]
D --> I[Gitlink removed from index; disk content remains]
I --> J[_cached_left explains move or delete]
H --> K[Commit removal, then family.py add]
J --> K
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-4 RV1025, 2026-10-20T01:03:16Z) RV1025, independent review of #1025 at What I ran
The C19 change: right, and consistent with #430
Mutants (mine, one per decision; each restored and checked by sha256)
7 of 8 are killed by the PR's own rows. The body's claimsAll of these match what I measured:
No text the PR changes is pinned in
CI
ItemsSHOULD 1: the body's first "What it cannot tell" is measured now, and it is not what the body predicts.
NIT 1: the body's second "What it cannot tell" is wrong on git 2.43.
NIT 2: no row reaches
NIT 3: "Its states are now rows in the new file" (the body, on the dropped compose test) leaves one out.
Choices for the reviewerI agree with all four:
Lane: openreposhape-4 (openRepoShape-4) |
…lict, so #1025 is tested against main as it stands, with #983's change to validate-family.py and #1026's fix of main's two red rows (#936) Main took twelve squashes since e076e3e: #1026, #958, #986, #1002, #1016, #1011, #1021, #1022, #973, #975, #1015 and #983. Only #983 touches validate-family.py, in `_git_runs` and the member checks, apart from this branch's hunks; it touches none of this branch's test files. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ules` not staged puts `git add -- .gitmodules` before a hand mount's `git rm --cached`, so a stage line printed after it, where git's rm exits 128, fails a row (#936) RV1025's NIT 2 on #1025: no row reached `_staging_said` in `_cached_rm_said`, so its mutant MG, which prints the stage line after the `--cached` line, passed every row of the new file and the compose file. The state is RV1025's probe: `git mv members/IRRS members/IRRS2`, staged, then a line appended to the work tree's `.gitmodules` by hand and not staged. The row pins the order (`git add -- .gitmodules`, then `git rm --cached -f -- members/IRRS2`), the text that numbers both, and what is left, then types every line in order, deletes the moved checkout as the text says, commits, and asks the gate in every mode and a plain clone. In MG's order the `git rm --cached -f` exits 128 (`please stage your changes to .gitmodules or stash them to proceed`). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… holding U+202E or another character the launcher refuses, by code point and in the launcher's words, before it prints the value or runs `git clone` (#953) (#1043) `setup-project.py`'s self-bootstrap refuses an `$OPENREPOSHAPE_REPO` holding a character the launcher refuses (U+202E and the rest of `MISDRAWN_CHARACTERS`). It refuses by code point, in the launcher's words, before it prints the value or hands it to git. Head `c42afa30`, one commit on origin/main `5b41d44f`. Only `setup-project.py` and a new `tests/test_setup_project_repo_misdrawn.py` change. No flag, no `--help` line and no doc changes. **`setup-project.py`** (section 0, just above `self_bootstrap`): - `MISDRAWN_CHARACTERS` is the launcher's list (`openRepoShape:502` at `5b41d44f`), entry for entry, in the package's tuple form. It is a copy, for `checked_value`'s reason: self-bootstrap mode runs before there is a checkout to read the launcher from. - `misdrawn_characters(value)` names the characters a value holds as `U+XXXX`, in code point order. It is the package's `standard.misdrawn_characters`, and it looks for characters, as `CONTROL_RE` does. - `checked_repo(value)` asks that first. It refuses with the launcher's `checked_repo` sentence, which names the code points and nothing else of the value. Then it calls `checked_value(..., PATH_RE)`, unchanged, for a leading `-` and a control character. - `self_bootstrap` reads a non-empty `$OPENREPOSHAPE_REPO` through `checked_repo`. Unset or empty still means the default. The check stays where `--org` is asked, so the order is the same: `--org` first, then the repository, then `openRepoShape setup` is printed, then `mkdtemp` and `git clone`. - The value's raw bytes are never printed. The refusal goes through `die`, so it is `REFUSED: ...` on stderr with exit 2. **`tests/test_setup_project_repo_misdrawn.py`**, 22 tests: - **Parity:** `test_the_misdrawn_table_is_the_launchers_list` reads `MISDRAWN_CHARACTERS=(...)` out of `openRepoShape` and compares the tuple. It also checks that `misdrawn_characters` names all of the launcher tests' `MISDRAWN`, each once and in order. `test_misdrawn_characters_names_the_launchers_and_nothing_else` runs the launcher's `SHARING_ITS_BYTES`. These follow #947's two tests for the package. - **Direct path, 12 tests:** two runs (`Atlas --org acme`, which is the issue's run, and `--org acme` alone) times six values: U+202E, U+00AD, U+200B, U+E0001 (two, three and four UTF-8 bytes), two characters at once, and a tab before U+202E (the launcher asks for the code point before the control character). - Each test runs a lone copy of the file under `call_loggers`' logging git, gh and curl. - Each checks the launcher tests' `assert_refused_saying`: exit 2, the launcher's sentence, an empty stdout, and no listed character, escape or `forged` in either stream. So no `E2 80 AE` gets out. - The git log must hold exactly one call: the checkout probe. - **Values that still go on, 7 tests:** the launcher's `NOT_CONTROLS` after `acme/`: e-acute, a no-break space, U+200C and U+200D where a script or an emoji needs them, U+2026, U+FF21 and a subdivision flag. Each is printed in the clone line and handed to `git clone` as before. - **Package path, 1 test:** a tree with no `.git` (the package's modules under `src/openreposhape/`, plus `setup-project.py`, `scaffold-project.py`, `shape-doctor.py` and `contracts/repository-naming.yaml`). `openRepoShape Atlas --org acme` is run from that tree's `src` with the issue's value. The package prints its one line (`the standard is the checkout this package sits in, <tree>.`) and hands over. The tree's `setup-project.py` refuses: exit 2, the launcher's sentence, no U+202E and no `forged` in either stream, and only the checkout probe in the git log. - Tests that use the logging stub are skipped on Windows. There `subprocess` starts `git.exe` and never the stub's `.cmd`, so the stub would log nothing. CI is Linux only (#597). - `setup.sh` has no twin of these tests. Its guard covers control characters and the two separators and no other listed character, by #940's design: the launcher refuses the rest first where it is the way in. **"No git call", stated exactly.** The refused run makes one git call, `git -C <dir> rev-parse --show-toplevel`. That is `is_shape_checkout`, which runs before `self_bootstrap` on every run past `--help` and `--preflight`. It asks whether the file stands in a checkout, which is how the run knows it is self-bootstrapping at all, and it names nothing of the value. No git call names the value, and there is no clone. Moving the check above the probe would also refuse the value on the developer path, which never reads it. It would also leave `setup-project.py` ordered differently from `setup.sh`, which probes first and guards only on its self-bootstrap path. These run `$Q/v118/w953/probe.py`. Each case uses a fresh scratch folder, a logging git stub that then runs the real git, a gh that fails, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false` and `GIT_CONFIG_GLOBAL=/dev/null`. "Before" is `git archive 5b41d44`; "after" is the worktree. Results are the same under git 2.43.0 and 2.47.1. | Case | Before (`5b41d44f`) | After | |---|---|---| | package, ZIP tree, `Atlas --org acme`, `acme/r[U+202E]forged` | exit 128; `E2 80 AE` on stdout and stderr; git: probe, then `clone --quiet -- acme/r[U+202E]forged /tmp/openreposhape-shape-...` | exit 2; no `E2 80 AE`; git: probe only; stdout is the package's one line | | direct, `Atlas --org acme`, same value | exit 128; raw bytes in the clone line; probe + clone | exit 2; refusal names `U+202E`; probe only | | direct, `--org acme`, same value | exit 128; probe + clone | exit 2; probe only | | direct, `acme/r[U+200B]forged` | exit 128; probe + clone | exit 2; names `U+200B` | | direct, `acme/r[U+E0001]forged` | exit 128; probe + clone | exit 2; names `U+E0001` | | direct, `acme/r[U+2026]b` (not listed) | exit 128; probe + clone | the same: probe + clone, exit 128 | The "after" refusal, from the probe: ``` REFUSED: OPENREPOSHAPE_REPO holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it. This tool will not put it on a `git clone` command line. Set it to a URL or a path. ``` The new file was run on main's `setup-project.py` (`git show 5b41d44:setup-project.py`, put back byte for byte afterwards): **15 failed, 7 passed**. - Failed: both parity tests (`AttributeError`: no `MISDRAWN_CHARACTERS`, no `misdrawn_characters`), all 12 direct tests, and the package test. - Passed: the 7 `NOT_CONTROLS` tests. They pass at main by design, because they guard against a check that is too broad. Each mutant was applied alone (`$Q/v118/w953/mutate.py`), the new file was run (22 tests), and both files were put back. **All 18 are killed.** | Mutant | Failed | |---|---| | m01 check removed (main's read) | 13 | | m02a check moved after the `(0) self-bootstrap` line | 13 | | m02b check moved after the clone line | 13 | | m03a only the value's first character checked | 15 | | m03b only the table's first entry checked | 13 | | m04 only U+202E checked | 8 (U+00AD, U+200B, U+E0001, two-at-once) | | m05 refusal exits 1 | 13 | | m06a table drops `E0001` | 4 | | m06b parity compares the first five entries + table drops `E0001` | 4 (parity's second assert, `SHARING_ITS_BYTES`, both U+E0001 tests) | | m06c parity compares the first five + table adds `200C` | 2 (`SHARING_ITS_BYTES`, the Persian/U+200C test) | | m06d parity compares the first five + `2060-206F` narrowed to `206E` | 2 | | m06e parity uses set inclusion + table drops `E0001` | 4 | | m07 a range read without its last code point | 15 | | m08 control check asked before the code points | 2 (both tab-then-U+202E tests) | | m09 characters named in the value's order | 3 | | m10 every character past ASCII refused | 7 (every `NOT_CONTROLS` test) | | m11 refusal quotes the value back | 13 | | m12 check skipped on a hand-over (`OPENREPOSHAPE_SELF_BOOTSTRAP=1`) | 1 (the package test alone) | All at `c42afa30`, one pytest process at a time: `/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed`, one reused basetemp, `EMAIL=service@opensoft.dev`, a stub gh first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`. | Run | git | Files | Result | |---|---|---|---| | h1 | 2.43.0 | `test_setup_project_repo_misdrawn.py` (22), `test_setup_project_ref_parts.py` (27), `test_openreposhape_package.py` (213), `test_readme_windows_quoting_sentence.py` (44), `test_release_install_check_children.py` (30), `test_repo_hygiene.py` (100), `test_agents_md_codes.py` (47), `test_cli_reference.py` (23) | 506 passed in 94.06s | | h2 | 2.43.0 | `test_setup_project_py.py` | 340 passed in 122.68s | | h3 | 2.43.0 | `test_setup_sh.py` | 191 passed in 96.62s | | h4 | 2.47.1 | `test_setup_project_repo_misdrawn.py`, `test_setup_project_ref_parts.py`, `test_setup_project_py.py` | 389 passed in 128.13s | | h5 | 2.43.0 | `test_no_auto_maintenance.py`, `test_setup_project_dry_run_offers.py`, `test_validator_child_encoding.py`, `test_setup_project_name_edges.py`, `test_setup_project_typed_name.py` | 353 passed, 2 skipped in 73.92s (the two skips: `GIT_CONFIG_GLOBAL` was set) | No `shell_quoted` call was added (count 0), so the README's and the handbook's Windows-quoting sentences and `NAMED` are unchanged. `setup-project.py` is not a pinned copy. The wheel takes it at build time (`pyproject.toml` `force-include`), and the tests that read its source (`test_setup_project_py.py`, `test_release_install_check_children.py`, `test_repo_hygiene.py`) pass. - `git merge-tree --write-tree` of `c42afa30` with origin/main `5b41d44f` (fetched with `-c gc.auto=0 -c maintenance.auto=false`) is clean, tree `59de4c261c228274a5cff18aff953211de7002e3`. - No open PR touches `setup-project.py` or the new test file. Every open PR's head was still merged in, and all ten are clean: #1041 `4a23c6e2`, #1039 `5f924500`, #1038 `fd1de5ab`, #1031 `eea70be1`, #1030 `9b4845bb`, #1028 `0c9eac6c`, #1027 `052f1e24`, #1025 `f83ca618`, #1023 `4d0aa1cf`, #954 `dc7fd7ea`. - **RV947's companion is left out.** That companion is the package's scaffold run asking the repository even beside a checkout. - Once this lands, no package run prints the value or hands it to git. Beside a git checkout, the hand-over takes `setup-project.py`'s developer path, which never reads `$OPENREPOSHAPE_REPO`. `self_bootstrap` and `standard.py` are the only shipped Python that read it. Beside a tree with no `.git`, this check refuses it. - The companion would change `standard.prepare`, which `--doctor` and the four tools share. They would then refuse a value they never use. That is the launcher's "checked once, before every run but `--help`" policy, and it is a decision about the package of its own. - `src/openreposhape/standard.py` is also open in #1038. - **Found while measuring, not changed here:** `CONTROL_RE` and `PATH_RE` hold `\x00-\x1f\x7f` only, so a C1 control passes `checked_value`. The launcher and the package refuse C1 (U+0080 to U+009F) in a repository. With `acme/r[U+009B]31mforged`, the clone line prints the raw `C2 9B`, before and after this change. `--into` and `--local-remote-dir` go through the same `PATH_RE` (read in the code, not measured). That would be a follow-up for `checked_value`, which every flag shares. Review: rv953, lane 5's independent reviewer, not the writer (w953): READY at `c42afa30`, the head that lands (issuecomment-6284626889), with no MUST. It rules #953's Verify met in substance: the refused run's only git call is the checkout probe every run makes, which names nothing of the value, and there is no clone. Its SHOULD is #459's documented C1 known limit (U031, L4-24), left with its test NIT for a follow-up on a ruling. CI (Brett Heap's ruling 27): H0 is the head c42afa3 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 5b41d44; and the local gate passed on its predicted squash tree e146bb5 over main e656199: the setup_project_repo_misdrawn, repo_hygiene, agents_md_codes, cli_reference sets, 192 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Closes #953. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-4, 2026-10-20T09:41:13Z) RV1025: fix round 1 is pushed. Please re-check the delta
Lane: openreposhape-4 (openRepoShape-4) |
|
(lane openRepoShape-4 RV1025, 2026-10-20T09:52:56Z) RV1025, re-check of #1025 at The delta
|
| run | result |
|---|---|
this head, the five files (the four touched ones, plus #983's tests/test_validate_family_git_not_runnable.py, which now shares the script) |
212 passed (21 + 54 + 53 + 25 + 59) |
MG, applied from my own mutate.py on this head, against the new file |
1 failed, 20 passed. The failure is the new row, on its line-order assertion. |
main 3864b18c's script (sha256 ca2c40b7), against state 1's rows, the new row and the controls |
8 failed, 3 passed. The new row fails: main prints git rm -- members/IRRS2 after the stage line. The 3 controls pass. |
After each swap I put the file back with git checkout and checked it: sha256 954696d0, blob 31e4d193, the head's. The clone ends clean at 4c0c52d0.
The body
- SHOULD 1: taken. "What it cannot tell" now gives my measurement, says why (
_gitmodules_listed) and that it is not a regression, and names the cure as validate-family.py (family root):gitmodules-path-unnamedreads only the work tree's.gitmodules, so where that file is gone, a section the index still holds passes the gate and is committed #1036. - NIT 1: taken. The linked-worktree item now says that on git 2.43 the plain
git rmexits 0 and moves nothing into the holder's store. - NIT 3: taken. "The tests" now names
STAGED_STATES'nothing-on-diskas the one dropped state, andtests/test_validate_family_unnamed_mounts.py:293as where its plain line is still pinned. - Closing references:
closingIssuesReferencesis[936]. The only closing keyword in the body is the last line's, for validate-family.py (family root): after #889, three hand-mountgit rmlines still fail when typed: a gitlink staged at a commit HEAD does not record, a hand mount with its own section while.gitmodulesis gone, and a hand clone with no section (exit 128) #936. - Merge-trees of
4c0c52d0, at 09:43Z, each clean and each the body's tree:- main
dc7cd49b:43ca9045. That merge changes only the four files; - the family root's bootstrap.py: a gitlink a member's own index gives no url, or a member's commit that a fetch reaching its remote did not bring, fails the run by name with its cure and no credential advice, where it said
family bootstrap okbeside git's exit 128 (#892) #954fe907c84:aeca9f00; - the family root's bootstrap.py and siblings.py: under GNU make 4.4's named-pipe jobserver, a member's make that says it is 4.4 or later is handed the holder's
MAKEFLAGSas it is, so it runs its jobs together again; every other make still gets no jobserver and no-j(#987) #10235cf60365:d6b30c04; - validate-family.py, validate-pins.py and both bootstrap.py copies: a
gitWindows cannot execute ([WinError 193], a namelessOSError) isgit-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1, and one test holds the four copies to one answer and one text (#1001) #1028411e0d6a:05a61026.
- main
- Main's four squashes after
5b41d44f(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, adopt-project.py, shape_materialize.py and standard.py: the Free-plan hint no longer calls an internal root private, adopt's docstring says its note for an internal root with an unread plan changed in #969, andprepare's docstring has no 104-character line (#994) #1038, adopt-project.py: the split commit's message andplan's report name each path, question and rule id one way only and never raw, the history scan'sgit-failedis one line, and the table pads a name as stdout prints it (#900) #1030, setup-project.py: the self-bootstrap refuses an$OPENREPOSHAPE_REPOholding U+202E or another character the launcher refuses, by code point and in the launcher's words, before it prints the value or runsgit clone(#953) #1043) touch none of the four files. - Open PRs: of the 7 others open at 09:43Z, only validate-family.py, validate-pins.py and both bootstrap.py copies: a
gitWindows cannot execute ([WinError 193], a namelessOSError) isgit-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1, and one test holds the four copies to one answer and one text (#1001) #1028 sharesvalidate-family.py. Its hunks are at lines 337 to 354, in_git_runs, as the body says.
CI
Run 39377078862 (pull_request on 4c0c52d0, merge ref b2623136) succeeded:
tests: 18668 passed, 371 skipped in 58 minutes, read from its log;tests-git-bashandpackagepassed.
Main's two red rows are gone, since #1026 is merged in, so there is no inherited failure left to discount.
The C19 change
My ruling at 927fabf7 stands. The merge does not touch _hand_cached or _cached_left. Lane 1's r956 is still asked in the body to confirm.
NIT, new, on #1036's text (not this PR)
#1036 cites "#1025's head 760ea814" and lines :1843-1844 and :3999 there.
- The commit:
760ea814is not in validate-family.py: a hand mount'sgit rmline works typed:git rm --cachedwhere a plain one refuses a staged gitlink, dies on a clone no section names or refuses what is no checkout, with what it leaves said, and a.gitmodulesgone from the work tree put back before thegit rmthat takes a section out (#936) #1025's history (927fabf7,5958536c,4c0c52d0). It is not at any of origin's 546 refs either, so nobody can fetch it. - The lines: those numbers are
927fabf7's. On this head, and on main once this lands with validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983 in it, they are::1883-1884(_gitmodules_listed'slexists);:4039(sections = _gitmodules_sections(root)).
- What goes wrong: whoever takes validate-family.py (family root):
gitmodules-path-unnamedreads only the work tree's.gitmodules, so where that file is gone, a section the index still holds passes the gate and is committed #1036 after this lands opens the wrong lines and cannot check out the head it names. - Fix: name
4c0c52d0, or main after the landing, and its line numbers.
Lane: openreposhape-4 (openRepoShape-4)
…hain link before an `=` that leaves a path after it, so the offer the gate prints for a link holding `=` verifies that link when typed as printed (#980) (#1044) For a declared-unverified chain link whose name holds `=`, the naming gate offers only sources that verify that link's tree when typed as printed. The gate offered `open=y` the source `--link-source open=y=<path>`, and read every `--link-source` key at the value's first `=`: the key was `open`, which no chain names, so the value was a bare PATH for the first link, `open=y` stayed declared-unverified under the same offer, exit 0. **The issue's direction (a)**, in `scripts/validate-repository-naming.py`. `LinkSources._read_source` reads a `--link-source` key the way `_read_remedy` reads a `--link-remedy` link since #949: the longest text before an `=` that names a link of a chain this run reads, by `pin_product_key`, and leaves a path after it. `__init__` and `no_path` both take the key and the path from it, so the path refused as empty is the one after the key's own `=`. For a chain whose links hold no `=`, every value is read exactly as before: a text holding `=` can only name a link that holds one. **What (a) keeps from the documented rules, and how each case was checked** (probes below, and one test row or more each): - A link holding `=`: `open=y=<tree>` verifies `open=y`, as printed and in capitals (`OPEN=Y=<tree>`, by `pin_product_key` as every key is). - Two links where one is a prefix of the other (`open` and `open=y` in one chain): `open=y=<tree>` is `open=y`'s and `open=<tree>` is `open`'s, in either order. On main both were `open`'s and the later replaced the earlier, so `open=y` read none. - A path that itself holds `=`: `open=y=<dir>/a=b/tree` is `open=y`'s with that whole path. A text holding `/` names no link, since a link's name is `repo_basename`'d, so an `=` after a `/` never ends the key. A path that begins like a longer link's name is given after `./`: `open=./y=dir` is `open`'s in a chain naming `open=y` too. - A key that names a link but leaves an empty path: `open=y=` is `PRODUCT=` for `open=y`, refused as `link-source-no-path`, exit 2, as `openXdox=` is (#442). On main it was a bare PATH asked for `openXdox`, a link the person did not name. Where a shorter link leaves a path (`open=y=` with `open` in the chain too), that link is the key, `open` with the path `y=`, as `--link-remedy openXdox=y=` gives `openXdox` the words `y=` where a chain names `openXdox=y`: the longest link that leaves a path wins, and `open=y` leaves none. - A value that is a real directory spelled like the whole value: `open=y=x` beside a directory `open=y=x` is still `open=y`'s, decided by the chain and not the disk, with `_also_a_directory`'s line; `./open=y=x` asks for the directory, as a bare PATH for the first link. - The key is read whole, where a remedy's link is stripped and cut at its last `/`: ` open=y=<tree>` and `opensoft/open=y=<tree>` name no link and stay bare PATHs, as on main. **One difference from `--link-remedy`, deliberately.** `--link-remedy open=y=` is a NOTE (its link falls back to the first `=`, `open`). `--link-source open=y=` is refused, because the #442 rule refuses `PRODUCT=` for every link, and `--link-remedy`'s own `openXdox=` refusal is made by argparse before the chains are known, which is why it cannot catch `open=y=`. `_read_source`'s docstring says so. **Not (b).** (a) makes the offer the gate already prints true, so the person who types it gets the verification; (b) would only stop offering it. Setup's step (4) is unchanged: it passes the gate its `--pin-source` as an absolute path, with a separator before every `=`, so no text before one names a link. **Docs.** The module docstring (a new paragraph, "A KEY IS THE LONGEST LINK A VALUE NAMES"), the `LinkSources`, `_link_remedy` and `_read_remedy` docstrings, the `--link-source` help, and `docs/cli.md` regenerated from it. `setup-project.py`'s `as_the_plan_reads_it` docstring stated the first-`=` rule, and now states this one. README, the handbook and AGENTS.md state no rule for how a `--link-source` key is read (README names `--link-source PRODUCT=<path>` only), so they are unchanged. The class docstring records that `validate-pins.py` still ends a `--pin-source` key at the first `=`; no scaffold writes a pin holding one (`--pin` names are `[A-Za-z0-9_.-]`), and it is left as its own change. Measured from a root scaffolded at main `5b41d44f` (local remotes, no network): - `update-shape.py check --upstream <this checkout> --at c64e85f` lists `upstream-changed scripts/validate-repository-naming.py`, with `upstream-changed 1 unchanged 10`. - `update-shape.py apply --yes --branch ...` exits 0, commits `contracts/shape-pin.yaml`, `project.yaml` and the copy, and the project's validators print `pins ok` and `manifest ok`. A fresh `check` prints `unchanged 11`, and the root's own naming gate exits 0. - The new sha256 of `scripts/validate-repository-naming.py`: `ec167fb0e259f90705db895a1a37f4adebd4cbf03ca2d5ae18f6d9a8df896548` (78664 bytes). No other pinned copy and no template changes. Run from empty folders, `<D>` a fresh folder, trees as named (`trees/y` declares `openInk`, `trees/x` declares `open=y`; for the chain with `open`, `trees/x2` declares `open` and `trees/o` declares `open=y`). The gate prints an arrow between links; it is written `->` here. | Probe | At main `5b41d44f` | At head `c64e85fe` | |---|---|---| | P1, the issue's row: `--referent-chain openXdox,open=y,openInk --link-source open=y=<D>/trees/y` | `link-source-unreadable ... could not answer for openXdox`, "It was read as a PATH: `open` is not a link", and `open=y` declared-unverified with the same offer; exit 0 | only `openXdox` declared-unverified (no source named it); no `link-source-unreadable`; exit 0 | | P2, P1 plus `openXdox=<D>/trees/x`, `--explain` | NOTE: the value not used, read as a PATH; `CHAIN openXdox -> open=y -> openInk [declared-unverified]` | `[verified]`, nothing warned or noted | | P3, chain `openXdox,open,open=y,openInk`, `open=<o>` then `open=y=<y>` | `link-source-unreadable: y=<D>/trees/y (--link-source open=...)`, NOTE `open=<o>` replaced; 2 links declared-unverified | `[verified]`, nothing warned or noted | | P4, `open=y=` (chain without `open`) | bare PATH `open=y=` asked for `openXdox`; exit 0 | `REFUSED link-source-no-path: --link-source open=y= names no path for open=y`; exit 2 | | P5, `open=y=` (chain with `open`) | `y= (--link-source open=...) could not answer for open` | the same | | P6, `open=y=<D>/trees/a=b/y` | read as a PATH, `[declared-unverified]` | `[verified]` | | P7, `open=y=x` beside a directory `open=y=x` | bare PATH, read as the directory for `openXdox` | `link-source-unreadable: x (--link-source open=y=...) could not answer for open=y`, then "`open=y=x` is also a directory; `--link-source ./open=y=x` reads it as one." | | P7b, `./open=y=x` | the directory, for `openXdox` | the same | | P8, chain with `open`: `open=./y=dir`, `open=y=<y>` | `open=y=<y>` read for `open`, replacing `open=./y=dir` | `[verified]` | Eleven new rows in `tests/test_chain_remedy_callers.py`, section "#980", reusing its helpers (`naming`, `tree`, `gate_warnings`, `UNREACHED`, `GATE_OFFER`, `EQUALS`), with no assert that joins conditions with `and`: - `test_the_offer_for_a_link_holding_an_equals_verifies_it[as-printed, in-capitals, a-path-holding-an-equals]`: reads the offer the gate prints for `open=y`, types it with a tree, and asserts the one WARNING left is `openXdox`'s, with no `link-source-unreadable` and no NOTE. - `test_each_link_reads_its_own_source_where_one_name_begins_another[open-first, open=y-first, a-path-that-begins-like-the-longer-link]` - `test_a_value_that_ends_at_a_link_holding_an_equals_names_no_path` - `test_a_shorter_link_that_leaves_a_path_is_the_key` - `test_a_link_holding_an_equals_wins_over_a_directory_spelled_like_it` - `test_a_key_is_read_whole[a-blank, an-owner]` **Failing at main.** The test file copied onto a `git archive` of `5b41d44f`: 8 of the 11 fail, each on the defect (the offer row's `open=y` WARNING and `link-source-unreadable`, the prefix rows' `[declared-unverified]`, `open=y=` exiting 0, the keyed reading not winning over the directory). The other 3 pass at main by design: they pin behaviour main already had (`test_a_shorter_link_that_leaves_a_path_is_the_key`, `test_a_key_is_read_whole[a-blank]` and `[an-owner]`), and each kills a mutant below. **Mutants**, each applied alone to the fixed gate in a scratch repository, then `test_chain_remedy_callers.py`, `test_link_source_chain_defaults.py`, `test_naming_policy.py`, `test_chain_link_fold.py` and `test_chain_pin_sources.py` (400 passed, 4 skipped with no mutant). All 11 killed: | Mutant | Failed | Killed by (new rows unless named) | |---|---|---| | M1 back to `typed.partition("=")` | 8 | the offer rows, the prefix rows, `open=y=`, the directory row | | M2 the shortest match instead of the longest | 3 | the prefix rows | | M3 the "leaves a path after it" check dropped | 1 | `test_a_shorter_link_that_leaves_a_path_is_the_key` | | M4a the `./PATH` rule dropped (`./` stripped from the key) | 3 | the directory row; `test_a_labelled_source_holding_an_equals_is_not_said_to_be_a_path`; `test_naming_policy.py::test_a_link_named_before_the_equals_is_the_key_even_over_a_directory` | | M4b the key cut at its last `/`, as a remedy's link is | 4 | as M4a, and `test_a_key_is_read_whole[an-owner]` | | M5 the empty-path fallback dropped | 6 | `open=y=`; `test_a_value_that_names_no_path_is_spelled`; 4 rows of `test_link_source_chain_defaults.py::test_a_link_source_that_names_no_path_is_refused` | | M6 the fallback at the first `=`, as a remedy's | 1 | `open=y=` | | M7 `no_path` taken after the first `=` | 1 | `open=y=` | | M8 the keyed path taken after the first `=` | 7 | the offer rows, the prefix rows, the directory row | | M10 the key stripped, as a remedy's link is | 1 | `test_a_key_is_read_whole[a-blank]` | | M12 the key not folded by `pin_product_key` | 56 | `in-capitals` and the prefix rows, and 52 existing rows | **Runs**, all on head `c64e85fe`, `/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed`, one reused basetemp per run, with `EMAIL=service@opensoft.dev`, a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false` and `GIT_CONFIG_GLOBAL=/dev/null`: - git 2.43.0 (`/usr/bin/git`), 31 files: the 25 that run the naming gate (`grep -l validate-repository-naming tests/`, the pinned-copy files `test_pinned_copy_text.py`, `test_update_shape_kept_helper.py`, `test_update_shape_unreadable.py` and `test_adopt_reencoded_files.py` among them, and `test_repo_hygiene.py`), the files that scan every script's source (`test_git_output_decoding.py`, `test_shape_advisory.py`, `test_no_auto_maintenance.py`, `test_non_utf8_git_failures.py`), and the trio's other two, `test_agents_md_codes.py` and `test_cli_reference.py`: **2964 passed, 5 skipped** in 1749 s, exit 0. - git 2.47.1 first on PATH, the 25 naming-gate files: **2680 passed, 3 skipped** in 1181 s, exit 0. No template changes. `grep` of `tests/` found no fixed digest or `^id:`-style pattern over the gate: the tests that read a root's copy of it (`test_shape_doctor.py`, `test_doctor_validator_verdicts.py`) compare it at run time, and both are in the first run, as are the source scanners above and `test_cli_reference.py`, which holds `docs/cli.md` to the help. - `git merge-tree --write-tree origin/main HEAD`, origin/main `5b41d44f` (fetched with `-c gc.auto=0 -c maintenance.auto=false`, and `git ls-remote` agrees): clean, tree `82780dc63d7f`, the head's own tree. - Open pull requests: none of the 9 open (#954, #1023, #1025, #1027, #1028, #1030, #1031, #1038, #1039) touches `scripts/validate-repository-naming.py`, `tests/test_chain_remedy_callers.py`, `setup-project.py` or `docs/cli.md`, so there is no merge-tree to run against them. Review: rv980, lane 5's independent reviewer, not the writer (w980): READY at `c64e85fe` (issuecomment-6288391970), with no MUST or SHOULD; the landing head only merges main over it. Its NIT-1 (three test rows for the corners of the old first-`=` split) and NIT-2 (a sentence in `validate-pins.py`) go to a follow-up. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 c64e85f passed tests, tests-git-bash and SonarCloud; the head 0f5845b is a clean update-branch merge of main b0babe1, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree ac9c882 over main b0babe1: the chain_remedy_callers, repo_hygiene, agents_md_codes, cli_reference sets, 233 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Closes #980. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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 3 days and 8 hours by commenting |
|
Main moved past eee089c: #1045 (00f6d7f) changed the holder's bootstrap.py and #1025 (6476c17) changed validate-family.py, two of the four copies of the git-not-runnable rule. The merge is clean, and the four copies' rule is still one text. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s where the work tree's `.gitmodules` is gone, the index's then HEAD's, and its lines put that file back first, so a section `git submodule add` staged is no longer committed past a passing gate (#1036) (#1069) RV1025's SHOULD 1 on #1025. In its state, `git submodule add` at members/IRQS, not committed, then the work tree's `.gitmodules` deleted (`MD .gitmodules`), the finding read the work tree's file alone and named nothing: every printed line exited 0, the gate passed in every mode, and the commit of what was staged recorded IRQS's section in HEAD's file. Measured on git 2.43.0, isolated config: with the work tree's file gone, `git submodule status members/IRQS` maps the path, which HEAD's file does not give, so git reads the index's; `git config -f .gitmodules --unset-all ...` on no file exits 5 and makes none; the index's file put back and both keys unset leave it byte-identical to HEAD's. - `_sections_as_git_reads`: the work tree's sections where the file is on disk, else the file git reads in its place (`_gitmodules_put_back`), through `_blob_sections`. `_given_as_git_reads` and `_blob_paths` derive from it, unchanged in what they answer, so #936's hand-mount put-back and this finding agree on one file. - `_check_members` hands it to `_check_gitmodules_named`, but where `.gitmodules` gives a value git dies on: that finding's lines put HEAD's file back and stage it, which drops a section the index's file alone gave. Every other reader of `.gitmodules` is as it was. - `_section_out` takes the put-back (`_file_put_back`, one for every finding) and prints it right before the keys' lines, saying so; the case-variant branch and `_gitlink_out_first` pass it. A gitlink outside `members/` keeps its `git rm --cached` first, which needs no file. Rows at the end of tests/test_family_hand_mount_git_rm_lines.py: the issue's state, the same at tools/x, a stray section, two sections served by one put-back, a hand mount whose plain `git rm` takes its section (no command), and the control where a value git dies on keeps what it read. Two rows of tests/test_family_holder_remedies_compose.py move with the fix: C1's `--no-members` run names the section giving `Members/IRRS` (no command), as it does with the file on disk, and rv882's SHOULD 2 row, which said the gate passed, now expects the `gitmodules-path-unnamed` the same state is with the file on disk, typed through to a passing gate. Round 2, for RV1069's review: validate-family.py: `gitmodules-path-unnamed` stages `.gitmodules` with a printed line where HEAD's file holds no section of the name it takes out, in `members/` and in another case as at a gitlink outside it, so `git commit` after the lines records nothing of a section `git submodule add` staged (#1036) RV1069's SHOULD 1 on #1069. In #1036's state the section's text ended "then commit .gitmodules" and printed no stage line: after the put-back and the `--unset-all` lines the work tree's file was HEAD's, the index's still held the section (`MM .gitmodules`), so `git commit` recorded it and `git commit -- .gitmodules` had nothing to commit. `_section_then` gives every branch one ending, from HEAD's file: the commit where it holds a section of that name, else "then stage .gitmodules: HEAD's holds no section of that name, so nothing of it is left to commit", the words the branch for a gitlink outside `members/` already said, now with `git add -- .gitmodules` printed after the keys' lines (`_section_out`), as the removal's written-back section and the wrong-case move print theirs. Rows: R1 and its tools/x sibling type only the printed lines, then `git commit` of what is staged; both fail on ab8ed9e (HEAD keeps `members/IRQS`; the stage line is missing) and pass here. `tests/test_family_unnamed_gitlink.py`'s `section_first` expects the stage line where the text ends "then stage". Closes #1036 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



(lane openRepoShape-4, DRAFT; head
4c0c52d0, fix round 1 for RV1025's review, with main5b41d44fmerged in; #889 landed as702cc23d, #956 as3864b18cand #983 as5b41d44f)Every
git rmline thattemplates/family-root/scripts/validate-family.py'smember-gitlink-unnamedprints for a member mounted by hand now works typed, and the gate passes after the commit. A plaingit rmrefuses a staged gitlink, dies on a clone no section names, and refuses what is no checkout. In those states the line is nowgit rm --cached, which touches no file, and the text says what stays on disk and what to do with it. Where the work tree's.gitmodulesis gone, the file is put back before thegit rmthat takes the hand mount's section out. Before this change, each of these lines exited 1 or 128 when typed, or the gate found the same thing again after the commit.#936 holds the three hand-mount states that #889 (#879) named under "What it cannot tell" in its round 2. The issue's goal is every hand mount's
git rmworking typed, so a fourth state of the same kind, what is no checkout, is fixed here too.What is on disk at a hand mount is asked as #956 asks it (#430's rule, which landed on main as #956). A path or a
.gitthis user cannot read is refusedmember-unreadableby name. Before, it ended in a traceback on Python 3.12, and on 3.14 it would read as a false "no checkout". This moves one of #956's test rows; see "Choices for the reviewer".Fix round 1 (RV1025, issuecomment-6280865402)
RV1025 found this PR READY at
927fabf7, with no MUST, one SHOULD and three NITs. This round's head is4c0c52d0, tree4d0c9deb:5958536cmerges main5b41d44finto927fabf7, with no conflict. Main had taken twelve squashes sincee076e3e8. They include tests/test_family_add_gitlink_and_null_members.py: a secondmembers:line is refused by the reader as a key written twice since #974, which turned main's tests red (#1024) #1026, which fixed main's two red rows, and validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983, which changesvalidate-family.pyin_git_runsand the member checks, apart from this delta's hunks. The four commits are unchanged.4c0c52d0.test_issue_1_a_gitmodules_change_not_staged_is_staged_before_cachedtypes RV1025's state:git mv members/IRRS members/IRRS2, staged, then a line appended to the work tree's.gitmodulesby hand and not staged.git add -- .gitmodulesbeforegit rm --cached -f -- members/IRRS2, and the text that numbers both. It then types every line in order, commits, and asks the gate in every mode and a plain clone.--cachedline, is killed by it. On this head with MG, the new file gives 1 failed, 20 passed, and the failure is this row. RV1025 measured thegit rm --cached -fexiting 128 in MG's order.3864b18c's script, which printsgit rm -- members/IRRS2after the stage line, the row fails too.gitmodules-path-unnamedreads only the work tree's.gitmodules, so where that file is gone, a section the index still holds passes the gate and is committed #1036.pull_request, passed. Itstestsjob gave 18668 passed, 371 skipped, 0 failed, andpackage,tests-git-bashand SonarCloud passed too.Before and Now
How it was measured.
familyfixture fromtests/test_family.py, with members IRRS and IRSS and the spare IRQS. A hand mount is atmembers/IRQS.ghfirst on PATH,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_CONFIG_GLOBALan empty file,GIT_CONFIG_NOSYSTEM=1,GIT_SSH_COMMAND=false, and local remotes only. Git is 2.43.0.gitline typed as printed, in order, split as a POSIX shell splits it and run with no shell;.gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889's landedvalidate-family.py, sha2568e7953e8. Main's file since validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956, sha256ca2c40b7, prints the same lines in these states: the new test file fails the same rows against it (see "The tests"). Now is927fabf7. This head4c0c52d0merges main5b41d44f, which brings validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983's change to the same script, and adds the NIT 2 row. Every row passes the same way on it (see "The run and the mutants").1. A gitlink the index stages at a commit HEAD does not record there, with something on disk at it
This is lane 1's SHOULD on #889, and
_staged_overis true for it. Seven states:git mv members/IRRS members/IRRS2, staged, either clean or with a commit in the checkout it moved;git submodule addnot committed;HEAD~1, thengit add.Before. The text printed
git rm -- <path>, which exits 1 in all seven. Git says one of two things:After the commit, the gate found the gitlink again. Where a section gives the path, it also found
gitmodules-path-unnamed.Now. The line is
git rm --cached -f -- <path>. Over an empty directory it isgit rm --cached -- <path>, with no-f, as_forceddecides. Either way it touches no file. The text says why, and quotes both refusals: "take it out of the index alone with the second, which touches no file (-f takes out a staged gitlink whose checkout has moved on or holds a file git does not track, which git rm otherwise refuses, and with --cached touches no file), since a plain git rm refuses it here: the index stages it at a commit HEAD does not record there, with something on disk at it (the following file has changes staged in the index, or, where what is on disk differs too, has staged content different from both the file and the HEAD, exit 1)". Where HEAD records nothing at the path, the text adds "; HEAD records nothing there, so nothing of it is left to commit". Where the work tree's.gitmodulesholds a change the index does not,git add -- .gitmodulescomes first, and the text numbers both. Git takes no gitlink out of the index until that change is staged ("please stage your changes to .gitmodules or stash them to proceed", exit 128). This is RV1025's NIT 2, a row since fix round 1.The text then says what is left on disk, and what to do with it before
family.py add, which refuses anything at the path (member-path-taken):git -C members/IRQS statusshows. Delete it by hand once nothing only it holds is needed, ...".A section that gives the path now prints its own
--unset-alllines, because--cachedleaves the section in place. Before, it printed none, because a plaingit rmwould take the section out with the gitlink.Typed in order, with what is left deleted by hand as the text says, then committed: nothing is left, the gate passes in every mode, and a plain clone checks out both members.
2. A hand mount with its own section, then a staged removal, with the work tree's
.gitmodulesgoneThe state:
git rm --cached members/IRRS, or a plaingit rm members/IRRS;.gitmodulesis deleted, or its deletion is staged.Before. The hand mount's
git rm -- members/IRQSexited 0 but took its section out of no file. The removal's put-back after it (git checkout -- .gitmodules, orHEAD's) brought the section back. After the commit, the gate foundgitmodules-path-unnamedin every mode.Now. The hand mount's lines put the file back first, from where git reads it, unless a finding before did (C1's single put-back):
git checkout -- .gitmodules;git checkout HEAD -- .gitmoduleswhere the deletion is staged.The text says "put .gitmodules, gone from the work tree, back from the index's with the second, so that the git rm takes its section out of that file and every other section stays".
The removal after it puts nothing back:
git rm --cached, its finding printsgit resetalone;git rm, it writes IRRS's section back key by key where the index's file lacks it.Typed in order and committed, in all four states: nothing is left, HEAD's
.gitmodulesgives both members, the gate passes in every mode, and a plain clone checks both out.3. A clone added by hand that no section names
The state: IRQS cloned into
members/IRQS, with a commit only that clone holds, thengit add members/IRQS, committed or not. No section gives the path.Before.
git rm -- members/IRQSdied withfatal: could not lookup name for submodule 'members/IRQS', exit 128. Git's rm first moves a.gitdirectory into the holder's store, and it names that store from a section of.gitmodules.Now. The line is
git rm --cached -- members/IRQS. Where the index stages it and HEAD records nothing, it isgit rm --cached -f -- members/IRQS. The text says: "The clone at members/IRQS, whose .git is a directory, stays there, untracked: it holds its commits there and nowhere else, andgit -C members/IRQS log --oneline HEAD --branches --not --remoteslists the ones not pushed, so move it out of the holder whole, by hand, rather than delete it, ...".Typed, that
git loglists the clone's commit. Then:Beyond the issue: what is on disk is no checkout
The state: a committed hand mount with no
.gitat its path. At the path is a directory holding a file, or a file.Before.
git rm -- members/IRQSexited 1 ("the following file has local modifications").Now. The line is
git rm --cached -- members/IRQS. The text says "What is on disk at members/IRQS, which is no checkout, stays there, untracked: move it out of the holder, or delete it by hand once nothing in it is needed, ...". Typed, deleted by hand and committed, the gate passes.What cannot be read (#430, as #956 asks it)
Before, on this branch's third commit moved onto main (
02bd9e10):members/at mode 000, with a hand mount staged or committed:_hand_cached'sPath.is_dir()ofmembers/IRQS/.gitended the gate in aPermissionErrortraceback on Python 3.12.is_dir(), which_hand_cachedasked first. On 3.14 (not run here),is_dir()answers False to a refusedstat, asos.path.lexistsdoes on every Python. So, read from the code, the text would say "no checkout". For the committed checkout, that is the reason for agit rm --cachedthe plain line does not need. For the staged one, it is what the--cachedline leaves.Now each is refused by name, exit 2, with the
chmodcure, as #956's other sites are:REFUSED member-unreadable: members/IRQS: cannot be read (Permission denied)where the folder above the path cannot be searched;REFUSED member-unreadable: members/IRQS/.git: cannot be read (Permission denied)where the checkout cannot be.With the mode back, the gate says what it said before.
A hand mount HEAD records is refused too, where #956 printed the plain
git rm(r956's C19). Withmembers/at mode 000, that line,git rm -- members/IRQS, exits 128 on git 2.43:So it was no line that works.
The controls
These print the plain
git rm, as before, and it works:The change
_hand_cachedasks git's records and what is on disk, as git's rm.c does, and never git's words:_anything_at). Nothing there is the plain line..gitdirectory at the path, with no section giving it in the file git reads._given_as_git_readsreads the work tree's file, else the index's, else HEAD's. It is asked first, so the.gitis asked only where no section gives the path._staged_over.git rev-parse HEAD:<path>differs from the index's commit, and anything is on disk at the path..gitentry.Around it:
_cached_leftsays what stays on disk._check_gitmodules_namedasks_hand_cachedtoo, so a section at a--cachedpath keeps its own lines._mounted_by_handis split into_plain_rm_saidand_cached_rm_said, which number the lines with_ordinal._plain_rm_saidputs the file back first through_file_put_back, C1's single put-back.tests/test_family_switch_branch_exists.py's guard can then see that the prose it stands under ends withBRANCH_LEFT. The lines after it are a list that the helpers append to, numbered from the second. This is the third commit,02bd9e10: the first complete run found the guard failing on a list that began with the_switch_linecall (see "The run and the mutants").family.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956 asks it._hand_cachedand_cached_leftuse validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956's_anything_atand_askedwhere they usedPath.is_dir()andos.path.lexists. This is the fourth commit,927fabf7.The tests
tests/test_family_hand_mount_git_rm_lines.pyis new, with 21 rows. Each of the first 19:gitline as printed, in order;git submodule update --init --recursivemust check out both members.git mv; an empty directory;git submodule addnot committed; a checkout moved toHEAD~1and added;git mvwith a commit in the moved checkout; a directory holding a file; a dangling link;git mvwith a hand edit of.gitmodulesnot staged (RV1025's NIT 2)git rm --cachedor a plaingit rmof IRRS, with the file deleted or its deletion stagedgit submodule addnot committed, each at mode 444: every mode refused by name, then the same findings with the mode backAgainst main's script: the new file was run in a worktree at main
3864b18c, whosevalidate-family.pyis sha256ca2c40b7. 17 failed, 3 passed. Every issue row, every beyond row and both mode-444 rows fail, and the 3 controls pass. The NIT 2 row, added in fix round 1, was run alone there and fails too.The dangling-link row is x889c's NIT A on validate-family.py and bootstrap.py: the holder's remedies, typed as printed in order, put a
.gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889 (issuecomment-6253217474). Nothing there tested what_staged_overreads at a dangling link. It is a commit of its own,e97aa0ea.Helpers: they are imported from validate-family.py and bootstrap.py: the holder's remedies, typed as printed in order, put a
.gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889'stests/test_family_holder_remedies_compose.py, fromtests/test_bump_leg_unopenable.py(made_unopenable,needs_a_mode) and from what those import; none is copied.validate-family.py and bootstrap.py: the holder's remedies, typed as printed in order, put a
.gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889's round-2 SHOULD test is dropped from that file, along with its table and constants (STAGED_REFUSED,PLAIN_REFUSED,STAGED_STATES,git_refusal). It pinnedgit rm -- <path>exiting 1 with the clause, which this change replaces. Its states are now rows in the new file, built from its helpers, which stay where they are, but one (RV1025's NIT 3). That one isSTAGED_STATES'nothing-on-disk, a gitlink staged by hand over nothing, whose plaingit rmthe dropped row typed and which exited 0. The new file'snothing-on-diskcontrol is committed (hand_mounted). The staged state's plain line is still pinned, untyped, bytests/test_validate_family_unnamed_mounts.py:293(test_a_member_mounted_by_hand_is_two_findings). The compose file keeps 54 tests.validate-family.py: a member's checkout, a shape copy, the shape pin or
family.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956'stests/test_validate_family_unreadable.py: its committed hand mount moves from the two "never asked" tests to the refusals:test_a_path_whose_stat_is_refused_is_refused_by_name[hand-mount-committed];test_a_folder_this_user_cannot_search_is_refused_by_name[hand-mount-committed-folder-000];[hand-mount-committed-444], for its.git.The two "never asked" tests keep their variant row, C20. The file has 53 tests. On
02bd9e10with main's copy of the file, 3 of 52 failed: the hand mount's mode-000 row and both C19 rows. With the fourth commit's reads put back whole, the hand mount rows of that file and the new file's two mode-444 rows give 5 failed, 3 passed (U-beforebelow).Choices for the reviewer
git rm --cached, never-fover a checkout's work.--cachedtouches no file. Where it needs-f(_forced), that-fstill touches no file. What stays on disk is said, and the person decides: move a clone out whole, and delete a checkout oncegit statusshows nothing they need. Say if you want the text to print themvor the delete as a line.Git's own rules, read from its records, not from its English. The three states are git's rm.c conditions:
submodules_absorb_gitdir_if_neededfor the clone, andcheck_local_modfor the staged gitlink and for what is no checkout. They are asked withrev-parse,config --bloband what is on disk. Git's words are quoted only to tell the person what they will see.The plain
git rmstays where it works. It takes the section out with the gitlink, and the texts validate-family.py: thegitmodules-path-unnamedremedy takes out the section of a submodule no row names and leaves its gitlink, so a plain clone'sgit submodule update --initexits 128 while the gate passes #878 and validate-family.py and bootstrap.py: the holder's remedies, typed as printed in order, put a.gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889 pin for it are unchanged. The three controls hold that.The file is put back before the
git rm, by C1's single put-back (_file_put_back), not by reordering findings. The removal after it then puts nothing back.In the
--cachedcase, the section prints its own--unset-alllines rather than the hand mount's lines adding a--remove-section. This follows family.py:addnever reads.gitmodules, so a section under ANOTHER name whosepathis alreadymembers/<P>(a "ghost" section) does not stop it: it exits 0 and commits two sections for one path,validate-family.pysaysfamily ok, and a recursive clone takes the LATER section, so with the ghost second it dies fetching the ghost's URL #300's by-key rule, and the section's finding already prints those lines.What is no checkout is fixed here too. The issue's goal names every hand mount's
git rmline, and the mechanism is the same.r956's C19 on validate-family.py: a member's checkout, a shape copy, the shape pin or
family.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956 changes for the hand mount, and only for it. C19 holds that a gitlink HEAD records at the index's own commit asks nothing of the disk, so a folder this user cannot search there changes nothing._forcedand_staged_overstill ask git first, and the variant row (C20) is unchanged.git rmlines still fail when typed: a gitlink staged at a commit HEAD does not record, a hand mount with its own section while.gitmodulesis gone, and a hand clone with no section (exit 128) #936, whether a hand mount's plaingit rmworks turns on what is on disk: a clone no section names, or what is no checkout. So_hand_cachedmust ask the disk.statas the plain line. That breaks EveryPath.is_file()/is_dir()/exists()guard in the shipped tools answers False to any OSError on Python 3.14 (3.12 and 3.13 raise PermissionError), so a directory this user cannot search reads as ABSENT: validate-pins.py skips its workflow@<sha>check, update-shape.py sayscopy-missingandupdate-pin-missing("adopt the project") for files that are there, the doctor says DRIFTED (5 copy-missing), and bump-leg.py's remaining guards, and every other tool's, skip or misreport instead of refusing by name #430's rule, and the plain line it would print exits 128 there (measured above).This head refuses it by name. Say if you want the other.
Lane 1's r956, who wrote C19's expectation on validate-family.py: a member's checkout, a shape copy, the shape pin or
family.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956: please confirm the new one, or say which you want. A committed hand mount withmembers/at mode 000 is now refusedmember-unreadable, exit 2, where C19 expected the control's finding, exit 1. Intests/test_validate_family_unreadable.py, itshand-mount-committedrows leavetest_a_gitlink_head_records_as_staged_asks_nothing_of_the_diskandtest_a_folder_under_a_gitlink_git_takes_is_never_asked, and it is refused by name intest_a_path_whose_stat_is_refused_is_refused_by_name[hand-mount-committed]andtest_a_folder_this_user_cannot_search_is_refused_by_name[hand-mount-committed-folder-000]and[hand-mount-committed-444]. The variant rows (C20) are unchanged.Base: main
3864b18c. validate-family.py and bootstrap.py: the holder's remedies, typed as printed in order, put a.gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889 (the family root's validate-family.py and bootstrap.py: with the work tree's.gitmodulesgone, three remedies undo each other or refuse; agit mv'd checkout is never named; bootstrap saysokbeside git's exit 128 #879), on which this work began, landed as squash702cc23d. validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956 (slice E of EveryPath.is_file()/is_dir()/exists()guard in the shipped tools answers False to any OSError on Python 3.14 (3.12 and 3.13 raise PermissionError), so a directory this user cannot search reads as ABSENT: validate-pins.py skips its workflow@<sha>check, update-shape.py sayscopy-missingandupdate-pin-missing("adopt the project") for files that are there, the doctor says DRIFTED (5 copy-missing), and bump-leg.py's remaining guards, and every other tool's, skip or misreport instead of refusing by name #430) landed as3864b18c.5b41d44f(5958536c, fix round 1), with no conflict. Since3864b18cmain took twenty squashes. Of them, only validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983 touchesvalidate-family.py, in_git_runsand the member checks, apart from this delta's hunks, which are now lines 2576 to 2988 of main's file. None touches this delta's test files.4c0c52d0with tree4d0c9deb, touchingtemplates/family-root/scripts/validate-family.py,tests/test_family_holder_remedies_compose.py, the newtests/test_family_hand_mount_git_rm_lines.pyand validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956'stests/test_validate_family_unreadable.py:8ac9cd89;e97aa0ea;02bd9e10;family.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956 asks it,927fabf7, pushed and reviewed by RV1025;5b41d44f,5958536c;4c0c52d0..gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889's29083a5f. Before the first push it was moved onto validate-family.py and bootstrap.py: the holder's remedies, typed as printed in order, put a.gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889's round-3 head32a5899f. It was then moved onto maincf66102f,c7a3ff16,2a78ae1a,6f27432c,04923f10and3864b18c, each time withgit rebase --onto. Nothing pushed was rewritten: fix round 1 merged main rather than move the branch.family.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956, changed none of the three patches:git range-diffgives=for each.family.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956's reads met validate-family.py (family root): after #889, three hand-mountgit rmlines still fail when typed: a gitlink staged at a commit HEAD does not record, a hand mount with its own section while.gitmodulesis gone, and a hand clone with no section (exit 128) #936's new ones there, and validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956's test file failed 3 of 52 on the moved third commit. The fourth commit resolves it.The pinned copy
scripts/validate-family.pyis a pinned copy in every holder (FAMILY_COPIED_VERBATIM).e50919bbfa1a8feaa949e424af10cb8619081b64c9552684b1f459eda5441191on main5b41d44f, after validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983, and954696d0a158b6afb1ef5134cd949f350414827c1213b9f62e1fd2d16e30b562on this head. Before the merge it wasca2c40b7on main3864b18candf4bca120on927fabf7.upstream-changedrow for it. validate-family.py and bootstrap.py: the holder's remedies, typed as printed in order, put a.gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889 and validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956 give the same row in v1.2.0, and so does validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983. Oneapplytakes all of them.scripts/repo_shape.py, and uses only validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956's own helpers.bootstrap.pyis unchanged.Shared files
git merge-tree --write-treeof this head4c0c52d0against main and against every open PR that shares a file with it or with the family root's scripts. Heads were read bygh pr listat 2026-10-20T09:39:15Z.dc7cd49b43ca9045fe907c84aeca9f005cf60365d6b30c04411e0d6a05a610265b41d44f. Its four squashes since, 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, adopt-project.py, shape_materialize.py and standard.py: the Free-plan hint no longer calls an internal root private, adopt's docstring says its note for an internal root with an unread plan changed in #969, andprepare's docstring has no 104-character line (#994) #1038, adopt-project.py: the split commit's message andplan's report name each path, question and rule id one way only and never raw, the history scan'sgit-failedis one line, and the table pads a name as stdout prints it (#900) #1030 and setup-project.py: the self-bootstrap refuses an$OPENREPOSHAPE_REPOholding U+202E or another character the launcher refuses, by code point and in the launcher's words, before it prints the value or runsgit clone(#953) #1043, touch none of this delta's files.git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983 and the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so ascripts/nobody may search no longer SKIPs every validator intobootstrap ok(#430, slice D) #958 have landed. validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983 landed on main as5b41d44fand is merged in here. the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so ascripts/nobody may search no longer SKIPs every validator intobootstrap ok(#430, slice D) #958 landed asaf0e60f2.gitWindows cannot execute ([WinError 193], a namelessOSError) isgit-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1, and one test holds the four copies to one answer and one text (#1001) #1028 (validate-family.py, validate-pins.py and both bootstrap.py copies: agitthat is not a Win32 application ([WinError 193]) ends in a traceback, exit 1, notgit-not-runnable#1001) sharesvalidate-family.py. It now stands on5b41d44f, validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983's squash. Its hunks of the file are in_git_runsalone (lines 337 to 354), apart from this delta's. It merges with main clean too.family bootstrap okbeside git's exit 128 (#892) #954 and the family root's bootstrap.py and siblings.py: under GNU make 4.4's named-pipe jobserver, a member's make that says it is 4.4 or later is handed the holder'sMAKEFLAGSas it is, so it runs its jobs together again; every other make still gets no jobserver and no-j(#987) #1023 share no file with this head. the family root's bootstrap.py: a gitlink a member's own index gives no url, or a member's commit that a fetch reaching its remote did not bring, fails the run by name with its cure and no credential advice, where it saidfamily bootstrap okbeside git's exit 128 (#892) #954 touches only the family root'sbootstrap.pyand a new test file. the family root's bootstrap.py and siblings.py: under GNU make 4.4's named-pipe jobserver, a member's make that says it is 4.4 or later is handed the holder'sMAKEFLAGSas it is, so it runs its jobs together again; every other make still gets no jobserver and no-j(#987) #1023 touches the family root'sbootstrap.pyandsiblings.py, and test files of its own.gh pr list --state open --json files, over the other 7 open PRs at 2026-10-20T09:39:15Z, names only validate-family.py, validate-pins.py and both bootstrap.py copies: agitWindows cannot execute ([WinError 193], a namelessOSError) isgit-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1, and one test holds the four copies to one answer and one text (#1001) #1028 forvalidate-family.py. No open PR touches the new file, the compose file,tests/test_validate_family_unreadable.pyortests/test_family_switch_branch_exists.py.The run and the mutants
The run of fix round 1 is CI's: run 39377078862,
pull_requeston this head4c0c52d0. It testsrefs/pull/1025/merge(b2623136), this head merged with main5b41d44f, which it already holds. Itstestsjob ranpython3 -m pytest tests -q, the whole suite, on CPython 3.14.8 and git 2.55.0. 18668 passed, 371 skipped, 0 failed, from 2026-10-20T05:51:04Z to 2026-10-20T06:49:06Z.tests-git-bash,packageand SonarCloud passed too.git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983'stests/test_validate_family_git_not_runnable.py, which the merge brings to the same script. It gave 2672 passed, 15 skipped, 0 failed, from 2026-10-20T07:00:44Z to 2026-10-20T07:30:15Z, with the 15 skips of the run before it.bootstrap.py's, were checked by sha256 against the commit's blobs.The run before it: one run, of 39 files, on
927fabf7, with Python 3.12.3 and git 2.43.0, in the environment above. 2612 passed, 15 skipped, 0 failed, from 2026-10-19T22:30:43Z to 2026-10-19T23:25:46Z. Afterwards, the four touched files' bytes, andbootstrap.py's, were checked by sha256 against the commit's blobs.927fabf7, on main3864b18c. Fix round 1's run has main5b41d44fmerged in..gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889 ran. Those are the compose file, every gate and bootstrap file that reaches the sites changed,test_shape_doctor.py, and the trio (test_repo_hygiene,test_cli_referenceandtest_agents_md_codes). Two more:tests/test_family_add_spelling_leftovers.py. It is the one test file on main that namesmember-gitlink-unnamedand was not in the set, and it runs the gate afterfamily.py add's remedies.tests/test_validate_family_unreadable.py, validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956's, which the fourth commit changes./usr/bin/python3 -m pytest -q -rs -p no:cacheprovider -o tmp_path_retention_policy=failed --basetemp=<scratch>/w936/bt <the 39 files>. The run unsets the launcher'sLANES_*variables, which CI never has (below), setsEMAIL, and keepsGIT_SSH_COMMAND=false.tests/test_shape_doctor.py, where git filter-repo is not installed, and three there for the PowerShell line, printed only on a Windows host. Intests/test_doctor_legs_members_next.py, two where git 2.43 crashes onsubmodule init(exit 139), and four where it dies on ashallowof-2**31, which CI's git 2.55 takes. Two intests/test_no_auto_maintenance.py, since the session began withGIT_CONFIG_GLOBALset.40e6e27b, the second commit as it stood on mainc7a3ff16. It gave 3 failed, 2484 passed, 15 skipped:tests/test_family_switch_branch_exists.py's guard and its non-vacuity test, withunguardednaming the line oflines = [_switch_line(...)]. The third commit fixes that, and both tests pass now.tests/test_doctor_gitlink_repairs.py::test_a_hand_committed_gitlink_names_both_bump_leg_commands. The cause is the environment, not this change. The lane launcher setsLANES_LANE, so the doctor's printedbump-leg.pylines carried--trailer 'Lane: openRepoShape-4'(shape_materialize.lane_trailer), and the test's expected command, built in process, did not. On mainc7a3ff16alone the test fails the same way withLANES_LANEset, and passes with it unset.family.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956, and a finisher's stop on927fabf7at 43%. In fix round 1, a run on a local head was stopped at 2% when validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983 landed, so that main with validate-family.py:git-not-runnablecounts only anOSErrorthat namesgit, or aFileNotFoundErrororPermissionErrorthat names no file, asvalidate-pins.pycounts it, and only around the git calls, so a member's checkout git could not enter, or a member check's own step, is never told to put git on PATH (#928) #983 is what is merged in. The first 40-file run on this head was stopped at 40% at a usage checkpoint. None had a failure by then. The one that went furthest with the third commit, on85dc61ac(on main2a78ae1a), reached 57%.Mutants: each puts one line's old behaviour back on its own.
927fabf7, selected with-k.U-*: the hand mount rows of validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956's file and the new file's two mode-444 rows.family.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956 changed code under them (_staged_over,_on_diskand_forced).family.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956's_staged_over, which asks_anything_atwhere it askedos.path.lexists._hand_cachednever answers "staged": the plaingit rm, as beforeSTAGED_ATquotes "changes staged in the index" alone--cachedpath prints no lines, as before--cachedleaves on disk_hand_cachednever answers "clone": the plaingit rm, as before--cachedtoo_hand_cachednever answers "no checkout"_staged_overasksexists, which a dangling link fails_hand_cachedasks the.gitbefore the path, so a folder above it at mode 000 is refused at<path>/.gitPath.is_dir()first, as before: a traceback on 3.12os.path.lexistsof the.git, as before_cached_leftasksPath.is_dir()of the.git, as before: a traceback on 3.1213 of 14 were killed.
git add -- .gitmodulesafter the--cachedline. At927fabf7it survived all 74 rows of the new file and the compose file. On this head4c0c52d0, the new file gives 1 failed, 20 passed with it, and the failure is the NIT 2 row. Its replacement text is read from RV1025's own mutant script. The script was restored and checked by sha256.U-no-checkout-lexistssurvives, with the head's outcome. Withos.path.lexistsback,_hand_cachedreads a.gitthis user cannot reach as "no checkout". The hand mount's text then asks_cached_left, and its_askedof the same.gitrefuses it by name, with the same line. Read from the code, no state tells the two apart. The read stays_anything_atbecause validate-family.py: a member's checkout, a shape copy, the shape pin orfamily.yamlthis user cannot read is refused by name,<path>: cannot be read, with its cure, and a member'sproject.yamlismember-manifest-unreadable, so no Python 3.12 traceback and no 3.14member-uninitialized,pinned but absent,does not existor update of a checkout that is there (#430) #956's rule is that every guard asks the operating system itself, with.gitmodules'slexistsas its one stated exception.U-left-anything-atis not run. It would put_cached_left's_anything_atof the.gitback toos.path.lexists. The_askedof the same.gitjust before it raises on any refusedstat, so the two lines answer alike.U-before, the fourth commit's reads put back whole (02bd9e10's script), gives 5 failed, 3 passed on the same selection.What it cannot tell
.gitmodulesgone (RV1025's SHOULD 1). RV1025 measured it, and so did fix round 1, on this script.git submodule add <IRSS> members/IRQS, not committed, then the work tree's.gitmodulesdeleted.member-gitlink-unnamed. Itsgit rm --cached -f -- members/IRQSexits 0 typed. With the checkout deleted as the text says, the gate passes in all three modes.git status --porcelainisMD .gitmodules. The index's file still holds[submodule "members/IRQS"], which the add staged, so the next commit records it. With the index's file back on disk (git checkout -- .gitmodules, which is what a clone of that commit has), the gate findsgitmodules-path-unnamedin all three modes, exit 1.gitmodules-path-unnamedreads only the work tree's file (_gitmodules_listed).git rm -- members/IRQSthere, which exits 1.gitmodules-path-unnamedreads only the work tree's.gitmodules, so where that file is gone, a section the index still holds passes the gate and is committed #1036: where the work tree's file is gone, read the one git reads in its place..gitfile naming a git directory outside this holder's store, such as a linked worktree of an outside clone, mounted by hand withgit worktree addand committed, with no section naming it (RV1025's NIT 1). Git's rm absorbs only a.gitthat is a directory (submodule_uses_gitfile).git rm -- members/IRQSthis head prints there exits 0._hand_cachedanswers None, since the.gitis a file.prunable.statin process. Windows ACLs and running as root (needs_a_mode) were not measured.family.py addafter the lines is not typed by these rows, as in validate-family.py and bootstrap.py: the holder's remedies, typed as printed in order, put a.gitmodulesgone from the work tree back once and never over another remedy's write, stage it before a gitlink'sgit rmand after a section written back, and name a checkoutgit mvmoved before the update, and bootstrap fails where git's init in this holder stopped with every member checked out, so no remedy undoes another or exits 128 (#879) #889. It mounts a hand mount again only where the person wants it.--unset-allof every key may leave an empty[submodule "<name>"]header, as family.py:addnever reads.gitmodules, so a section under ANOTHER name whosepathis alreadymembers/<P>(a "ghost" section) does not stop it: it exits 0 and commits two sections for one path,validate-family.pysaysfamily ok, and a recursive clone takes the LATER section, so with the ghost second it dies fetching the ghost's URL #300's remedy does. The gate reads no path from it.Closes #936
🤖 Generated with Claude Code
Summary by Sourcery
Ensure validation emits hand-mount removal instructions that work across staged, unnamed, non-checkout, and unreadable member states.
New Features:
git rmcommand emitted for unsupported hand-mounted members executable, using cached removal where a plain removal would fail and explaining the resulting on-disk state.Bug Fixes:
.gitmodulesbefore removing a hand-mounted member so its configuration section is actually removed.Enhancements:
git rmguidance for supported hand-mount states while emitting section cleanup commands when cached removal leaves.gitmodulesentries behind.Tests:
.gitmodules, unnamed clones, non-checkout contents, supported controls, and unreadable paths, including typing every emitted command through to a passing validation gate.