Repository navigation
shape-doctor.py: a gitlink beside a .gitmodules the leg's index tracks and this disk lacks is named from the index's copy, which git reads, with its own way out that restores the file first (git restore .gitmodules), as #1080 names one beside other sections (#1093) - #1109
Conversation
…cks and this disk lacks is named from the index's copy, which git reads, with its own way out that restores the file first (`git restore .gitmodules`), as #1080 names one beside other sections (#1093) #1092 read the `.gitmodules` on this disk, so where the leg's index tracks one and the disk lacks it (deleted, not staged) the doctor named nothing, though git reads the index's copy and `git submodule status` exits 128 beside one with no section for the gitlink. - `unregistered_gitlinks` reads the index's copy there (`registered_in_the_index`: `:.gitmodules`, up to its first bad line, as `gitmodules_entry` reads it) and names each gitlink it does not register, whether it holds no section or sections for other paths, under its own `unregistered_beside: index_copy`. - `index_copy_clause` says it, a note as #914's and #1080's are: restore the file first, then a section in it, or the staged `git rm --cached`. Each way out typed as printed exits 0, and the index's copy keeps every other section. - An unmerged `.gitmodules` still names nothing (git reads no copy then); the limits sentence now says when. - HEAD's and the index's copy are read by one helper, `registered_in_blob`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideUpdates shape-doctor.py to identify unregistered gitlinks using the index’s Sequence diagram for detecting unregistered gitlinks from the index copysequenceDiagram
participant Doctor as shape-doctor.py
participant Index as Git index
participant Git as Git
participant Status as git submodule status
Doctor->>Doctor: unregistered_gitlinks()
Doctor->>Doctor: Check .gitmodules is tracked and missing on disk
Doctor->>Git: registered_in_the_index()
Git->>Index: rev-parse --verify :.gitmodules
Index-->>Git: Index blob or no entry
alt Index copy exists
Doctor->>Git: registered_in_blob(:.gitmodules)
Git-->>Doctor: Sections up to first bad line
Doctor->>Doctor: Name gitlinks not registered by index copy
Git->>Status: Read index .gitmodules
Status-->>Git: Exit 128 for unregistered gitlink
else .gitmodules is unmerged
Git-->>Doctor: None
Doctor->>Doctor: Name nothing
end
Flow diagram for index-copy remediationflowchart TD
A[.gitmodules tracked in index but missing on disk] --> B[Read index copy with registered_in_the_index]
B --> C{Does index copy register gitlink?}
C -->|Yes| D[Do not name gitlink]
C -->|No| E[Emit index_copy note]
E --> F[git restore .gitmodules]
F --> G{Choose remediation}
G -->|Keep gitlink| H[Add path and url section]
G -->|Remove gitlink| I[git add .gitmodules]
I --> J[git rm --cached <its path in the leg>]
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-3, 2026-10-21T13:39:08Z) Independent review RV1109 of #1109 at 8ec615f: READY (no MUST). Three SHOULDs before the landing word. SHOULD-1 (tests): no test checks that the index's copy is read before HEAD's. Every SHOULD-2 (land order with #1111): the textual merge is clean, but on main + #1111 + #1109, tests/test_path_guard_hygiene.py fails on the new bare guard at shape-doctor.py:8865, SHOULD-3 (skip-worktree): with .gitmodules marked skip-worktree and missing from disk, git reads the index's copy (status 128) and the doctor names the gitlink, but the first printed step, NITs: (1) Sourcery appended "## Summary by Sourcery" after the robot line (it contains U+2019); restore the body from W1093's pr-body.md before landing. (2) shape-doctor.py:8850 "exits 0 ... no submodule mapping found" is not true where the copy has a bad line before the section: the clone exits 128 with "bad config line". (3) family.py prints Checked: every probe disagreement with Lane 3's plan: READY, with a fix round before the landing word. In the next usage window W1093, the writer, takes SHOULD-1 (the deleted-over-head rows), SHOULD-3 (the skip-worktree state named as a limit), NIT-2 and NIT-5 (the docstring words), and NIT-1 (the body restored). RV1109 then checks the new head. SHOULD-2: #1111 (lane 1, for #430, in v1.2.0) lands before this follow-up, so #1109 adds the BENIGN entry when it is brought up to a main that holds #1111. NIT-3 (one restore spelling across the tools) is a possible follow-up ruling, and NIT-4 needs nothing. #1109 stays a draft with no milestone and lands after the v1.2.0 tag, on a later word. Lane: openreposhape-3 (openRepoShape-3) |
…s` gives the other answer hold that the index's copy is read first, and the docstrings name the skip-worktree limit and why the staging step stays (#1093) Review round 1 (RV1109: READY, no MUST). - SHOULD-1: `gitmodules_beside_gitlink` takes `deleted-over-head`, which commits the opposite copy with the gitlink, makes it the pin, then stages the case's copy and deletes it from disk. The rows `deleted-over-head-registering` and `deleted-over-head-with-other-sections` kill RV1109's M3b (HEAD's copy read where HEAD has one). - SHOULD-3: `.gitmodules` marked skip-worktree is named, but `git restore .gitmodules` exits 1 there; `index_copy_clause` and the module docstring name it as a limit (family.py's `gitmodules-sparse`). - NIT-2: a recursive clone exits 128 with `bad config line` where the copy holds a line git refuses. - NIT-5: `git add .gitmodules` stays, since `git rm --cached` exits 128 once a section was written for another submodule. Docstrings and tests only: no non-docstring string of shape-doctor.py changed (ast). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-21T14:18:29Z) RV1109 round 2 at ab14050: READY.
NITs: shape-doctor.py:8868 is 88 characters after the rewrap. The SHOULD-2 note covers only #1111 landing first; if #1109 lands first, #1111 carries the BENIGN entry. Lane 3's plan: #1109 is READY at ab14050. It is a follow-up that lands after the v1.2.0 tag, on a later word, and #1111 (lane 1, for #430, in v1.2.0) is due before it. So #1109 carries the BENIGN entry (SHOULD-2) when it is brought up to a main that holds #1111, and NIT-6's rewrap goes in that same commit, which RV1109 then checks. If the order turns out the other way, lane 3 tells lane 1 on #1111 that #1111 carries the entry (NIT-7). Lane: openreposhape-3 (openRepoShape-3) |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…w `os.path.lexists(mount / adopt.GITMODULES)` in `unregistered_gitlinks` is on `BENIGN` as asked after git listed the leg's index, so the path-guard test passes with #1111 merged, and the docstring paragraph on a recursive clone is wrapped to 79 (#1093) Review round 2 (RV1109: SHOULD-2 and NIT-6), on top of the merge of origin/main at 038dafb (#1111). - SHOULD-2: with #1111 merged, `test_no_bare_guard_outside_a_stat_helper_or_the_allowlist[shape-doctor.py]` failed on the bare guard at `unregistered_gitlinks`. The entry sits in `BENIGN` with the table's alphabetical order, tagged ASKED: the guard is asked only after `.gitmodules` is among the paths git listed in the leg's index (`leg_tracked_files`), by `lstat`, in a mount `mount_read` read. - NIT-6: the `unregistered_gitlinks` docstring paragraph that began at 88 characters is rewrapped to 75. With docstrings stripped, `ast` shows shape-doctor.py identical to the merge. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-21T23:04:07Z) RV1109c (round 3, fresh reviewer) at e446ff5: READY. No MUST, no SHOULD.
NITs: (8) the body's round-2 Merges line says no other open PR touches these files; #1144 does now. Add the line above at the next body edit. (9) 1e73379 and e446ff5 carry a Co-Authored-By naming Claude Sonnet 5.5, where writer-common asks for Opus 5.5. The squash replaces both commits, so choose the trailer in the squash message. Lane 3's plan: #1109 is READY at e446ff5, and its SHOULD-2 owed to #1111 is done. NIT-8's line goes into the body at the next body edit, which is the landing's update-branch after the v1.2.0 tag. Whichever of #1109 and #1144 lands second re-checks their merge. The trailer is set in the explicit squash message. #1109 stays a draft with no milestone until then, and lands on a later word. Lane: openreposhape-3 (openRepoShape-3) |
…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) (#1124) ## What this does RV1069's review of #1069 (the PR for #1036) left four follow-ups, recorded in #1077's body (NIT 1, NIT 2) and in issuecomment-6303768948 (NIT 3, NIT 4). Each was measured on main before this change. The only script changed is `templates/family-root/scripts/validate-family.py`, which has no pinned copy. - **NIT 1, `gitmodules-path-twice` read the work tree's `.gitmodules` alone.** A second section for `members/IRRS`, staged in the index's file with 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. - Where the work tree's file is gone, it names the line that puts back the file git reads first. That put-back is recorded, so no later finding puts the file back over the hand edit. - It ends "then stage and commit .gitmodules". - **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 says "the index's file, which git reads here" where that is the one read. - Its lines put that file back, not HEAD's. They end "then stage", or "then stage and commit" where HEAD's gives the value too. - Where HEAD's file alone gives the value and the work tree's is gone, it says "Commit the index's .gitmodules, which git reads here and does not give it", with no line, so a fix staged there is kept. - `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 before (the work tree's file, then HEAD's, then the one read in its place), so with no git to run, `git-not-runnable` names what it named. - **NIT 3, a plain `git commit` after a section's lines.** Where HEAD's file held the section, `gitmodules-path-unnamed`'s lines ended "then commit .gitmodules" and staged nothing. 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) now prints `git add -- .gitmodules` after the keys, and says "then stage and commit" where HEAD holds the section. - **NIT 4, a test gap.** No row covered the case-variant branch's stage line where HEAD holds no section of the name. RV1069's probe `test_r2_stray_not_in_head` is that row now. ## New rows: `tests/test_validate_family_index_gitmodules.py` Every row types only the printed lines and does by hand only what the text says in words, then makes a plain `git commit`. The gate and a plain clone then pass. - N1: a second section in the index's file alone. - N2: a value in a named member's section, in the index's file alone. - N2b: HEAD's file gives the value, and the index's is mended. - N2c: as N2b, with OTHER's section out of both files, so `member-gitmodules-absent` puts the index's file back first. - N3: a section HEAD holds (a stale one, or `tools/x` with its gitlink), with the file on disk, deleted, or its deletion staged. - N4: the stray `ghost` (RV1069's probe), on disk and deleted. - Fix round 1: path-twice's put-back is recorded, so `gitmodules-path-unnamed` after it prints none (RV1124's P7, on `deleted` and `deletion-staged`); and the value's url is written back from the file git reads, not HEAD's (RV1124's P10). See the fix-round section below. **Before and after.** On main's script the file gives 10 failed and 2 passed. The 2 are N4's rows, which pass there by design (NIT 4 is a test gap) and kill the case-variant mutant M8 below. On this head it gives 12 passed. After fix round 1 the file has 15 rows: on main's script it gives 13 failed and 2 passed (the 2 are N4's), and on this head 15 passed. ## Older rows changed, and why Each one pinned the behaviour #1077 names as the defect. - **"then commit" with no stage line (NIT 3).** - `SECTION_REMEDY` and a new `STAGE` constant (`test_validate_family_unnamed_mounts.py`), and its rows (b) and (c), plus the two-sections row. - `THEN_COMMIT` and `section_first`, which always ends with the stage line now (`test_family_unnamed_gitlink.py`), and its two-sections and control rows. - Two `removal(...)` assertions in `test_validate_family_remedies_typed.py`. - `ghost_out`, R1's replace anchor and R6 in `test_family_hand_mount_git_rm_lines.py`. - Each section in these rows is committed, so HEAD holds it: a plain commit after the old lines left it there. - **Path-twice's ending (NIT 1 and NIT 3).** `test_family_add_refusals.py`'s path-twice row ends "then stage and commit": the commit records the index's file, not the work tree's hand edit. - **HEAD's file put back where git reads the index's (NIT 2).** - `taken_out(put_back=...)` and the two gone-file value rows in `test_family_gitmodules_values.py`. These now read `HERE_AND_HEAD`; one is renamed `..._put_it_back_once`, since its old name said HEAD's file is put back. - The first line of `test_round1_nit_6` in `test_family_holder_remedies_compose.py`. - The value row in `test_family_holder_bootstrap_texts.py`, renamed `test_a_value_finding_that_put_the_file_back_is_not_put_back_over`. - These are now `git checkout -- .gitmodules`, the index's file. - **#1036's R5 control (`test_1036_control_a_value_git_dies_on_keeps_what_it_read`).** - Round 1 put back HEAD's file, which dropped IRQS's section that only the index's file gave. - The value's lines put back the index's file now, so the commit after round 1 records that section, as it does with the file on disk. The next run names it, in every mode. - The row now types round 2 as well, then a plain `git commit`; nothing is left, and the gate and a plain clone pass. - What the control pins is unchanged: where git reads a file that gives a value git dies on, `gitmodules-path-unnamed` reads nothing from it. ## Gates Every run used `/usr/bin/python3 -m pytest -p no:cacheprovider` with: - `EMAIL` set; - `GIT_ALLOW_PROTOCOL`, `GIT_SSH_COMMAND` and the `LANES_*` variables unset; - an empty `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_NOSYSTEM=1`; - test author and committer identities; - a stub `gh` first on PATH, `GH_TOKEN=bogus` and an empty `GH_CONFIG_DIR`. - **Targeted set: 78 files.** - The files that name `gitmodules-path-twice`, `gitmodules-value-refused`, `gitmodules-path-unnamed` or `validate-family` in the head's tree, plus every test file that imports from a test module this changes. - This includes `test_pinned_copy_text`, `test_repo_hygiene`, `test_cli_reference` and `test_agents_md_codes`. - Result: 0 failed in all 78 files, 4390 passed and 34 skipped. - Shards 0 to 4 and 6 to 9 (70 files) were run at `0cd1c3f5`, the three WIP commits this head squashes (the same tree, and the same sha256 of the script): 3379 passed, 25 skipped. - Shard 5 (8 files) was run last, at this head (`672845cc`): 1011 passed, 9 skipped, 0 failed. Two earlier runs of it were cut by checkpoints, at 49% and at 28%, with 0 failures both times. Its files are `test_doctor_legs_members_next.py`, `test_family_add_spelling_leftovers.py`, `test_family_docs_one_spelling.py`, `test_family_makefile_siblings.py`, `test_family_remove_gitmodules.py`, `test_shape_doctor.py`, `test_update_shape_kept_helper.py` and `test_validate_family_unreadable.py`. - The PR's CI `tests` job runs the full suite. - **Older rows.** The 33 rows the change moved, which failed on the new script before their update, now pass with the new rows file (45 passed). ## Mutants Each mutant was run against the new rows file and the older rows that pin the changed paths. The file was restored after each one, and its sha256 checked against the head's. | Mutant | Rows that failed | |---|---| | M1: the value check reads the work tree's file alone | 8 (N2, N2b, N2c, two gitmodules_values rows, round1 NIT 6, the bootstrap_texts row, R5) | | M2: path-twice reads the work tree's file alone | 1 (N1) | | M3: no "commit the index's" branch where HEAD's alone gives the value | 2 (N2b, N2c) | | M4: the value's put-back is HEAD's, as before | 6 | | M5: the value's text does not say to stage the file | 1 (N2) | | M6: no stage line in `members/` | 7 | | M7: no stage line outside `members/` | 3 | | M8: no stage line in the case-variant branch (NIT 4) | 2 (N4 on disk, deleted) | | M9: "then commit" where HEAD holds the section | 10 | | M10: path-twice names no put-back | 1 (N1) | | M11: path-twice ends "then commit" | 3 | | M12: `member-gitmodules-absent` puts nothing back after any value finding | 1 (N2c) | | M13: the file git reads is asked before HEAD's | 3 (the without_git rows) | ## Merge-trees `git merge-tree --write-tree` against `origin/main` 823624d is clean, and was clean against 6da757c, the base the gates ran on. Main has since taken #1101 and #1102; neither touches a file of this PR. No open PR touches these files. #1109, the closest in subject, merges clean too. ## Follow-up, not in this PR RV1124 measured the suspected counterpart of NIT 3 (its N1) and two more older defects (N2 and N6). All three are older than this PR and self-correcting or known limits; the lane files them as the follow-up #1150, and none is fixed here. - N1: the work tree's `.gitmodules` is on disk, and both it and the index's give a value HEAD's does not. The value's lines say nothing of staging, so a plain `git commit` after them records the index's value; the next run names it in HEAD. Main prints the same text and gives the same result. - N2: an index blob with a line git cannot parse after a value is `family ok` (`_blob_listed` returns nothing on git's exit 128). - N6: an unmerged `.gitmodules` (stages 1 to 3), which v1.1.1's release notes list as a known limit. ## Fix round 1: RV1124's review of `672845cc` (S1, N3, N4, N5) RV1124's review of this PR at `672845cc` found one SHOULD and six NITs. This round fixes S1, N3, N4 and N5, in one commit on top of it (`8dfa32bd`), with no amend and no force. N1, N2 and N6 are left to the follow-up #1150. - **S1 (SHOULD): #1111's `test_path_guard_hygiene.py` failed the new guard.** `_listed_as_git_reads` opened with a bare `os.path.lexists(root / GITMODULES)`. Its one caller, `_check_gitmodules_values`, asks it only where the work tree's file is gone (`if present else _listed_as_git_reads(root)`), and `_gitmodules_put_back`, which the function calls next, asks the same `lexists` and is on the test's allowlist. The guard is dropped; the function is `spec = _gitmodules_put_back(root)[0]` and the blob read. On the scratch merge of main `038dafba9` and this head, `test_path_guard_hygiene.py` gives 49 passed, with no failure for this file and no stale `BENIGN` entry. - **N3: two behaviours had no row.** Mutants A and B survived all 439 rows of the PR's nine files. Two rows, written from RV1124's probes P10 and P7: - `test_1077_a_url_git_dies_on_is_written_back_as_the_file_git_reads_gave_it` (P10): the index's file gives a url that is not HEAD's, then a `url` with no value; the line that writes the url back takes it from the index's file. - `test_1077_path_twice_records_its_put_back_for_the_next_finding` (P7, on `deleted` and `deletion-staged`): HEAD's file holds `second` and a stale `members/Old`; path-twice's put-back is recorded, so `gitmodules-path-unnamed` after it prints none, and typed in order then a plain `git commit`, HEAD loses both. - **N4: path-twice's wording.** "back from HEAD's, as the index holds none first" put "first" on the wrong phrase. It says "back first from {whose}, `{put_back}`" now. Only `TWICE_PUT_BACK` in the new rows file pinned the old phrase. - **N5: a stale docstring.** `_gitlink_out_first` said its text ends "then commit .gitmodules"; it now says it ends as `_section_then` does, "then stage and commit" or "then stage". **Rows.** The new rows file has 15 rows. On main's script (`6da757cb`) it gives 13 failed and 2 passed (the 2 are N4's, by design). On `672845cc` it gives 14 passed and 1 failed: the first row (`test_1077_a_second_section_the_index_alone_gives_is_path_twice`) fails on N4's old wording. The P7 and P10 rows pass on `672845cc`, whose code was right; they kill the mutants below. On this head, 15 passed. **Mutants.** Each ran in its own worktree over the new rows file, and the script was restored after each and its sha256 checked. | Mutant | Rows that failed | |---|---| | A: the value's write-back from HEAD's where the work tree's file is gone | 1 (the P10 row: the last line wrote the HEAD url, not the staged one) | | B: path-twice names its put-back without recording it (`_file_put_back(root, None)`) | 2 (the P7 rows: `gitmodules-path-unnamed` printed the put-back again) | **Gate.** On a scratch merge of main `038dafba9` and `2d4cb64b`, whose tree is this head's: the usual environment (`EMAIL`, an empty `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_NOSYSTEM=1`, test identities, a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`). 0 failed, 3458 passed and 27 skipped in all. - `test_path_guard_hygiene.py`: 49 passed. - The 64 files of every `tests/test_family*.py` and `tests/test_validate_family*.py` (the PR's nine among them), plus `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_pinned_copy_text.py` and `test_doctor_validator_rows.py`: 2964 passed and 24 skipped, in four shards (685; 997 and 20 skipped; 557 and 4 skipped; 725). Those four shards were cut by a checkpoint and re-run on the same merge with the same lists. - `test_shape_doctor.py`, which runs `validate-family.py`, in 8 shards by test id, each list non-empty: 445 passed and 3 skipped. **Merge-trees.** `git merge-tree --write-tree origin/main HEAD` at `8dfa32bd` against main `038dafba9` is clean (tree `23c7194f0b6a08f56b93bdc80a05924e90e703a4`). Closes #1077 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 10 hours and 10 minutes by commenting |



What changed
A follow-up outside v1.2.0 (Brett Heap, 2026-10-21T10:32:04Z). It lands only after the v1.2.0 tag, on a later merge word.
The state #1093 asks for. A leg's index tracks
.gitmodules, but this disk lacks it (deleted, the deletion not staged). git'sconfig_from_gitmodulesthen reads the index's copy, andgit submodule statusexits 128 for a gitlink that copy does not register. At main the doctor named nothing there, because it read only the file on this disk.The index's copy is read (
shape-doctor.py):unregistered_gitlinkshas a third branch. Where the index tracks.gitmodulesandlstatfinds none on this disk,registered_in_the_indexreads:.gitmodulesasgitmodules_entrydoes:gitmodules_listing(mount, ["--blob", ":.gitmodules"], partial=True), thensubmodule_keys(..., working_tree=False), so up to its first bad line..gitmodulessection registers, beside a.gitmoduleswhose sections name other paths or beside none, is named by no row, though a recursive clone of the leg leaves it empty #1080) and the index's copy are read by one helper,registered_in_blob.registered_unindexedcalls it and behaves as before: RV1092d'sprobe_d.pyrows are identical at main and at this head..gitmodulesof whitespace is asked on every run, a dropped file whose blob the root also holds counts as root,_answered_legsis quadratic, and two validator code families have no placeholder row #914's state) or sections for other paths (shape-doctor.py: a leg's gitlink that no.gitmodulessection registers, beside a.gitmoduleswhose sections name other paths or beside none, is named by no row, though a recursive clone of the leg leaves it empty #1080's state). A copy that registers it names nothing.Its own value and clause.
unregistered_beside: index_copy(BESIDE_INDEX_COPY), documented beside the other leg keys and in the module docstring's leg-key prose.index_copy_clauseprints the way out: "first restore that.gitmodulesin its leg (git restore .gitmodules), then give each a section with itspathandurlin it, or, once it is staged (git add .gitmodules), remove its gitlink withgit rm --cached <its path in the leg>in its leg".git add .gitmodulesstages its deletion (measured: exit 0, and the index then tracks none), so every other submodule's section leaves the index with it. And no section can be written in a file that is not there.git restore .gitmodules,git add .gitmodules,git rm --cached vendor/lib: each exits 0, the gitlink leaves the index, and the index's copy is unchanged;git restore .gitmodules, a section appended,git add .gitmodules: each exits 0, thengit submodule statusexits 0.vendor/libempty, andgit submodule statusin the clone exits 128, as the clause says.Note or finding: a note. Like #914's and #1080's clauses, it is said in the row and never a finding. The row stays the
noteit was, since the policy still asks about the tracked.gitmodules, orokwheresubmodules:records the gitlink (casedeleted-and-recorded).An unmerged
.gitmodulesstill names nothing. git reads no copy then. With the file gone from this disk, the index holds no:.gitmodules(rev-parse --verifyfails), soregistered_in_the_indexreturns None. The limits sentence (module docstring, andunregistered_gitlinks) now says what the doctor really does, as RV1092e's NIT suggested: it names nothing there only while the copy on this disk is the conflict, registers the submodule, or is not there.The module docstring's limits sentence no longer lists #1093's state.
Kept byte for byte. I compared every non-docstring string literal of
shape-doctor.pywithast(RV1092d'sstrings.py), at main and at this head.index_copy,rev-parse,-q,--verify,:(the f-string piece of:.gitmodules) and""(the clause's empty return)..gitmodulesof whitespace is asked on every run, a dropped file whose blob the root also holds counts as root,_answered_legsis quadratic, and two validator code families have no placeholder row #914's clause and entry, shape-doctor.py: a leg's gitlink that no.gitmodulessection registers, beside a.gitmoduleswhose sections name other paths or beside none, is named by no row, though a recursive clone of the leg leaves it empty #1080's two clauses and entries, and every refusal code are unchanged, and their existing cases pass unchanged.Decisions the direction left open
index_copy. It names what git reads in this state.missing_gitmoduleswould read too close tono_gitmodules.git restore .gitmodules,git rm --cachedalone also exits 0 (measured). The clause still says "once it is staged (git add .gitmodules)", as the direction asks and as adopt-project.py and shape-doctor.py: a.gitmodulesof whitespace is asked on every run, a dropped file whose blob the root also holds counts as root,_answered_legsis quadratic, and two validator code families have no placeholder row #914's and shape-doctor.py: a leg's gitlink that no.gitmodulessection registers, beside a.gitmoduleswhose sections name other paths or beside none, is named by no row, though a recursive clone of the leg leaves it empty #1080's clauses word it. Right after the restore it stages nothing, and it keeps the removal true if the restored file is then edited.bad config line, 128), and the doctor names nothing, as beside any file git refuses. The removal still works as printed (casedeleted-read-to-its-first-bad-line). A section needs that line mended too.index_copy_clause's docstring says so.update = bogus) names nothing, as a refused file on this disk does. It is probed and tested (deleted-and-refused-whole).Out of scope
.gitmodulesthat a leg's index tracks but this disk lacks, a gitlink with no section in the index's copy is not named, though git reads that copy andgit submodule statusexits 128 #1093 has no "Needs a ruling" or "Documented limit" section..gitmodules(still names nothing, as directed);.gitmodulesgit refuses whole;.gitmodules;tests/test_shape_doctor.pyis unchanged: none of its tests forced a change.Tests
At
8ec615f4, one file at a time, under the stub gh,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_CONFIG_GLOBAL=/dev/nullandEMAILexported:tests/test_doctor_plan_overrides.pytests/test_doctor_recorded_answers.pytests/test_doctor_next_is_a_command.pytests/test_shape_doctor.py(once)tests/test_scaffold_leg_path_collisions.pytests/test_repo_hygiene.pytests/test_no_auto_maintenance.pytests/test_agents_md_codes.pytests/test_cli_reference.pytests/test_update_add_codes_documented.pyNew cases in
tests/test_doctor_plan_overrides.py:test_a_gitlink_no_section_registers_is_said:deleted-and-registering,deleted-with-no-section,deleted-with-other-sections,deleted-and-recorded,deleted-read-to-its-first-bad-line,deleted-read-to-a-bare-path,deleted-and-refused-whole;test_a_section_written_for_the_gitlink_registers_it[deleted-with-other-sections];test_an_unmerged_gitmodules_this_disk_lacks_names_nothing.pyflakes:
shape-doctor.py: no warning at main or at this head.tests/test_doctor_plan_overrides.py: no new kind of warning. The new test'sstandardandprojectparameters add two more of the file's existing fixture-shadowing messages ("redefinition of unused 'standard'/'project'"), as every fixture-taking test there does.Mutants
Each mutant was run with
-k "no_section_registers or section_written or unmerged_gitmodules_this_disk"(28 cases). After each run, the file was restored andcmpsaid identical. Code with docstrings stripped (ast) is identical between the tree the mutants ran on and8ec615f4.deleted-with-no-section,deleted-with-other-sections,deleted-and-recorded,deleted-read-to-its-first-bad-line,deleted-read-to-a-bare-pathrev-parse --verify :.gitmodulesfirsttest_an_unmerged_gitmodules_this_disk_lacks_names_nothingpartial=Falseinregistered_in_blobin-head-read-to-its-first-bad-line,deleted-read-to-its-first-bad-lineworking_tree=Trueinregistered_in_blobdeleted-read-to-a-bare-pathdeleted-and-refused-wholeindex_copy_clauseleft out ofheld_clauses.gitmodulesgone) andtest_a_section_written_for_the_gitlink_registers_it[deleted-with-other-sections]Probes
Each probe is a fresh leg, compared against
git submodule status(git 2.43). In each row the index tracks.gitmodules, and this disk lacks it unless the case says otherwise..gitmodulesthat a leg's index tracks but this disk lacks, a gitlink with no section in the index's copy is not named, though git reads that copy andgit submodule statusexits 128 #1093's state (the first 21 below) disagreed with git. At this head, every row agrees except the 3 copies git refuses whole, which are the refused-file limit.probe_d.py(24 rows) gives identical rows at main and at this head.probe.py(32 rows) changes in one row only:tracked-missing-on-disk-other, which at main was[] None NOand at this head is['vendor/lib'] index_copy yes.Merges
At the first push (current ones are under "Review round 2").
git merge-tree --write-tree, after a fresh fetch:16f9a0e8(shape_materialize.py: the fetch for a branch's answer leaves out every refspec of the repository's own onto the branches' names, whatever its source, and every one that puts a branch under a name not its own, sofamily.py bumpandbump-leg.pyno longer fail where the remote hasxandrelease/xand the repository's owngit fetch originworks (#1066) #1087 landed): clean.shape-doctor.pyor these tests.visibility:says it is the visibility the run asked for, and that a repository the run reuses keeps its own, where it said "this is all three repositories' visibility" (#913) #1102, scaffold-project.py: the--reuse-empty-repohelp, its docs and each remedy that sends a person into that re-run say ssh, a credential helper or askpass can still make it wait, and how to keep it from waiting (#915) #1101, family.py: a failed remove's by-hand lines spell a leg mounted at a path that is not UTF-8 as a printf word of octal escapes ending in a slash, sogit -C <member>/<leg>andsubmodule update -- <leg>typed in any POSIX shell name that leg (#1086) #1098, tests.yml: the full suite keeps its temporary files underrunner.temp, not/tmp, so it runs on Ubuntu 26.04 and thetestsjob goes back toubuntu-latest(#1096) #1097, shape_materialize.py, adopt-project.py and bump-leg.py: a tag a clone holds without its commit is one it cannot answer for,checksays a commit it cannot ask was not asked, and a pinned product YAML 1.1 or 1.2 reads as no text is quoted (#912) #1090, the family root's bootstrap.py:credential_adviceandended_onsay a member's failed clone ends git's update with 1 before any checkout but a leg's lets it go on, andkilled_saidsays git's update was itself killed, not that it went on (#1085) #1088 and the family root's siblings.py: with a token in use, the clone and the fetch that carry it run in bootstrap.py'suntraced()environment, somake siblingsandfamily.py siblingswrite the token to no trace the caller asks git for (#1055) #1075 merge clean.checknames what the clone's.githolds as git keeps refs, and asks a pin's file through the links the root keeps, so a link into.gitor onto a pin's file is refused before any leg or adopted with the copy beside it (#904) #1091 conflicts inadopt-project.py. It conflicts the same way with main alone, so the conflict is adopt-project.py:checknames what the clone's.githolds as git keeps refs, and asks a pin's file through the links the root keeps, so a link into.gitor onto a pin's file is refused before any leg or adopted with the copy beside it (#904) #1091's own.Review round 1 (RV1109: READY)
RV1109 found no MUST. This round is commit
ab140500, on top of8ec615f4. It changes docstrings and tests only: theastcheck ofshape-doctor.pyshows no non-docstring string changed, and with docstrings stripped the code is identical to8ec615f4.gitmodules_beside_gitlinktakeskept="deleted-over-head". It commits the opposite copy with the gitlink first, makes that commit the pin, then stages the case's copy and deletes the file from this disk. Two new rows use it:deleted-over-head-registering:SECTIONEDin the index,OTHER_SECTIONin HEAD, nothing named;deleted-over-head-with-other-sections: the reverse, named underindex_copy.mut_rv.py, both new rows fail (2 failed, 28 passed). M3 re-run: KILLED, 7 failed. Each time the file was restored andcmpsays identical..gitmodulesis marked skip-worktree and missing from this disk, the gitlink is named (git reads the index's copy, status 128). Butgit restore .gitmodulesexits 1 there, asgit checkout -- .gitmodulesdoes (measured).index_copy_clause's docstring and the module docstring now name this as a limit: family.py'sgitmodules-sparsestate.unregistered_gitlinksdocstring now says a recursive clone andgit submodule statusboth exit 128 withbad config linewhere the copy holds a line git refuses.index_copy_clause's docstring says whygit add .gitmodulesstays: once a section is written for one submodule,git rm --cachedof another exits 128 until the file is staged.e446ff58b, below): theBENIGNentry is intests/test_path_guard_hygiene.pynow that lane 1's tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file/is_dir/exists guard in a shipped tool fails by file and line unless its_is_aor a reasoned allowlist covers it, and aproject.yamllinked into an unsearchable folder isyaml-unreadable(#430) #1111 has landed on main.Tests at
ab140500(one at a time, under the stub gh, withEMAILexported):tests/test_doctor_plan_overrides.pytests/test_doctor_recorded_answers.pytests/test_doctor_next_is_a_command.pytests/test_agents_md_codes.pypyflakes:
shape-doctor.pyhas no warnings.tests/test_doctor_plan_overrides.pyhas the same 30 messages as at8ec615f4.Review round 2 (RV1109 round 2: READY; SHOULD-2 and NIT-6 done)
Lane 1's #1111 (
tests/test_path_guard_hygiene.py, #430 slice Z) merged to main at 2026-10-21T18:42:21Z as038dafba9dbedd9405b316cc749a04dffe02bb36. This round is the merge of that main into this branch (1e733793f, no conflict) and one commit on top of it,e446ff58b. It changes a test table and a docstring: with docstrings stripped,astshowsshape-doctor.pyidentical to the merge.e446ff58b. The semantic conflict RV1109 predicted is real. At the merge,tests/test_path_guard_hygiene.pygave1 failed, 48 passed in 7.68s:test_no_bare_guard_outside_a_stat_helper_or_the_allowlist[shape-doctor.py]: "shape-doctor.py:8899, inunregistered_gitlinks:os.path.lexists(mount / adopt.GITMODULES)is a bare guard."BENIGN, tagged ASKED:(SD, "unregistered_gitlinks", "os.path.lexists(mount / adopt.GITMODULES)"), reason "a leg git listed the index of first, bylstat(shape-doctor.py: beside a.gitmodulesthat a leg's index tracks but this disk lacks, a gitlink with no section in the index's copy is not named, though git reads that copy andgit submodule statusexits 128 #1093)".adopt.GITMODULES in paths, andpathsis the leg's index asleg_tracked_fileslists it (gitls-files), in a mountmount_readread first. It sits where the table's alphabetical order puts it, afterunloaded_manifest_row, not next toregistered_unindexed, whose entry has the same reason.unregistered_gitlinksdocstring paragraph that began at 88 characters is rewrapped to 75, the same words._is_aor a reasoned allowlist covers it, and aproject.yamllinked into an unsearchable folder isyaml-unreadable(#430) #1111 landed first, so this PR carries the entry and tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file/is_dir/exists guard in a shipped tool fails by file and line unless its_is_aor a reasoned allowlist covers it, and aproject.yamllinked into an unsearchable folder isyaml-unreadable(#430) #1111 owes nothing.Tests at
e446ff58b(one at a time, under the stub gh, withEMAILexported,timeout 2400):tests/test_path_guard_hygiene.pytests/test_shape_doctor.pytests/test_scaffold_leg_path_collisions.pytests/test_doctor_plan_overrides.pytests/test_repo_hygiene.pytests/test_doctor_recorded_answers.py,tests/test_doctor_next_is_a_command.pyandtests/test_agents_md_codes.pywere not run again this round: they were run atab140500, and since then only the merged #1111 code and the docstring rewrap changed inshape-doctor.py.Mutant. The new
BENIGNentry dropped:tests/test_path_guard_hygiene.pygives1 failed, 48 passed in 9.34s, the same test as at the merge. The file was restored andcmpsays identical.Merges (
git merge-tree --write-tree, after a fresh fetch):038dafba9(tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file/is_dir/exists guard in a shipped tool fails by file and line unless its_is_aor a reasoned allowlist covers it, and aproject.yamllinked into an unsearchable folder isyaml-unreadable(#430) #1111 itself): clean, treed7815c9a707c2e6750c7aa8f094ccb4df82b9294, the head's own tree, since the head holds main..gitmodulesthe leg's index tracks and this disk lacks is named from the index's copy, which git reads, with its own way out that restores the file first (git restore .gitmodules), as #1080 names one beside other sections (#1093) #1109 touchesshape-doctor.pyortests/test_path_guard_hygiene.py, so there is nothing else to merge against.Closes #1093
Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code
Summary by Sourcery
Teach shape-doctor to diagnose unregistered gitlinks using the index's
.gitmodulescopy when the working-tree file is missing, and explain how to resolve them.New Features:
.gitmodulesis still tracked in the index but does not register them, and report the index copy as the source of truth..gitmodulesbefore either adding its submodule section or removing the gitlink.Bug Fixes:
.gitmodulesfiles, including precedence over HEAD and partial parsing behavior.Enhancements:
.gitmodulesblob registration logic across HEAD and index copies and document the remaining unmerged, refused-file, and sparse-checkout limits.Documentation:
.gitmodulescases.Tests:
.gitmodulesbehavior.