Repository navigation
validate-family.py: the check for values git dies on reads the index's .gitmodules beside the work tree's and its stage remedies end git add -- .gitmodules, an index file git cannot parse is refused, and an unmerged one is read as none (#1150) - #1227
brettheap wants to merge 11 commits into
Conversation
…git dies on read the `.gitmodules` git reads, the index's where the work tree's is gone, and every section's lines end `git add -- .gitmodules`, so a plain `git commit` after them takes the section out of HEAD (#1077) RV1069's review of #1069 (#1036) found four follow-ups, each measured on main before this change. NIT 1. `gitmodules-path-twice` read the work tree's file alone. A second section for members/IRRS staged in the index's file, the work tree's deleted, was `family ok` in every mode, and a clean clone of the commit after it failed its update and its gate. It reads the sections as git reads them now (`_sections_as_git_reads`), as `gitmodules-path-unnamed` does since #1036, and where the work tree's file is gone it names the line that puts back the file git reads first, recorded so no later finding puts it back over the hand edit. NIT 2. The value check read the work tree's file and HEAD's, never the index's. An `update = !true` staged in the index's file alone, in a named member's section, was `family ok` while git's own `submodule status` exited 128. It reads the file git reads (`_listed_as_git_reads`) and names the index's where that is the one read. Its lines put that file back, not HEAD's, and end "then stage", or "then stage and commit" where HEAD's gives the value too. Where HEAD's alone gives it, the text says to commit the index's file, with no line, so a fix staged there is kept, and `member-gitmodules-absent` then puts the file back itself (`_check_gitlinks_registered` asks `Report.put_back` alone). Git is asked in the order it was asked before, the work tree's file, then HEAD's, then the one read in its place, so `git-not-runnable` names what it named. NIT 3. Where HEAD's file held the section, `gitmodules-path-unnamed`'s lines ended "then commit .gitmodules" and staged nothing, so a plain `git commit` after them left the section in HEAD and a clone's gate failed. Every branch, in members/, outside it and in another case, prints `git add -- .gitmodules` after the keys and says "then stage and commit" there; path-twice ends "then stage and commit" too. NIT 4. A row now covers the case-variant branch where HEAD's file holds no section of the name (RV1069's probe `test_r2_stray_not_in_head`). tests/test_validate_family_index_gitmodules.py holds the new rows: each types only the printed lines, and what the text says in words, then a plain `git commit`. The older rows that pinned the old text, HEAD's put-back and #1036's control are updated to the new behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…its own, so #1111's hygiene test passes, path-twice says "back first from" the file it puts back, and two rows pin the value's write-back and path-twice's recorded put-back (#1077) Fix round 1 on #1124, from RV1124's review at 672845c. S1. `_listed_as_git_reads` opened with a bare `os.path.lexists(root / GITMODULES)`, which #1111's `test_path_guard_hygiene.py` fails by file and line. Its one caller, `_check_gitmodules_values`, asks it only where the work tree's file is gone, and `_gitmodules_put_back`, which the function calls next, asks the same `lexists` (it is on the test's allowlist), so the guard is dropped. N3. Two behaviours had no row; mutants A and B survived all 439 rows of the PR's nine files. A row for each, from RV1124's probes P10 and P7: - the value's write-back reads the file git reads, not HEAD's (A); - path-twice records its put-back, so `gitmodules-path-unnamed` after it prints none and a plain commit keeps neither `second` nor `members/Old` in HEAD (B), where the work tree's file is deleted or its deletion is staged. N4. Path-twice said "back from HEAD's, as the index holds none first": "first" sat on the wrong phrase. It says "back first from {whose}". N5. `_gitlink_out_first`'s docstring said its text ends "then commit .gitmodules"; every branch stages now, and it says which words each branch ends with. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s `.gitmodules` beside the work tree's and its stage remedies end `git add -- .gitmodules`, an index file git cannot parse is refused, and an unmerged one is read as none (#1150) RV1124's review of #1124 (pull/1124#issuecomment-6321372469) found three things outside #1124's fix round, each older than #1124 and measured again on main 961640c and on #1124's head 8dfa32b before this change: - N1: where the work tree's `.gitmodules` is on disk, the check never read the index's, which a plain `git commit` records. A value in both was taken out of the work tree's by a line that staged nothing, so the commit after it recorded the value in HEAD; a value in the index's alone was `family ok`. The check reads the index's too now, names the latter ("in the index's file alone"), and every remedy that says to stage the file ends with the line that does. - N2: an index's file with a line git cannot parse was read as nothing (`git config --blob` exits 128 there), so a value or a section before the line was never named. It is `gitmodules-unreadable` now, as HEAD's is. - N6: while a merge leaves `.gitmodules` unmerged in the index, git's submodule commands read no file, and the checks read the work tree's or HEAD's. They read none now; a value HEAD's gives says to resolve the merge without the key, then stage and commit. One finding changes: a value only the work tree's file gives, on disk, while unmerged, HEAD's clean, is no finding until the merge is resolved and staged, as git reads no file until then. Stacked on #1124 at 8dfa32b: every item also holds on main, but the code each fix changes (`_blob_listed`, `READ_HERE`, `_listed_as_git_reads`, the stage line) is #1124's. Closes #1150 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ules` and the work tree's give, with no index's file, is pinned to end with the line that stages it, as HEAD's value, not the index's, is what adds that line (#1150) Mutant M3 of 442615e (`if committed or staged:` cut to `if staged:` in `_value_taken_out`) survived all 170 rows of the three changed test files: in every row whose lines take out a value HEAD's file gives, the index's file gives it too, or the work tree holds no `.gitmodules`, so `staged` was true there anyway. The new row has the work tree's file and HEAD's give `update = !true`, and `git rm --cached` has taken `.gitmodules` out of the index. Under M3 the finding ends at the `--unset-all` line, and the plain `git commit` after it records `.gitmodules` gone from HEAD, and the gate then finds `member-gitmodules-absent`, exit 1. The row fails under M3, on #1124's head 8dfa32b and on main 961640c, and passes on 442615e. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e for: templates/family-root/scripts/validate-family.py tests/test_family_gitmodules_values.py tests/test_validate_family_index_gitmodules.py Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideImplementationUpdate
TestsUpdate existing value-validation tests to expect the explicit staging command and the new refusal for an unparsable indexed
Revise index-file tests to type the printed staging command and verify that an index-only value remedy leaves the committed configuration clean.
Add regression coverage for index-only and differing indexed values, unreadable indexed files, unmerged
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Independent review of #1227 (#1150) for lane openRepoShape-5 PARTIAL at The usage limit stopped the review before the mutants and the rows-on-main run. What did run already gives two MUSTs. All probes ran offline through MUSTM1. The PR no longer merges cleanly in behaviour with current main: one row fails.
M2. While
SHOULDS1. A staged value followed by a work-tree-only value still ends in HEAD (validate-family.py:2116-2129).
S2. In a sparse checkout that leaves
NIT
Checked and true
Not checked
(Posted by lane openRepoShape-5's coordinator. rv1227 is lane 5's independent opus reviewer; it wrote none of #1227. This review is PARTIAL: the usage limit stopped it before its mutants and the rows-on-main run. Its two MUSTs stand, so #1227 is NOT READY. Lane 5 takes M1, M2, S1 and S2 as a fix round: a merge of main that re-pins #1208's row, the unmerged-section reads, and the stage line. A completed review of the new head follows.) Lane: openreposhape-5 (openRepoShape-5) |
#1208 touches validate-family.py (a docstring) and tests/test_validate_family_index_gitmodules.py (a new row); the text merges cleanly. The new row's re-pin to this branch's stage line follows in its own commit (rv1227's M1). Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ue HEAD gives where the deletion of `.gitmodules` is staged expects the stage line last and types no `git add` by hand, since this branch's lines end with it rv1227's M1 on #1227. Main's #1208 added the row while its finding ended at the `--unset-all` line; this branch's `_value_taken_out` ends every line set that says to stage the file with `git add -- .gitmodules`. Measured on the merge (git 2.43.0): the three printed lines typed in order, then a plain `git commit`, leave HEAD without the value, the index and the work tree equal to it and `git status` clean, and the gate passes; typed without the stage line, the commit exits 1 (`no changes added to commit`) and HEAD keeps the value. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and the init stop ask nothing, a value the work tree alone gives after a staging line is staged again, and another index value of the key is named, not "gives it too" rv1227's review of #1227 (issuecomment-6374575208), its M2, S1, N1, N2, N3, N4 and N5, each measured on git 2.43.0 on the branch merged with main e04826d (d95308e) before this change: - M2. With `.gitmodules` at conflict stages, git reads no file: `git submodule status` and `update --init` exit 128 at every gitlink, and a plain clone of HEAD checks tools/x out. `_sections_as_git_reads`' none made `gitlink-unnamed` say no section gave tools/x and a plain clone stopped there, and `member-uninitialized` say to write back a section the file gave. `_check_gitlinks_elsewhere` and `_init_stop_said` ask nothing while unmerged now. The hand mount's reads keep reading none, as git's own `git rm` does there: its plain `git rm` of a clone dies (`could not lookup name for submodule`, exit 128), and `--cached`, the line it prints, exits 0. - S1. `_value_taken_out` takes `again`: after a finding whose lines end `git add -- .gitmodules`, a value the work tree's file alone gives ends with that line too, and says why (`STAGED_ABOVE`). - N4. Where the index's file gives the key another value, the finding names it (`INDEX_GIVES`, `_values_said`) in place of "gives it too". - N1, N5. `_blob_paths` and `_blob_sections` say they refuse an index's file git cannot parse; `GIVEN_WHERE`'s comment says `(False, False)`. tests/test_validate_family_index_value_unmerged.py: rows for M2 (e2d, two ids; e2e), S1 (e4o1, e4o2), N4 (e4c), N2 (e1b), N3 (a real modify/delete conflict, and the sections row's on-disk id). S2, the sparse checkout, is a Limit of #1227 and a follow-up issue. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review of #1227 (#1150) for lane openRepoShape-5, by rv1227b (opus), at 0d65ea8: NOT READY PARTIAL. The coordinator stopped this review at 83% usage, so it is incomplete. Nothing I measured found a MUST or a SHOULD. I marked it NOT READY only because most of the brief was not checked: the mutants, the rows on main, the 2.47.1 gate, the hygiene gate and the merge-trees are still open. Do not read it as READY. How the runs were set up:
What I checked, with logs1. rv1227's 14 probes, re-run at the head
2. Gate
3. Body numbers, re-measured
4. Code read of the fix-round diff (
FindingsMUST: none found in what ran. SHOULD: none found in what ran. NIT, and leads that were not measured:
Not done (what is left)
Notes and the CHECKPOINT are in Lane: openreposhape-5 (openRepoShape-5) |
|
(lane openRepoShape-5, 2026-11-08T14:29:53Z) Independent review of #1227 (#1150) for lane openRepoShape-5, by rv1227b and rv1227c (opus), at 0d65ea8: NOT READY Why NOT READY: one SHOULD, and it needs only a test. The code is right in every state I measured. But two of my mutants on the S1 code survive all 181 rows of the PR's three files, and I showed with probes that each one puts a value git dies on into HEAD. Adding a row that kills K6 and K8 is all that stands between this PR and READY. No MUST was found. Setup
Carried forward from rv1227b (issuecomment-6385625905, measured at 0d65ea8 on git 2.43.0)
What rv1227c did (items 1 to 7 of R2)1. Edge probes. The probe file is
2. Rows on main. I wrote main's script into
3. Mutants. Each was applied alone in
4. Gates at 0d65ea8 (dirty=0 in every START line).
5. Merge-trees (
6. The body's counts and "What ran where".
7. #1242 as the follow-up for S2: fair.
FindingsMUST: none. SHOULD
NIT
Not done
Lane: openreposhape-5 (openRepoShape-5) |
…e S1 fix carries to every later finding is pinned for a third value and for a committed value the index lacks, and N4's row also for an index file giving the value and another rv1227c's independent review of #1227 (issuecomment-6722728204), its SHOULD S1, test-only. Two mutants of the S1 code survived all 181 rows of the PR's three files, and probes showed each puts a value git dies on into HEAD: K6 (`staging = committed or staged`, the stage line not carried past the next finding) and K8 (`staging = staging or staged`, a committed value's stage line not carried). The S1 row pinned only the finding right after a value that is both staged and committed. validate-family.py does not change. - test_every_later_value_the_work_tree_alone_gives_stages_again (new, rv1227c's p2a): `update = !true` staged for IRRS, then `update = !false` and `fetchRecurseSubmodules = sometimes` in IRSS's section, the work tree's file alone. The second and third findings each say `STAGED_ABOVE` and end with the stage line; typed in order, then a plain `git commit`, HEAD is as it was. K6 fails it. - test_a_committed_value_the_index_lacks_stages_the_later_ones_again (new, rv1227c's p8): IRRS `!true` committed, the index's file staged without it, the work tree's giving IRRS `!true` and IRSS `!false`. IRRS's finding is committed and not staged, and its `git add` stages IRSS's value all the same, so IRSS's ends staging again. K8 fails it. - test_another_value_the_index_s_file_gives_the_key_is_named gains an id (rv1227c's p3c, its K11): the index's file giving `!true` and `!false` beside the work tree's `!true` names both. The old id is `[another-value]` now, the new one `[this-value-and-another]`. - `staged_again` builds the later finding's text for the S1 row and the new rows, which the S1 row spelled out inline. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
|
(lane openRepoShape-5, 2026-11-08T15:51:20Z) Independent delta review of #1227 (#1150) fix round 2 for lane openRepoShape-5, by rv1227d (sonnet), 0d65ea8..2d56c19: READY I found no MUST and no SHOULD. READY is for head What I checked, and what held
NIT 1: the Goal's "except" clause and the new "Two reads" Limits bullet are incomplete.
NIT 2: the P4 Limits bullet and #1254 describe one of the three forms.
NIT 3: two further survivors on the S1 clearing, text-only. These are my own mutants, outside the K-list. Both ran with 184 rows, 2.43.0, and the script was restored each time (sha256
NIT 4: smaller items.
Not done
Housekeeping
Files (all under
Lane: openreposhape-5 (openRepoShape-5) |



Goal
#1150: after this PR the family gate's check for values git dies on (
gitmodules-value-refused) reads the index's.gitmodulesbeside the work tree's, so each remedy that says to stage the file ends with the line that stages it (a later value the work tree alone gives too, once a line above stages the file) and a value only the index gives is named; an index file git cannot parse is refused asgitmodules-unreadableinstead of read as nothing; and while a merge leaves.gitmodulesunmerged the checks read no file git would read (neither the index's nor the work tree's), as git reads none then, sogitlink-unnamedand the init stop ask nothing in that state. The checks that ask about HEAD, which a clone reads, still read HEAD's file, and the staged-removal check can read the work tree's, as the Limits say.This is a DRAFT on
main. It is the follow-up lane 4 filed as #1150 from RV1124's review of #1124 (pull/1124#issuecomment-6321372469): the review's N1, N2 and N6 were outside #1124's own review round, each is older than #1124, and each was measured again before this change. #1124 has landed asdea964aa1, and this branch was restacked onto it (see "The restack"). Fix round 1 answers rv1227's review (issuecomment-6374575208) and merges maine04826db4(#1208). Fix round 2 answers rv1227b's and rv1227c's review (issuecomment-6722728204): one SHOULD, test-only (see "Fix round 2"). The one script istemplates/family-root/scripts/validate-family.py; with its tests the change is four files, +1128 and -78 lines (git diff --shortstat origin/main...HEAD -- <the four files>, against the merge basee04826db4with main0d5704b3d).Fix round 1: rv1227's review
rv1227's independent review (issuecomment-6374575208, PARTIAL at
b031f4938, NOT READY) found M1, M2, S1, S2 and N1 to N6. Each was measured again before the change: git 2.43.0, offline through lane 5'srun9.sh, on the branch merged with maine04826db4(d95308e95). The probe isv124/n1150/d/probes/test_n1150d_probe.pyin lane 5's scratch, never committed; its logs are inv124/n1150/d/probes-out/merged-d95308e/.d95308e95merges maine04826db4(test_validate_family_index_gitmodules.py and validate-family.py: a row pins path-twice's wording of whose file it puts back, the index's or HEAD's as the index holds none, a row pins the value check's reading of HEAD's where the deletion of.gitmodulesis staged, and_listed_as_git_reads's docstring says it returns nothing where the work tree's file is on disk (#1181) #1208); its tree isa151d1110, the onegit merge-treepredicted. test_validate_family_index_gitmodules.py and validate-family.py: a row pins path-twice's wording of whose file it puts back, the index's or HEAD's as the index holds none, a row pins the value check's reading of HEAD's where the deletion of.gitmodulesis staged, and_listed_as_git_reads's docstring says it returns nothing where the work tree's file is on disk (#1181) #1208's new rowtest_1077_a_value_head_gives_where_the_deletion_is_staged_puts_head_s_backpinned the finding's lines ending at--unset-alland typedgit addby hand.m1-*.log):update = !truecommitted, the deletion of.gitmodulesstaged. The three printed lines (git checkout HEAD -- .gitmodules, the--unset-all,git add -- .gitmodules) typed in order, then a plaingit commit: exit 0, HEAD holds no value, the index and the work tree equal it,git statusis clean, and the gate passes. Typed without the stage line, the commit exits 1 (no changes added to commit) and HEAD keeps!true. The merged answer is right.8938d17df): the row expectsgit add -- .gitmoduleslast and types nogit addby hand..gitmodulesis unmerged.m2-*.log): with.gitmodulesat stages 1 to 3, git reads no file.git submodule statusandgit submodule update --initexit 128 at every gitlink (no submodule mapping found,No url found for submodule path 'tools/x', and'members/IRRS'for the whole update), and a plain clone of HEAD checkstools/xout (exit 0). The head saidgitlink-unnamedoftools/x(no section gives it, a plain clone stops there) and, for a member not checked out,member-uninitializedsaid to write back a section the file gave. The family bootstrap, in the second state, saysNOT CHECKED OUT, and not for want of a credential: members/IRRS (git reads no .gitmodules while a merge leaves it unmerged). Resolve the merge in .gitmodules and `git add .gitmodules`, then run this again._check_gitlinks_elsewhere(gitlink-unnamed) and_init_stop_saidask nothing while.gitmodulesis unmerged, as no section is read then.gitlink-unnamedsays what a clone does, and which section the commit after the merge records is the merge's to say; resolved and staged, the gitlink is asked about again.member-uninitializedgives the plain update, whosemake bootstrapnames the cure.git rmreads there (measured): a plaingit rmof a clone whose.gitis a directory, at a path a section gives, dies (could not lookup name for submodule 'members/IRQS', exit 128), andgit rm --cached, the line_hand_cachedprints because it reads none, exits 0 (main read the work tree's file and printed the plain line, which dies). With the work tree's file deleted, the plaingit rmexits 0 and takes no section, so_plain_rm_saidputs nothing back; HEAD's put back would resolve the merge to HEAD's side._given_as_git_reads' docstring says so._value_taken_outtakesagain, set by_check_gitmodules_valuesonce a finding before it ends with the stage line: a later finding whose value the work tree's file alone gives then ends withgit add -- .gitmodulestoo, and says why (STAGED_ABOVE: "A line above stages the work tree's .gitmodules, this value with it, so the lines below end staging it again.")..gitmodulesout: kept out of this PR, as a Limit and the follow-up validate-family.py: in a sparse checkout that leaves.gitmodulesout, the put-back linegit checkout -- .gitmodulesmatches no file and everygit add -- .gitmodulesthe findings print exits 1, so no printed line set cures a finding there #1242.s2-*.log):git checkout --ignore-skip-worktree-bits -- .gitmodulesputs the index's file back (exit 0, the bit cleared), the--unset-allthen exits 0, butgit add -- .gitmodulesstill exits 1 (outside of your sparse-checkout definition);git add --sparse -- .gitmodulesexits 0, and the lines so typed end well.STAGE_LINE) that about ten findings print, most of them not this PR's. Fixing only this PR's two findings would leave the others failing in the same state._blob_pathsand_blob_sectionssay they refuse an index's file git cannot parse (gitmodules-unreadable, through_blob_listed) and read none for nospec.[on-disk]id for the sections row (rv1227's e2f). A value git dies on cannot stand in for the section in the modify/delete row: with one in the work tree's file,git mergeitself dies (invalid value, exit 128, measured:n3-*.log).The index's file, which a plain `git commit` records, gives it the value `!false`.,INDEX_GIVES,_values_said) in place of "gives it too".GIVEN_WHERE's comment says what(False, False)is.Fix round 2: rv1227b's and rv1227c's review
The independent review (issuecomment-6722728204, at
0d65ea80c, NOT READY) found no MUST and one SHOULD, S1, which needs a test and no change to the script.validate-family.pyis byte for byte0d65ea80c's (blob18a0942d4, sha25630dc80fe29de). The one commit,2d56c1906, changestests/test_validate_family_index_value_unmerged.pyalone (109 insertions and 13 deletions), and the branch is a fast-forward of0d65ea80c. Git 2.43.0 unless a line says 2.47.1, offline through lane 5'srun10.sh(stubgh,GH_TOKEN=bogus-l5,GIT_SSH_COMMAND=false,GIT_CONFIG_GLOBAL=/dev/null); the logs are in lane 5's folderwork/n1150g/.staging = staging or committed or stagedin_check_gitmodules_values. rv1227c's mutants K6 (staging = committed or staged: the stage line is not carried past the next finding) and K8 (staging = staging or staged: a committed value's stage line does not carry) each passed all 181 rows of the three files, and its probes showed that each puts a value git dies on into HEAD (p2m:fetchRecurseSubmodules = sometimes; p8:update = !false). The S1 row pinned only the finding right after a value that is both staged and committed. Two rows are added, and one id;staged_againbuilds the later finding's text for the S1 row and the new rows, which the S1 row spelled out inline.test_every_later_value_the_work_tree_alone_gives_stages_again(rv1227c's p2a, a third value).update = !trueis staged for IRRS, thenupdate = !falseandfetchRecurseSubmodules = sometimesare given in IRSS's section, in the work tree's file alone. The gate gives three findings, and the second and third each carrySTAGED_ABOVEand end with the stage line. The printed lines are typed in order and a plaingit commitfollows: HEAD is as it was,git statusis clean, the gate passes in every mode and a plain clone checks both members out. K6 fails it.test_a_committed_value_the_index_lacks_stages_the_later_ones_again(rv1227c's p8, a[committed-not-staged]id).update = !trueis committed for IRRS, the index's file is then staged without it, and the work tree's file gives IRRS!trueagain and IRSSupdate = !false. IRRS's finding is committed and not staged, and itsgit add -- .gitmodulesstages IRSS's value all the same, so IRSS's finding ends staging the file again. The same typed lines and checks. K8 fails it.test_another_value_the_index_s_file_gives_the_key_is_named[this-value-and-another](rv1227c's p3c, the optional id). The index's file gives IRRSupdateboth!trueand!falsebeside the work tree's!true, and the finding names both ("gives it the value!trueand the value!false"). The N4 row is one parametrized row of two ids now:[another-value], the old row, and this one. K11 fails it..gitmodulesis unmerged,member-gitlink-removal-stagedprintsgit checkout HEAD -- .gitmodules"as the index holds none", and that line resolves the merge to HEAD's side, dropping the other side's stage and overwriting a hand resolution on disk #1254 and validate-family.py: a staged deletion of.gitmoduleswith the file still on disk isfamily ok, and a plaingit committhen drops.gitmodulesfrom HEAD, so every member's gitlink is left empty in a recursive clone #1255, not changes here; the Limits below name NIT 1's. NIT 4 (a gitlink outsidemembers/that no file gives gets no question while unmerged; true on main) is the existing Limit's. K1 and K5 (NIT 5) are not touched by this round and were not run again. NIT 6 is optional and unchanged.Rows that fail on main (fix round 2). Run at
2d56c1906withvalidate-family.pyas main held it frome04826db4to11560eb9f(blob172647b3e, sha25630abb4ea28e6), and again with the script as main0d5704b3dholds it (blobee26aede6, sha25607f843df31b7, which differs by105b5daf9's 12 added and 3 removed lines). Each was written into the worktree, then the head's script was restored and its sha25630dc80fe29dechecked,git statusclean after (mut/MAIN.logandmut/summary.log;mut/MAIN2.logandmut/summary2.log).[this-value-and-another]), and the 142 that pass are the same 142.test_every_later_value_the_work_tree_alone_gives_stages_again,test_a_committed_value_the_index_lacks_stages_the_later_ones_againandtest_another_value_the_index_s_file_gives_the_key_is_named[this-value-and-another]; the old N4 id,[another-value], and the S1 row's[staged]and[committed]fail as before. Each new id fails at its first assertion,found == [...], at index 0: main's first finding ends at its--unset-allline, where the row expectsgit add -- .gitmoduleslast.Mutants (fix round 2). Each was applied alone in a detached worktree at
2d56c1906(work/n1150g/mut/mutants.py: rv1227c's file, one exact replacement checked to occur once), the three test files were run in full throughrun10.sh, the head's script was restored and its sha25630dc80fe29dechecked, andgit statuswas clean after each (mut/summary.log, one log per mutant beside it). No other run used the worktree while a mutant was applied.staging = committed or staged(the stage line is not carried past the next finding)test_every_later_value_the_work_tree_alone_gives_stages_again(new)staging = staging or staged(a committed value's stage line does not carry)test_a_committed_value_the_index_lacks_stages_the_later_ones_again(new)set(staged) >= set(values)(the index's file giving the work tree's value and another says "gives it too")test_another_value_the_index_s_file_gives_the_key_is_named[this-value-and-another](new id)gitlink-unnamedasks while unmerged where the work tree's file is on disktest_no_gitlink_outside_members_is_asked_about_while_unmerged[on-disk]test_a_member_not_checked_out_while_unmerged_is_told_the_plain_updateagainis not cleared for a finding that stages anyway (again = again)test_two_keys_with_no_gitmodules_in_the_work_tree_put_it_back_onceINDEX_GIVESnames the work tree's valuestest_another_value_the_index_s_file_gives_the_key_is_namedAll seven are killed. K6, K8 and K11 survived the 181 rows of the first run (rv1227c); K2, K3, K7 and K13 were killed then and still are, by the same rows.
Runs (fix round 2). All through
run10.sh, eachSTARTline recording the head anddirty=0; the full suite was not run.logs/g243-f3.loglogs/g247-f3.logtests/test_path_guard_hygiene.py,tests/test_repo_hygiene.py,tests/test_agents_md_codes.py,tests/test_cli_reference.py,tests/test_pinned_copy_text.pylogs/hyg243.log804213b71of the head with mainb66d8f0cclogs/merged243c.log875f5aee3with main105b5daf9logs/merged243b.logfda589e96with main11560eb9flogs/merged243.logWhat was measured before the change (the first round)
All probes are offline:
run9.shputs a stubghfirst on PATH and setsGIT_SSH_COMMAND=falseandGIT_CONFIG_GLOBAL=/dev/null, and git is 2.43.0. The probe istests/test_n1150_probe.py, a scratch file that is not committed; its logs areprobe-main.log(main961640c6, 2026-10-22T20:08Z) andprobe-parent.log(#1124's head8dfa32bd, 20:09Z), in lane 5's scratchv124/n1150/. The scratch families have gitlinks atmembers/IRRSandmembers/IRSS. Paths such asmut/...,logs/...andprobe-*.logbelow are in that scratch folder.#1124's script was main's script in the first round.
validate-family.pyis blobdfb50769fat8dfa32bd, atdea964aa1and atorigin/main(50ce8ab15), andgit diff 8dfa32bd dea964aa1overvalidate-family.pyand the two older test files this PR changes is empty. So the column "on #1124's head" below is main's script as it stands.N1, the index's file beside the work tree's on disk.
update = !trueis in IRRS's section, in the work tree's file on disk and staged; HEAD's file is clean.gitmodules-path-twiceand the check for values git dies on read the.gitmodulesgit reads, the index's where the work tree's is gone, and every section's lines endgit add -- .gitmodules, so a plaingit commitafter them takes the section out of HEAD (#1077) #1124's head both givegitmodules-value-refused(exit 1, in--no-membersand in members mode), and its one line isgit config -f .gitmodules --unset-all submodule.members/IRRS.update, with no stage line.git commitafter that line exits 0 and HEAD then gives!true(P1 HEAD gives !true after: True). The next gate run names it "in HEAD".update = !trueis in the index's file alone; the work tree's file is clean on disk, and so is HEAD's.submodule statusexits 0. The gate isfamily ok, exit 0, in--no-membersand in members mode, on main and on validate-family.py:gitmodules-path-twiceand the check for values git dies on read the.gitmodulesgit reads, the index's where the work tree's is gone, and every section's lines endgit add -- .gitmodules, so a plaingit commitafter them takes the section out of HEAD (#1077) #1124's head.git commitrecords!truein HEAD (P1b HEAD gives !true after: True).N2, an index file with a line git cannot parse.
update = !truein IRRS's section, then the linethis is no config line; the work tree's file is deleted.submodule statusexits 128 (fatal: invalid value for 'submodule.members/IRRS.update').git config --blobon that file exits 128 too, after it printed the value.family ok, exit 0, in both modes. On main the value check never reads the index's file at all (validate-family.py: gitmodules-path-twice and the check for values git refuses to read still read the work tree's (or HEAD's) .gitmodules where git reads the index's #1077's cause). On validate-family.py:gitmodules-path-twiceand the check for values git dies on read the.gitmodulesgit reads, the index's where the work tree's is gone, and every section's lines endgit add -- .gitmodules, so a plaingit commitafter them takes the section out of HEAD (#1077) #1124's head_blob_listedreturns nothing on that exit 128.members/Oldsection in place of the value, then the bad line. Git'ssubmodule statusexits 0 (error: bad config line 10 in blob: git reads up to the line). The gate isfamily okin both modes, on main and on validate-family.py:gitmodules-path-twiceand the check for values git dies on read the.gitmodulesgit reads, the index's where the work tree's is gone, and every section's lines endgit add -- .gitmodules, so a plaingit commitafter them takes the section out of HEAD (#1077) #1124's head: the sections read nothing on the 128.gitmodules-unreadablefor HEAD's, exit 2, on main and on validate-family.py:gitmodules-path-twiceand the check for values git dies on read the.gitmodulesgit reads, the index's where the work tree's is gone, and every section's lines endgit add -- .gitmodules, so a plaingit commitafter them takes the section out of HEAD (#1077) #1124's head.N6, an unmerged
.gitmodules(stages 1 to 3 in the index, none at stage 0).!true. Git'ssubmodule statusexits 128 (no submodule mapping found in .gitmodules for path 'members/IRRS'): git reads no file.gitmodules-value-refused, exit 1. Main says "in HEAD ... not in the work tree" and puts HEAD's file back withgit checkout HEAD -- .gitmodules; validate-family.py:gitmodules-path-twiceand the check for values git dies on read the.gitmodulesgit reads, the index's where the work tree's is gone, and every section's lines endgit add -- .gitmodules, so a plaingit commitafter them takes the section out of HEAD (#1077) #1124's head says "here and in HEAD" and "puts back HEAD's, as the index holds none", but the index does hold the file, at its conflict stages.!true; HEAD's is clean. Git reads no file (exit 128). The gate, on main and on validate-family.py:gitmodules-path-twiceand the check for values git dies on read the.gitmodulesgit reads, the index's where the work tree's is gone, and every section's lines endgit add -- .gitmodules, so a plaingit commitafter them takes the section out of HEAD (#1077) #1124's head, reads the work tree's file and says "in the work tree, not in HEAD", exit 1.family okon main and on validate-family.py:gitmodules-path-twiceand the check for values git dies on read the.gitmodulesgit reads, the index's where the work tree's is gone, and every section's lines endgit add -- .gitmodules, so a plaingit commitafter them takes the section out of HEAD (#1077) #1124's head, and git reads nothing (exit 128).Why the branch was cut on #1124. Every item above holds on main
961640c6too, but the code each change touches (_blob_listed,READ_HERE,_listed_as_git_reads, the "then stage" ending) was #1124's, and a branch on main would have rewritten the hunks #1124 rewrites.What changed (the first round)
templates/family-root/scripts/validate-family.py(+195 and -33 lines againstdea964aa1):_index_listedreads the index's file at stage 0 (rev-parse --verify --quiet :.gitmodules, then_blob_listed), and_check_gitmodules_valuesasks it where the work tree's file is on disk. Where that file is gone, git already reads the index's (validate-family.py:gitmodules-path-twiceand the check for values git dies on read the.gitmodulesgit reads, the index's where the work tree's is gone, and every section's lines endgit add -- .gitmodules, so a plaingit commitafter them takes the section out of HEAD (#1077) #1124).GIVEN_WHEREentry: "in the index's file alone, which a plaingit commitrecords, not in the work tree or in HEAD".VALUE_DIESsays how git dies there: in a clone of the commit that records it.git commitrecords, gives it too." (INDEX_TOO).git add -- .gitmodules(STAGE_LINE):_value_taken_outwhere HEAD's file or the index's gives the value, or the work tree's file is gone, and the new_value_not_herewhere the work tree's file does not give it ("Stage [and commit] the work tree's .gitmodules, which does not give it:" and the line).gitmodules-unreadable._blob_listedrefuses with that code, as_head_listeddoes for HEAD's, naming the index's file and git's ownbad config linetext, and its remediation says to mend the file, stage it withgit add -- .gitmodules, and run again.git config --blobprints the entries before such a line and then exits 128; the old read returned nothing on that 128 and so dropped them. This is the second of the two options validate-family.py: the check for values git dies on never reads the index's.gitmoduleswhere the work tree's is on disk, an index blob with a line git cannot parse after a value isfamily ok, and an unmerged.gitmodulesis read as the work tree's or HEAD's where git reads no file (follow-ups from RV1124's review of #1124) #1150 names (the refusal), not the first (reading up to the line as bootstrap does); it is the same code HEAD's file already gets (P9h)..gitmodulesunmerged._gitmodules_unmerged(git ls-files -s, no stage 0) is the test, as bootstrap'sgitmodules_sourcereads it.UNMERGED_HERE: "the file git reads here, which is none while a merge leaves .gitmodules unmerged") and keeps HEAD's, which a clone reads. Its remedy is "Resolve the merge in .gitmodules by hand, leaving that key out, then stage and commit .gitmodules:" and the stage line, with no put-back line._sections_as_git_readsreturns no section while unmerged, sogitmodules-path-twiceandgitmodules-path-unnamedread none and print nogit checkout HEAD -- .gitmodules..gitmodulesis unmerged and HEAD's is clean, wasgitmodules-value-refusedand isfamily oknow, until the merge is resolved and staged. Git reads no file there either, so the value is no finding yet. Aftergit add, it is named as N1's. See Limits.WHAT IS CHECKEDbullets and the docstrings of the functions above say all of this.tests/test_validate_family_index_value_unmerged.py(new, +410 lines, 9 rows in 7 functions). Every row types only the printedgitlines, in order, and does by hand only what the text says in words; then a plaingit commitis made, and the row asserts that HEAD is as it should be, that the gate passes in every mode, and that a plain clone checks both members out (ends_well). The premises are measured in each row: git's own exit code, and a plain clone of the commit that records the staged file.test_a_value_the_work_tree_s_file_and_the_index_s_give_ends_staging_it.test_a_value_the_index_s_file_alone_gives_is_named_beside_a_clean_one(a plain clone of the commit that records it dies, exit 128).test_a_value_here_and_in_head_ends_staging_it_with_no_index_s_file. HEAD's file and the work tree's giveupdate = !true, andgit rm --cachedhas taken.gitmodulesout of the index. See "Mutants".test_an_index_s_file_git_cannot_parse_is_refused_unreadable([a-value],[a-section]): git'ssubmodule statusexits 128 and 0, the gate refuses with exit 2 in all three modes, the remediation'sgit checkoutandgit addlines are typed, and then the finding the file gave before the bad line is the one left.test_a_value_head_gives_while_a_merge_leaves_the_file_unmerged([deleted],[on-disk]).test_a_value_the_work_tree_alone_gives_while_unmerged_waits_for_git: every mode exits 0 while unmerged; aftergit add, the finding is N1's and its lines, typed, end well.test_no_section_is_read_while_a_merge_leaves_the_file_unmerged: no finding and nogit checkout HEADwhile unmerged; resolved to HEAD's side and staged,gitmodules-path-unnamednames the stale section.tests/test_family_gitmodules_values.py(+27 and -14 lines) andtests/test_validate_family_index_gitmodules.py(+11 and -12 lines): older rows re-pinned to the stage line and the refusal.taken_out, the expected finding every row of the first file builds, ends withSTAGEwherever HEAD's file gives the value, andWHERE["head-only"]ends ":" and the stage line.test_the_index_s_file_cut_short_is_put_back_from_the_indexhad as its premise that the gate cannot name the cut line, because it read the index's file as nothing. The premise is now the refusal: exit 2,REFUSED gitmodules-unreadable: the index's .gitmodules could not be read as git reads it:, andbad config linenamed. The row's cure is unchanged.test_1077_rows (the index alone gives a value; a url git dies on is written back) expect the stage line last and no longer typegit addby hand.Rows that fail on main (fix round 1)
Run on 2026-10-24 at this head,
0d65ea80c, withvalidate-family.pyas main50869f384holds it (blob172647b3e, sha25630abb4ea28e6, unchanged sincee04826db4) written into the worktree, then the head's script (blob18a0942d4, sha25630dc80fe29de) restored and its sha256 checked,git statusclean after. The logs are in lane 5's scratchv124/n1150/d/mut/(summary.logholds the runner's lines).The three test files in full: 39 failed, 142 passed (
main-full.log, 218.81s; the failed ids are listed inmain-full-failed.txt). They are the 29 rows that failed in the first round's comparison, with main's script then (blobdfb50769f, see below), plus the 10 ids this round adds or re-pins. The 142 that pass are rows that name nothing this PR changes, as many as in the first round's comparison.The 10 ids of the fix round, run on their own: all fail (
main.log, 11 ids, 11 failed in 26.01s; the 11th is the sections row's[deleted]id, which the first round added).test_1077_a_value_head_gives_where_the_deletion_is_staged_puts_head_s_back(M1, #1208's row re-pinned)--unset-all; the row expectsgit add -- .gitmoduleslasttest_a_value_the_work_tree_alone_gives_after_a_staging_one_stages_again[staged],[committed](S1)STAGED_ABOVEsentence and ends at its--unset-all, with no stage linetest_another_value_the_index_s_file_gives_the_key_is_named(N4)--unset-all; the row expects the index's value!falsenamed and the stage line lasttest_an_index_s_file_git_cannot_parse_beside_a_clean_one_is_refused(N2)family ok, exit 0, where the row expects exit 2 andgitmodules-unreadabletest_no_section_is_read_while_a_merge_leaves_the_file_unmerged[on-disk]andtest_no_section_is_read_in_a_modify_delete_conflict(N3)members/Oldsection out, where the row expects every mode to pass while unmergedtest_no_gitlink_outside_members_is_asked_about_while_unmerged[deleted],[on-disk](M2)--unset-all submodule.tools/x.urland the stage line, where the row expects every mode to pass while unmergedtest_a_member_not_checked_out_while_unmerged_is_told_the_plain_update(M2)member-uninitialized's remediation says to write the section formembers/IRRSback, where the row expects the plain updateRows that fail on main (the first round)
The three test files at the first round's head,
b031f4938, were run againstvalidate-family.pyas main holds it (blobdfb50769f, #1124's script, written into the worktree and restored, sha256844c1bee1d62checked; the run is at4b70f2ec6, whose four files are byte-identical tob031f4938's): 29 failed, 142 passed (mut/parent-full-4b70f2e.log). The 142 are rows that do not name anything this PR changes.test_a_value_git_dies_on_fails_bootstrap_and_every_mode_of_the_gate(8 ids),test_a_holder_with_every_member_checked_out_fails_on_it_too,test_a_value_git_dies_on_in_the_index_s_file_is_named_and_mended,test_two_keys_with_no_gitmodules_in_the_work_tree_put_it_back_once,test_a_url_no_row_gives_one_for_is_written_back_as_a_placeholder,test_a_url_or_path_left_with_no_value_is_written_back_as_add_writes_it(3 ids)git add -- .gitmodulesafter ittest_the_gate_says_which_file_gives_it[head-only]test_the_index_s_file_cut_short_is_put_back_from_the_index(2 ids)family ok, exit 0, where the row expects exit 2 andgitmodules-unreadabletest_1077_rows--unset-allor the url line, and no stage linetests/test_validate_family_index_value_unmerged.pyAt the first fold (
442615eb3), the same comparison over the 15-row selection the mutant runner uses gave 14 failed, 1 passed (mut/parent.log). The M3 row alone, run on the tree before it was committed: 1 passed on the head's script, 1 failed with #1124's script and 1 failed with main's script961640c6(mut/row1-head.log,row1-parent.log,row1-main.log).Mutants (fix round 1)
Run on 2026-10-24 at this head,
0d65ea80c, with git 2.43.0. Each of the 10 was applied alone tovalidate-family.pyin the worktree (v124/n1150/d/mut/mutants.pyin lane 5's scratch: one exact replacement, checked to occur once in the committed script), its rows were run throughrun9.sh, and the head's script was restored with sha25630dc80fe29dechecked;git statuswas clean after each (d/mut/summary.log, one log per mutant beside it). No other test run or gate used the worktree while a mutant was applied. R1 to R5 are rv1227's own (v129/rv1227/probes/mutants.py): R1, R2, R4 and R5 byte for byte, and R3 the same change moved to the line N4 rewrote. D1 to D5 are one for each new behaviour of this round. 10 of 10 are killed. Each ran over its own rows (the-kselections ind/mut/run-mut.sh), not the full files._gitmodules_unmergedis true only with all three of stages 1 to 3, so not in a modify/delete conflict's twotest_no_section_is_read_in_a_modify_delete_conflict_sections_as_git_readsreads the work tree's file on disk before it asks whether.gitmodulesis unmerged[on-disk]id and the modify/delete row (the[deleted]id passes)INDEX_TOOsentence is said wherever the work tree's file gives the value, where HEAD's gives it too (where[0]forwhere == (True, False))test_a_value_the_work_tree_alone_gives_after_a_staging_one_stages_again[committed]test_the_gate_says_which_file_gives_it[head-only]_index_listedreads an index file git cannot parse as nothing where the work tree's file is on disk: the refusal is swallowedtest_an_index_s_file_git_cannot_parse_beside_a_clean_one_is_refused(N2)againis never passed (again=False), so no later finding stages the file again[staged]and[committed]stagingis carried only by a value HEAD's file gives (committed), not by a staged one[staged]idINDEX_TOO) for any value the index's file gives the key, not only the same valuetest_another_value_the_index_s_file_gives_the_key_is_named_check_gitlinks_elsewhere(gitlink-unnamed) asks while.gitmodulesis unmergedtest_no_gitlink_outside_members_is_asked_about_while_unmerged,[deleted]and[on-disk]_init_stop_saidasks while.gitmodulesis unmergedtest_a_member_not_checked_out_while_unmerged_is_told_the_plain_updateMutants (the first round)
Each of the 13 was applied alone in the worktree (
mut/mutants.py: one exact replacement, checked to apply once), the rows run throughrun9.sh, and the script restored with sha256844c1bee1d62checked;git statuswas clean after (dirty after: 0). 13 of 13 are killed. Twelve of them (all but M3) ran at442615eb3over a 15-row selection: the 8 rows the new file had then, plus the 7 older rows the change re-pinned (test_a_value_git_dies_on_fails_bootstrap_and_every_mode_of_the_gate[update-command], both ids oftest_the_gate_says_which_file_gives_it, both oftest_the_index_s_file_cut_short_is_put_back_from_the_index, and the twotest_1077_rows). No later commit of the first round changed the script (blobb81da47adat442615eb3,4b70f2ec6andb031f4938)._index_listedreturns nothing: the index's file is never read beside the work tree's[a-value], P6bstaged=not present or (name, key) in in_indexcut tostaged=not present[a-value], P6bif committed or staged:cut toif staged:in_value_taken_out442615eb3: 15 passed over the selection, and 170 passed over the three full files. Killed at4b70f2ec6: 1 failed (the M3 row), 170 passed, over the three full filesINDEX_TOOsentence is not said_value_not_hereends with a full stop, no stage linetest_the_gate_says_which_file_gives_it[head-only]_blob_listedreturns nothing on the 128, as #1124's did[a-value]and[a-section], both ids of the cut-short row_gitmodules_unmergedis never true_gitmodules_unmergedis true with a stage 0 entry too (bool(stages))_sections_as_git_readsstill reads a section while unmerged_index_listedreads the blob without first asking whether the index holds one at stage 0[on-disk], P6bM3, why it survived and what kills it. M3 differs from the head only where the work tree's file is on disk and gives the value, HEAD's gives it, and the index's does not. Every existing row with HEAD's value also had it in the index, or no work tree file, so
stagedwas true anyway. The M3 row has that state. Under M3 the finding ends at the--unset-allline, a plaingit commitafter it records.gitmodulesgone from HEAD (ls-tree HEAD .gitmodulesis empty,?? .gitmodules), and the gate then findsmember-gitmodules-absent, exit 1 (mut/probe-M3.log). On the head, HEAD keeps.gitmodules,git statusis clean and the gate isfamily ok(mut/probe-head.log). The row is the only change of content added since the fold (4b70f2ec6, +30 lines).Runs (fix round 1)
All through lane 5's
run9.sh, eachSTARTline recording the head anddirty=0, with the stubgh,GIT_SSH_COMMAND=false,GIT_CONFIG_GLOBAL=/dev/nullandEMAILexported. The logs are in lane 5's scratchv124/n1150/d/logs/. The full suite was not run. The runs with main's script and with the mutants are in their own sections above.At this head,
0d65ea80c(2026-10-23T22:03Z to 22:16Z):tests/test_path_guard_hygiene.py,tests/test_repo_hygiene.py,tests/test_agents_md_codes.py,tests/test_cli_reference.py,tests/test_pinned_copy_text.pygate243-e-0d65ea8.loggate247-e-0d65ea8.logtests/test_family_holder_remedies_compose.py,tests/test_family_unnamed_gitlink.pyandtests/test_validate_family_remedies_typed.py;tests/test_family_holder_bootstrap_texts.pyandtests/test_family_hand_mount_git_rm_lines.py;tests/test_validate_family_typed_states.pyandtests/test_validate_family_unnamed_mounts.py;tests/test_family_bootstrap_gate_order.pyandtests/test_family_head_lost_gitmodules.pydisc9-A.logtodisc9-D.logAt
8938d17df, the M1 commit (2026-10-23T21:41Z to 21:52Z):gate243-m1-8938d17.loggate247-m1-8938d17.logBefore
0d65ea80cwas committed, its tree ran on the work tree it was then committed from, byte for byte (dirty=2, so these are not gates): the new test file, 18 passed in 85.45s (disc-new-e.log); the two older test files, 163 passed in 203.44s (disc-old2-e.log).No row failed in any of these runs.
Runs (the first round)
All through lane 5's
run9.sh, eachSTARTline recording the head anddirty=0, with the stubgh,GIT_SSH_COMMAND=false,GIT_CONFIG_GLOBAL=/dev/nullandEMAILexported. The full suite was not run.At the first round's head,
b031f4938(2026-10-23T18:24Z to 18:34Z; logs in lane 5's scratchv129/g1150/):tests/test_pinned_copy_text.py,tests/test_template_text_placeholders.py,tests/test_repo_hygiene.py,tests/test_agents_md_codes.py,tests/test_cli_reference.pygate243.loggate247.logAt
4b70f2ec6(2026-10-23T12:10Z to 12:15Z; its four files areb031f4938's byte for byte;v124/n1150/logs/):tests/test_repo_hygiene.pygate243-4b70f2e.loggate247-4b70f2e.logAt
f78107c38, a local WIP commit that was folded into442615eb3before any push (its tree equals the fold's; 2026-10-22T22:18Z): the three test files, 2.43.0, 170 passed in 230.25s (upd-f78107c.log).At
442615eb3, the fold (2026-10-22T22:24Z to 22:54Z; same script and same two older test files, the new file one row shorter; these ran on #1124's head as the base, before the restack):tests/test_family*.pyandtests/test_validate_family*.py, shard A (30 files)tests/test_family_operation_reftable.pyrows, which need git 2.45 or later)gate-shardA.logtests/test_family_init_values.py, 1 intests/test_family_member_make_jobserver.py)gate-shardB.loggate247.logtests/test_path_guard_hygiene.py,tests/test_repo_hygiene.py,tests/test_agents_md_codes.py,tests/test_cli_reference.py,tests/test_pinned_copy_text.py, on a scratch merge7aefff483of the fold and main961640c68(the stack lacked the first file)hyg.logNo failure in any run.
The restack
The branch was cut on #1124's head
8dfa32bd. After #1124 landed, lane 5'srestack3.shrestacked it with three merges; nothing was amended, rebased or forced. First #1124's final head2d3bea2df(merge1004b6a73). Then its squashdea964aa1449b62d9c2bbafc972bbc344631e0b2, taking the branch's side for the three files that conflicted,validate-family.py,tests/test_family_gitmodules_values.pyandtests/test_validate_family_index_gitmodules.py(mergeb031f4938fa8604969e523a093da2735d7fb3523). Then main, which wasdea964aa1itself when the restack ran at 18:22Z (that commit is dated 18:16Z and the next main commit 18:32Z), so the third merge found nothing to add and made no commit. The push was a fast-forward from4b70f2ec6, andgit ls-remotereadsb031f4938onfix/1150-index-gitmodules-stacked. The four files atb031f4938are byte-identical to4b70f2ec6's (an emptygit diff 4b70f2ec6 b031f4938over them), and againstdea964aa1the three older ones differ by this PR's lines alone, so the mutants, the rows on #1124's script and the runs at4b70f2ec6describeb031f4938's bytes.Merges
Read again at 2026-11-08T15:50Z by rv1227d:
origin/mainisdffc42265.git merge-tree --write-tree origin/main HEADexits 0 with treec010514b5, which differs from main by the four files alone (+1128/-78).7317354efpassed the three test files and the five hygiene files on git 2.43.0 (563)..gitfile this user cannot open, or whose git directory (its own.git, or the one its gitfile names) this user cannot search, is refusedmember-unreadablenaming that part, and no longermember-commit-unresolvable, which said the pinned commit was missing and prescribed a fetch (#1221) #1246 (0deac3a658) and bootstrap.py, validate-family.py and siblings.py: a root or holder whose.gitthis user cannot read is refusedgit-repo-unreadable, exit 2, not a traceback ornot-a-git-repo, and the doctor'smembersrow names it once, never drift (#1148) #1172 (2eabec8e05). validate-manifest.py, validate-family.py, family.py and update-shape.py: anid:, a date or arepository:ending in a line feed is refused by name, and a pin'ssource_repository:that is no path orowner/reponame is named as the pin's (#1139) #1166 has landed, as105b5daf9.The rest of this section was read at 2026-11-08T15:07:43Z with git 2.43.0:
origin/mainat0d5704b3dand the branch at2d56c1906(work/n1150g/logs/merge-trees-final.login lane 5's folder). The branch holds main up toe04826db4(#1208, merged in fix round 1 asd95308e95). Main has moved three times while this round ran, and is at0d5704b3d, 11 commits paste04826db4; one of the 11 touches this PR's four files:105b5daf9(#1166, landed) changesvalidate-family.py(the date and repository patterns end in\Z, an id is matched withfullmatch), at hunks away from this PR's, and merges clean. So main is not merged again.git merge-tree --write-tree origin/main HEADexits 0 and writes treed0180654c, which differs from main by this PR's four files alone (git diff --name-only, +1128 and -78 lines).git diff --name-only origin/main...HEADnames the same four. Main moved while the gates ran, so the three test files and the five hygiene files ran on git 2.43.0 on a scratch merge commit of the head with each of three mains (two parents, no ref), 563 passed each time: with11560eb9f(fda589e96), with105b5daf9(875f5aee3) and withb66d8f0cc(804213b71;update-shape.pyand one test). The merge with the main read last,0d5704b3d(a docstring inscripts/shape_materialize.py), was merge-treed, not gated.gh pr list --state open --limit 100 --json number,headRefName,headRefOid,isDraft,files; none lists more than 9 files, so no list was cut short. Besides this one, three touched any of the four files, allvalidate-family.pyalone. Eachgit merge-tree --write-tree <head> HEADexits 0, with heads read fresh withgh pr view N --json headRefOid:.gitthis user cannot read is refusedgit-repo-unreadable, exit 2, not a traceback ornot-a-git-repo, and the doctor'smembersrow names it once, never drift (#1148) #1172 (fix/1148-pinned-find-repo-root-refuses, onfix/1146-root-git-unreadable-not-drift, head2eabec8e05): treeb371a126a;id:, a date or arepository:ending in a line feed is refused by name, and a pin'ssource_repository:that is no path orowner/reponame is named as the pin's (#1139) #1166 (fix/1139-anchored-patterns, head5c8afefd44): tree557564bce. It has landed since, as105b5daf9, which the gated merges above include from the second on;.gitfile this user cannot open, or whose git directory (its own.git, or the one its gitfile names) this user cannot search, is refusedmember-unreadablenaming that part, and no longermember-commit-unresolvable, which said the pinned commit was missing and prescribed a fetch (#1221) #1246 (l3-1221-member-git-unreadable, head0deac3a658): treeffaedee72.Limits
gitmodules-value-refusedon main and on validate-family.py:gitmodules-path-twiceand the check for values git dies on read the.gitmodulesgit reads, the index's where the work tree's is gone, and every section's lines endgit add -- .gitmodules, so a plaingit commitafter them takes the section out of HEAD (#1077) #1124's head and isfamily ok(exit 0, every mode) now, until the merge is resolved and staged; the row holds it, and lane 5's coordinator accepted it. Git reads no file in that state either (submodule statusexits 128,no submodule mapping found): it dies at the first gitlink, not on the value. Aftergit add, the finding comes back, with N1's lines..gitmodulesis still no finding of its own. The gate isfamily okwhile git'ssubmodule statusexits 128, and since the fix round (M2) it also asks no question of a gitlink outsidemembers/and says no init stop while the file is unmerged:member-uninitializedgives the plain update, which exits 128 until the merge is resolved, and itsmake bootstrapnames that cure. validate-family.py: the check for values git dies on never reads the index's.gitmoduleswhere the work tree's is on disk, an index blob with a line git cannot parse after a value isfamily ok, and an unmerged.gitmodulesis read as the work tree's or HEAD's where git reads no file (follow-ups from RV1124's review of #1124) #1150 cites v1.1.1's release notes, which list the unmerged case as a known limit; this PR makes the checks agree with git on which file is read and adds no finding for the state. No documentation file is changed by this PR, and whether a page that describes the limit should change was not checked..gitmodulesunmerged: git refuses any commit then (Committing is not possible because you have unmerged files, exit 128). The value check's own remedy there says to resolve the merge first; the others say nothing of it.git switch -c, exits 128 (rv1227c's P5, measured; main is the same). Git saysfatal: cannot switch branch while merging, in a modify/delete conflict on.gitmodules, in a conflict in another file, and after a cleangit merge --no-commit. With.gitmodulesunmerged its plaingit rmexits 128 too (Cannot change unmerged .gitmodules), as the next bullet says. A line set that ends in a commit is not the only one that cannot be typed in a merge. Not changed here.git rmdies while.gitmodulesis unmerged and on disk (measured, git 2.43.0,m2-hand-checkout-section-unmerged-on-disk.log):Cannot change unmerged .gitmodules, resolve merge conflicts first, exit 128, after git removed the checkout from disk; the gitlink stays in the index, and its store under.git/modulesis kept. Main prints the same plaingit rmin that state, and_hand_cachedasks no question of the merge. Not changed here..gitmodulesout (rv1227's S2), measured: the put-backgit checkout -- .gitmodulesexits 1 (did not match any file(s)), so the--unset-allafter it exits 5, andgit add -- .gitmodulesexits 1 (outside of your sparse-checkout definition) even once the file is back; a plaingit commitafter the lines records what the index held.--ignore-skip-worktree-bitsandgit add --sparsework there. This holds for every finding that prints a put-back or a stage line, as on main. The follow-up is validate-family.py: in a sparse checkout that leaves.gitmodulesout, the put-back linegit checkout -- .gitmodulesmatches no file and everygit add -- .gitmodulesthe findings print exits 1, so no printed line set cures a finding there #1242.gitmodules-unreadableon that file first, as main does (rv1227's e2b). The unmerged path is reached by a merge resolved by hand and not yet staged (the rows' synthetic stages 1 to 3), or by a modify/delete conflict (stages 1 and 2, or 1 and 3).member-gitlink-removal-stagedwhile.gitmodulesis unmerged (rv1227c's P4, measured; main is the same). Where the work tree's file is deleted, its put-back (_section_back, through_gitmodules_put_back) finds no index entry at stage 0, so it falls to HEAD's file and printsgit checkout HEAD -- .gitmodules, "from HEAD's, as the index holds none". Where the file is on disk and gives no section for the member,_section_backprints the same line itself, in its on-disk branch, with theSECTION_TAKENwords ("putting that file back as HEAD has it and dropping any other change to it"), and never calls_gitmodules_put_back. So a fix must guard both places (rv1227d's NIT 2). That reading is wrong while the index holds the file at its conflict stages, and the line does more than put a file back.git checkout HEAD -- .gitmodulesresolves the conflict to HEAD's side: it writes HEAD's blob at stage 0 and drops the other side's stage, and the merge reads as resolved (git statusclean, the gatefamily ok).branch = resolved-by-handwas lost). With a hand resolution on disk that still gives the path, onlygit reset -q -- <path>is printed and.gitmodulesstays unmerged..gitmodulesis unmerged,member-gitlink-removal-stagedprintsgit checkout HEAD -- .gitmodules"as the index holds none", and that line resolves the merge to HEAD's side, dropping the other side's stage and overwriting a hand resolution on disk #1254. Not changed here..gitmodulesis unmerged, as the Goal says, since HEAD's is what a clone reads (rv1227d's NIT 1, issuecomment-6723609032):test_a_value_head_gives_while_a_merge_leaves_the_file_unmergedpins it);member-gitmodules-absentandmember-gitmodules-url-absent(_check_gitlinks_registered);member-gitlink-unnamed), which asks HEAD's sections whether a clone fetches the gitlink (_gitlink_unnamed_said), with its helpers_section_then,_gitlink_put_backand_move_section;The script's unmerged guards read nothing from any file. This PR changes none of these reads.
2d56c1906), the three test files (184) and the five hygiene files (379) ran on git 2.43.0, together on the scratch merges with main named under Merges (563), and the three test files ran on 2.47.1 (184). The script is byte for byte0d65ea80c's, where the nine files that pin the script's texts elsewhere ran on 2.43.0 (330 passed); no file imports the changed test file, so they were not run again. The 60 files oftests/test_family*.pyandtests/test_validate_family*.pyran at442615eb3alone, before the restack and fix round 1, on git 2.43.0, and the 4 reftable rows they skip there ran on no git. Fix round 2's mutants ran over the three files in full, and fix round 1's over their own rows. Main's commits sincee04826db4are not merged: the one that touchesvalidate-family.py(105b5daf9) merges clean and the scratch merges ran, and no gate ran on the predicted merge with an open pull request's head: those were merge-treed only. The full suite was not run.Closes #1150
Lane: openreposhape-5 (openRepoShape-5)
🤖 Generated with Claude Code
Summary by Sourcery
Make family validation follow Git’s
.gitmodulesindex and merge-state behavior while producing complete, reliable remediation commands.New Features:
.gitmodulesvalue and section checks with Git’s index and merge-conflict read semantics..gitmodulesfiles asgitmodules-unreadablewith actionable remediation..gitmodulesconsistently, including subsequent findings after an earlier staging command.Bug Fixes:
.gitmodulescontent while the index contains an unmerged file..gitmodulesduring an unresolved merge.Tests: