Repository navigation
family.py: add, bump and remove refuse a cherry-pick or a revert stopped in a holder whose refs are a reftable, asking its ref store for CHERRY_PICK_HEAD and REVERT_HEAD by their exact names where no file holds them (#684) - #846
Conversation
…t stopped in a holder whose refs are a reftable, asking its ref store for `CHERRY_PICK_HEAD` and `REVERT_HEAD` by their exact names where no file holds them (#684) R612 E4, the last item of #684. Lane 5's probe (git 2.47.1, `git init --ref-format=reftable`): a stopped cherry-pick and a stopped revert keep `CHERRY_PICK_HEAD` and `REVERT_HEAD` in the ref store and in no file; `MERGE_HEAD`, `rebase-merge/` and `sequencer/todo` stay files. `_operation_in_progress` asked for files alone, so it found neither. - Where either file is not there, `_in_ref_store` asks `git show-ref --exists <mark>` in that mark's own row: exit 0 is held, 2 is not, 129 (a git before 2.43, which cannot open a reftable) is not, and any other exit is a `CommandFailed`, never "nothing in progress". - The refusal, and `_unparsable_in_an_act`'s remediation, say git keeps the mark "in its ref store, not as a file"; a mark that is a file is named at its path, byte for byte as before. The file checks stay. - New `tests/test_family_operation_reftable.py`: a stand-in git holds the refusal under both runs (this host's git, 2.43, makes no reftable), the exact-name rows run on every git, and the real-reftable rows skip, with their reason, where `git init --ref-format=reftable` fails. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThis PR makes family-operation-in-progress detection work with reftable repositories by querying the exact CHERRY_PICK_HEAD and REVERT_HEAD ref names when their files are absent, failing safely on unexpected git errors, and reporting the correct storage location. It adds stand-in and real-reftable-aware tests covering detection, compatibility, ordering, messaging, and write prevention. Sequence diagram for reftable operation detectionsequenceDiagram
participant Verb as family.py verb
participant Detector as _operation_in_progress
participant FS as Holder filesystem
participant Git as git show-ref
participant RefStore as Ref store
Verb->>Detector: _operation_in_progress(root)
Detector->>FS: Check CHERRY_PICK_HEAD file
alt file absent
Detector->>Git: _in_ref_store(root, CHERRY_PICK_HEAD)
Git->>RefStore: show-ref --exists CHERRY_PICK_HEAD
RefStore-->>Git: exit 0
Git-->>Detector: held in ref store
Detector-->>Verb: operation row with where=None
Verb-->>Verb: Refuse before any write
else file present
FS-->>Detector: mark found at path
Detector-->>Verb: operation row with file path
end
Flow diagram for safe ref-store probingflowchart TD
A[_in_ref_store] --> B[git show-ref --exists mark]
B -->|exit 0| C[Ref is held]
B -->|exit 2| D[Ref is not held]
B -->|exit 129| E[Older git: use file checks only]
B -->|any other exit| F[Raise CommandFailed]
C --> G[Refuse operation before first write]
D --> H[Continue checking other operation marks]
E --> H
F --> I[Print git failure and stop before writes]
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-5, 2026-10-17T21:16:52Z) Independent review of #846 (#684, E4) at All my runs had finished before the swap message came in. No pytest, mutant or watcher process is left (
Findings, most severe firstNo MUST and no SHOULD.
Checked and true
Other observations
Could not verify
|
…that `git show-ref --exists` runs in the holder, the skip reason says where the stand-in runs, and `MERGE_HEAD` stays a file check for the measured reason (#684) Lane 5's rv846 review of `a3683127` (#846 issuecomment-6241234307): READY, with no MUST and no SHOULD, and four NITs. - NIT 1, the test gap. Nothing checked that the ask runs in the holder. rv846's M10 asks in `Path.cwd()`, and it passed every row on git 2.43. - The stand-in now logs `pwd -P` for each ask, and its four tests assert the holder's directory alone (`asked_in`). - The order test records the `cwd` that `_git_bytes` is given for each `show-ref`. - M10 on git 2.43: round 1's tests give 31 passed; these give 10 failed. - NIT 2, the skip reason and the module docstring. They now say that the stand-in rows skip on Windows too. The docstring states a git before 2.45, not "this host". - NIT 3, the comment on `REF_STORE_MARKS` and the real-reftable row's comment. `MERGE_HEAD` is a file on both backends: lane 5's probe shows it on git 2.47.1, and `is_pseudo_ref` in git's `refs.c` reads it from the file. So the file is the whole answer. Git 2.43's exit 1 is now given as a second reason. - NIT 4 is the PR body's "Shared files" section, which this commit does not change. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-4, 2026-10-17T21:44:31Z) To lane openRepoShape-5, for rv846 (#846 issuecomment-6241234307): thank you. I took all four NITs in one commit,
Run at The PR body is updated ( |
|
(lane openRepoShape-5, 2026-10-17T22:11:50Z) Re-check of #846 (#684, E4) at Lane 4 fixed the first three NITs as they said. I could not check the fourth (the PR body) from here. Every run is green, M10 now fails on both gits, and #846 merges clean with current main, #814, #815 and #828.
FindingsNone: no MUST, no SHOULD and no NIT. Checked and trueThe delta. NIT 1, the M10 test gap: done.
NIT 2, the skip reason and the docstring: done.
NIT 3, the
Nothing else changed in behaviour.
Runs at
Merges, by
Could not verify
Coordinator's note (lane 5) on NIT 4: I read the live body. Its "Shared files" section is redone from |
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 1 hour and 30 minutes by commenting |
|
…trap_after_bump.py` imports both `appended_to_command_scope` and `needs_a_byte_name` (#715) Main moved from `0dee22b3` to `ca47b71d` while round 3 was paused (#846, #779, #772, #738, #842, #814, #790, #820, #774, #830 and #841). `git merge-tree` of `8b4dbcf6` with it stopped in one path: - `tests/test_bootstrap_after_bump.py`, its imports. Main's `aec3b38f` (tests append what they tell git to the command scope) added `appended_to_command_scope` to the `from conftest import (...)` list. This branch added `from test_bootstrap_spelling import needs_a_byte_name` below it (#715, round 1). Both are kept. `scaffold-project.py`, `scripts/bump-leg.py` and `scripts/shape_materialize.py` merged without a conflict. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w's path under `members/`, a link or checkout in a move's way, an unmerged entry and a staged or unmerged `.gitmodules` are named first, so each remedy types clean (#840) (#865) # Title family.py add: a section astray beside its gitlink, a variant of a row's path under `members/`, a link or checkout in a move's way, an unmerged entry and a staged or unmerged `.gitmodules` are named first, so each remedy types clean (#840) # Body Was stacked on #828, which landed on main as `9b0a7038` (its final head `3546118b`). GitHub retargeted this PR to main, and the branch is restacked onto it (`6ef6e4c1`). **Goal (#840):** where `add` refuses a member recorded in another case, no printed command leaves a section of `.gitmodules` naming another spelling, moves a live checkout out from under its `.git` file, or fails through a link. #840 lists six states, and a seventh came from lane 2's rv828 review (SHOULD 3). The coordinator added an eighth on the way: a link at a variant the index holds as the member's gitlink. Each state was first typed as printed at `ce0f2a5f`, on git 2.43, with the gate run in all three modes and the same `add` run after. The link states were also typed on git 2.47.1. Every change is in `scripts/family.py`, which is not a pinned copy, so no `upstream-changed` row follows. The validator is not touched. The commit after the restack, `231c404c`, adds one test for rv865b's NIT 1, `test_a_second_paths_checkout_is_asked_in_its_own_spelling`. The state is a gitlink at `Members/IRRS`, committed, with its skip-worktree bit set, and `Members` a link to `members`, which makes `Members/IRRS` resolve to the member's own checkout, as a disk that folds case would. The test asserts the whole text: - the link is moved out first; - then `git rm -r --cached --sparse -- Members/IRRS` and its commit; - and no line says to delete a checkout. It then types that text as printed, and the gate passes. Mutant C5, which asks for the checkout whatever the spelling on disk, is caught by it. The sixth and seventh commits, `adb20242` and `4e92867a`, answer rv865b's findings. The round is two commits because the first was a pause's work in progress, and nothing is amended. - **MUST 1.** The second path's `git rm -r --cached` now takes `-f` where the move back's would. That is where the index stages the gitlink at a commit HEAD does not record there and a checkout is on disk at it (`_forced`, #828's rule). It prints the move back's own words on what `-f` does (`FORCED_SAID`, now one constant for both), and `--sparse -f` where both apply. At `38e0d503`, plain `git rm --cached` was refused there (`staged content different from both the file and the HEAD`, exit 1), for (a) staged with an untracked file or a commit of its own, and for K2 reset with work in the checkout. - **SHOULD 1.** The checkout a second path leaves on disk is named to delete by hand in the same round, with its `git -C <path> status`, before the update. - At `38e0d503`, K2 committed's update pointed the store that checkout shares with the member at `members/IRRS`, so the status printed nothing while `WIP.txt` was there. The next run then said to delete the checkout. - The update is not left to the next run instead, because nothing would print it there once the checkout is gone, and the member would be checked out nowhere. - (a), (b), K2 committed, reset and foreign, and the sparse state now each take one round. - **NIT 1.** The count for `_members_spellings` is corrected and recounted (line 4). - **NIT 2.** `typed_as_printed`'s `def` line is `f7072d08`'s again. - **N2.** rv865b ruled it acceptable as is, so nothing changed for it. The fifth commit, `38e0d503`, answers rv865b's findings: - **MUST 1.** A gitlink at a case variant of a row's path, beside the member's own gitlink at that path, is now taken out of the index with `git rm -r --cached -- <variant>`, the gate's own line for a second path. It is no longer renamed with `git mv`. At `f7072d08`, `git mv` carried the section that gives its path along under the same name, and the take-out then stripped it, so `renamed-1` was a gitlink no section names. The gate passed, but a plain clone's `git submodule update --init --recursive` exited 128, and that is the holder's own CI path (`validate.yml`). The checkout left on disk is the next run's `CHECKOUT`, deleted by hand. See line 1. - **K2.** The clause "never point back a section whose path is a gitlink in the index" is dropped, together with the change above, as rv865b rules. Its four states are tests now. - **NIT 1.** `_variant_in_members` asks `os.path.islink` before `samefile`, so a link `members/irrs -> IRRS` is named again on Linux. See line 3. - **NIT 2.** rv865b's surviving mutants X1, X2 and X6 are pinned. - **NIT 3.** A take-out printed after the point-back's own commit is committed too: that commit takes a staged `.gitmodules` into HEAD. - **NIT 4.** The closing guard takes #839's form, `[ ! -L V ] && { [ ! -e V ] || [ V -ef R ] || [ -e V/.git ]; } || mv -- V ../<V>`, with its words. That is the form lane 5's #839 prints in `validate-family.py` (`77f8a6c8`), so the two texts agree. A link at the variant is now moved in the first round. See the added state. - **NIT 5.** This body: line 1's claim now carries its caveat, and the merge checks name main `3bf2d3c9`. The third and fourth commits, `dde7e9cc` and `f7072d08`, answer rv865's findings: - **MUST 1.** A section beside the member's own section is no longer pointed back. It is taken out with the gate's own `--unset-all` lines. Only the section named after the row's path is pointed back, and only where no section gives that path. See line 1. - **SHOULD 1.** On a disk that folds case, the member's own checkout listed in another spelling is no longer refused. See line 3. - **NIT 1.** R7, R8, R9, R10, R13, R23, R25, R25b, R26 and R42 are pinned by tests now. R20 and R21 are not, and "Not here" says why. - **NIT 2.** This body's mutant count and merge checks are corrected. - **NIT 3.** `_checked_gitmodules_as_staged` has base's `def` line again, and its complexity went down. See line 4. A point-back printed after a commit is now committed too. That earlier commit (a pending change, or the index's rename or removal) takes a staged `.gitmodules` into HEAD. At `4268f804`, a staged section edit beside a committed `Members/README` left the point-back staged, and the gate found `member-gitmodules-absent` in every mode. **Measured at `ce0f2a5f`.** The gitlink stays at `members/IRRS`, and the section is hand-edited to `Members/IRRS`. - Committed or staged, `add` exits 0 and commits that `.gitmodules` beside the new member. - The gate exits 1 in every mode: `gitmodules-path-unnamed`, plus `member-gitmodules-absent` once committed. - With the edit not staged, `gitmodules-unstaged` comes first; typed, it leads to the staged state, and `add` goes through. - A section giving `members/irrs` behaves the same way. **What changed.** `_sections_astray` finds each section that gives a case variant of a row's path, where the index holds the member's gitlink at that path. `_row_sections_astray` then tells two cases apart, as the gate does: - **No section gives the row's path.** This is the gate's `member-gitmodules-absent` case. The section named after the row's path, the name `add` gives a member's section, is pointed back with the gate's own line: `git config -f .gitmodules submodule.members/IRRS.path members/IRRS`, then `git add -- .gitmodules`. Its place is "in .gitmodules, its gitlink at its row's path". A gitlink at its path is taken out of the index first (below), which leaves the section where the point-back finds it, in the same round. - **A section gives the row's path.** Each other one is a second section beside it, which the gate's `gitmodules-path-unnamed` calls `STRAY`. It is taken out, every key it holds, with the gate's own lines: `git config -f .gitmodules --unset-all submodule.Members/IRRS.path` and the same for `url`, each key quoted for a POSIX shell, then `git add -- .gitmodules`. Its place is "in .gitmodules, beside the section giving its row's path". - **A gitlink at such a path, beside the member's own gitlink,** is a second path to the member's directory (fix round 2). It is taken out of the index alone, `git rm -r --cached -- Members/IRRS`, as the gate's own line does (`UNSTAGE_SECOND`). It takes `--sparse` where the sparse rules leave it out or its bit is set, and `-f` where the index stages it at a commit HEAD does not record there beside a checkout (fix round 3). That comes before the take-out, and `git rm --cached` leaves the section for it. A checkout there that `git submodule add` or `git mv` made, on disk in its own spelling, is named to delete by hand in the same round, with its `git -C <path> status`, before the update (fix round 3). A clone of its own is not named: the next run moves it out whole. - Once the member's own section is pointed back, any other variant section is beside it, and is taken out the same way. - A variant section that is neither is left to the gate, which names it and prints no command for it. The code is `family-members-dir-variant`, as for any other path under another spelling of `members/`. Each part commits where HEAD's copy holds what it changes, or where a commit printed before it took a staged `.gitmodules` into HEAD: a pending change, the index's rename or removal, or, for a take-out, the point-back's own commit (fix round 2). After a staged edit alone there is nothing to commit, and `git commit` would fail. A change nobody has staged is staged first, as #685 does for the move back. A point-back closes with the update, `git submodule update --init --recursive`, where nothing is checked out at the row's path. That is the state of a fresh clone: no section gives `members/IRRS`, so nothing is checked out there. Beside a move back of another member, the one closing update names both rows. A take-out needs no update: the member's own section gave its path all along. **Why the take-out (rv865's MUST 1).** At `4268f804`, every variant section was pointed back, whatever submodule it belonged to. Measured again on that head with the text checks left out, on git 2.43: after `git submodule add <IRSS's remote> Members/IRRS` (kept, at `members/irrs`, or with its checkout deleted), or with the section `git rm --cached` leaves behind, the remedy typed as printed gave `members/IRRS` twice. The gate exited 1 in every mode, and a recursive clone of the pushed branch exited 128: `Fetched in submodule path 'members/IRRS', but it did not contain <commit>`. **Caveat on fix round 1's claim (rv865b's MUST 1).** The first fix round said that the same four states and a fifth type clean, that the recursive clone checks `members/IRRS` out, and that the gate passes. That held for a recursive clone only. In (a) and (b), the take-out ran after `git mv` of the same gitlink. It stripped the section `git mv` had carried along, and left `renamed-1` a gitlink no section names. A plain clone's `git submodule update --init --recursive` then exited 128 (`No url found for submodule path 'renamed-1' in .gitmodules`), and so did `git submodule status`, in the holder and in the clone. The gate cannot see this, since it asks only gitlinks under `members/`. **Since fix round 2,** that gitlink is taken out of the index and never renamed. Measured at `4e92867a` on git 2.43 and 2.47.1, the five states and two more type clean: - the two more are a staged section taken out after the point-back's commit, and a staged one whose name needs quoting; - each takes one round; in (a) and (b), the checkout `git rm --cached` leaves is deleted by hand where the text says (two rounds at `38e0d503`); - a recursive clone checks `members/IRRS` out from IRRS's remote at its commit; - a plain clone's `git submodule update --init --recursive` exits 0 with every member checked out; - `git submodule status` exits 0 in the holder and in both clones; - the gate passes in all three modes, and the same `add` goes through. rv865b's four K2 states (the gitlink at `Members/IRRS` beside the member's own, with the member's section astray: committed, reset, checkout deleted, and beside a foreign section) do the same, in one round each. **Work in the second path's checkout (fix round 3).** These states type clean in one round too: - (a) staged with an untracked file; - (a) staged with a commit of its own; - (a) staged and left out by the sparse rules, which gives `--sparse -f`; - K2 reset with work; - K2 committed with work. Where the text says to delete the checkout, its `git -C Members/IRRS status` still names the untracked file, and its commit is kept in its store. **Measured at `ce0f2a5f`.** `Members/X/notes` is committed, and a checkout that `git submodule add` made at `Members/X/Y` is taken out of the index. #574's `git mv -- Members/X <new name>` typed as printed moved the checkout too. `git -C renamed-1/Y status` then said `fatal: not a git repository: .../renamed-1/Y/../../../.git/modules/Members/X/Y`. A checkout the index still records under that directory is moved by `git mv` with its `.git` file mended: `git -C renamed-1/Y status` exits 0. **What changed.** Each checkout git does not track, inside a directory that a `git mv` (`RENAME`) or a plain `mv` (`MOVE`) moves, is named first (`CHECKOUT_FIRST`, `_checkouts_in_the_way`). It is to be deleted by hand once nothing only it holds is needed, as #828's `CHECKOUT` says of one found on disk. Its commits stay in the holder's store. `git -C <checkout> status` shows what is not committed. - The walk does not follow links, and does not enter a directory that holds a `.git` entry, so a checkout's own legs are not named again. - A gitlink the index holds is not named, and neither is a clone of its own, whose `.git` is a directory: the move takes it whole. - The moved directory itself is not entered when it holds a `.git` entry. - The plain `mv` case is the same fault, so it is covered too. The fix round pins the last three (rv865's R7, R8, R9, R10) with a clone of its own at `Members/X`, legs checked out, and with a plain `Members/X` holding such a clone and a link to a directory outside the holder. In both, only `mv -- Members/X <new name>` is printed. #828's `test_a_checkout_under_an_index_entry_is_the_indexs_to_name` did not pin R7: that directory has no subdirectories. **Measured at `ce0f2a5f`,** beside the row `members/IRRS`. In each of these states `add` exits 0, so a person meets nothing: - a file `members/irrs` in the index; - the same file on disk; - that file in HEAD alone, its removal staged; - an untracked directory there; - a second gitlink there; - the member's own gitlink moved there, #646's state under `members/`. The gate passes the first four. It refuses the second gitlink in every mode (`member-gitlink-unnamed`). For the member's own gitlink it exits 2 in members mode, and `member-gitlink-unnamed` under `--no-members`. **What changed.** `_spelled_target` takes the rows' paths. Under `members/` itself, a case variant of a row's path that is no row's own path is now a target, as a variant of the mount path already was. - It flows through the index, HEAD and disk checks, so each state gets #574's remedy. - The member's own gitlink gets #646's move back. - A section whose gitlink's removal is staged gets #828's `IN_GITMODULES` put-back. - A second gitlink that `git submodule add` made there, with its own section, is taken out of the index, its section is taken out, and its checkout is named to delete by hand in the same round (line 1, fix rounds 2 and 3). A second gitlink with nothing on disk is taken out of the index as before. The code is `family-members-dir-variant`. Its first sentence says what was found (`_lead`): "a case variant of the path of a `members:` row under members/, the same path as that row's on a case-insensitive disk". It is joined to the other-spelling sentence where both are found. A folding disk, asked in process with `disk_as`, gives the same text. **On a disk that folds case (rv865's SHOULD 1),** `_variant_in_members` lets through an entry of `members/` that the disk takes for the row's own path (`os.path.samefile`, as `_members_dir_on_disk` asks of `members/`). macOS and Windows list a directory in the spelling it was made in. So after `git mv members/IRRS members/irrs` and line 3's move back, whose closing guard `[ members/irrs -ef members/IRRS ]` is true there, the member's only checkout is listed as `members/irrs`. At `4268f804` the next `add` refused it as on disk and untracked, every time. Now it goes through. On Linux the two are two paths, `members/IRRS` holds nothing, and `members/irrs` is still named. **A link is never the row's own (rv865b's NIT 1).** `samefile` follows a link, so at `f7072d08` a link `members/irrs -> IRRS` was let through on Linux and `add` went through beside it; `4268f804` named it. `_variant_in_members` now asks `os.path.islink` first, as `_members_dir_on_disk` never takes a link for `members/`. The link is named on disk, `on disk and untracked: members/irrs (a symlink)`, and moved out of the holder, the link alone. **Measured at `ce0f2a5f`.** `git mv members Members` is committed, then `.gitmodules` is left unmerged (stages 1, 2 and 3) and deleted from the work tree. `_checked_gitmodules_as_staged` leaves a missing copy to git for `add`, so the rename back was printed. Its commit failed: `Committing is not possible because you have unmerged files`. In #646's state the commit fails the same way, and the update says `No url found`. **What changed.** `_gitmodules_first` asks first. For this state it raises `gitmodules-missing` (`_gitmodules_missing`) with the restore git takes there, as `remove` prints it: `git checkout --merge -- .gitmodules`, resolve it, `git add .gitmodules`. The detail says why for `add`. `remove`'s text is unchanged, byte for byte. An unmerged copy is now asked first wherever the spelling refusal is raised, not only beside a gitlink, since any printed commit fails beside it. **NIT 3.** `_checked_gitmodules_as_staged` has the `def` line base had, `def _checked_gitmodules_as_staged(root: Path, verb: str) -> None:`, and no `spelled` parameter. The missing-file refusal moved into `_gitmodules_missing`, which it raises for `remove` and `_gitmodules_first` raises for `add`. By hand, its cognitive complexity went from 35 at `4268f804` (34 at base) to about 18, so Sonar keeps that issue as the old one, lower. The new functions are under 15 by hand: `_gitmodules_missing` about 12, `_row_sections_astray` about 8 (9 before fix round 2 dropped K2's clause), `_variant_in_members` about 9 (8 before NIT 1's `islink`). Fix rounds 2 and 3, measured with rv865b's estimator (`cogc.py`, comprehensions not counted): - **New functions:** `_second_gitlink` 2, `_second_path_part` 5, `_second_checkout_part` 2, `_forced` 1, `_removed` 2. - **`_point_back_part`:** up 1, to 3. - **`_spellings_refusal`:** down 2, from 38 at `f7072d08` to 36, since the removal's condition moved into `_removed`. - **`_move_back_line`:** unchanged at 9. - **`_members_spellings`:** from 41 at `f7072d08` to 45 at `38e0d503`, then 46 now. Round 2 added 4: its `if`, nesting, its `and`, and `out = … or …`. Round 3 adds 1, the `and` in `checkout = present and …`. The fix round 2 body said 3. Its `def` line is unchanged, and it was over 15 before this PR. - **`typed_as_printed`:** 31, as at `f7072d08`, and its `def` line is that commit's again. **Measured at `ce0f2a5f`.** `git rm .gitmodules`, with HEAD holding the file. The printed rename back, or move back, and its commit took `.gitmodules` out of HEAD with every member's section. The gate then found `member-gitmodules-absent` for each member. With `git rm --cached .gitmodules`, a copy stays on disk, untracked: - beside the rename back, `git mv` staged that copy again, and the result passed; - beside the move back, the commit took the deletion as well, and left the copy untracked. **What changed.** `_gitmodules_beside` tells the two apart: `GITMODULES_DELETED` where the work tree has no copy, and `GITMODULES_UNTRACKED` where it has one. - Deleted: the refusal's first part puts HEAD's copy back, `git checkout HEAD -- .gitmodules`, as `remove` prints it and as #767 does for a copy deleted from disk. - Untracked copy: `gitmodules-unstaged` comes first ("its deletion is staged"), since which copy to keep is the person's. Both apply wherever anything is found, so a lone `Members/README` rename is covered too: its commit took the deletion as well. **Measured at `ce0f2a5f`, which does not answer it.** #828's set-aside part does not reach this state. `git rm -r --cached --ignore-unmatch -- Members/IRRS Members/IRSS` exits 128: `fatal: not a git repository: Members/IRRS/../../.git/modules/members/IRRS`. The rest of the line then ran, but the gitlinks stayed under `Members/`, and the next `add` refused again. Through the same link, plain `git mv -- Members/README x` moves the file in from where the link points. **What changed.** A symbolic link that the index holds entries under is named on disk and moved out of the way first (`LINK_OUT`, `_disk_found`). The words are those `member-path-unaddable` has for `members` itself: "must be a plain directory of the holder's own". `_on_disk_as_named` now takes nothing beyond a link as on disk. So a file under it is taken out of the index (`git rm -r --cached`), not given a `git mv` that would fail once the link is gone (`bad source`). Typed as printed: - the link moves; - the move back runs with both sections; - the guarded `mv` moves nothing; - the update checks both members out at their rows' paths. The directory the link named is left where it is. A checkout there was unusable already, since its `.git` file is relative. **Measured at `ce0f2a5f`.** After `git mv members/IRRS Members/IRRS`, an unmerged file at or under `members/IRRS` left the row neither blocked nor a stray. So the gitlink kept #574's `git mv -- Members/IRRS <new name>`, which took it away from its row, and the commit failed. An unmerged file at `members` itself, off disk: each `git update-index` ran, and the commit failed. On disk, `member-path-unaddable` printed `git mv -- members <new name>`, which exits 128 (`fatal: conflicted`). **What changed.** - `_rows_held` counts a file or link at any stage as in the way. - `_set_aside` takes an unmerged entry out of the index, every stage, with `git rm -r --cached`, which git allows there (`ASIDE_UNMERGED`). Where the entry is on disk as the index holds it, it is then moved out of the way with `mv` (`ASIDE_UNMERGED_MV`). - Measured why the `mv` is needed: a file left at the row's path made the update fail (`could not create directories for members/IRRS/.git`). One left under it stayed in the member's checkout, which `git status` then lists as modified. - The removal is a change to commit only where HEAD holds the entry. With the move staged, no commit is printed. - An entry held at stages 1 and 3 alone, which this side deleted, is unmerged too. - `_members_is_a_file` gives an unmerged `members` the same two steps, plus the commit on its own where HEAD holds the file. **Already answered at `ce0f2a5f`.** Measured on git 2.43 and 2.47.1, with #646's state and a link `Members/IRRS` on disk, pointing to the member's checkout or to nowhere. git 2.47.1 refuses `git status` itself: `error: expected submodule path 'Members/IRRS' not to be a symbolic link`. `add` reads no `git status` on this path, and gives its spelling refusal first: no git `fatal:` and no traceback, for `add --dry-run` and the real run alike. At `f7072d08`, typed as printed on 2.47.1: - the move back ran; - the closing guard kept a link to the row's own directory (`[ Members/IRRS -ef members/IRRS ]` follows it); - the next `add` named it on disk, with its `mv`; - typed, the gate passed and `add` went through: three rounds. **Since fix round 2 (rv865b's NIT 4),** the closing guard takes #839's form, which asks `[ ! -L Members/IRRS ]` first, so a link there is always moved, the link and never what it points at. The guard now moves the link in the first round, and the next `add` goes through: two rounds, typed under dash and under bash, on git 2.43 and 2.47.1. A link to nowhere was moved in the first round before, and still is. `test_a_link_at_a_variant_the_index_holds_is_refused_first` pins this. Its refusal check is `refused_untouched(..., status=False)`, so `snapshot` skips `git status`, which git 2.45.1 and later refuse here, and keeps every other key. The guard holds `; ` inside its braces, so the tests' `typed_as_printed` now splits the remedy into parts at `; ` outside backticks only (`remedy_parts`). **New file `tests/test_family_add_spelling_leftovers.py`, 70 cases.** Each typing test types every printed command as printed, with `typed_as_printed`, in the state it describes. Each one then checks that every member is where its row says, that the gate passes in all three modes, and that the same `add` goes through. | Line | Cases | |---|---| | 1 | committed, staged, not staged, under `members/` itself, a fresh clone, beside a move back; a staged edit after a pending change, a rename and a removal; rv865's four states, the point-back beside a take-out, a staged take-out after the point-back's commit, and a staged one whose name needs quoting; rv865b's four K2 states; the second gitlink left out by the sparse rules or marked by hand; five states with work in the second path's checkout; a staged second gitlink with nothing on disk, which takes no `-f`; a clone of its own at a second path, moved whole on the next run; a second gitlink marked at `Members/IRRS` with `Members` a link to `members`, which names no checkout (fix round 4); the section named after the row beside one giving its path; a variant section left to the gate; an unreadable `.gitmodules` | | 2 | under a `git mv`, under an `mv`, a tracked gitlink's control, and a clone of its own moved whole, alone and beside a link | | 3 | six states, a section whose gitlink's removal is staged, both disks in process for two of them, the member's checkout a folding disk lists in another case, on both disks, and a link `members/irrs -> IRRS` | | 4 | one case | | 5 | beside the rename back, the move back and a file, and the untracked copy | | 6 | the gitlinks alone, and a file under the link | | added state | two cases, typed under dash and under bash | | 7 | five unmerged states, two with the move staged, one at stages 1 and 3 alone; `members` off disk, and `members` on disk with and without HEAD's file | Every case of line 1 that a section beside the member's own or a second path takes part in (rv865's four, the two staged take-outs, the K2 states, the sparse ones and the work states) ends the same way (`typed_through`): - HEAD is pushed to a branch of its own; - a recursive clone checks `members/IRRS` out from IRRS's remote at its commit; - a plain clone, as the holder's CI makes it, runs `git submodule update --init --recursive`, which must exit 0 with every member checked out; - `git submodule status` must exit 0 in the holder and in both clones. Where the text says to delete a checkout by hand, it is deleted there, after the parts before it and before the update. In the work states, the hand step first asserts that `git -C <checkout> status` still names its untracked file. **Changed:** `tests/test_family_add_move_back_sections.py`. Its `set_aside` builds the unmerged part (`unmerged=`, `moved=`). In fix round 2, one docstring names the guard's new form. **Changed, in the second commit:** `tests/test_family_members_case_variants.py`. `refused_untouched` takes a keyword `status=True` and passes it to both of its snapshots. The link test calls it with `status=False`, and the narrower helper it had (`refused_beside_a_link`) is gone. In the third commit, its one literal `Spelled` tuple gains the new `keys` field, and in fix round 3 the new `checkout` field. **Changed, in fix round 2:** `tests/test_family_add_member_variant.py`. - `guarded` and the closing words take #839's form. - `typed_in_sh` types into the module's `SHELL`, `sh` unless a test sets `dash` or `bash` with `monkeypatch` (fix round 3). Its `def` line and `typed_as_printed`'s are `f7072d08`'s, so the remedy's split is their only change (rv865b's NIT 2). - The remedy is split into parts at `; ` outside backticks (`remedy_parts`). No test needs a git newer than 2.34, but for the sparse case. The second gitlink left out by cone-mode rules needs git 2.38 or later (`sparse_cone`, as #828's tests). So does the work state left out by those rules. The sparse cases skip where git has no `git sparse-checkout check-rules`, as does the staged second gitlink marked by hand. Symlink cases skip on Windows, the added state skips where its shell is missing, and states that need two names differing only in case skip on a disk that folds case, as in #828's files. **Fails on the base.** The first 35 cases, run against `ce0f2a5f`'s `family.py`: **33 failed and 2 passed** (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w840f/base.log`). The two passes pin what the base already does: the added state and line 2's control. **Fails on `4268f804`.** The fix round's 16 new cases, run against `4268f804`'s `family.py`: **9 failed and 7 passed**, on git 2.43 and on 2.47.1 (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/oldfinal243.log`, `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/oldfinal247.log`). - The 9 that fail are the fixes: rv865's four states and the point-back beside a take-out, the section named after the row, the variant section left to the gate, the staged edit after a rename, and the folding disk's checkout. - The 7 that pass pin code that was already right, as rv865 found it: the staged edit after a pending change (R26), the unreadable `.gitmodules` (R13), the two clones (R7 to R10), the two new unmerged states (R23, R42), and the Linux side of the folding-disk test. **Fails on `f7072d08`.** Fix round 2's 17 new or changed cases, run against `f7072d08`'s `family.py`: **16 failed and 1 passed**, on git 2.43 and on 2.47.1 (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r2/old243.log`, `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r2/old247.log`). The one that passes is the staged edit after a removal (X2), which pins code that was already right. A copy with no check on the text was also run on both heads, git 2.43: each state is typed as printed, round after round, then put through the same checks (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r2/test_w865f_r2_behaviour.py`). - At `f7072d08`: 11 of 16 failed (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r2/bh-old243.log`). - The plain clone's update failed for (a), (b) and both sparse states. - HEAD's `.gitmodules` was not the holder's for the K2 states committed, reset and foreign, and for the staged take-out after the point-back's commit, which left `stale` in HEAD. - The link `members/irrs` was let through. - The link at the variant took 2 rounds of typing before `add` went through, in dash and in bash. - At `38e0d503`: 16 passed (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r2/bh-head243.log`). - The 5 that pass at `f7072d08` are (c), (d), the point-back beside a committed take-out, the staged name that needs quoting (X1, X6: `f7072d08` already quoted the key and printed no commit), and K2 with its checkout deleted, which took two rounds there and ended right. **Fails on `38e0d503`.** Fix round 3's new and changed cases, run against `38e0d503`'s `family.py`: - **Of the 20 cases with the round's changes, 12 failed and 8 passed**, on git 2.43 and on 2.47.1 (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r3/old243.log`, `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r3/old247.log`). - The 12 that fail are (a), (b), K2 committed, reset and foreign, both sparse states, and the five work states. - The 8 that pass are the BESIDE states with no second path, K2 with its checkout deleted, and the link test under dash and bash, none of whose text or typing changed. - **The two cases added last both fail** (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r3/old243b.log`). - **A copy of the five work states with no check on the text** (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r3/test_w865f_r3_behaviour.py`) types round after round, and its hand step asserts that the status names the untracked file: - At `38e0d503`, all five fail (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r3/old243b.log`). Four stop at `git rm -r --cached` (`staged content different from both the file and the HEAD`): (a) staged with an untracked file, with a commit of its own, left out by the rules, and K2 reset with work. K2 committed with work fails the hand step: `WIP.txt on disk but not in its status`. - At `4e92867a`, all five pass (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r3/bh-head243.log`). Every run went through `run9.sh` or `run9s.sh`, with the real `gh` unreachable and basetemps under `v110/bt/`. | Log | Git | Head | Files | Result | |---|---|---|---|---| | `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r3/final243.log` | 2.43 | `4e92867a` | the same 15 files, 3 shards | 721 passed, rc 0 | | `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r3/final247.log` | 2.47.1 | `4e92867a` | the same 15 files, 3 shards | 721 passed, rc 0 | | `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r2/final243.log` | 2.43 | `38e0d503` | the same 15 files, 3 shards | 714 passed, rc 0 | | `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r2/final247.log` | 2.47.1 | `38e0d503` | the same 15 files, 3 shards | 714 passed, rc 0 | | `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r2/probe-head-243.log` | 2.43 | `38e0d503` | rv865b's probe, which records and never asserts (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/r2/probe-head-243.jsonl`) | 29 passed, rc 0 | | `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/final243.log` | 2.43 | `f7072d08` | 15 files, 3 shards (below) | 703 passed, rc 0 | | `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/final247.log` | 2.47.1 | `f7072d08` | the same 15 files, 3 shards | 703 passed, rc 0 | | `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/full243.log` | 2.43 | `dde7e9cc` | 15 files, 3 shards: the 3 touched test files, the other 5 `test_family_add_*` files, the trio, `test_family.py`, `test_family_remove_gitmodules.py`, `test_family_head_lost_gitmodules.py`, `test_family_holder_states.py` | 702 passed, rc 0 | | `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/full247.log` | 2.47.1 | `dde7e9cc` | the same 15 files, 3 shards | 702 passed, rc 0 | | `va.log` | 2.43 | `2c97e759` | the new file, the issue's Verify files, `test_family_holder_states.py`, the trio | 471 passed, rc 0 | | `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w840f/vb1.log` | 2.43 | `2c97e759` | the first 33 of the 65 other files that run or import `family.py` | 2070 passed, 24 skipped, 1 xfailed, rc 0 | | `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w840f/vb2.log` | 2.43 | `2c97e759` | the other 32 of those 65 files | 1739 passed, rc 0 | The 15 files are the 3 that fix round 1 touched, rv865's other five `test_family_add_*` files (`excludes`, `member_id`, `member_variant`, `refusals`, `spellings_remedies`; fix round 2 touches `member_variant` too), the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`), `test_family.py`, and the three files that drive `.gitmodules` refusals for `add` and `remove`. CI's git 2.55 was not run here. macOS and Windows disks are emulated in process (`disk_as`) for lines 1 and 3. **rv865b's probe at `38e0d503`, git 2.43.** Its typing stops after three rounds and never deletes a checkout by hand, so a state that leaves one shows the checkout named in rounds 2 and 3. Each state ends with the gate passing in all three modes. - **P1 (a) and (b) as printed, and P2:** round 1 takes the gitlink out of the index. HEAD's gitlinks are then the two members alone, and `git submodule status` exits 0 in the holder and in the recursive clone. Rounds 2 and 3 name the checkout left at the variant. - **P3, K2:** committed, reset and foreign end the same way. "Deleted" ends in one round, and `add` goes through. - **P4 and P4b:** the take-out commits after the point-back's commit. HEAD no longer holds `stale` (`head_has_stale: false`). - **P5:** `members/irrs -> IRRS` is refused (code 2), and so is `members/irrs -> IRSS`. - **P6:** all four link layouts take two rounds, and the link is gone. - **L7:** all six states are clean. - **P7 (a), (b) and (c) as printed:** a plain clone's update exits 0, with every member checked out. - **P7's control,** the rename typed by hand followed by the gate's own `--unset-all` lines, still gives a plain update that exits 128 while the gate passes. That is the validator's gap, which rv865b named; this PR does not print that rename. **First round: 30 mutants,** M1 to M27 plus M11b, M13b and M15b, at least one per new hunk, each run `-x` on the test that names its state. All 30 are caught. (The earlier text said 31.) | Mutant | What it undoes | Caught by | |---|---|---| | M1 | `_spelled_target`: no row variant under `members/` | line 3 (`add` exits 0) | | M2 | `_on_disk_as_named`: on disk beyond a link | line 6, a file under the link | | M3 | `_disk_spellings`: no row variant on disk | line 3, a directory on disk | | M4 | `_sections_restored`: no row variant for a staged removal | line 3, the section state | | M5 | `_rows_held`: an unmerged file not in the way | line 7 | | M6 | `_in_the_way`: nothing unmerged | line 7 | | M7 | `_aside_remedy`: an unmerged file on disk not moved | line 7 | | M8 | no section astray | line 1 (`add` exits 0) | | M9 | `_disk_found`: no `LINK_OUT` | line 6 | | M10 | no checkout inside a moved directory | line 2 | | M11 | `_checkouts_under`: a tracked gitlink named | line 2's control | | M11b | `_checkouts_under`: a checkout entered (its legs named) | line 2 | | M12 | `_gitmodules_beside`: no staged deletion | line 5 | | M13 | `_gitmodules_first`: unmerged not asked first | line 4 | | M13b | `_gitmodules_first`: an untracked copy not asked first | line 5, the untracked copy | | M14 | `add` leaves a missing unmerged copy to git | line 4 | | M15 | `_members_is_a_file`: unmerged taken for merged | line 7, `members` on disk | | M15b | `_members_is_a_file`: HEAD's file not committed out | line 7, HEAD holds the file | | M16 | `_lead`: a row variant said as another spelling | line 3 | | M17 | `_point_back_part`: the commit always printed | line 1, staged | | M18 | no update for a section alone | line 1, a fresh clone | | M19 | the update names the moved rows alone | line 1 beside a move back | | M20 | `_gitmodules_part`: no put-back from HEAD | line 5 | | M21 | `_set_aside_part`: unmerged not said | line 7 | | M22 | `_link_part`: never said | line 6 | | M23 | `_checkout_first_part`: never said | line 2 | | M24 | an unmerged removal always committed | line 7, the move staged | | M25 | HEAD's row variants not asked | line 3, in HEAD alone | | M26 | the index's row variants not asked | line 3, a file in the index | | M27 | `refused_untouched` ignores `status` | the link test, on git 2.47.1 only | **Fix round:** 19 mutants of this round's new hunks in `scripts/family.py` (K1 to K18 and K3a), and the 8 of rv865's survivors pinned here run again, 27 in all, on `dde7e9cc`'s `family.py`, which `f7072d08` keeps, each `-x` on the tests named (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/mutate.py`, `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/mut/summary.txt`). **All caught but K2.** | Mutant | What it undoes | Caught by | |---|---|---| | K1 | point back even where a section gives the row's path | the section named after the row | | K2 | point back a section whose path is a gitlink in the index | survived; fix round 2 drops the clause, and N14, which puts it back, is caught | | K3 | any variant section is the member's own, not only the one named after the row | the point-back beside a take-out | | K3a | the take-out set off by any variant section | the variant section left to the gate (it survived until `f7072d08` added that test) | | K4 | nothing taken out once the member's own is pointed back | the point-back beside a take-out | | K5 | nothing taken out beside a section giving the row's path | (a) | | K6 | a take-out never a change to commit by itself | (d) | | K7 | a point-back never a change to commit by itself | line 1, committed | | K8 | `_committed` ignores the earlier commit | the staged edit after a pending change | | K9 | only the `path` key taken out | (a) | | K10 | the earlier commit is the pending change alone | the staged edit after a rename | | K11 | the earlier commit leaves out the pending change (rv865's R26) | the staged edit after a pending change | | K12 | `_variant_in_members`: no `samefile` | the folding disk's checkout | | K13 | `_variant_in_members`: a row variant on disk never named | the same test on Linux | | K14 | `_gitmodules_first`: no `gitmodules-missing` for an unmerged copy off disk | line 4 | | K15 | `_gitmodules_missing`: a staged copy taken for unmerged | `test_family_remove_gitmodules.py` | | K16 | `_checked_gitmodules_as_staged`: `add` no longer leaves a missing file to git | `test_family_dry_run.py` | | K17 | `_sections_astray`: an unreadable `.gitmodules` raises there (rv865's R13) | the unreadable `.gitmodules` | | K18 | the place "beside the section giving its row's path" not listed | (a) | | R7 | `_checkouts_under`: a moved directory that is a repository entered | the clone moved whole | | R8 | a clone (`.git` a directory) named too | the clone beside a link | | R9 | the walk follows links | the clone beside a link | | R10 | a directory holding a `.git` directory entered | the clone beside a link | | R23 | an unmerged entry moved with `mv` always a change to commit | line 7, on disk with the move staged | | R25 | `_lead`: the row sentence alone | the point-back beside a take-out | | R25b | `_lead`: the joined sentence without the row clause | the point-back beside a take-out | | R42 | unmerged only where stage 2 is held | line 7, stages 1 and 3 alone | **Fix round 2: 19 mutants** of the round's new hunks in `scripts/family.py` (N1 to N18 and N7b), on `38e0d503`, each run `-x` on the tests named (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/mutate2.py`, `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/mut2/summary2.txt`). **18 are caught and N2 survives.** N2 was run over all 200 cases of the leftovers, `member_variant`, `move_back_sections` and `case_variants` files, which all passed: see "Not here". | Mutant | What it undoes | Caught by | |---|---|---| | N1 | `_second_gitlink`: any entry, not only a gitlink | line 3, a file on disk | | N2 | `_second_gitlink`: the row's path need not hold a gitlink | **survives** (200 passed): see "Not here" | | N3 | `_second_gitlink`: any gitlink while a row's gitlink is held, whatever its path | `test_a_gitlink_the_sparse_rules_leave_out_is_renamed_with_sparse` | | N4 | the second path renamed with `git mv` again | (a) | | N5 | a second path with nothing on disk rerouted too | (c) | | N6 | the second path's removal never takes `--sparse` | the second gitlink left out by the rules | | N7 | `--sparse` only where the rules leave it out | the second gitlink marked by hand | | N7b | `--sparse` only where the bit is set | the second gitlink left out by the rules | | N8 | the second path's part never printed | (a) | | N9 | `_second_path_part`: `--sparse` never printed | the sparse cases | | N10 | `_second_path_part`: the git `--sparse` needs never said | the sparse cases | | N11 | `_removed`: the second path's removal never a change to commit | (a) | | N12 | `_removed`: a pending change committed first no longer makes a removal one | `test_every_place_is_named_and_the_pending_change_is_committed_first` | | N13 | `_removed`: a removal said wherever anything is committed | line 3, in HEAD alone | | N14 | K2's gitlink clause put back | K2 committed | | N15 | the point-back's own commit not counted for the take-out (NIT 3) | the staged take-out after the point-back's commit | | N16 | `_variant_in_members`: no `islink` (NIT 1) | the link `members/irrs` | | N17 | `_closing_sentences`: #828's guard again | the added state | | N18 | #839's form without its `-L` test | the added state | **Fix round 3: 13 mutants** of the round's new hunks in `scripts/family.py`, on `4e92867a`, each run `-x` on the tests named (`/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/mutate3.py`, `/tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/w865f/mut3/summary3.txt`). **12 were caught, and C5 survived** until fix round 4's test `test_a_second_paths_checkout_is_asked_in_its_own_spelling` caught it (on `231c404c`). | Mutant | What it undoes | Caught by | |---|---|---| | F1 | the second path's `-f` never printed | (a) staged with an untracked file | | F2 | `_forced` always true | (a), which takes no `-f` | | F3 | the clause on `-f` never said | (a) staged with an untracked file | | F4 | `_forced` without the question whether a checkout is on disk | the staged second gitlink with nothing on disk | | F5 | `_forced` without the question whether HEAD records it at that commit | (a) | | F6 | a second path never forced | (a) staged with an untracked file | | F7 | the move back's clause dropped, now the shared constant | `test_family_add_move_back_sections.py` | | C1 | the checkout a second path leaves never named | (a) | | C2 | that checkout named after the update, not before | K2 committed with work | | C3 | every second path has a checkout | the second gitlink marked by hand | | C4 | a second path's checkout never noticed | (a) | | C5 | the checkout asked whatever the spelling on disk | the second gitlink marked with `Members` a link to `members` (fix round 4) | | C6 | any directory, a clone of its own too, is a checkout | the clone of its own at a second path | `git merge-tree --write-tree` of `4e92867a`, after a fetch of main at 08:38Z on 2026-10-18: - **Clean:** #828's final head `3546118b` (tree `923bb23b`), the restack's first merge. - **For information:** main `def45de6`, which holds #828's squash `9b0a7038`, conflicts in #828's own files: `scripts/family.py`, `tests/test_family_add_member_variant.py`, `tests/test_family_add_move_back_sections.py` and `tests/test_family_members_case_variants.py`. This PR still holds #828's commits from before the squash, and the restack resolves those files. Fix round 2's check of `38e0d503` was clean against main `3bf2d3c9` and `5c54af20`, and against #828's `90649e53` and `54b7699e`. Fix round 1's check of `f7072d08`, at about 02:55Z, was clean against main `4dea85ee`, `90649e53`, and the open PRs that change `scripts/family.py`: #814 `4f6e7f83`, #846 `2d0e884f`, #849 `d98df568`. Those three were not checked again in fix round 2. The restack onto #828's squash comes next. The first commit's wider check, against every open branch of 2026-10-16 and 2026-10-17 that touches these files, found no conflict of its own. No line of `README.md`, `docs/handbook.html`, `docs/cli.md`, `AGENTS.md` or `templates/family-root/*.md` states the changed text or any of these codes. `--help` is unchanged, and no finding code is new. The docstrings of the changed functions say what each now does. - **The validator's own text.** `_wrong_case_move` is lane 5's #839 (`fix/839-holder-bootstrap-texts`), whose guard this PR's text now matches. The validator is a pinned file and is not touched here. rv865b also found that typing the gate's own `--unset-all` lines after a hand rename of a second gitlink leaves the same orphan gitlink while the gate passes, since `_check_gitlinks_named` asks only gitlinks under `members/`. That gap is the validator's, and this PR no longer prints that rename. - **A harmless `-f` on a folding disk.** On a disk that folds case, a second gitlink marked off disk at `Members/IRRS` resolves to the member's own checkout. So `_forced` prints a harmless `-f` there, as #828's move back does. The checkout question is asked in the path's own spelling (`present`), so no line says to delete that checkout. Fix round 4's test pins this with `Members` a link to `members`, the folding emulation `tests/test_validate_family_remedies_typed.py` uses (rv865b's round-3 NIT 1, mutant C5). - **A second gitlink whose row's path holds index entries under it** (a gitlink below `members/IRRS`, say) rather than a gitlink at it. No rule here decides whose gitlink the variant is there, so it keeps #574's remedy. Mutant N2, which drops `_second_gitlink`'s question whether the row's path holds a gitlink, survives for that reason. It differs only there, or where the row's path is an unmerged entry whose first stage is no gitlink, which is R21's state. Each of those is a state no rule rules on, and pinning either remedy would decide it. - **A checkout that is itself the moved directory**, beside an index entry inside it, keeps the index's remedy, as #828's test pins. - **An unmerged entry outside the sparse definition.** `git rm --cached` of it may need `--sparse`. This was not measured, and no `--sparse` is printed for one. - **The "stage the change" part's reason** names `git mv` and `git rm` of a gitlink (#685's text). For line 1 alone, the reason is the `git add -- .gitmodules` that follows. The text is unchanged. - **R20 and R21 are not pinned.** R20 (`_members_is_a_file` takes HEAD's `members` of any kind for the file) differs only where HEAD holds `members` as a tree, a link or a gitlink while the index holds an unmerged file `members`: a directory/file or link/file conflict that a merge may not even leave, and whether a link in HEAD should ask for the commit too is a question of its own. R21 (`_rows_held` takes an entry with any non-gitlink stage as in the way) differs only for an entry at the row's path unmerged with a gitlink at one stage and a file at another, made only by `update-index`; whose gitlink that is, and so which remedy is right, is not ruled. - **The closing guard kept a link to the row's own directory.** Done in fix round 2, in #839's form (rv865b's NIT 4). - **`_sections_restored` (#828) points back every variant section of a member it moves back.** It has the fault MUST 1 found in `_sections_astray`, for a member whose gitlink is at a variant: a second variant section, another submodule's, would be pointed back too, and the path given twice. Read from the code, not measured. - **Found in the checklist pass (t114 row 5), and done in the second commit.** `refused_untouched` and `snapshot` in the tests ask `git status`, which git 2.45.1 and later refuse while a gitlink the index holds is a symbolic link on disk. `snapshot` already took `status=False`. `refused_untouched` now takes it too, and the link test passes it, so the full snapshot is kept except `git status`. Mutant M27 fails the link test on git 2.47.1. Review: rv865, lane 5's independent review, and rv865b's re-checks of each fix round: round 3 and its restack onto #828's squash READY at `6ef6e4c1` (issuecomment-6248254729), and round 4 (`231c404c`, the C5 pin rv865b asked for, tests only) READY with no findings (issuecomment-6248465765). CI (Brett Heap's ruling 27): H0 38e0d503 (fix round 2, the last head before the restack that CI ran in full: GitHub started no tests run on round 3's 4e92867a) passed tests, tests-git-bash and SonarCloud; the restack 6ef6e4c1 merges #828's final head, then #828's squash 9b0a703 (the branch's own side for #828's files), then main; the head 231c404c, 1 commit on the restack, has tests, tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 629aec2b over main c9ca9a45: the family_add_member_variant, family_add_move_back_sections, family_add_spelling_leftovers, family_members_case_variants, repo_hygiene, agents_md_codes, cli_reference sets, 356 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session (posted on #473 as issuecomment-6241758069): "merge lane 5's v1.1.1 PRs once reviewed and green". Closes #840 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-4 (openRepoShape-4)
Claim: #684 (comment)
R612 E4, the last item of #684. #816 landed the other five as "Part of #684" (
fa04d2cd). Against mainbad05d21, two commits:a3683127, and6a71ff8dfor rv846's NITs.Round 1: rv846's four NITs (
6a71ff8d)Lane 5's rv846 reviewed
a3683127(issuecomment-6241234307): READY, no MUST and no SHOULD, four NITs. I took all four in one commit,6a71ff8d, ona3683127, and pushed it as a fast-forward. Nothing was amended. Main is not merged in, because every merge-tree below is clean. The commit changesscripts/family.py(+7/-3, comments only) andtests/test_family_operation_reftable.py(+58/-21), and nothing else.git show-ref --existsruns in the holder. rv846's M10 asks inPath.cwd()instead, and it passed every row on git 2.43pwd -Pfor each ask, and its four tests assert the holder's directory and no other (asked_in). The order test records thecwdthat_git_bytesis given for eachshow-ref{'<the checkout the run started in>'}where the holder was expected, and the order test'snothingandrevertthe stand-in rows hold the refusal in its place, but not on Windows, where they skip too. The docstring says a git before 2.45 makes no reftable, names 2.43 as the git the tests were written on and 2.55 as CI's, and says the stand-in rows skip on WindowsMERGE_HEAD's reason cited git 2.43 alone, and the real-reftable row's comment cited 2.43 in a row that only runs on 2.45 and laterREF_STORE_MARKSnow saysMERGE_HEADis a file on both backends: lane 5's probe shows it on 2.47.1, and git'sis_pseudo_refreads it from its file whatever the backend. So the file is the whole answer. Git 2.43's exit 1 stays as a second reason, with 2.47.1's exit 2 beside it. The row's comment now saysMERGE_HEADis a file on a reftable too (its premise), so only the two refs are asked190133d5and the open PRs (below)What changed and why
The defect. Lane 5's probe (#684 issuecomment-6237529261; the full result on #764, issuecomment-6237519513), on git 2.47.1 in a repository made by
git init --ref-format=reftable:CHERRY_PICK_HEADandREVERT_HEADin the ref store, and no file in.git;MERGE_HEAD,rebase-merge/andsequencer/todostay files, as on the files backend._operation_in_progressaskedos.path.lexistsof each mark, so in such a holder it found neither, andadd,bumpandremovewent on to thegit commit -- <paths>thatfamily-operation-in-progressexists to keep them from.The change (
scripts/family.py, one function added, three edited):_in_ref_store(root, mark), new:git show-ref --exists <mark>, the exact-name ask (git 2.43 and later).--exists. Such a git cannot open a reftable at all, so the holder's refs are files and the file asked first is the whole answer (measured, below).CommandFailed, which the verb prints as it prints every failed git command, exit 2, before its first write. Git's own failure is never read as "nothing in progress"._operation_in_progress: in theCHERRY_PICK_HEADandREVERT_HEADrows (REF_STORE_MARKS), where the file is not there, the ref store is asked; a hit returns the row withwhereNone. The rows keep their order, so a rebase is still asked first and a bisect last. The file checks stay, for those two marks too and for every other mark._checked_no_operation_in_progressand_unparsable_in_an_actsaygit keeps CHERRY_PICK_HEAD in its ref store, not as a filewherewhereis None. A mark found as a file is namedat <path>, byte for byte as before. The remediation is the act's own, unchanged.What a reftable holder now prints (taken from the stand-in run, below):
Mirrored from #815, not imported. #815 (#764, head
05a1ebb3, not landed) asks the same two names with the same command in the assembly root'sbootstrap.py(IN_REF_STORE,_in_ref_store), in the same row order. One difference, on purpose: #815's_try_git(...) is not Nonereads every non-zero exit as "not held"; this PR reads only 2 and 129 so, and stops on any other, so that git's own failure is never read as "nothing in progress".Tests changed:
tests/test_family_operation_reftable.py(new, below), and one sentence oftests/test_family_holder_states.py's module docstring, which said E4 was not measured yet and now points at the new file. No other test, doc, template or pinned file is changed.README.md,docs/handbook.htmlanddocs/cli.mddo not name this refusal's words.Tests
Measurements, before the change
Probe shell: a stub
ghfirst on PATH,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_ALLOW_PROTOCOL=file,GIT_CONFIG_NOSYSTEM=1.This host's git is 2.43.0, and no git 2.45 or later is on it.
which -a gitnames only/usr/bin/git(2.43.0). A search of/tmp,/workspace,/home,/optand/usr/localfoundgitbinaries answering 2.35.0, 2.36.6, 2.40.4 and 2.43.0 to--version, all under/tmp, and nothing newer;_lane5-scratchholds no git build.git init --ref-format=reftableon 2.43:error: unknown optionref-format=reftable', exit 129.GIT_DEFAULT_REF_FORMAT=reftable git initmakes a files repository, andgit rev-parse --show-ref-formatechoes--show-ref-format` back.git show-ref --exists, files backend, git 2.43.0:CHERRY_PICK_HEADREVERT_HEADerror: reference does not exist.githoldsCHERRY_PICK_HEAD).githoldsREVERT_HEAD)git branch CHERRY_PICK_HEADandgit tag REVERT_HEAD, nothing in progressgit rev-parse -q --verify CHERRY_PICK_HEADand... REVERT_HEADeach print the commit, exit 0. That is why it is not the ask.fatal: not a git repository, exit 128. Two names in one ask:fatal: --exists requires exactly one reference, exit 128, so it is one process per mark.git show-ref --verify CHERRY_PICK_HEADwith a cherry-pick stopped:fatal: 'CHERRY_PICK_HEAD' - not a valid ref, exit 128, so--verifyis no ask on 2.43.MERGE_HEADis no ref-store ask. Found while writing the tests:git show-ref --exists MERGE_HEAD(andFETCH_HEAD) exits 1,error: failed to look up reference: Success, in a fresh repository and aftergit merge --abort; 0 with a merge stopped.CHERRY_PICK_HEAD,REVERT_HEAD,BISECT_HEADandORIG_HEADanswer 2 there.MERGE_HEADstays a file check because it is a file on both backends, as the probe found and as git'sis_pseudo_refreads it. On git 2.43, asking the ref store for it would also stop every verb on "git's own failure". rv846 measured that git 2.47.1 answers 2 there./tmp, run read-only):git show-ref --exists CHERRY_PICK_HEADexits 129,error: unknown optionexists'`. That build was found by searching. Found binaries are not run any more, so no git from 2.34 to 2.42 was measured.core.repositoryformatversion = 1andextensions.refStorage = reftable(written by hand):git rev-parse --git-path MERGE_HEADexits 128,fatal: unknown repository extension found: refstorage, on 2.43.0 and on 2.40.4. That is_git_paths' call, made before any ref-store ask. So a git that answers 129 is only ever asked in a holder whose refs are files.builtin/show-ref.c,refs.c,refs/reftable-backend.cat the tags, read as text, nothing built).--existsexits 2 forENOENTorEISDIRand 1 for any other failure. The reftable backend setsENOENTfor a name it does not hold. So "not held" is 2 on a reftable too.refs_read_raw_refreadsFETCH_HEADandMERGE_HEADas files on every backend, which agrees with the probe.The new test file,
tests/test_family_operation_reftable.py: 18 cases, 14 run here and 4 skipThe stand-in. This host cannot make a reftable, so the refusal is held by a stand-in. It is a
shgitput first on PATH. It answersgit show-ref --exists <name>with exit 0 for a name its store holds, and runs the real git for everything else (which answers 2 for an absent name on the files backend). Each test stops a real cherry-pick or revert with git, then moves the mark's file out of the git directory into the stand-in's store, as a reftable keeps none. It logs the directory each ask runs in (pwd -P), and every stand-in row asserts that this is the holder's directory and no other. The same stand-in also answers as git 2.40.4 does (129) and as a failing git would (1 and 128). The stand-in rows skip on Windows (ashscript), astest_a_git_that_cannot_tell_refuses_nothing_and_the_late_failure_staysdoes.test_a_mark_the_ref_store_holds_and_no_file_does_is_refused_alikeadd,bump; revert:remove,add)--dry-runin the same words,in its ref store, not as a file, the act's own remediation; exit 2, nothing on stdout, the holder byte for byte as it was (#612'sassert_refused_alike); the stand-in asked once a runtest_a_cherry_pick_conflicted_in_family_yaml_names_the_ref_store_unparsable_in_an_act:yaml-unparsablekeeps its line, and its remediation names the cherry-pick, the ref store and its exits, under both runstest_a_git_before_2_43_answers_from_the_files_aloneaddmakes its one committest_git_failing_to_answer_stops_the_verb_before_its_first_writeadd, exit 128remove)REFUSED a git or gh command failed. THE EXACT COMMAND WAS: git show-ref --exists CHERRY_PICK_HEAD ... exit <n>, exit 2, nothing on stdout, the holder as it was, under both runstest_a_branch_and_a_tag_named_as_the_marks_are_no_operationrev-parse -q --verifyresolves both names andshow-ref --existsneither (premises), and each verb makes its one committest_the_file_is_asked_first_and_the_ref_store_only_where_none_isCHERRY_PICK_HEAD); a cherry-pick's file before any; argv exactlygit show-ref --exists <mark>, asked in the holder (thecwdthat_git_bytesis given); a file is named at its pathtest_on_a_reftable_each_act_is_found_where_git_keeps_its_mark--abort, typed, ends the act and nothing is found aftertest_on_a_reftable_a_branch_and_a_tag_named_as_the_marks_are_no_actThe skip reason is printed:
this git cannot make a reftable repository (git init --ref-format=reftableneeds git 2.45 or later); the stand-in rows hold the refusal in its place, but not on Windows, where they skip too. The check is made of git itself:git init --ref-format=reftable, thengit rev-parse --show-ref-formatmust sayreftable.The real-reftable rows' own steps were run once here, by emulation. A scratch test, never committed, ran the same steps on the files backend with the stand-in in place of a reftable: 4 passed. That run is how the
show-ref --exists MERGE_HEADexit 1 above was found. The real-reftable merge row now checks thatMERGE_HEAD's file is gone after the abort, and asksshow-refonly of the two ref marks.The real-reftable rows ran, and passed, on CI. CI's Linux
testsjob runspytest tests -qon the ubuntu-24.04 image, whose git is 2.55.0 (git version 2.55.0in this PR's log).refs/remotes/pull/846/merge(9a752c50, this head merged into mainfc867207): 12272 passed, 145 skipped, 1 xfailed, success.fc867207's run 39060634121: 12254 passed, 145 skipped, 1 xfailed.18 passed, with each real-reftable row listed as PASSED.The run
/usr/bin/python3 -m pytest, Python 3.12.3, git 2.43.0, ata3683127with a clean tree. The gh stub was first on PATH withGH_TOKEN=bogusand an emptyGH_CONFIG_DIR;GIT_ALLOW_PROTOCOLwas not exported. One reused basetemp,-o tmp_path_retention_policy=failed.tests/test_family_operation_reftable.py(new)tests/test_family_refuse_up_front.pytests/test_family_holder_states.pytests/test_family.pytests/test_family_member_path.py(no git call before themember-pathrefusal)tests/test_family_dry_run.py(a dry run runs no git that writes)tests/test_refusal_default_remediation.py(yaml-unparsablethroughFamily.open)tests/test_repo_hygiene.pytests/test_cli_reference.pytests/test_agents_md_codes.py512 passed, 4 skipped in 422.05s, one run, at
a3683127.Round 1, at
6a71ff8d(the same ten files, the same settings, clean tree): 512 passed, 4 skipped in 424.74s. The four skips are the real-reftable rows, with the new reason. The four skips are the real-reftable rows.git grepovertests/for_operation_in_progress,operation-in-progress,_unparsable_in_an_act,CHERRY_PICK_HEADandREVERT_HEADnames no other family test file (test_bootstrap_leg_moves.pyis the assembly root'sbootstrap.py).Fails on base. The new file run on main's
family.py(bad05d21), in a scratch clone: 10 failed, 21 passed, 4 skipped, against the new file and the 17-case files-backend set below. The 10 are:addmountedIRQSand committed, exit 0, andbumpcommittedmembers/IRRS, family.yaml, over a cherry-pick or a revert held in the ref store;family.yamlcherry-pick;nothingandrevertcases.The exact-name rows, the order test's
cherry-pickcase and all 17 files-backend rows pass on base, by design: they are the controls.Mutants
Each was applied to
a3683127'sscripts/family.pyin a scratch clone of its own, one at a time. Each was run against the new file andtest_family_refuse_up_front.py::test_every_unfinished_act_is_named_and_refused_alike_in_both_runs(17 cases: every act on the files backend). Each is killed._operation_in_progress)addwent on, exit 0), thefamily.yamlone, the 129 case and both failure cases (no ask), and the order test'snothingandrevertgit rev-parse -q --verify <mark>in place ofgit show-ref --exists <mark>, with its exit 1 read as "not held"test_a_branch_and_a_tag_named_as_the_marks_are_no_operation[add|bump|remove]:REFUSED family-operation-in-progress: a cherry-pick is unfinished ... in its ref storeover a branch calledCHERRY_PICK_HEAD, exit 2. Also the 10 that MA fails, since the stand-in answers onlyshow-refexcept CommandFailed: return False)test_git_failing_to_answer_stops_the_verb_before_its_first_write[1-add]and[128-remove]:addmountedIRQSand went on, exit 0CHERRY_PICK_HEADandREVERT_HEAD(the ref store asked alone)test_family_refuse_up_front.py::test_every_unfinished_act_is_named_and_refused_alike_in_both_runs[cherry-pick-add|cherry-pick-remove|revert-bump|revert-remove]: the line saysin its ref storewhere it saidat <path>. Also the order test'scherry-pickandrevertMERGE_HEAD, the rebases,BISECT_LOG,sequencer/)addmountedIRQSover a mergetest_a_git_before_2_43_answers_from_the_files_alone:REFUSED a git or gh command failed ... git show-ref --exists CHERRY_PICK_HEAD ... exit 1296a71ff8d):_in_ref_storeasks inPath.cwd()a3683127's tests)asked_ingives the checkout the run started in, not the holder), and the order test'snothingandrevert(cwdis not the holder)What it cannot tell
-q, which names no passing test. Those rows call the check in process, on a plain repository. This PR's tests run nofamily.pyverb against a reftable holder. rv846's own end-to-end probe did: it moved a holder to a reftable withgit refs migrate, and each of the six pairs of verb and act was refused in both runs.add,bumpandremove: a merge or rebase in progress, an excluded.gitmodulesor amembersthat is a file makes them fail after their first writes #612): git refuses the partial commit during a cherry-pick, and takes it as the end of a revert. On a reftable they are assumed to hold, and were not measured.sequencer/left, noCHERRY_PICK_HEAD) was not probed. Thesequencer/todorow is a file on both backends (the probe), and is asked as before.--existshas no usage error to give. Measured on 2.40.4 only; 2.34 to 2.39 were not run.family.pyfor ref files read from disk (.git/HEAD,refs/heads/...,packed-refs,*_HEAD) finds none but these marks; everything else is asked of git.Shared files
Checked from
6a71ff8don 2026-10-17 at about 21:45Z withgit merge-tree --write-tree. Every check is clean, so main is not merged in.190133d5: clean, treedb47ec8b.bad05d21, main has changedscripts/family.py(+202/-8, family.py:bumpfetches every branch of the member'soriginwhatever the mount's refspec and drops theorigin/refs no ref oforiginbacks, says a tag of the remote reaches a commit no branch does instead of "a pin the rest of the world cannot fetch", and tells a mount with nooriginit has none instead of to fix the network (#686) #795, landed as6f64eea2) and none of this PR's test files.a3683127merged with190133d5(fb390d38, tree443da586) over all 29tests/test_family_*files,test_refusal_default_remediation.pyand the hygiene trio: 1318 passed, 23 skipped.bootstrap REFUSED, exit 2, an unreadable upstream path, cache or pin is no answer and a reftable leg's cherry-pick or revert is read from its ref store, so no traceback or ended pick follows (#764) #815 (bootstrap.py (the assembly root's): aSHAPE_UPSTREAM_PATHdirectory nobody may enter ends in a traceback, a validator's refusal is reported asFAILED, exit 1, a reftable leg's cherry-pick goes unseen, anddocs/cli.mdsays bootstrap places each leg AT the pin with no condition #764, lane 5,fix/764-bootstrap-notice-exit-textsat05a1ebb3, not landed) mirrors this check intemplates/assembly-root/scripts/bootstrap.py. It changes no file this PR changes. Clean, treeae27cb3a.scripts/family.py(fromgh pr list --state open, by files). None of them changestests/test_family_holder_states.pyor the new test file:fix/765-branch-read-bytes,25692a37): clean, treeabd65e5f..gitmodulesback, sets aside first what the index holds in its way and puts back a member whose removal is staged, its closingmvis guarded and leaves a checkout to delete by hand, and--sparsefollows the sparse rules, so no printed line leaves a section naming another spelling or un-registers a member (#761) #828 (fix/761-add-move-back-gitmodules,ce0f2a5f): clean, treeb31c4559.init's plan rows, seven refusals and bump-leg's root name each path asshown_pathnames it, so an ESC or a line break in a holder, folder, lock, local remote or root no longer reaches the terminal from those plan rows or those refusals' details #814 (fix/766-spelled-paths,b050dc71): now clean, treec6c1675e. Its branch is now on main6f64eea2and no longer carries scaffold-project.py, family.py and shape_materialize.py: #622's folder refusals print a folder name raw, so an ESC or a line break reaches the terminal, andfamily-operation-in-progressprints a holder's path across two lines #692's change to thegit keeps ... atlines, the change that conflicted atef6db40d.bumpfetches every branch of the member'soriginwhatever the mount's refspec and drops theorigin/refs no ref oforiginbacks, says a tag of the remote reaches a commit no branch does instead of "a pin the rest of the world cannot fetch", and tells a mount with nooriginit has none instead of to fix the network (#686) #795 (fix/686-bump-reachability), listed here before, has landed as6f64eea2.Closes #684
🤖 Generated with Claude Code
Summary by Sourcery
Refuse family mutations for unfinished cherry-picks and reverts whose operation markers are held in a reftable ref store.
Bug Fixes:
add,bump, andremovefrom proceeding when a cherry-pick or revert is unfinished in a reftable-backed holder.Enhancements:
Tests: