Repository navigation
bootstrap.py and validate-family.py (family root): a .gitmodules in the index or HEAD with one bad line is read as no sections where git reads those before it, so a no-credential run fails wrongly, and the value rules are written twice #856
Description
Goal: where git reads a family holder's .gitmodules from the index or from HEAD and takes the sections before a line it cannot parse, make bootstrap reads the same sections, and the rules that say which .gitmodules values git dies on are written once, or held to one answer by a test.
Filed by lane openRepoShape-5, from the checklist candidates of #768's PR (#819, writer f819: "a .gitmodules copy in the index or HEAD with one bad line is read as having no sections", measured on git 2.43 and 2.47.1; and rv819's NIT 7, which #819's fix round left: "the rules are written twice"). #819 lists the first as "Left out: a blob .gitmodules with a config syntax error: git reads the entries before the bad line; gitmodules_sections reads none, as before", and lane 5's #839 names it in its "Not here" and files it nowhere; this is the place for both.
Pinned copies. templates/family-root/scripts/bootstrap.py and templates/family-root/scripts/validate-family.py are pinned in every holder (FAMILY_COPIED_VERBATIM, scripts/shape_materialize.py:525). Bundle this with the other v1.1.1 edits to them (#768's PR #819, #762's PR #820 and #763's PR #822 edit one or both; #839 files more), so each holder takes one upstream-changed row per file.
Lands after: lane 5's #768 (PR #819, fix/768-gitmodules-values, head e1379a9d): the blob reading and the value rules are #819's, and main has neither in bootstrap.py. path:line is at e1379a9d, except where a line says main (validate-family.py:1144-1155, scripts/repo_shape.py:1088-1120, bootstrap.py:94 and validate-family.py:113), which are at main fc867207.
Lines
-
A
.gitmodulesblob with one bad line is read as no sections. The git half is measured here on git 2.43.0, the only git on this machine (f819 measured 2.43 and 2.47.1). A scratch holder had two members, Atlas and Nova; a seventh line,branch = "unterminated(an open quote), was added at the end of Nova's section and committed; thengit submodule deinit -f --alland.gitmoduleswas removed from the work tree, the state where git reads the index's file, then HEAD's.git config -z --blob HEAD:.gitmodules --listprints the path and url of both sections, thenerror: bad config line 7 in blob HEAD:.gitmodulesandfatal: error processing config file(s), and exits 128.--blob :.gitmodules, the index's blob, does the same.git submodule update --init --recursiveprintserror: bad config line 7 in blob <oid>twice, exits 0, and checks out both members.- The same line in a work tree FILE is a die:
git submodule deinitdied reading it,fatal: bad config line 7 in file <path>/.gitmodules, andgit config -z --file .gitmodules --listexits 128 with the same line. That case stays as bootstrap.py and validate-family.py: a.gitmodulesvalue git dies on as it reads the file failsmake bootstrapnaming the key, and the family gate names it in every mode, soupdate = Noneno longer endsfamily bootstrap ok(#768) #819 has it (unreadable).
The bootstrap half is read from source and from f819's measurement, not run here.
gitmodules_sections(templates/family-root/scripts/bootstrap.py:360-396) lists the blob withgit_out(["config", "-z", *source, "--list"])(:384), which raisesgit-failedon the 128 and drops what git printed first (scripts/repo_shape.py:1088-1120on main), andexcept Refusal(:386-387) returns every field empty, withunreadableset only where the work tree holds the file (os.path.isfile(root / GITMODULES), false here). With no credential, each row git left not checked out is then named "no section of .gitmodules gives that path" and the run fails, where git checked the members out and the credential was all that was missing. Direction: for a blob, keep what git printed before it died: rungit config -z --blob <source> --listwithsubprocess.runasgit_outdoes and, where it exits 128 withbad config lineon stderr, hand the entries on stdout togitmodules_entriesas the sections git reads.ends_blob_reading(:448-456) already stops the reading at a key with no value, which is the same stop for a bad line. A bad line before the first section gives nothing, as git does. -
The
.gitmodulesvalue rules are written twice, and a third time in the test. Read from source, not measured. Bootstrap holdsNO_VALUE_REFUSED(:130),UPDATE_VALUES(:143),TAKES(:145),refused_value(:488) andgit_bool(:529); the gate holdsGIT_TAKES(templates/family-root/scripts/validate-family.py:1106),_values_refused(:1230) and_git_takes_bool(:1258); andtests/test_family_gitmodules_values.py:74holds aTAKESof the texts both say. Git's rules (parse_configinsubmodule-config.c) move with git, and a change made in one script is a defect in the other until somebody makes it there too. Direction: decide first where one copy lives.scripts/repo_shape.pyis the module both scripts import (bootstrap.py:94andvalidate-family.py:113on main), but it is a shape copy every project digest-pins, so a function added there puts anupstream-changed scripts/repo_shape.pyrow in front of every assembly root and every holder;scripts/shape_materialize.py:1178-1181and:1953-1960give that as the reason other shared helpers live elsewhere (there for a rule no project's copy asks; here both family scripts ask it, and an assembly root takes the row for nothing). The other ways: a module of the family root's own, listed inFAMILY_COPIED_VERBATIMso each holder gets it beside the two scripts, or two copies held to one answer by a parity test that runs one table of(key, value)cases throughrefused_valueand_values_refusedand fails where they differ. Whichever is chosen, the three copies above become one.
Not here: the gate's _head_gitmodules (validate-family.py:1144-1155 on main) refuses a HEAD blob git cannot list as gitmodules-unreadable, and that is right: it asks the file a recursive clone gets, which lands in the clone's work tree, where git dies reading it. Bootstrap's blob reading is for a holder whose work tree has no file. The two differ on purpose, and the gate is not a line of this issue. #819's other candidates are #839's (the credential advice) or are said in #839's "Not here" (submodule.<name>.active that is no boolean; _sections_listed reading a key with no value as the empty string).
Verify: tests/test_family_gitmodules_values.py (new in #819), tests/test_family_bootstrap_gate_order.py, tests/test_validate_family_unnamed_mounts.py, tests/test_family_head_lost_gitmodules.py and tests/test_pinned_copy_text.py pass on Linux. For the first line, a case beside test_a_key_with_no_value_ends_git_s_reading_of_the_index_s_file (tests/test_family_gitmodules_values.py:434 at e1379a9d): a plain clone whose work tree lost .gitmodules uncommitted, index and HEAD holding it with a line git cannot parse, no credential, the line at the end, after the first section, and inside a section; in each, git's submodule update and bootstrap agree on which rows are read, and the run ends as it does for a credential that is the only thing missing. A mutant that puts the except Refusal back as it is, caught. For the second line, the parity test above, or an identity assertion if the rules move to one module, and a mutant that changes one TAKES entry in one script, caught. No Windows or macOS run: CI is Linux only (#597).
Overlaps. #819 edits both scripts; #820 and #822 edit validate-family.py; #821, #823 and #827 edit scripts/repo_shape.py, which only the second line's first option touches. Lane 5's #839 (filed) works in the same two scripts, on the credential advice, run_member_target and _wrong_case_move. Both scripts are pinned copies, as said above. No README or handbook sentence is made false.
Parent: #459.
Milestone: v1.1.1. The first line needs a work tree without .gitmodules (deleted and not committed, or a sparse checkout that leaves it out), a bad line in the index's or HEAD's copy, and no credential; git's own error: line prints above the failure, so the person sees what is wrong. The second is a maintenance risk with no failure today. Neither is a regression, and neither blocks v1.1.0.
🤖 Generated with Claude Code
Activity
brettheap commentedon Oct 18, 2026
CLAIMED — lane openRepoShape-3, session b30e8b49-f249-4668-adb1-1dbfa93c09e1@Eagle, 2026-10-18T05:15:44Z, for #856
Logged in
lanes/log/openRepoShape-3.mdat2f6de52183f9cc9afa43d9f118c29b22406364ed.The three reads (lane-collision-protocol Rule 1):
bootstrap.py: a `.gitmodules` blob git read only up to a line it cann…
bootstrap.py: where git's reading of the index's or HEAD's `.gitmodul…
bootstrap.py: the index's `.gitmodules` with a line git cannot parse …
docs/release-notes/v1.1.1.md: the v1.1.1 notes take RV883's NIT list …
docs/release-notes/v1.1.1.md: the v1.1.1 notes take RV883's round 3 a…
bootstrap.py: a `.gitmodules` blob with a line git cannot parse is re…
docs/release-notes/v1.1.1.md: the v1.1.1 notes run to main a249910, w…