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 10 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 GuideUpdates family validation to mirror Git’s Sequence diagram for staged and work-tree .gitmodules value validationsequenceDiagram
participant Gate as FamilyGate
participant WT as WorkTree
participant Index as Stage0Index
participant Head as HEAD
participant Git as Git
Gate->>WT: _gitmodules_listed()
Gate->>Head: _head_listed()
Gate->>Git: _gitmodules_unmerged()
alt work-tree .gitmodules exists
Gate->>Index: _index_listed()
Index->>Git: rev-parse --verify --quiet :.gitmodules
Index->>Git: config --blob : .gitmodules --list
Gate->>Gate: Compare work-tree, index, and HEAD values
Gate-->>Gate: Add INDEX_TOO when index also gives value
else work-tree .gitmodules is absent
Gate->>Index: _index_listed()
Index->>Git: config --blob :.gitmodules --list
Gate-->>Gate: Validate the index file Git reads here
end
Gate-->>Gate: Emit gitmodules-value-refused
Gate-->>WT: Remediation ends with git add -- .gitmodules
State diagram for unmerged .gitmodules handlingstateDiagram-v2
[*] --> Stage0
[*] --> Unmerged
Stage0 --> WorkTreeRead: .gitmodules on disk
Stage0 --> IndexRead: no work-tree file
IndexRead --> Unreadable: git config --blob fails
IndexRead --> Valid: blob parses
WorkTreeRead --> ValueCheck
Valid --> ValueCheck
Unmerged --> NoFileRead: no stage-0 entry
NoFileRead --> AwaitResolution: no value or section read
AwaitResolution --> Stage0: git add -- .gitmodules
Unreadable --> Repair: mend file and stage it
Repair --> Stage0
ValueCheck --> [*]
Flow diagram for Git-aware .gitmodules validationflowchart TD
A["Check .gitmodules"] --> B{".gitmodules unmerged?"}
B -->|Yes| C["Read no file"]
C --> D["Use HEAD only for comparison"]
D --> E["Resolve merge, then git add -- .gitmodules"]
B -->|No| F{"Work-tree file exists?"}
F -->|Yes| G["Read work-tree file"]
F -->|No| H["Read stage-0 index file"]
H --> I{"Index blob parseable?"}
I -->|No| J["Refuse as gitmodules-unreadable"]
I -->|Yes| K["Compare values with HEAD"]
G --> K
K --> L["Report gitmodules-value-refused when Git would die"]
L --> M["Apply remediation and git add -- .gitmodules"]
File-Level Changes
Assessment against linked issues
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) |



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 the checks read no file while a merge leaves.gitmodulesunmerged, as git reads none then, sogitlink-unnamedand the init stop ask nothing in that state.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). The one script istemplates/family-root/scripts/validate-family.py; with its tests the change is four files, +1032 and -78 lines against main50869f384(git diff --shortstat origin/main HEAD -- <the four files>).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.What 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 at 2026-10-24T14:16:57Z with git 2.43.0,
origin/mainat50869f384(#1200) and the branch at0d65ea80c(v124/n1150/d/logs/merge-trees-f-0d65ea8.login lane 5's scratch). The branch holds main up toe04826db4(#1208, merged in fix round 1 asd95308e95). Main's two commits since then touch none of this PR's four files (git diff --name-only e04826db4 origin/main -- <the four files>is empty), so main is not merged again:0ed3e57f8(#1121) changesadopt-project.pyandtests/test_adopt_next_make_reads_first.py, and50869f384(#1200) changesshape-doctor.pyandtests/test_doctor_unreadable_paths.py.git merge-tree --write-tree origin/main HEADexits 0 and writes tree776447116, which differs from main by this PR's four files alone (git diff --name-only).git diff --name-only origin/main...HEADnames the same four.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, two touch any of the four files, bothvalidate-family.pyonly. Eachgit merge-tree --write-tree <head> HEADexits 0:.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, head5aa0f116de): tree9de2f1352;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, head5102b42945): treeab205e677.0ed3e57f8(d/logs/merge-trees-0d65ea8.log, with the same two trees for 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 and 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) and at8938d17df(d/logs/merge-trees-8938d17.log).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 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. Its put-back (_section_back, through_gitmodules_put_back) finds no index entry at stage 0 there, so by reading it falls to HEAD's file,git checkout HEAD -- .gitmodules, "HEAD's, as the index holds none", which resolves the merge to HEAD's side. No row runs that state.tests/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. Each fix-round mutant ran over its own rows, not the full files. Main's commits sincee04826db4touch none of the four files and are not merged, and no gate ran on the predicted merge with main50869f384or with either 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
Align family validation with Git’s effective
.gitmodulesreads across the work tree, index, HEAD, and unmerged states.New Features:
.gitmodulesalongside the work-tree and HEAD versions, identifying values that a commit would record..gitmodulesstates consistently with Git by treating them as unreadable by the checks until the merge is resolved..gitmodulesfiles asgitmodules-unreadablewith remediation guidance.Bug Fixes:
.gitmodulesremains unmerged.Enhancements:
.gitmodulesinclude the requiredgit add -- .gitmodulesstaging command and accurately describe indexed values and merge states.Tests:
.gitmodulescontent, unmerged conflicts, staged remedies, and related section checks.