Skip to content

adopt-project.py: plan runs with GIT_NO_LAZY_FETCH=1, refuses a partial-clone --source that lacks objects as source-partial-clone, and names a partial --pin-source it could read only by fetching and the fetch that makes it whole (#1162) - #1190

Draft
brettheap wants to merge 34 commits into
mainfrom
fix/1162-plan-partial-clone-no-fetch

Conversation

@brettheap

@brettheap brettheap commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1162

Stacked on #1145 (PR #1180), and lands after it. This branch is on #1180's head a0e8cded, merged in by 160c2dce0 ("Merge #1180's a0e8cde (restacked on main, r1180d's NITs) into #1190"), with no conflict. #1180 now targets main: after #1119 squash-landed as b7edd881, #1180 was restacked through three merges (#1119's final head 9b856443, its squash b7edd881, then main 50ce8ab1), and a0e8cded fixed r1180d's NIT-1 to NIT-3 (comments, two docstrings and two rows' assertions; no flag, code path or help text). This PR's own changed lines against a0e8cded equal those of git diff -U0 a4ec51d2 7f3e1280, 496 +/- lines, line for line, in the same three files. Before that, it was on #1180's head a4ec51d2, merged in by 7f3e12802 ("Merge #1180's a4ec51d (the walk ends with --) into #1190"), after the earlier merges d4d6e2a7a of #1180's head b316f415 and 00f959c61 of its head 6cd96b8b. Its three own commits were made on #1180's earlier head 2c8082a2: cbb007528 code and docs, 5a12a033c tests, 0d3089aef a comment fix. A fourth, 91a937e76, made on the second merge, changes plan's --source help and docs/cli.md alone (item 5 below). The first merge had two conflict hunks in adopt-project.py and the second had three, all resolved by taking both sides (below, under the reruns); the third had none. The second merge brings in #1180's two commits since 6cd96b8b (41230f640, b316f4157) and nothing of this PR's. It changes three files against 00f959c61 (adopt-project.py, docs/cli.md, tests/test_adopt_partial_clone_source.py), and its changed lines are git diff 6cd96b8b1 b316f415's, 323 of them, line for line, but for two places where this branch's own words stand: the ", and by plan (#1162)" in the docstring of _refuse_a_partial_clone_short and the "plan, check and execute" in the Refusal's detail. The third merge, 7f3e12802, brings in #1180's one commit since b316f415, a4ec51d2e, and nothing of this PR's: the source-partial-clone walk (_objects_lacking in adopt-project.py) and the command its refusal prints both end with --, so a file of the source's work tree named as HEAD's id is no path of the walk, and tests/test_adopt_partial_clone_source.py gains one row (38 rows now, 37 before) and has its docstring and expected walk text changed in one existing row. adopt-project.py changes by 16 lines (11 added, 5 removed: a comment, a docstring and two lines of code). The merge had no conflict: its tree, 6f740ccb7, equals the tree of git merge-tree --write-tree 91a937e76 a4ec51d2. It changes two files against 91a937e76 (adopt-project.py, tests/test_adopt_partial_clone_source.py; docs/cli.md is the same), and its changed lines are git diff b316f415 a4ec51d2's, 45 of them, line for line.

Outside v1.2.0, by Brett Heap's ruling of 2026-10-21T10:32:04Z ("leave the follow-ups out of v1.2.0").

What

#1145 made check and execute never fetch into a partial-clone --source. plan reads the same source with git, and each --pin's digest from --pin-source with git ls-tree -r, and was left as it was. In a partial clone (git clone --filter) git fetches an object the clone lacks from its promisor remote on any read that needs it, so plan made network calls it never announced, and wrote into a clone the person only meant to read.

Measured on #1145's head 2c8082a2, git 2.43.0, Python 3.12.3 (the promisor's upload-pack replaced by a counting stub):

plan of ... before (2c8082a) after (this PR)
--source, --filter=blob:none --no-checkout (28 objects lacking) exit 0, plan written, 26 upload-pack runs, .promisor files 1 -> 27 exit 2 source-partial-clone, no plan, 0 runs, objects/ unchanged
--source, --filter=blob:none checked out (2 history blobs lacking) exit 0, plan written, 0 runs (check and execute refuse it since #1145) exit 2 source-partial-clone, no plan, 0 runs
--source, full clone, or a partial clone cured by git fetch --refetch --no-filter exit 0, 0 runs the same
--pin-source, --filter=tree:0 --no-checkout exit 0, 1 run, .promisor 1 -> 2, then the digest read exit 2 pin-source-unreadable, with the partial-clone sentence and the cure, no plan, 0 runs, unchanged
--pin-source, --filter=blob:none that lacks the pinned commit exit 0, 1 run, .promisor 1 -> 2 exit 2 pin-source-unreadable, as above
--pin-source, full clone; --filter=blob:none that holds the commit (checked out or not) exit 0, 0 runs, digest equal to a full clone's the same
--pin-source, a full clone that lacks the commit; a folder inside a partial clone pin-source-unreadable / pin-source-not-a-clone, the reader's words the same

What changed, in adopt-project.py:

  1. @_lazy_fetch_off() on cmd_plan. NO_LAZY_FETCH and _lazy_fetch_off moved above cmd_plan (a decorator needs its name at definition), into a section "plan never fetches into a partial clone either (adopt-project.py plan: a partial-clone --source is fetched into, as check and execute were before #1145 #1162)". Every git plan starts inherits GIT_NO_LAZY_FETCH=1; the environment is put back afterwards.
  2. _refuse_a_partial_clone_short(source) runs in cmd_plan right after Source.of(...), as it does in check and execute. Its detail now says "plan, check and execute never fetch into the source". plan's --source help names source-partial-clone and the git fetch --refetch --no-filter <remote> cure.
  3. _pins_read_unfetched(args) wraps declared_pin_values in _checked_plan_inputs. Where the reader raised pin-source-unreadable and _promisor_remotes(args.pin_source) names a remote, it re-raises the SAME code from exc, with the reader's detail kept and a sentence added (the clone is a partial clone, git fetches what it lacks on any read that needs it, plan never fetches into a --pin-source), and a remediation that says no plan was written and gives the cure. --pin-source's help says which partial clones are read and which are refused.
  4. docs/cli.md regenerated with scripts/render-cli-reference.py.
  5. 91a937e76: plan's --source help says a partial clone short of an object "its branches, tags or HEAD reach" is refused, where it said "its branches or tags" (4 lines of adopt-project.py, 2 added and 2 removed), and docs/cli.md is regenerated from it: 13 lines, 7 added and 6 removed, all in that help's one paragraph, whose later lines only re-wrap (the words after the changed sentence are the same; argparse breaks source-partial-clone at its hyphen, as it breaks work-dir- at line 983). scripts/render-cli-reference.py --check says the file is what the generator produces. No behaviour changes.
  6. 359a749c0 (r1190's NIT-2): --pin-source's help no longer says a partial pin clone is refused "since plan never fetches into it" on every git. It says that on a git that honours GIT_NO_LAZY_FETCH (2.45 or later, or a security release from 2.39.4 on) such a clone is refused as pin-source-unreadable with nothing fetched into it, and that an older git fetches what it lacks into it, as Known limits says. docs/cli.md is regenerated from it (3 lines out, 6 in).
  7. 359a749c0 (r1190's NIT-3): the remediation of _pins_read_unfetched ends with the source refusal's sentence, through the same _filter_key: "Later fetches stay filtered while remote.origin.partialclonefilter is set, so the clone can lack objects again." A later pin to a newer commit in a tree:0 clone lacks its trees again.

What #1180 changed since the first merge reaches plan through the one function it shares with check and execute. At the merge head d4d6e2a7a, _refuse_a_partial_clone_short walks the source's HEAD as well as its branches and tags (_head_id and HEAD_ID: HEAD's id from git rev-parse --verify -q HEAD, which reads no object, and an unborn HEAD is read as before). _objects_lacking takes that id, so the refusal says "branches, tags and HEAD", and its git fsck sentence says what fsck names in a partial clone. The --source help of check and execute (and its lines in docs/cli.md) says "branches, tags or HEAD"; plan's own --source help said "branches or tags" at d4d6e2a7a and says "branches, tags or HEAD" since 91a937e76 (item 5). Read from the code, not measured for plan: a source whose HEAD is detached at a commit no branch or tag reaches, and that lacks an object, is refused by plan as by check and execute. This PR's 15 rows still pin plan's refusals for the branch and tag cases, and #1180's new HEAD rows run check and execute. #1180's third commit, a4ec51d2e, ends the walk with -- (the argv _objects_lacking runs, and the walk the refusal prints), so a file of the source's work tree named as HEAD's id no longer makes git exit 128 over a source that lacks nothing. plan reaches it through the same function; that is read from the code, not measured for plan, as #1180's new row runs check.

Why

  • A partial --pin-source is not refused for being partial. A digest is read from the pinned commit's trees, and a --filter=blob:none clone that holds the commit holds them: measured, it gives the digest a full clone does, with 0 runs, checked out or not. Refusing it would break a working setup for no reason. Only a read that failed is asked about, so a partial clone that holds the commit and its trees is read as any clone is, and a clone that is no partial one keeps the reader's refusal untouched.
  • The refusal keeps the reader's code and words. pin_digest_from_source's remediation ("point --pin-source at a clone that HAS the pinned commit, git fetch --all first") names no cause for a tree:0 clone that holds the commit and lacks its trees. The sentence added says it is a partial clone, and the cure is git fetch --refetch --no-filter <remote> there, or a full clone, or dropping --pin-source. shape_materialize.py and scaffold-project.py are not changed.
  • Two layers, as check and execute have them. The variable keeps every read unfetched (the pin read included: where the reader runs git ls-tree, git stops at a missing object instead of fetching it). The guard refuses a --source short of objects before any read, so a git that ignores the variable still never fetches into the source. A full clone is asked its config alone, which reads no object.
  • plan refuses what check would refuse, before the plan exists. A plan written from a source check and execute then refuse sent the person through a plan they could not use.

Tests

New file tests/test_adopt_plan_partial_clone.py, 15 rows (14, and since 359a749c0 the order row, r1190's NIT-1). It imports #1145's Source, its fixtures (adopter, made, planned_once, no_ambient_variable) and skip_where_git_ignores_the_variable from tests/test_adopt_partial_clone_source.py, and counts the promisor's upload-pack runs with the same stub.

rows what they hold base 2c8082a2 head 0d3089ae
test_plan_refuses_a_partial_clone_source_that_lacks_objects[nocheckout, checkout] exit 2 source-partial-clone, no plan, 0 runs, objects/ unchanged 2 failed passed
test_plan_reads_a_full_clone_and_a_cured_partial_clone_as_before[full, cured] plan written, 0 runs passed passed
test_plan_s_guard_alone_refuses_where_git_ignores_the_variable NO_LAZY_FETCH patched to a name no git reads: the guard alone refuses failed passed
test_plan_reads_a_pin_clone_that_holds_the_commit_and_its_trees[full, blobnone, blobnone-checkout, tree0-cured] plan written, the digest of a full clone's, 0 runs passed passed
test_plan_refuses_a_partial_pin_source_it_could_read_only_by_fetching[tree0, lacks-commit] pin-source-unreadable naming the partial clone and --refetch --no-filter, and (since 359a749c0) that a later fetch stays filtered, 0 runs, pin clone unchanged 2 failed passed
test_a_partial_source_is_refused_before_a_partial_pin_source_is_read (since 359a749c0) a nocheckout --source and a tree:0 --pin-source together: source-partial-clone, no pin-source-unreadable, no plan, nothing fetched into either clone, on any git failed (base a0e8cded) passed (359a749c0)
test_a_full_pin_clone_that_lacks_the_commit_is_refused_as_before the reader's refusal, no added sentence passed passed
test_a_folder_inside_a_partial_pin_clone_is_no_clone_as_before pin-source-not-a-clone, no added sentence passed passed
test_the_variable_is_what_keeps_a_partial_pin_source_unfetched tree:0 pin clone: refused with nothing fetched; with the constant patched, the plan is written after a fetch failed passed

Base: 6 failed, 8 passed. Head: 14 passed (9.8 s). No assertion reads text that depends on the Python version. Against #1180's new head 6cd96b8b (the new file alone, with none of this PR's code): the same 6 rows fail and the same 8 pass. At the merge heads 00f959c61, d4d6e2a7a, 7f3e12802 and 160c2dce0 the 14 rows pass (call (a) of each rerun below). Against this PR's base now, #1180's head a0e8cded (its adopt-project.py and docs/cli.md put into a scratch clone of 359a749c0, the 15-row file run there): 7 failed, 8 passed in 25.93 s, the same 6 and the order row, whose plan writes the plan. Head 359a749c0: 15 passed.

Mutants: 8 of 8 killed, no survivor (runner restores each in a finally, clone of the tree). Run again at the first merge head 00f959c61, and again at the second, d4d6e2a7a (a fresh clone each time; adopt-project.py's blob, d70f0dd4d then 745328f1f, checked equal to HEAD's after each mutant): 8 of 8 killed each time, by the same rows (the counts below are from the run at d4d6e2a7a, each one the same as at 00f959c61):

mutant killed by
M1 no @_lazy_fetch_off() on cmd_plan the two pin refusals, ..._the_variable_is_what_keeps_... (3 failed)
M2 no guard call in cmd_plan both source refusals, the guard alone (3 failed)
M3 no pin re-raise both pin refusals (2 failed)
M4 the re-raise for a full clone too full clone lacking the commit, folder inside a partial clone (2 failed)
M5 the re-raise for any refusal code folder inside a partial clone (1 failed)
M6 the reader's words dropped both pin refusals (2 failed)
M7 the reader's remediation kept both pin refusals (2 failed)
M8 the environment not restored the guard alone, ..._the_variable_... (2 failed)

Round 2 (r1190's NITs), 2 of 2 killed. r1190 ran 14 mutants against 7f3e12802 and 13 were killed. Its survivor, here "r1190's M3", moves the guard after _checked_plan_inputs, so after the pin read. _checked_plan_inputs reads no object of the source, so no fetch row could see the move. Each mutant below was applied to 359a749c0's adopt-project.py in a scratch clone and the whole 15-row file was run. Then the file was restored, its blob checked equal to HEAD's 889af8ffe, and the file run again (15 passed each time):

mutant mutated killed by
r1190's M3: the guard moved after _checked_plan_inputs 1 failed, 14 passed: plan said REFUSED pin-source-unreadable where source-partial-clone is asserted the order row
the pin refusal's later-fetch sentence dropped (NIT-3) 2 failed, 13 passed both pin refusals, tree0 and lacks-commit

Gate, on head 0d3089ae, git 2.43.0, Python 3.12.3, each call under timeout 540 with its own basetemp, at most two at once, under the probe environment (a stub gh, a bogus token, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, EMAIL set, LANES_LANE unset). plan's children now inherit GIT_NO_LAZY_FETCH=1; the files that dump a child's environment (test_setup_project_py.py in H5, test_setup_sh.py in H8, test_doctor_validator_rows.py in H11, test_cli_reference.py in G7) passed, and so did test_shape_doctor.py (G2 to G5).

call files result
G1 test_path_guard_hygiene.py, test_no_auto_maintenance.py 107 passed, 2 skipped (the session began with GIT_CONFIG_GLOBAL set), 23 s
G2 to G5 test_shape_doctor.py, 4 node-id shards of 112 111 passed, 1 skipped; 112 passed; 111 passed, 1 skipped; 111 passed, 1 skipped. The 3 skips are the PowerShell row, which runs only on a Windows host
G6 test_scaffold_leg_path_collisions.py 104 passed, 7 s
G7 test_agents_md_codes.py, test_cli_reference.py, test_repo_hygiene.py 170 passed, 41 s
H1 to H3 test_adopt_plan_yaml_quoting.py, 3 node-id shards of 180 180 passed in each, 540 in all
H4 test_validate_pins_named_and_spelled.py 344 passed, 60 s
H5 test_setup_project_py.py 340 passed, 114 s
H6 test_naming_policy.py, test_chain_pin_sources.py 316 passed, 26 s
H7 test_adopt_root_symlinks.py, test_validate_pins_spelled_and_named.py 311 passed, 3 skipped: the reftable rows need git 2.45
H8 test_setup_sh.py, test_adopt_finding_spelling.py 319 passed, 112 s
H9 test_adopt_execute_refusals.py, test_pin_source_env_collisions.py 318 passed, 299 s
H10 test_adopt_pins.py, test_template_text_placeholders.py, test_adopt_plan_out_checked.py 319 passed, 322 s
H11 test_doctor_non_ascii_paths.py, test_dry_run_parity.py, test_doctor_validator_rows.py 320 passed, 288 s
H12 test_scaffold_free_text_values.py, test_scaffold_manifest_values.py, test_adopt_plan_flag_refusals_first.py 315 passed
H13 test_adopt_question_and_submodule_spelling.py, test_pinned_copy_text.py, test_adopt_manifest_values.py, test_pin_source_top_level.py 319 passed, 187 s
H14 test_assembly_root_unreadable_paths.py, test_pin_source_unreadable.py, test_adopt_entry_paths.py, test_adopt_template_values.py 320 passed, 176 s
H15 test_family_refuse_up_front.py, test_validate_pins_without_git.py, test_scaffold_pin_and_reuse.py, test_update_shape_landing.py 307 passed
H16 test_chain_remedy_callers.py, test_adopt_plan.py, test_git_output_decoding.py, test_family_add_refusals.py, test_scaffold_device_names.py 314 passed, 1 skipped: the plan row backed by a real clone, which SHAPE_MEDXEHR_CLONE names
H17 test_github_name_limits.py, test_adopt_plan_escapes.py, test_stdout_encoding.py, test_adopt_plan_leg_path_help.py and 3 more (7 files) 319 passed, 1 skipped (Windows' own default), 363 s
H18 test_reader_refusals_named.py, test_readme_windows_quoting_sentence.py and 6 more (8 files) 320 passed, 272 s
H19 10 small files (test_doctor_partial_clone.py, test_pin_annotated_tag.py, ...) 313 passed
H20 test_adopt_partial_clone_source.py, test_adopt_plan_partial_clone.py, test_doctor_no_lazy_fetch.py and 5 more 149 passed

6629 passed, 10 skipped, 0 failed in 27 calls. w1162b ran 14 calls: 3935 passed, 3 skipped. Lane 1 ran the other 13 at the same head after LAST STEP: 2694 passed, 7 skipped. Each call was under 9 minutes. H1 to H20 hold the 70 files that grep -l -i -E 'partial|promisor|pin-source|pin_source' tests/*.py finds and three named ones (test_adopt_plan_leg_path_help.py, test_adopt_plan_out_checked.py, test_adopt_plan_value_order.py), less conftest.py (it has no tests) and the two files G6 and G7 already run: 70 files, 5808 tests. The new file's own 14 rows passed alone (above) and test_adopt_partial_clone_source.py's 22 are in H20.

Reruns at the merge head 00f959c61. The merge changed two files against 0d3089aef: adopt-project.py (the source-partial-clone refusal: its remediation now differs by whether git could walk the refs, and says a later fetch stays filtered while remote.<name>.partialclonefilter is set; the new helper _filter_key; docstrings and comments) and tests/test_adopt_partial_clone_source.py (#1180's rows, 26 now, 22 before). docs/cli.md, --help, tests/test_adopt_plan_partial_clone.py and every other file are the same as at 0d3089aef. The two conflict hunks were _lazy_fetch_off, which this branch moved above cmd_plan and #1180 reworded where it stood (its sentence about _not_a_whole_clone is now in the moved copy, ahead of this branch's pin-source sentence), and the Refusal of _refuse_a_partial_clone_short (this branch's "plan, check and execute never fetch into the source", with #1180's remediation). Same probe environment and limits as the gate above.

call files at 00f959c61 at 0d3089aef
(a) test_adopt_plan_partial_clone.py, test_adopt_partial_clone_source.py 40 passed (14 + 26), 16 s 14 + 22, in H20
(b) H20: test_adopt_partial_clone_source.py, test_update_shape_partial_rollback.py, test_adopt_plan_value_order.py, test_non_utf8_name_helpers.py, test_update_shape_merged_away.py, test_chain_link_fold.py, test_doctor_no_lazy_fetch.py, test_adopt_plan_partial_clone.py 153 passed, 124 s 149 passed (the 4 more are #1180's new rows, from 70f324859)
(c) G1: test_path_guard_hygiene.py, test_no_auto_maintenance.py 107 passed, 2 skipped, 12 s 107 passed, 2 skipped
(d) any other file that names a function the merge changed (_lazy_fetch_off, _refuse_a_partial_clone_short, _filter_key) none: a grep of tests/ finds only the two files of (a) and test_doctor_no_lazy_fetch.py (in (b); its match is the doctor's own lazy_fetch_off test)
extra G7: test_agents_md_codes.py, test_cli_reference.py, test_repo_hygiene.py 170 passed, 25 s 170 passed
extra G6: test_scaffold_leg_path_collisions.py 104 passed, 4 s 104 passed
extra G2 to G5: test_shape_doctor.py, the same 4 node-id shards 111 passed, 1 skipped; 112 passed; 111 passed, 1 skipped; 111 passed, 1 skipped the same
mutants the 8 above, against the merged adopt-project.py 8 of 8 killed 8 of 8 killed

Reruns at the second merge head d4d6e2a7a. The merge changed three files against 00f959c61: adopt-project.py (_head_id and HEAD_ID; _objects_lacking walks the id; the refusal's and the git fsck sentence's words; the check and execute --source help), docs/cli.md (that help) and tests/test_adopt_partial_clone_source.py (#1180's new rows: 37 now, 26 before). tests/test_adopt_plan_partial_clone.py and every other file are the same as at 00f959c61. The three conflict hunks, all taken as both sides: the SOURCE_PARTIAL_CLONE comment (this branch's layout, with NO_LAZY_FETCH above cmd_plan, and #1180's "branches, tags or HEAD" words), the docstring of _refuse_a_partial_clone_short (#1180's text, with this branch's ", and by plan (#1162)"), and the Refusal's detail (#1180's new text, with "plan, check and execute never fetch into the source"). Checked by parsing the merged file and #1180's: the raise and the docstring equal #1180's plus those plan words. Same probe environment and limits as the gate above (git 2.43.0, Python 3.12.3, timeout 540 per call, own basetemp, at most two calls at once, 22 to 25 GB free before each).

call files at d4d6e2a7a at 00f959c61
(a) test_adopt_plan_partial_clone.py, test_adopt_partial_clone_source.py 51 passed (14 + 37), 31 s 40 passed (14 + 26)
(b) H20: the 8 files named above 164 passed, 203 s 153 passed
(c) G1: test_path_guard_hygiene.py, test_no_auto_maintenance.py 107 passed, 2 skipped, 28 s 107 passed, 2 skipped
(d) G7: test_agents_md_codes.py, test_cli_reference.py, test_repo_hygiene.py 170 passed, 46 s 170 passed
(e) G6: test_scaffold_leg_path_collisions.py 104 passed, 7 s 104 passed
(f) test_adopt_source_clone_record.py, test_doctor_partial_clone.py, test_pin_annotated_tag.py 161 passed, 149 s not run
(g) G2 to G5: test_shape_doctor.py, the same 4 node-id shards 111 passed, 1 skipped (181 s); 112 passed (167 s); 111 passed, 1 skipped (185 s); 111 passed, 1 skipped (202 s) the same counts
mutants the 8 above, against the merged adopt-project.py, a fresh clone 8 of 8 killed 8 of 8 killed

1202 passed, 5 skipped, 0 failed in 10 calls at d4d6e2a7a (the 5 skips: G1's 2, as above, and the PowerShell row, once in each of three doctor shards). Each call was under 9 minutes. (f) holds the files that name something the merge changed: a grep of tests/ for source-partial-clone, _objects_lacking, LACKING_WALK, _head_id, HEAD_ID, _refuse_a_partial_clone_short, "branches or tags", "lacks at least" and "refetch --no-filter" finds only test_adopt_partial_clone_source.py and test_adopt_plan_partial_clone.py (both in (a) and (b)) and test_doctor_partial_clone.py (here: its _objects_lacking is the doctor's own), with two more that read a source's clone record and a pin's annotated tag. The doctor shards are the gate rules' for any change to adopt-project.py, and so is (e).

Reruns at the help commit 91a937e76. It changes one help string in adopt-project.py and the lines of docs/cli.md made from it; no test reads the old wording (a grep of tests/ for "branches or", "or tags reach" and "branches or tags" finds nothing). Same probe environment and limits as the gate above (git 2.43.0, Python 3.12.3, own basetemp per call, 31 GB free).

call files at 91a937e76 at d4d6e2a7a
(a) G7: test_agents_md_codes.py, test_cli_reference.py, test_repo_hygiene.py 170 passed, 38 s 170 passed
(b) test_adopt_plan_partial_clone.py 14 passed, 10 s 14 passed

Reruns at the third merge head 7f3e12802 (#1180's a4ec51d2: the walk ends with --). Two calls, run at once, each in its own basetemp, under the probe environment of the gate above (a stub gh, a bogus token, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, EMAIL set, LANES_LANE unset), git 2.43.0, Python 3.12.3, 49 GB free and 3 other pytest runs in the container before the start. Each call was under 9 minutes.

call files at 7f3e12802 before
(a) test_adopt_partial_clone_source.py, test_adopt_plan_partial_clone.py 52 passed (38 + 14), 47 s 51 passed (37 + 14) at d4d6e2a7a
(b) G7: test_agents_md_codes.py, test_cli_reference.py, test_repo_hygiene.py 170 passed (47 + 23 + 100), 47 s 170 passed at 91a937e76

222 passed, 0 skipped, 0 failed in 2 calls at 7f3e12802. The one new row is among the 38. test_cli_reference.py passed, so docs/cli.md is what scripts/render-cli-reference.py produces and needed no regeneration; the merge does not change docs/cli.md. A grep of tests/ for source-partial-clone, _objects_lacking, LACKING_WALK, _head_id, HEAD_ID, _refuse_a_partial_clone_short, "lacks at least" and "refetch --no-filter", run at this head, finds the same three files as at d4d6e2a7a: the two of (a), and test_doctor_partial_clone.py (its _objects_lacking is the doctor's own; not rerun, since no function it reads is changed). None of the merge's 45 changed lines holds is_file, is_dir, exists, is_symlink or os.path.

Reruns at the fourth merge head 160c2dce0 and at 359a749c0 (r1190's NITs). Under the probe environment of the gate above (a stub gh, a bogus token, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, EMAIL set, LANES_LANE unset), git 2.43.0, Python 3.12.3, each call in its own basetemp, under 9 minutes, at most two at once, 45 GB or more free before each.

call head files result
(a) 160c2dce0 test_adopt_plan_partial_clone.py, test_adopt_partial_clone_source.py 52 passed (14 + 38), 43.84 s
(b) 359a749c0 the same two files 53 passed (15 + 38), 38.44 s
(c) 359a749c0 G7: test_agents_md_codes.py, test_cli_reference.py, test_repo_hygiene.py 171 passed, 39.38 s (collected: 170 at 7f3e12802, 171 at 160c2dce0 and 359a749c0; the one more comes in with main, through #1180's restack)
(d) a0e8cded's tool the 15-row file (base) 7 failed, 8 passed, 25.93 s
mutants 359a749c0 r1190's M3 and the dropped later-fetch sentence 2 of 2 killed

scripts/render-cli-reference.py wrote docs/cli.md from the new help, and --check then said [ok] docs/cli.md is what the generator produces. Lane 1 runs the full landing gate. H1 to H20, G1 to G6 and the 8 mutants of round 1 did not run at either head.

The rest of the gate ran at 0d3089aef, before any merge: G1 to G7 and H1 to H20. Of those, G1 to G7 and H20 ran again at each of the first two merge heads (above). H1 to H19 were not rerun at any merge head, nor at 91a937e76 or 7f3e12802, where only the calls of the two tables above ran: G1, G2 to G6, H20 and the 8 mutants were not rerun at either of those two heads. The merges brought in nothing but #1180's changes, which reach only a source that is a partial clone (_objects_lacking and _head_id are called from _refuse_a_partial_clone_short alone, after it has returned for a clone with no promisor remote) and the check and execute --source help text, and, at 7f3e12802, the -- the walk ends with (a change in _objects_lacking alone, which none of the 8 mutants is in); and that grep finds none of H1 to H19's files but test_doctor_partial_clone.py (H19), which (f) reran at d4d6e2a7a and nothing has run since.

Known limits

  • A git that ignores GIT_NO_LAZY_FETCH still fetches into a partial --pin-source, as the --pin-source help now says too. Git honours the variable from 2.45.0 and in the security releases of 2024-05-14 back to 2.39.4 (CVE-2024-32465; Ubuntu's 2.43.0-1ubuntu7.1 and later). Before that, and in unpatched 2.40-2.44, a partial --pin-source short of the pinned commit or its trees is fetched into, as before adopt-project.py plan: a partial-clone --source is fetched into, as check and execute were before #1145 #1162. There is no guard for pins, by design: a clone that holds the commit and its trees must stay readable. The --source is kept whole on every git, by the guard.
  • A partial --pin-source that git cannot read for another reason than a missing object keeps the reader's own refusal, with the partial-clone sentence added, because the clone is partial; the cure it names is then not what fails it. The reader's own words come first in the detail.
  • plan does not walk a --source that is not a partial clone: the guard reads its config alone.

Not in this PR

scaffold-project.py --pin-source reads its pin with the same declared_pin_values and has the same fetch. Lane 1 files that as its own follow-up, outside v1.2.0 by the same ruling; the scaffold is not changed here.

Lane: openreposhape-1 (openRepoShape-1)

🤖 Generated with Claude Code

Summary by Sourcery

Prevent planning from fetching into partial clones while preserving support for partial pin sources that already contain the required objects.

New Features:

  • Prevent plan from lazily fetching missing objects into partial-clone sources and refuse incomplete sources before generating a plan.
  • Support readable partial --pin-source clones while clearly refusing pin sources that require fetching and providing remediation guidance.

Bug Fixes:

  • Ensure partial-clone reads do not silently trigger network fetches or mutate repositories during planning.

Enhancements:

  • Extend partial-clone detection to source branches, tags, and HEAD reachability, with diagnostics and recovery instructions shared across plan, check, and execute.
  • Document partial-clone behavior and Git-version limitations in the CLI reference.

Documentation:

  • Regenerate the CLI reference with updated source and pin-source partial-clone guidance.

Tests:

  • Add comprehensive coverage for partial source and pin-source behavior, fetch prevention, refusal ordering, promisor remotes, HEAD handling, and environment restoration.

Brett Heap and others added 19 commits October 21, 2026 12:58
…` of the source in the work directory, and `_resolved` and `_answers` make their repositories as #910's throwaways are made (#1083)

Work in progress for #1083, stopped at a usage checkpoint.

Built: `_split_clone_asked` (asked first in `_entry_findings`; a failure is
`adopt-git-unasked`, which `execute` refuses before any leg); `_resolved`
and `_answers` made with `_throwaway`/`_throwaway_made` in the work
directory, no hooks, `--template=<empty>`, `-b`, the source's format,
under `_inherited_env()`; `_throwaway_git`; `_submodule_findings` skips the
url questions once one throwaway could not be asked. 16 rows added or
changed in tests/test_adopt_check_throwaway_repos.py (45 rows: all pass on
this head; 14 fail and 31 pass over 2a119f2's adopt-project.py).

Left: the TMPDIR docstring in tests/test_adopt_reencoded_files.py, the
gate, and the PR body.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e_checkout_writes_is_never_written_through` says the throwaway repository is made in the work directory, as #910 makes it, and no longer in `TMPDIR` (#1083)

The last item of #1083's (c): the docstring still said the throwaway
repository the link is asked in is in `TMPDIR`. Since #910 it is made in
the work directory, which is as deep below `tmp_path` as the `TMPDIR` the
row sets, so the row asks the same question and only its words change.
(The issue's `adopt-project.py:466` names `_pin_row_problem` at 74c1770;
this docstring is the one PR #1031's body named.)

Gate on the merged tree (main 6da757c): see the pull request body.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… parent git hands it, as execute's does, and no variable naming another repository; _resolved and _answers read their environment before their throwaway (#1083)

r1119's fix round on #1119.

SHOULD-2: `_split_clone_asked` makes the clone in `_split_clone_env()`:
`_inherited_env()` plus `GIT_CONFIG_PARAMETERS` where it is set. Measured on
bdcf995 with git 2.43, `GIT_CONFIG_PARAMETERS="'init.defaultbranch'='a..b'"`
gave `plan ok` from `check`, and `execute` made both legs and stopped
extracting the spec leg. Now `check` says `adopt-git-unasked` and `execute`
refuses before any leg, as for the setting in a config file or in
`GIT_CONFIG_COUNT`. A parent's `-c core.hooksPath` runs no hook: the clone's
own `-c` comes after it.

SHOULD-1: a row runs `check` under `GIT_OBJECT_DIRECTORY` naming the source's
own objects and holds the source's object directory unchanged. It kills M6:
in the whole environment the clone wrote `objects/info/alternates` into the
source. A row under `GIT_DIR` naming the source, whose own config rewrites
the on-disk url, kills M14 (`_answers` in the whole environment) and holds
the source's config unchanged.

NIT-1: the docstring says git ignores `--shared` for a shallow source, so
that clone is a full copy, a cost `execute`'s own clones pay too.
NIT-2: the unmade-work-directory NOTE says it asked nothing about the urls of
the submodules the plan keeps either.
NIT-3: `_resolved`, `_answers` and the clone read their environment before
they make their throwaway, so a raise there leaves none behind.

tests/test_adopt_check_throwaway_repos.py: 63 rows (45 before), 18 new.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….fsmonitor=false`, and the rows r1119b named pass on main: the clone-env row appends to the environment's count, and the ref-probe row fails only the probe's `git init`

r1119b's second review of #1119, after the merge of main 038dafb (#1091, #1111):

- MUST-1: `test_the_clone_takes_a_parent_gits_settings_and_no_other_repository`
  built its `GIT_CONFIG_COUNT` from `{}`, which
  `test_no_test_sets_a_git_config_count_of_its_own` refuses. It now appends
  to `os.environ`, and asserts the last entry kept and no `attr.tree`.
- MUST-2: main's `test_check_says_where_git_would_not_make_the_ref_probe`
  set `GIT_DEFAULT_HASH=bogus`, which now stops the split's own clone
  first (`adopt-git-unasked`, exit 1). A stub `git` first on PATH fails
  only `git init` of a `refs-probe-*` directory, with git's words; skipped
  on Windows.
- SHOULD-A: `_throwaway_made` returns `-c core.fsmonitor=false` beside
  `core.hooksPath`, as the rehearsal and `_stored_as_written` give theirs.
  A counting fsmonitor program ran 8 times in `_resolved`'s two
  repositories in one `check`; now 0, in the global config and in
  `GIT_CONFIG_COUNT`. New rows: that one and its control.
- NIT-A: a row with `--work-dir` inside the source, whose config rewrites
  the url (kills R8).
- NIT-B: a row with that rewrite handed down by a parent git in
  `GIT_CONFIG_PARAMETERS`, which `_answers` leaves out (kills A18).
- NIT-C: `_split_clone_asked`'s docstring says a shallow source makes the
  clone run the `uploadpack.packObjectsHook` of protected config, as
  `execute`'s clones do.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inside the source also rewrites the url to a network, so a `--get-url` asked around the throwaway, which names it in a NOTE as not asked, fails it

f1119c's mutant R8a, which moved only `_answers`' `git ls-remote
--get-url` out of its throwaway repository into the throwaway's own
directory, survived the row that kills r1119b's R8: the expansion there
only decides between the disk and a network, and the url itself was
still asked in the throwaway. A source config that rewrites the on-disk
url to `https://f1119c.invalid/Thing` makes R8a print the NOTE of a url
on a network it did not ask; on this head `check` prints none, inside
the source or outside it. Now killed by the row's `a-network` case.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r system config as where the `uploadpack.packObjectsHook` a shallow source's clone runs comes from, since a parent's `-c` did not reach it

cea2553's NIT-C sentence said "protected config ... or a `-c`". f1119c
measured it on git 2.43, with a shallow source and a counting hook:

- in the global config, 1 run in `check` and 1 in a clone made as
  `_extract_leg` makes it;
- in the source's own config, 0 and 0;
- in `GIT_CONFIG_PARAMETERS`, as a parent `git -c` hands it down, 0 and 0.

So the `-c` is dropped. The commit message of cea2553 says "of
protected config" for the same sentence; this is the measured form.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…` and refuse a partial-clone source that lacks an object its branches or tags reach as `source-partial-clone`, so neither fetches into the source any more (#1145)

Measured with git 2.43 on a `--filter=blob:none --no-checkout` source:
`check` ran the source's promisor `upload-pack` 26 times and wrote 26
promisor packs into it; on this commit it runs it 0 times and leaves the
source's `objects/` unchanged. A full clone and a partial clone given
every object again (`git fetch --refetch --no-filter origin`) still
check `plan ok`.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rce` name `source-partial-clone`, the source it refuses, and the fetch that makes it whole (#1145)

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e a partial-clone source that lacks objects as `source-partial-clone` and run no `upload-pack`, and read no commit the plan names that the source lacks (#1145)

Each row counts the promisor's `upload-pack` runs through a counting
`remote.origin.uploadpack` and hashes the source's `objects/`. On #1119's
head bba168b, 18 of the 21 rows fail: `check` says `plan ok` for a
`--no-checkout` partial clone, a checked-out one, and one whose own
config sets `protocol.file.allow=always`; `execute` dies in `git
filter-repo` after both legs; the guard alone is not there; and the
plan's commit, read by the Triad advisory and by `_not_a_whole_clone`,
is fetched once. The full clone, the cured clone and the cured
`execute` pass on both.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fails is refused on any git, and not read as lacking nothing (#1145)

The hand-written branch at a commit the source lacks makes `git rev-list
--missing=print` exit 128 only before git 2.45; from 2.45 it lists the
commit as lacking, so a `None` read as `0` would pass that row there. A
walk made to fail in-process (`LACKING_WALK` with an option no git has)
holds the `None` path on every git. It fails on bba168b (`plan ok`).

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…=1`, refuses a partial-clone `--source` that lacks objects as `source-partial-clone`, and says a partial `--pin-source` it could read only by fetching is a partial clone and how to make it whole (#1162)

Measured on #1145's head 2c8082a with git 2.43: `plan` ran a
`--filter=blob:none --no-checkout` source's promisor `upload-pack` 26
times (`.promisor` 1 -> 27), and once for a `--filter=tree:0`
`--pin-source` and for a `--filter=blob:none` one lacking the pinned
commit. Now `cmd_plan` is under `_lazy_fetch_off` (moved above it, since
a decorator needs it at definition), asks `_refuse_a_partial_clone_short`
right after `Source.of`, and `_pins_read_unfetched` re-raises the pin
reader's `pin-source-unreadable` for a partial clone with that reason and
the `git fetch --refetch --no-filter <remote>` cure. A blob:none pin
clone that holds the commit is read as before. `--source` and
`--pin-source` help say so; docs/cli.md regenerated with
scripts/render-cli-reference.py.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e `--source` that lacks objects, and a partial `--pin-source` it could read only by fetching, and runs no `upload-pack` in either (#1162)

Rows reuse #1145's `Source` (the counting `upload-pack`, the hash of
`objects/`); a pin clone runs the same wrapper, so one log counts both.
On base 2c8082a: 6 rows fail (the `--no-checkout` and checked-out
partial `--source`, the guard alone, the `tree:0` and lacks-commit pin
clones, the variable through a pin read), 8 controls pass (full and cured
sources; full, blob:none, checked-out blob:none and cured tree:0 pin
clones; a full pin clone lacking the commit; a folder inside a partial pin
clone). On head all 14 pass.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…leaves a `tree:0` pin clone short of its trees: measured with git 2.43, it fetched them (#1162)

`git fetch --all` in a `--filter=tree:0 --no-checkout` clone at its
tip ran the remote's `upload-pack` twice and left all 3 trees present,
so `ls-tree -r HEAD` then read under `GIT_NO_LAZY_FETCH=1`. The comment
now says why the reader's remediation fell short: it asks for a clone
that has the pinned commit, and names no cause for a clone that holds
it and lacks its trees.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…blob the source lacks is refused through `check` and `execute` as `source-partial-clone`, and the remote `extensions.partialclone` names comes last where it is a promisor too, so neither the `--tags` of the walk nor that order is left unpinned (#1145)

A cured partial clone that fetches an annotated tag with the filter it keeps
lacks the blob its commit changes, and only the tag reaches it: branches lack
0 objects, tags 1. With `--tags` out of `LACKING_WALK`, `check` said `plan
ok` and `execute` made both legs, then died in `git filter-repo`.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ource-partial-clone` refusal says a later fetch stays filtered while `remote.<name>.partialclonefilter` is set, says git could not walk the refs, and names `git fsck`, where the walk fails, and the comments say `_not_a_whole_clone` reads a cloned plan's commit before the guard in any partial clone (#1145)

The refetch the refusal names leaves `remote.<name>.partialclonefilter` as it
was, so the next plain fetch can leave the source lacking again. Where
`git rev-list` cannot walk the branches and tags, nothing is known of what
is lacking: the refusal said "lacks what its branches and tags reach, as far
as git can tell", and named a refetch that does not bring in a ref outside
the remote's refspec. It now says git could not walk them and names `git
fsck`, as `Source.tree` does, with the refetch as a second way. The code
stays `source-partial-clone`, so the doc trio is unchanged.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One conflict hunk in adopt-project.py per side, both resolved by taking both
changes. `_lazy_fetch_off` moved above `cmd_plan` on this branch, and #1180
reworded its docstring where it stood: the reworded sentence now sits in the
moved copy, ahead of this branch's pin-source sentence. The `Refusal` of
`_refuse_a_partial_clone_short` keeps this branch's "`plan`, `check` and
`execute` never fetch into the source" and takes #1180's `remediation`.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR makes adopt-project.py plan consistently non-fetching: it refuses incomplete partial --source clones up front, prevents lazy fetches during pin reads, and reports actionable errors only when a partial --pin-source lacks required objects, with CLI documentation and comprehensive regression tests updated accordingly.

File-Level Changes

Change Details Files
Prevent plan from causing implicit network fetches when reading partial clones, while preserving readable partial pin sources.
  • Run the entire command with GIT_NO_LAZY_FETCH=1 and restore the caller’s environment afterward.
  • Reject incomplete partial --source repositories before object reads, using source-partial-clone and a no-filter refetch remediation.
  • Wrap pin evaluation to preserve existing reader refusals while adding partial-clone context and remediation only when a missing object caused the failure.
  • Allow partial --pin-source repositories when the pinned commit and trees are available locally; refuse only those requiring a fetch.
  • Update CLI help and regenerate the command reference with the new refusal behavior.
adopt-project.py
docs/cli.md
Add focused coverage proving plan is fetch-free and distinguishes usable, incomplete, and non-clone pin sources.
  • Verify incomplete partial sources are rejected without writing a plan or changing object storage, including when Git ignores the environment variable.
  • Verify full and sufficiently populated partial pin clones continue to produce the same digest without fetching.
  • Verify incomplete partial pin clones retain pin-source-unreadable and receive the partial-clone remediation, while full clones and nested paths retain their original refusals.
  • Verify the lazy-fetch environment variable is restored and that disabling it exposes the fetch behavior.
tests/test_adopt_plan_partial_clone.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Brett Heap and others added 2 commits October 23, 2026 07:48
…ce.py: the `source-partial-clone` walk also reaches the source's HEAD where `git rev-parse --verify -q HEAD` gives an id, so a HEAD detached at a commit no branch or tag reaches is refused through `check` and `execute` and an unborn HEAD is read as before, and the `git fsck` sentence says what fsck names in a partial clone, and the count and walk-failure words are pinned (#1145)

SHOULD-1 of r1180b: `LACKING_WALK` ended at `--branches --tags`, but `execute`'s `git clone` checks out the source's HEAD. HEAD is walked by its id, asked with `rev-parse --verify -q HEAD` and not `HEAD^{commit}`, which fetched a commit the clone lacks (measured with git 2.43, 1 `upload-pack` run) and exits 1 under `GIT_NO_LAZY_FETCH`. A plain `HEAD` fails on an unborn HEAD, exit 128, and on a file named HEAD in the work tree.

NIT-1: the `None` remedy names what `git fsck` shows, a branch, tag or HEAD whose object is lacking, and no object a promisor remote holds. NIT-2 and NIT-3: the count arm's words and the failing walk's command, `exited`, the source's path after `git fsck` in, and the cure are asserted.

Lane: openreposhape-1 (openRepoShape-1)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… blob the source lacks names HEAD in what its branches and tags reach, and a file of the source named HEAD is no path of the walk (#1145)

Lane: openreposhape-1 (openRepoShape-1)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Brett Heap and others added 2 commits October 23, 2026 15:27
…, with branches and tags, as what a partial clone must not lack (#1162)

The guard `plan` asks walks the source's branches, tags and HEAD, as `check` and `execute` do, but the `plan --source` help said only branches or tags. The help now says "its branches, tags or HEAD reach", and docs/cli.md is regenerated from it with scripts/render-cli-reference.py.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd execute a default branch git will not take share one helper each, so SonarCloud's new-code duplication on #1119 falls under its 3% gate (#1083)

The parent-git rows and the global-config rows repeated one body each; each body is now a module-level helper that takes the function building the environment, and no row asserts or sets up anything else than before. All test ids keep their names.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ource-partial-clone` walk ends with `--`, so a file of the source's work tree named as HEAD's id is no path of it (#1145)

`_objects_lacking` walked `git rev-list --objects --missing=print --branches --tags <id>`, and a file of the source's work tree named as HEAD's id made git exit 128 (`ambiguous argument '<id>': both revision and filename`), so `check` and `execute` refused a source that lacks nothing as `source-partial-clone`. The walk now ends with `--`, after the id, so no file of the work tree is read as a path of it, and the walk the refusal names ends with `--` too. The new row puts a file named as HEAD's id in a cured partial clone's work tree and expects `plan ok` with nothing fetched; it fails at `b316f415` and passes here, and the failing-walk row, born and unborn, which asserts the walk as named, now expects the `--`.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Brett Heap and others added 2 commits October 23, 2026 15:39
Lane: openreposhape-1 (openRepoShape-1)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s/test_adopt_check_throwaway_repos.py

Lane: openreposhape-1 (openRepoShape-1)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-23T18:32:09Z) Independent review r1190 of #1190 at 7f3e1280: READY, with 3 NITs. No MUST and no SHOULD. The reviewer stopped at lane 1's LAST STEP before its step 5, the gate. Lane 1 ran that gate afterwards with its own runner, at 7f3e1280 in the probe environment, and every call passed. It also closed the review's two open checks.

Lane 1 takes the 3 NITs in a fix round.

PR #1190 review: PARTIAL, stopped at LAST STEP (18:17Z)

Verdict so far: no MUST or SHOULD found; 3 NITs. I can't give READY yet, because the gate (step 5) was never run.

Reviewed head 7f3e12802b5512c5a6136524a5f6ce209cbfef51, the PR's own diff a4ec51d2...7f3e1280. Bench: git 2.43.0 (1ubuntu7.3), Python 3.12.3, probe environment (stub gh, bogus token, empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, EMAIL set, LANES_LANE unset).

What I covered

Step 1: reading the change. Correct.

  • Placement: cmd_plan carries @_lazy_fetch_off(). _refuse_a_partial_clone_short(source) runs right after Source.of(...), before PathPolicy.load, _checked_plan_inputs (the pin read), source.tree() and the plan. That is the same place check and execute call it.
  • Reads before the guard: the only reads of the source before it are the ones check and execute also make before theirs: rev-parse --show-toplevel, the origin config, _default_branch and show-ref --verify. None of them reads a blob.
  • Refusal text: it is the shared function. Its detail now names plan, and its "run this command again" remediation fits plan.
  • --source help: follows check's SOURCE_OVERRIDE_HELP ("branches, tags or HEAD", "before anything reads it") and is true on every git, because the guard does not depend on the variable.
  • _pins_read_unfetched: acts only on pin-source-unreadable, and only where the pin clone is a partial clone. It keeps the code and the reader's detail, and replaces the remediation.

Step 2: base against head. Matches the body.

  • Base (a4ec51d2's tool plus the new test file): 6 failed, 8 passed. Each failure is for the right reason: five rows give assert 0 == 2 (the plan was written), and the variable row gives assert 1 == 0 (one upload-pack run).
  • Head: 14 passed (10.3 s).
  • Under the 3.14-like pathlib shim: head 14 passed; base 6 failed, 8 passed.

Step 3: mutants. 13 of 14 killed.

  • Run in a clone at 7f3e1280. The adopt-project.py blob equalled HEAD's (6f6fd2184) after each mutant, and the clone is clean.
  • Killed: the decorator removed, the guard removed, the guard moved after source.tree(), the wrapper unused, the re-raise for any refusal code, the re-raise for any clone, another code, the detail dropped, the cure replaced by fetch --all, the environment not restored, the variable set to "0", the reader's remediation kept, and the help reworded without regenerating the docs (test_cli_reference, 5 failed).
  • Survivor M3: the guard moved after _checked_plan_inputs still passes all 14 rows (NIT 1).

Step 4: probes, all at head, 0 upload-pack runs and objects/ unchanged in every one.

Shape Head Base a4ec51d2
HEAD detached at a commit only a deleted tag reached, 1 blob lacking exit 2 source-partial-clone exit 0, 0 runs
Unborn HEAD, --filter=blob:none --no-checkout clone (the refusal names 28 objects lacking) exit 2 exit 0, 26 runs
Unborn HEAD, cured clone plan written not run
A branch at a commit the clone lacks exit 2, "could not walk" text with the git fsck remediation exit 0, 26 runs
Partial --source and tree:0 --pin-source together exit 2 source-partial-clone exit 0, 27 runs
--source as a sub-folder source-not-top-level, unchanged not run
--source as a linked worktree of a partial clone exit 2 not run
tree:0 pin clone, and one with remote.origin.promisor unset (filter key only) pin-source-unreadable with the partial-clone sentence and the refetch cure not run

The HEAD-only case was "read from the code, not measured for plan" in the body; it is now measured, and it holds.

Body claims checked, all true:

  • 91a937e76 changes adopt-project.py 2/2 and docs/cli.md 7/6 lines.
  • 7f3e1280 against 91a937e76 changes adopt-project.py 11/5 and the test file 24/5.
  • The tree 6f740ccb7 equals what merge-tree --write-tree 91a937e76 a4ec51d2 gives.
  • Both diffs have 45 changed lines, identical line for line.
  • Base 6 failed / 8 passed, head 14 passed.

Findings

NIT 1: a missing order row (mutant M3 survives).

  • Where: adopt-project.py around line 6400 (the guard call in cmd_plan), and tests/test_adopt_plan_partial_clone.py.
  • Evidence: with the guard moved after _checked_plan_inputs, mutants.py reports M3 guard moved after _checked_plan_inputs (after the pin read): SURVIVED (14 passed in 8.95s). _checked_plan_inputs reads no object of the source, so no fetch row can see the move; what changes is which refusal comes first. Today the order is right: my "both" probe at head gives source-partial-clone.
  • Fix: add one row with a partial --source and a tree:0 --pin-source (or a mismatched --org) that asserts REFUSED source-partial-clone and no pin-source-unreadable.

NIT 2: the --pin-source help overstates.

  • Where: plan's --pin-source help in adopt-project.py (~line 18238), and docs/cli.md lines 589-593.
  • Evidence: the help says "...is refused as pin-source-unreadable, since plan never fetches into it". That holds only on a git that honours GIT_NO_LAZY_FETCH. The PR body's Known limits and the _lazy_fetch_off docstring both say an older or unpatched git still fetches into a partial --pin-source, and there is no guard for pins by design.
  • Fix: qualify it ("on a git that honours GIT_NO_LAZY_FETCH: 2.45, or a 2.39.4+ security release"), then regenerate docs/cli.md.

NIT 3: the pin cure leaves out the filter caveat.

What is left

  1. Step 5, the gate, not run:
    • tests/test_adopt_plan_partial_clone.py with tests/test_adopt_partial_clone_source.py in one call. This matters because the new file imports the other file's module-scoped fixtures.
    • The doc trio.
    • tests/test_scaffold_leg_path_collisions.py.
    • The doctor's 4 shards.
  2. The body's closing form: line 1 is Closes #1162, but I did not grep the rest of the body for other close/fix/resolve forms. closingIssuesReferences is [], which is expected while the base is adopt-project.py: check and execute run with GIT_NO_LAZY_FETCH=1 and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach as source-partial-clone, so neither fetches into the source any more (#1145) #1180's branch.
  3. The stacking statement: I did not read adopt-project.py: check and execute run with GIT_NO_LAZY_FETCH=1 and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach as source-partial-clone, so neither fetches into the source any more (#1145) #1180's current head or its base branch tip back from GitHub to confirm both are still a4ec51d2.

No run of mine is live; every run was in the foreground and has ended. The review worktree and the mutant clone are clean.

Files are in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1190:

  • findings.md
  • mutants.py
  • mutants.out
  • probe.py
  • base-run.log
  • head-run.log

Review worktree: /workspace/projects/openRepoShape-worktrees/l1-r1190

Lane: openreposhape-1 (openRepoShape-1)

Brett Heap and others added 6 commits October 23, 2026 20:25
…o-fetch

Step 1 of the three-merge restack after #1119 squash-landed on main as b7edd88.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 2 of the three-merge restack. git diff 9b85644 b7edd88 is empty for
#1119's files, so any conflict there takes ours, the branch's version.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 3 of the three-merge restack after #1119's squash b7edd88.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xt beside the `source-partial-clone` walk says a plain `HEAD` fails on an unborn HEAD as `bad revision 'HEAD'` now that the walk ends with `--`, and the rows pin git's own words after the failed walk's exit and the exact count the source lacks (#1145)

r1180d's NIT-1 to NIT-3.

- NIT-1: the `HEAD_ID` comment, the `_objects_lacking` docstring and two
  row docstrings described the walk as it was before the `--`. Measured
  with git 2.43: `rev-list ... HEAD --` on an unborn HEAD says
  `fatal: bad revision 'HEAD'` (exit 128); a file named `HEAD` is read as
  a path (`both revision and filename`) only without the `--`.
- NIT-2: the failing-walk row asks this git for its own words, running the
  same walk, and asserts `exited <rc>: <words>`. Git 2.43 prints its usage
  (exit 129) for `--no-such-option`, which does not name the option.
  Kills M15 (git's words dropped).
- NIT-3: the count row counts the `?` lines of the tool's own walk
  (`--branches --tags HEAD --`), asserts more than 1, and asserts
  `that lacks at least <n> object(s) `. Kills M17 (count held at 1).

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1180 restacked on main after #1119's squash b7edd88 through three merges
(c3f8a2b, ef73f75, b18d2e8), then a0e8cde fixed r1180d's NIT-1 to NIT-3.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….py: a row pins that `plan` refuses a partial `--source` before it reads a partial `--pin-source`, the `--pin-source` help says a partial pin clone is left unfetched only on a git that honours `GIT_NO_LAZY_FETCH`, and the pin refusal says a later fetch stays filtered (#1162)

r1190's NIT-1 to NIT-3.

- NIT-1: `test_a_partial_source_is_refused_before_a_partial_pin_source_is_read`,
  a `nocheckout` `--source` and a `tree:0` `--pin-source` together:
  `source-partial-clone`, no `pin-source-unreadable`, no plan, nothing
  fetched into either clone. Kills M3 (the guard moved after
  `_checked_plan_inputs`).
- NIT-2: the `--pin-source` help names the gits that honour
  `GIT_NO_LAZY_FETCH` (2.45 or later, or a security release from 2.39.4
  on) and says an older git fetches into the clone; `docs/cli.md`
  regenerated by `scripts/render-cli-reference.py`.
- NIT-3: `_pins_read_unfetched`'s remediation ends with the source
  refusal's `_filter_key` sentence, and the pin-refusal row asserts it.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-23T20:50:03Z) Lane 1's delta check of #1190 since r1190's READY at 7f3e1280: the round is sound, with one NIT for lane 1 to check before it lands.

Lane: openreposhape-1 (openRepoShape-1)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-23T21:03:53Z) Lane 1 withdraws its NIT on the help's "2.45 or later" (issuecomment-6374465346): the text is right as written, so READY carries to 359a749c once tests is green there.

Lane: openreposhape-1 (openRepoShape-1)

brettheap pushed a commit that referenced this pull request Oct 23, 2026
…nothing into a partial clone on a git that honours GIT_NO_LAZY_FETCH, and that an older git fetches (#1182)

"the scaffold never fetches into it" held with no condition, where the
PR's own known limits say a git that ignores `GIT_NO_LAZY_FETCH` fetches
as before (r1223 on #1223, NIT 1). The sentence is #1190's: on a git that
honours it (2.45 or later, or a security release from 2.39.4 on).
docs/cli.md regenerated with scripts/render-cli-reference.py.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 24, 2026
…` and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach as `source-partial-clone`, so neither fetches into the source any more (#1145) (#1180)

`check` promises never to write into the source, and a partial clone (`git clone --filter`) broke that through git itself. A read that needs an object the clone lacks fetches it from the promisor remote. That runs the `upload-pack` the source's config names, and writes a promisor pack into the source's `objects/`.

- `check` and `execute` now run git with `GIT_NO_LAZY_FETCH=1`. They refuse a partial-clone source that lacks an object its branches, tags or HEAD reach as `source-partial-clone`. The refusal names how many objects it lacks and the cure for each promisor remote, `git fetch --refetch --no-filter <remote>`. The walk is `git rev-list --objects --missing=print --branches --tags HEAD --`.
- Measured with git 2.43, on a `--filter=blob:none --no-checkout` clone:
  - On base, `check` ran `upload-pack` 26 times and wrote 26 promisor packs into the source. `execute` failed with `Blob not found` after both leg repositories were made.
  - On head, both exit 2 with `source-partial-clone`, run nothing, leave `objects/` unchanged and make no leg.
  - A full clone and a cured partial clone adopt as before.
- `tests/test_adopt_partial_clone_source.py` holds the rows, and `docs/cli.md` gains the help text.

Review: lane 1's independent reviewers found `2c8082a2` NOT READY for one SHOULD (#1180 issuecomment-6351633105), and `6cd96b8b` NOT READY for one SHOULD (issuecomment-6356200375). They found `b316f415` provisionally READY (issuecomment-6364551003), and `a4ec51d2` READY with 4 NITs (issuecomment-6372009946). After #1119 landed, the branch was restacked onto main by three merges, and its changed lines equal the reviewed delta. Lane 1's delta check of the NIT round `a0e8cded` found the tool's AST unchanged apart from docstrings, and the two new rows kill the surviving mutants M15 and M17 (issuecomment-6374457465).

Tests, under a stub `gh` with a bogus token: at `a0e8cded`, lane 1's gate gave 650 passed and 3 skipped over the PR's test file, the throwaway and single-repository-record files, the doc trio, the path-guard and no-auto-maintenance scans, `tests/test_scaffold_leg_path_collisions.py` and `tests/test_doctor_partial_clone.py`. The doctor's 4 shards passed on #1190's head `359a749c`, whose tree is this PR's plus #1190's change.

It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its H0 `a0e8cded` is green on Linux and SonarCloud. The branch was never rebased. After H0, the only commit is update-branch's clean merge of main, giving 58a2175, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: 2 pytest calls over `tests/test_adopt_partial_clone_source.py`, `tests/test_path_guard_hygiene.py`, `tests/test_no_auto_maintenance.py`, `tests/test_scaffold_leg_path_collisions.py`: 321 passed, 2 skipped.

Closes #1145

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from fix/1145-partial-clone-source-no-fetch to main October 24, 2026 14:39

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

adopt-project.py plan: a partial-clone --source is fetched into, as check and execute were before #1145

1 participant