Repository navigation
adopt-project.py: a .gitmodules value git's submodule reader refuses (update = bogus, update =, update = !cmd, shallow = maybe, fetchRecurseSubmodules = bogus) is a check finding and an execute refusal, so execute no longer dies in git fast-export after both leg repositories exist, whether the tip or only the history holds the gitlink (#215) - #240
Conversation
…is a `check` finding and an `execute` refusal when the tip holds no submodule but the history of the source's branches and tags does, and so is `ignore`, `update` or `branch` with no value beside any submodule, so `execute` no longer dies in `git fast-export` after both leg repositories exist (#195) `git filter-repo` runs `git fast-export --all` in a clone of the source, and git's submodule reader reads the clone's checked-out `.gitmodules` for every gitlink an exported commit adds, changes or removes. #166 asked the tip tree alone, so a source whose history held a submodule and whose tip held none, with a tip `.gitmodules` holding a key with no value, passed `check` as `plan ok`, and `execute` died in `git fast-export` after both leg repositories were made: a corrected re-run then met `leg-remote-exists`. A line git cannot parse was passed the same way when the plan dropped the file, and was said to fail in the clone when the plan kept it. The rule, measured with git 2.43 and `git filter-repo` 2.47.0 on main a5b09c3, as on 20e13f0 and e4decfb before it: - Where the tip `.gitmodules` cannot be read for its submodules and the tip holds no gitlink, `_exported_gitlink` asks ONE streamed `git log` for the diffs fast-export makes -- the plan's commit and every branch and tag of the source, which is what `git clone` copies; root and first-parent merge diffs; no renames; `--ignore-submodules=none`, `--no-replace-objects`, `--no-show-signature` -- and stops at the first gitlink. If there is one and the plan extracts a leg, `plan-gitmodules-unreadable` names the commit and what it does to the gitlink, wherever the plan sends `.gitmodules`, and `execute` refuses it before any leg exists. Measured failing on main: a gitlink on the branch adopted, on a side branch merged, on a branch never merged (added, or added and moved), on a commit only a tag reaches, in the root commit of an orphan branch, added and removed by merges alone, and hidden by a replace ref. - Where neither the tip nor that history holds a gitlink, a key with no value still registers nothing (#166's S3), and a plan that extracts no leg is still not refused. A branch is a `refs/heads/` ref of the source: an `org/repo` clone's other branches and a commit only a detached HEAD reaches are in neither the extraction nor the scan, and adopt as on main. - `ignore`, `update` and `branch` with no value are read as `path` and `url` were: git's submodule reader dies on each (`branch` crashes git 2.43, exit 139, with nothing said). On main a submodule beside one, at the tip or in the history, passed `check`. `fetchRecurseSubmodules` and `shallow` with no value are read as true and still adopt. Unchanged, diffed over 37 measured sources: `plan`'s output and the plan it writes are byte-identical to main's in every one, and so is `check`'s output on the same plan in the 18 whose verdict does not change. `.gitmodules` values git rejects (`update=bogus`, `update=`, `update=!cmd`, `shallow=maybe`, `fetchRecurseSubmodules=bogus`) still pass `check`; that follow-up is #215. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s (`update = bogus`, `update =`, `update = !cmd`, `shallow = maybe`, `fetchRecurseSubmodules = bogus`) is a `check` finding and an `execute` refusal, so `execute` no longer dies in `git fast-export` after both leg repositories exist, whether the tip or only the history holds the gitlink (#215) git's submodule reader (submodule-config.c, `parse_config`) types three keys of a `.gitmodules` and dies on a value it refuses for one of them. `git filter-repo` runs `git fast-export --all`, which runs that reader on the clone's checked-out `.gitmodules` for every gitlink an exported commit adds, changes or removes (#195). `git config --list` reads every such value and `_registrations` checked none, so `check` said `plan ok`, and `execute` died in the extraction, with nothing on stderr (#171), after both leg repositories were made: a corrected re-run then met `leg-remote-exists`. Measured on main a5b09c3 and on 0023dde (#226), the tip holding the gitlink or only an earlier commit. The rule follows git's source. For these three keys it is the same in git 2.39.5 through 2.51.0, but for the one bound named below, and it was checked against git 2.43 over 252 `.gitmodules` cases with `git fast-export`: - git reads the checked-out file through `gitmodules_cb`, which sets `overwrite`, so every value is read, a key given twice included, and git dies on the first key it refuses in the file. The finding names that key, whether it is one of #195's keys with no value or one of these. A name git ignores (`check_submodule_name`) is never read. - `update` (`parse_submodule_update_strategy`, submodule.c) is `checkout`, `rebase`, `merge` or `none`, exactly. A `!command` is refused in a `.gitmodules`, and so is anything else, an empty value included: `invalid value for 'submodule.<name>.update'`. - `shallow` (`git_config_bool`) is a boolean `git_parse_maybe_bool` reads: empty, `true`/`yes`/`on`/`false`/`no`/`off` in any ASCII case, or an integer `git_parse_int` takes, with a `k`, `m` or `g` unit. Anything else: `bad boolean config value '<value>' for '<key>'`. - `fetchRecurseSubmodules` (`parse_fetch_recurse`, with `die_on_error` for the checked-out file) is such a boolean or exactly `on-demand`. Anything else: `bad <key> argument: <value>`. - The finding is #195's `plan-gitmodules-unreadable`, ending where git fails: the extraction beside a gitlink at the tip, and the history, naming the commit, where only the history holds one. `execute` refuses it through #168's shared list before any leg exists. Its words are git's, with the key and the value spelled as `plan` spells a path, and it says which values `update` and `fetchRecurseSubmodules` take. - Not refused, because git reads them: `ignore` with a value it does not know (`ignore = bogus` warns and goes on), and any value of `path`, `url` or `branch` (`branch =`). With no gitlink at the tip or in the history, a refused value still registers nothing and adopts, as #166's S3 does for a key with no value: nothing runs the reader on it. - Where git versions or C libraries differ, `check` takes what any of them reads. git 2.50 moved `git_parse_signed`'s bound below zero by one (`(-max - 1) / factor`), so -2147483648, -2097152k, -2048m and -2g pass `check` though git 2.43 refuses them. A `0b` number passes too: a git built on glibc 2.38 or later reads it (git 2.43 here does), and others refuse it. Those few are left to the extraction, as on main. Measured on main a5b09c3, on 0023dde and on this head, with git 2.43.0 and git filter-repo 2.47.0. Each of the five refused lines, at the tip or only in the history, was `check` 0 and `execute` 2 with both leg repositories left on main and on 0023dde. It is now `check` 1 and `execute` 2, refused with no remotes directory made. With no gitlink anywhere each is `check` 0 and `execute` 0 on all three heads, and `ignore = bogus` and `branch =` are `check` 0 and `execute` 0, `adoption verified`, in every position on all three. Unchanged, diffed byte for byte over 23 sources on the three heads: `plan`'s exit, output and plan file in all 23, and `check`'s exit and output in the 13 whose verdict does not change. Left out: #195's `missing value for '<key>'` still prints the key in git's quotes, not spelled, so a submodule name holding a control character reaches that finding raw. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…thout its `info/grafts` and passes a type change by, so a graft no longer hides a gitlink the extraction exports, and a gitlink only type changes move is no longer refused (#195) Fix round 1 on PR #226, from the independent review: - A graft in the source's `.git/info/grafts` hid the commit that added a gitlink from the scan's `git log`, and `--no-replace-objects` leaves grafts on. A clone copies no grafts file, so `git fast-export` in the extraction's clone exported that commit: `check` said 0 and `execute` died after both leg repositories were made, as on main. The scan now runs with `GIT_GRAFT_FILE` naming a file that does not exist, and names the commit (`removes vendor/b`). - git's diff asks the submodule reader about a changed path only when both of its sides are gitlinks, so a type change (a file or symlink becoming a gitlink, or the reverse) never reads `.gitmodules`. A gitlink that only ever entered and left by type changes adopted on main and was refused here; `_first_gitlink` now passes `T` by. One that enters by a type change and leaves by a removal is still found at the removal. - The docstring's cost claim is measured again: 100,001 commits in 4.9 to 5.2 s against 4.2 to 4.5 s for `git fast-export --no-data` over the same history, 5010 commits in 0.15 s. Tests: two `HISTORIES` shapes (a graft, entering by a type change), a graft test that checks its premise with `git fast-export` in a clone, and a type-change control for a file and a symlink that checks `git fast-export` in a clone exits 0. Lane: openreposhape-3 (openRepoShape-3) 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 1 day and 4 hours by commenting |
|
(lane openRepoShape-3, 2026-10-11T19:40Z) @codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Reviewer's GuideThe PR makes Sequence diagram for .gitmodules validation before adoptionsequenceDiagram
participant User
participant Check as check
participant Registrations as _registrations
participant Gitmodules as .gitmodules
participant Execute as execute
participant FastExport as git fast-export
User->>Check: check plan
Check->>Registrations: _registrations(git config --list)
Registrations->>Gitmodules: inspect typed values
alt Git-refused value on a relevant gitlink
Registrations-->>Check: plan-gitmodules-unreadable
Check-->>User: finding
User->>Execute: execute plan
Execute-->>User: refuse before leg repositories
else Accepted value or no gitlink
Registrations-->>Check: registrations
Check-->>User: plan ok
User->>Execute: execute plan
Execute->>FastExport: export source history
FastExport-->>Execute: extraction succeeds
Execute-->>User: adoption verified
end
State diagram for adoption with unreadable .gitmodulesstateDiagram-v2
[*] --> Plan
Plan --> Check
Check --> Refused: typed value Git rejects and a gitlink exists
Check --> Ready: value accepted or no gitlink in tip/history
Refused --> [*]: plan-gitmodules-unreadable
Ready --> Execute
Execute --> Verified: fast-export succeeds
Execute --> ExtractionFailure: Git rejects during fast-export
Verified --> [*]
ExtractionFailure --> [*]
Flow diagram for Git-compatible .gitmodules value checksflowchart TD
A[Read each .gitmodules entry] --> B{Relevant submodule name?}
B -- No --> C[Ignore entry]
B -- Yes --> D{Typed key?}
D -- No --> E[Apply existing registration handling]
D -- Yes --> F{Key and value accepted by Git?}
F -- Yes --> E
F -- No --> G[Return Git-style refusal text]
G --> H[Record plan-gitmodules-unreadable]
H --> I[check reports finding]
I --> J[execute refuses before leg repositories]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-2, 2026-10-11T20:13:53Z) Lane openRepoShape-2 reviewed PR #240 at c800efd read-only, in its own detached worktrees. Nothing was authored, and this lane holds no claim. MERGEABLE WITH NITS. Sequencing: merge after #226, and only once this branch has taken #226's current head f48c669. It need not take main 54aa69a first. Findings:
The body links #215 with Reproduction: on a5b09c3 the issue's table shows Tests:
Merges:
Report: |
… fix/adopt-gitmodules-rejected-values Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… .gitmodules test helpers through conftest.py, cover a gitlink only type changes move Fix round 1 on PR #240, after merging #226's fix round 1 (f48c669) in the merge commit before this one: - `check` refuses what ANY git `execute` may run refuses (lane 2's nit at adopt-project.py:370 and :404). It took git 2.50's wider bound below zero and glibc 2.38's `0b` numbers, so on git 2.49 and older `shallow = -2147483648` (or -2097152k, -2048m, -2g, however spelled) passed `check` and `execute` died in the extraction with both leg repositories left; measured here with git 2.43 on c800efd and on the merge, for `shallow` and `fetchRecurseSubmodules`, at the tip and in the history. A refusal at `check` costs an edit to an absurd value; a death at `execute` costs two leg repositories. `_who_refuses_a_boolean` replaces `_git_reads_as_a_boolean`: None where every git reads the value, `EVERY_GIT` where none does, or why only some do, which the finding then says: "a number git 2.49 and older refuse, and 2.50 and later read", or "a `0b` number, which only a git built on glibc 2.38 or later reads". Every other value reads exactly as before; the in-suite differential against the installed git leaves these values out and still passes. - SonarCloud's duplication (6.3% on new code against 3%): `as_human`, `put_gitmodules`, `link`, `named`, `exported_in_a_clone`, `planned`, `findings` and `in_the_history`, and the `execute` refusal block both files repeated, now `refused_before_any_leg`, live in tests/conftest.py as plain helpers beside `write_plan` and `resolve`, with GITMODULES and GITLINK. tests/test_adopt_history_gitlinks.py and tests/test_adopt_gitmodules_values.py import them; no assertion either file made changes. - python:S5778: the helper builds the `Namespace` before `pytest.raises`, so only `cmd_execute` is inside the block (in both files' test). - A gitlink that only type changes move, beside a value git refuses: on c800efd `check` refused it, though `git fast-export` in a clone exits 0; with f48c669 merged it passes and adopts. A `type-changes` position covers it: `check` passes each refused line there, the clone's export exits 0, and `update = bogus` adopts and clones end to end. - `test_who_refuses_a_number_as_a_boolean` states each bound for each unit, the values one above, and the `0b` cases; the three numbers are refused at the tip and in the history by `check` and by `execute` before any leg exists, and pass beside no gitlink at all. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-11T21:37Z) fix round 1 (independent review, codex is out of quota): merged #226's fix round 1 ( |
…rejected-values Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… glibc alone The verifier's optional nit 1 on fix round 1: C23 makes a base-0 `strtoimax` read `0b`/`0B`, so any C library that implements it reads a `0b` number, not glibc alone. SOME_LIBRARIES_REFUSE now says "a `0b` number, which a git reads only where its C library reads C23 binary numbers (glibc 2.38 or later, for one)", and the comments that said "only glibc" or "refused by every other" say the same. Wording only: what `check` refuses is unchanged. Nit 2 (the frame "git cannot read the source's .gitmodules" over a value the installed git reads) is left: the frame is #226's shared finding builder on main, and the parenthesis already says which gits refuse it. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-11T23:19Z) fix round 2 (independent review, codex is out of quota): #226 landed as |
|
…opt-record-submodule-placement Brings in c2ff2b0 (#215) and c3a9645 (#241) on top of d94d155. No rebase. c2ff2b0 changes adopt-project.py, as this branch does, which is why main is merged again so soon after d94d155. c3a9645 changes scaffold-project.py alone. No conflicts. Checked: - adopt-project.py: #215 makes a `.gitmodules` value that git's submodule reader refuses (`update = bogus`, `shallow = maybe`, ...) #195's `plan-gitmodules-unreadable` finding, which `execute` refuses before any leg exists. It changes `_registrations`, `Source` and `_where_git_fails`, and adds `_refused_value`, `_who_refuses_a_boolean` and their constants. This branch's `submodule_record`, `_resolution_text` and the record's preamble are unchanged by the merge, and so are `_answered_legs`, `_kept_in_a_leg`, `_commit_the_split`, `cmd_execute`, `_refuse_what_check_finds`, `_entry_findings`, `_submodule_findings`, `submodule_plan_problems`, `Plan`, `y`, `emit` and `walk`. So, as for #195, a source #215 refuses gets no leg, no split and no record, and the record is still spelled by `double_quoted`. - No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, shape-doctor.py, validate-manifest.py or update-shape.py. Each merged module's names are the base's plus what either side added. No definition was changed by both sides. - The branch's diff against main is still #189 alone: 12 files, +1670 -157, file by file the same as against d94d155. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, #205, #215, #241 and #249/#250 merge with no conflict, and `_entry_findings` still hands `_leg_findings` the tree and ends with the root-symlink findings origin/main moved 60588d5 -> b30595c while the targeted runs of 8f624aa went: 018ce0d (#207 via #222), 67a398a (#272 via #274), d94d155 (#205 via #232), c2ff2b0 (#215 via #240), c3a9645 (#241 via #256) and b30595c (#249 and #250 via #284). It is merged again because c2ff2b0 changes adopt-project.py and b30595c changes scripts/shape_materialize.py's `render`. Conflicts: none. `git merge-tree --write-tree` gave tree be093a5, and the merge stages the same tree. Checked on that tree: - `_entry_findings` still calls `_leg_findings(plan, tree)` and ends with `_root_symlink_findings(plan, source, tree)`. - No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py or scripts/shape_materialize.py, none is loaded but bound nowhere (AST), and main changed the signature of no function this branch calls. - Both sides are intact by +/- line sets: each side's change against the merge base 60588d5 reappears, line for line, against the other side. - c2ff2b0 adds helpers to tests/conftest.py (`planned`, `GITLINK`, `findings` and others) and no fixture. tests/test_adopt_root_symlinks.py imports its conftest names one by one and none of these, so its own `planned` and `GITLINK` are unaffected. - b30595c's `render` spells a double-quoted YAML value with `double_quoted`; this branch's walker reads `assembly_root_paths()` and never renders. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main brings three commits since d94d155: - c2ff2b0 (#215 via #240): adopt-project.py refuses a `.gitmodules` value that git's submodule reader dies on. It adds helpers to tests/conftest.py, adds tests/test_adopt_gitmodules_values.py, and moves helpers out of tests/test_adopt_history_gitlinks.py; - c3a9645 (#241 via #256): scaffold-project.py's `--pin` collisions; - b30595c (#249, #250 via #284): repo_shape.py's quoted scalars, and shape_materialize.py's `render()` spells a value between a YAML template's double quotes. One conflict, in tests/conftest.py. Both sides append after `resolve()`: main its `.gitmodules` helpers, this branch `spelled()`. Both are kept, main's block first. Against main, the merged file differs by exactly this branch's own diff. Against this branch, it differs by main's own diff, but for where git places two added blank lines. adopt-project.py merged without a conflict, and each top-level definition comes whole from one side. c2ff2b0 changes `_registrations`, `_where_git_fails` and `Source.registered_submodules`, and adds `_refused_value`, `_who_refuses_a_boolean` and their constants. This branch changes none of them, and its own `Source.__init__` and `Source.open` are unchanged. The new refusal comes through `_entry_findings`. `cmd_execute` calls that in `_refuse_what_check_finds`, before `_refuse_an_occupied_work_dir` and `_create_leg_remotes`, so the refusal is raised before any leg exists and no notice is due. Main's `refused_before_any_leg` asserts that the remotes directory is never made. No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py or tests/conftest.py. None of them reads a global it does not bind. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#241, #249/#250, #204, #203 and #238 merge with no conflict, and a `.gitmodules` value git refuses is answered before #190's relative-url check, which reads no url from a file git cannot read Main since d94d155, the main GitHub's update-branch merged at 8ce5614: c2ff2b0 (#215 via #240), c3a9645 (#241 via #256), b30595c (#249/#250 via #284), be2790e (#204 via #227), 6d8d88b (#203 via #236) and ede15ce (#238 via #287). No conflict hunk. Three files were changed on both sides; `git merge` auto-merged each, and every hunk of each side lies apart from the other's. Each side's +/- lines are identical before and after the merge in all three (`git diff -U0` d94d155..8ce5614 against ede15ce..merge, and d94d155..ede15ce against 8ce5614..merge). Line numbers are the merged file's. adopt-project.py - main, c2ff2b0 (#215): 384-506, `SUBMODULE_TYPED_KEY_RE` to `_refused_value` and `_who_refuses_a_boolean`; 510-515 and 522-524, `_registrations`' docstring and its return of `_refused_value`'s words; 791-797, `Source.registered_submodules`' docstring; 2181-2185, `_where_git_fails`' docstring. - #190: 36-38 and 51-53, the module docstring; 102-103, the `repo_shape` import list; 334-339, `Registrations.url`; 2252-2501, `RELATIVE_URL` to `_relative_url_findings`; 2503-2548, `_submodule_findings` and `_entry_findings` with `local_remote_dir` and the NOTEs; 2608 and 2630, `cmd_check`; 3070-3092, `_repository_urls` and `_remote_urls`; 3111-3216, `_refuse_what_check_finds` and `_as_resolved_here`; 3443, `cmd_execute`. scripts/repo_shape.py - main, b30595c (#249/#250): 173-535, the YAML reader's quote boundaries, from the comment after `YamlError` to `_split_flow`. - #190: 1975-1985, `REMOTE_PORT_RE` and `REMOTE_HOST_ALIASES`; 2099-2116, `remote_key`'s docstring and body. tests/test_shape_doctor.py - #190: 2662-2693, `test_a_port_and_githubs_ssh_host_are_not_part_of_a_remotes_key`. - main, ede15ce (#238): 5209-5808, the validator-PYTHONPATH tests after `test_a_drifted_roots_next_command_runs_when_it_is_pasted`. Every other file is one side's blob: main's for the 17 main changed, and 8ce5614's for tests/test_adopt_relative_submodule_url.py and tests/test_adopt_submodules.py. The merge's diff against main is #190's five files, with the stat it had against d94d155 (1097+, 41-). No function or class is bound twice at the top level of adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, shape-doctor.py, scripts/family.py or the three test files. The helpers c2ff2b0 moved into tests/conftest.py (`planned`, `findings`, ...) are plain functions, and tests/test_adopt_relative_submodule_url.py imports seven named helpers from conftest and defines its own `planned`. HOW #215 AND #190 MEET. `_submodule_findings` lists `submodule_plan_problems` first, which holds #215's `plan-gitmodules-unreadable`, then `_relative_url_findings`. Where git's submodule reader refuses a value of the source's `.gitmodules`, `registered_submodules` answers with git's words, a string, and `_kept_relative_urls` reads no url from a string. So a source with both problems gets #215's finding alone, and no NOTE either. #190's finding is not lost: once the value is fixed, `check` reports it, and `execute` refuses it through the same list. #190 adds lines only after #215's, so it can hide nothing of #215's. A relative url kept in a leg is always beside a gitlink kept in a leg, where `_where_git_fails` names a failure, so the unreadable finding is always printed whenever #190's would be skipped. Measured on this tree with a source holding `upstream/dependency`, `url = ../dependency` and `update = bogus`, with `.gitmodules` and `upstream/` kept in code for testorg: - `origin` in otherorg: `check` 1 with `plan-gitmodules-unreadable` alone; `execute --local-remote-dir` 2, `REFUSED plan-gitmodules-unreadable`, with no remotes directory made. With `update = checkout`: `check` 1, `plan-submodule-relative-url`. - `origin` in testorg: the same `check` 1 and `execute` 2, with no NOTE. With `update = checkout`: `check` 0 and the NOTE. - At 8ce5614, before #215, the first source was `plan-submodule-relative-url`. The second was `check` 0 with the NOTE, over a file `git fast-export` dies on. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-3 (openRepoShape-3)
Claim: #215 (comment)
Based on main. #226 (issue #195) landed as
aaa40eb, and this PR's base is nowmain. Its fix round 1 (f48c669) was merged into this branch first (a340d0a), thenmaind89fffa(72f2bb6), so the PR shows only this issue's work:c800efd, fix round 1 (ddb1419) and fix round 2 (5521e82, wording).What
git's submodule reader (submodule-config.c,
parse_config) types three keys of a.gitmodules, and dies on a value it refuses for one of them.git filter-reporunsgit fast-export --all, which runs that reader on the clone's checked-out.gitmodulesfor every gitlink the exported history adds, removes or moves (#195).git config --listreads all of these values, and_registrationschecked none of them. Sochecksaidplan ok, andexecutedied inside the extraction after both leg repositories were made._registrationsnow refuses what git refuses. It returns git's words as a string, which is #195's single gate. The tip-or-history logic andexecute's refusal through #168's shared list are #226's, on main, and are unchanged here.update(parse_submodule_update_strategy, submodule.c) must be exactlycheckout,rebase,mergeornone. A!commandis refused in a.gitmodules, and so is anything else, an empty value included.shallow(git_config_bool) must be a booleangit_parse_maybe_boolreads. That is an empty value;true,yes,on,false,nooroffin any ASCII case; or an integergit_parse_inttakes: hex, octal or decimal, with ak,morgunit, within anint.fetchRecurseSubmodules(parse_fetch_recurse, withdie_on_errorfor the checked-out file) must be such a boolean or exactlyon-demand.gitmodules_cb, which setsoverwrite. Soupdate = checkoutfollowed byupdate = bogusstill dies. The finding names the first key in the file that git dies on, whether it is one of adopt-project.py: a.gitmoduleswith a key with no value passescheckwhen the tip holds no submodule but the history does, andexecutedies ingit fast-exportafter both legs are pushed #195's keys with no value or one of these. A name git ignores (check_submodule_name) is never read.plan-gitmodules-unreadable; no new id. It uses git's words with the key and the value spelled by_spelled, and says which valuesupdateandfetchRecurseSubmodulestake. For example:git cannot read the source's .gitmodules for its submodules (invalid value for `submodule.b.update`: `bogus`, which is not one of `checkout`, `rebase`, `merge`, `none`), and the source holds a submodule, so `git filter-repo` fails on that file when `execute` extracts a leg, wherever the plan sends the two. Where only the history holds the gitlink, it ends with adopt-project.py: a.gitmoduleswith a key with no value passescheckwhen the tip holds no submodule but the history does, andexecutedies ingit fast-exportafter both legs are pushed #195'sFAILS_IN_THE_HISTORYand names the commit.checkrefuses what ANY of them refuses (fix round 1). A refusal atcheckcosts an edit to an absurd value; a death atexecutecosts two leg repositories. I read git's source at 2.39.5 and at every minor release from 2.40.0 to 2.51.0. The three parsers are the same in each, with one exception: git 2.50 movedgit_parse_signed's bound below zero by one ((-max - 1) / factor; 2.49 has-max / factor). And a0bnumber is read only by a git whose C library'sstrtoimaxreads it (glibc 2.38 or later). So:-2147483648,-2097152k,-2048mand-2g, however spelled, are refused, and the finding says…, a number git 2.49 and older refuse, and 2.50 and later read.0bnumber is refused, and the finding says…, a `0b` number, which a git reads only where its C library reads C23 binary numbers (glibc 2.38 or later, for one)(fix round 2 widened this from glibc alone).checkrefuses those few values although the installed git reads them. The finding's parenthesis says which gits refuse each, under adopt-project.py: a.gitmodulesgit's submodule reader cannot read is acheckfinding and anexecuterefusal when the tip holds no submodule but the history of the source's branches and tags does, and so isignore,updateorbranchwith no value beside any submodule, soexecuteno longer dies ingit fast-exportafter both leg repositories exist (#195) #226's framegit cannot read the source's .gitmodules.ignorewith a value git does not know.ignore = boguswarns and goes on.path,urlorbranch,branch =included..gitmodulesto one leg and a submodule it registers to another passescheckandexecute, saysadoption verified, and the assembly's recursive clone then fails:planshould ask,checkshould find,executeshould refuse #166's S3, nothing runs the reader on it, so it still registers nothing and adopts..gitmodulesgit's submodule reader cannot read is acheckfinding and anexecuterefusal when the tip holds no submodule but the history of the source's branches and tags does, and so isignore,updateorbranchwith no value beside any submodule, soexecuteno longer dies ingit fast-exportafter both leg repositories exist (#195) #226's fix round, on main, passes a type change by.Why
The issue's whole table, measured on this machine with git 2.43.0 (glibc 2.39) and git filter-repo 2.47.0. The source's
.gitmodulesis[submodule "b"]withpath = vendor/b,url = ../band the extra line. The plan answers.gitmodules, andvendor/where the tip has something there, ascode..gitmodules.In the cells, "legs left" means both
Northwind-spec.gitandNorthwind-code.gitexist in--local-remote-dirafter the run.git fast-export --allin a clone (TIP and HISTORY)4835fb45521e82update = bogusfatal: invalid value for 'submodule.b.update'REFUSED plan-gitmodules-unreadable, no remotes dirupdate =update = !cmdshallow = maybefatal: bad boolean config value 'maybe' for 'submodule.b.shallow'fetchRecurseSubmodules = bogusfatal: bad submodule.b.fetchrecursesubmodules argument: bogusignore = bogusbranch =exec 2printed nothing on stderr (adopt-project.pyexecuteexits 2 with nothing on stderr whengit filter-repofails, after both leg remotes exist:_build_the_legsswallowsCommandFailedon the premise thatrunalready reported it, andrunonly raises #171), and its last stdout line namesremotes/Northwind-code.git.check's last line wasplan ok. maina5b09c3and adopt-project.py: a.gitmodulesgit's submodule reader cannot read is acheckfinding and anexecuterefusal when the tip holds no submodule but the history of the source's branches and tags does, and so isignore,updateorbranchwith no value beside any submodule, soexecuteno longer dies ingit fast-exportafter both leg repositories exist (#195) #226's0023ddemeasured the same in the first round, andc800efdandddb1419measured as this head does.REFUSED plan-gitmodules-unreadable: 1 finding(s) in …, and no remotes directory is made. "verified" meansadoption verifiedwas inexecute's stdout.Fix round 1, the same driver with a fourth position, TYPE:
vendor/ba file, then a gitlink, then a file again, and no commit that adds or removes a gitlink.c800efda340d0a(c800efd + f48c669)ddb1419, and this head5521e82shallow = -2147483648(git 2.43 export: rc 128bad boolean config value)fetchRecurseSubmodules = -2g(rc 128bad … argument: -2g)shallow = 0b1(rc 0 here: glibc 2.39)update = bogus(rc 0: a type change is never read)Read as git reads it. 252
.gitmodulescases written into a worktree holding a gitlink commit,git fast-export --allrun on each (git 2.43.0), re-run onddb1419. The cases cover booleans, integers with units, hex, octal and binary, the bounds, quoted whitespace, non-ASCII, case,on-demand,updatevalues, keys given twice, other and ignored names, and key order._registrationsagrees with git on 248. The 4 others are0b1and0B0forshallowandfetchRecurseSubmodules, which this glibc 2.39 git reads andcheckrefuses on purpose. The 10 git 2.50 bound cases, which c800efd passed and git 2.43 refuses, now agree.Byte-identical. I ran
plan, and thencheckon the same answered plan, over 23 sources (conftest's synthetic source, one with no.gitmodules, and the seven rows in three positions), and diffed the bytes with the plan path normalised. First round, maina5b09c3, #226's0023ddeandc800efd:plan's exit code, stdout, stderr and plan file are identical in all 23;check's are identical in the 13 whose verdict does not change, and the other 10 go fromplan okto the one finding. Fix round 1,c800efdagainstddb1419:planandcheckidentical in all 23.Tests
tests/test_adopt_gitmodules_values.py, 187 cases:check's exact finding, andexecute's refusal in process with no remotes directory made.checkpasses, and there the clone'sgit fast-export --allexits 0._registrationswith that git's own reader ingit fast-exportfor 85 lines. It leaves out the values on which versions or C libraries differ, so it holds on each runner's git.test_who_refuses_a_number_as_a_boolean: each bound for each unit, the value one past it, and the0bcases.ignore = bogusandbranch =adopting at the tip and in the history;update = boguswith no gitlink anywhere, and beside a gitlink only type changes move, adopting, with the assembly'sgit clone --recurse-submodulesexiting 0.The shared helpers (
as_human,put_gitmodules,link,named,exported_in_a_clone,planned,findings,in_the_history, and the in-processexecuterefusal,refused_before_any_leg) now live intests/conftest.py, and bothtest_adopt_gitmodules_values.pyand #226'stest_adopt_history_gitlinks.pyimport them. No assertion in either file changed in that move.refused_before_any_legbuilds itsNamespacebeforepytest.raises(python:S5778).Mutations, each run against the new file: restoring the wider bound and the
0breading fails 20 of its cases; dropping #226's type-change skip in_first_gitlinkfails 9 (every type-change case).Targeted set on
5521e8281eded4f008f80a3bb89b978ca8dcb18c(maind89fffamerged, so main's newtest_adopt_*files are in it), run with git-filter-repo 2.47.0 first on PATH as/usr/bin/python3 -m pytest tests/test_adopt_*.py tests/test_shape_doctor.py tests/test_repo_hygiene.py tests/test_cli_reference.py -q:1309 passed, 1 skipped in 587.98s (0:09:47)(onddb1419, before the main merge:1233 passed, 1 skipped). The skip istests/test_adopt_plan.py:393, the MedxEHR-backed plan test, which is skipped unlessSHAPE_MEDXEHR_CLONEnames a clone of MedxSoft/MedxEHR.The Windows job installs no git-filter-repo, so it skips the end-to-end tests in the new file (7 cases,
needs_filter_repo). The differential test needs no git-filter-repo and runs on all three operating systems.No
--helptext changes, sodocs/cli.mdis untouched. README.md and AGENTS.md are not edited.Composes with
maind89fffais merged in (72f2bb6). Its merge base with this branch wasa5b09c3, so git saw #226's change on both sides: four conflicting hunks inadopt-project.py, each a #215 sentence or the_refused_valuecall against #226's text, and an add/add intests/test_adopt_history_gitlinks.py(the helpers moved to conftest). Each conflicting hunk took this branch's side, and every hunk that merged cleanly kept main's changes (#230's, #234's and #246's among them). The resulting tree is exactly the tree of a merge computed withf48c669as its base (757f81f), and main'saaa40ebcarriesf48c669'sadopt-project.pychange line for line and itstest_adopt_history_gitlinks.pyunchanged. The PR's diff against main is the same four files as before.Trial merges (
git merge-tree --write-tree <sibling> 5521e82) against every pushed branch that touchesadopt-project.py,tests/conftest.pyor either test file, at 2026-10-11 ~23:10Z:fix/adopt-check-unsafe-path(#231, issue #196)355ab2dfix/adopt-clone-without-template(#268, issue #243)65073c1fix/adopt-late-failure-notice(also editstests/conftest.py)cdfb308(local5a8b649)fix/adopt-plan-quote-yaml-keywordsa0ff15b(local44c8c17)adopt-project.pyhunk, the same block it conflicts in againstmain, so this branch adds nonefix/adopt-record-submodule-placement2f5cda9fix/adopt-relative-submodule-url(#245, issue #190)0b996fdfix/adopt-report-filter-repo-failure(#184, issue #171)f416e13fix/adopt-root-kept-symlinks04792d2(localb37159a)adopt-project.pyandscripts/shape_materialize.py(one hunk each; local: oneadopt-project.pyhunk), byte-identical to its conflict againstmain, so this branch adds nonefix/adopt-spell-finding-paths(#223, issue #212)508befafix/adopt-template-values-utf8(#270, issue #255)aed9190fix/plan-literal-follow-up-grep(#228, issue #216)f0d4334#226's branch, #230, #234 and #246 have landed on
mainand come with the merge.mainhas since moved toeb6df75(#220,shape-doctor.pyand its test); it trial-merges CLEAN and is not merged here. A sibling that editstest_adopt_history_gitlinks.py's helper block would conflict with their removal here; none of the branches above does.Left out
.gitmoduleswith a key with no value passescheckwhen the tip holds no submodule but the history does, andexecutedies ingit fast-exportafter both legs are pushed #195'smissing value for '<key>'still prints the key in git's single quotes, not through_spelled, so a submodule name holding a control character reaches that finding raw. That is adopt-project.py: a.gitmodulesgit's submodule reader cannot read is acheckfinding and anexecuterefusal when the tip holds no submodule but the history of the source's branches and tags does, and so isignore,updateorbranchwith no value beside any submodule, soexecuteno longer dies ingit fast-exportafter both leg repositories exist (#195) #226's text, or adopt-project.pycheckandexecuteprint a source path raw in a FINDING, so a\r,\n,\x85, U+2028 or ESC in a name overwrites, splits or escapes the line on a terminal, whereplan's questions spell the same path with_spelled#212's scope..gitmodulesgit's submodule reader cannot read is acheckfinding and anexecuterefusal when the tip holds no submodule but the history of the source's branches and tags does, and so isignore,updateorbranchwith no value beside any submodule, soexecuteno longer dies ingit fast-exportafter both leg repositories exist (#195) #226: the history scan reads--branches, which includes a prior adoption'sadopt/three-repo-shape, a branch_extract_leg'scheckout -Bshadows in the clone; on a re-run over a once-adopted sourcechecknames a gitlink the extraction never exports. That is adopt-project.py: a.gitmodulesgit's submodule reader cannot read is acheckfinding and anexecuterefusal when the tip holds no submodule but the history of the source's branches and tags does, and so isignore,updateorbranchwith no value beside any submodule, soexecuteno longer dies ingit fast-exportafter both leg repositories exist (#195) #226's scan, and this finding inherits it.Closes #215
🤖 Generated with Claude Code
Summary by Sourcery
Reject
.gitmodulesvalues that Git's submodule reader cannot process before adoption begins, while preserving valid and contextually unread values.New Features:
plan-gitmodules-unreadablefindings during checks..gitmodulesvalues are detected.Bug Fixes:
git fast-exportfailures caused by invalidupdate,shallow, orfetchRecurseSubmodulesvalues.Enhancements:
.gitmodulestest helpers and add comprehensive coverage for accepted, rejected, repeated, ignored, malformed, and history-only values.Tests: