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

Merged
brettheap merged 39 commits into
mainfrom
fix/1162-plan-partial-clone-no-fetch
Nov 8, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1162

Was stacked on #1145 (PR #1180). #1180 squash-landed on main as 4829418f on 2026-10-24, GitHub retargeted this PR to main, and the branch is restacked on main by three merges (below).

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

Restacked after #1180 landed

#1180 squash-landed on main as 4829418f. This branch was restacked on it by three merges, with no rebase, and now holds main bd3e9c14:

  1. 457af8871 merges 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 final head 58a21752, which is update-branch's merge of main 50869f38 into a0e8cded and has the squash's tree, f27deeb0. There was no conflict. Its tree, 8c5f2988, is what git merge-tree --write-tree 359a749c 58a21752 predicted.
  2. 1f2743331 merges the squash 4829418f and keeps this branch's side for 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 files (adopt-project.py, docs/cli.md, tests/test_adopt_partial_clone_source.py) and for the six paths main brought into 58a21752. Without that, git conflicts in adopt-project.py. Its tree is its first parent's, 8c5f2988, so the merge changes no file.
  3. e42134e10 merges main bd3e9c14. Since the squash, main has added shape-doctor.py: a gitlink beside a .gitmodules the leg's index tracks and this disk lacks is named from the index's copy, which git reads, with its own way out that restores the file first (git restore .gitmodules), as #1080 names one beside other sections (#1093) #1109 and test_family_bump_fetch_refspecs.py: the comment on WHICH_CLAUSE and the docstring of test_which_clause_leaves_each_refspec_out say the source refs/h takes the one ref git completes it to, which may be a branch or tag, not no branch (#1230) #1235, which touch shape-doctor.py, tests/test_doctor_plan_overrides.py, tests/test_family_bump_fetch_refspecs.py and tests/test_path_guard_hygiene.py. There was no conflict, and this PR changes none of those files.

This PR's own change has not changed. The table counts the + and - lines of git diff -U0, leaving out the file headers:

diff + - sha256 of those lines
reviewed: a0e8cded..359a749c 480 49 cec9aed8f69174e9988792efa64717f0e3ba42ece2b3e87e2c5aba789c76dff7
main now: bd3e9c14..e42134e1 480 49 the same: the lines match
the squash: 4829418f..e42134e1 752 102 27bd16c949564bee481400805b7022d9c6028a7338a0600abed2fedab537bcdc
the squash, this PR's 3 files alone 480 49 the same as reviewed: the lines match
the squash, main's 4 files alone 272 53 ce9ea8c4130966a6005a5f56dd8f323c743f96e3b2a25725998436cf5f7c0ba1, the same as 4829418f..bd3e9c14

So the 272 + 53 lines the squash diff has beyond the reviewed delta are what main brought in after the squash. Diffed with context, bd3e9c14..e42134e1 and the reviewed delta differ in only three lines: adopt-project.py's index line, and two hunk headers that move down 37 lines because of main's lines above them. docs/cli.md and tests/test_adopt_plan_partial_clone.py have the same blobs as at 359a749c.

Tests at e42134e1. Each file ran in the probe environment (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) on git 2.43.0 and Python 3.12.3. Each call had its own basetemp and finished in under 9 minutes, and no more than two ran at once:

call files result
(a) test_adopt_partial_clone_source.py, test_adopt_plan_partial_clone.py 53 passed (38 + 15), 31.49 s
(b) test_path_guard_hygiene.py, test_no_auto_maintenance.py, test_scaffold_leg_path_collisions.py 283 passed, 2 skipped (the session began with GIT_CONFIG_GLOBAL set), 35.98 s
(c) test_agents_md_codes.py, test_cli_reference.py, test_repo_hygiene.py 171 passed, 40.09 s

507 passed, 2 skipped, 0 failed in 3 calls. None of the other files above was run again at this head. Lane 1 runs the full landing gate.

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 plan from fetching into partial clones while retaining support for partial pin sources that already contain the required objects.

New Features:

  • Make plan safely handle partial Git clones without implicitly fetching missing objects.

Bug Fixes:

  • Refuse incomplete partial-clone --source repositories before creating a plan.
  • Report actionable pin-source-unreadable errors when a partial pin source lacks objects needed to read a pin.

Enhancements:

  • Preserve readable partial pin sources while preventing lazy fetches through GIT_NO_LAZY_FETCH.
  • Document partial-clone behavior, refusal codes, and remediation in the CLI reference.

Documentation:

  • Regenerate the CLI documentation to describe partial source and pin-source handling.

Tests:

  • Add coverage for source and pin-source partial-clone refusals, fetch prevention, ordering, remediation, 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

Implementation

Run plan with lazy fetching disabled, refuse an incomplete partial-clone source before reading it, and preserve the pin reader’s refusal while explaining how to make an unreadable partial pin clone whole.

  • adopt-project.py L6221-6334, L6401-6404, L13433-13437 (+6 small edits)

UI

Explain in the plan CLI help which partial source and pin clones are refused, why, and how users can make them whole.

  • adopt-project.py L18337-18344, L18435-18443

Documentation

Regenerate the CLI reference to reflect the partial-clone behavior and remediation described by the command help.

  • docs/cli.md L482-489, L596-603

Tests

Cover that planning leaves partial clones unfetched, refuses incomplete sources and unreadable partial pin sources, and preserves behavior for readable or non-partial clones.

  • tests/test_adopt_plan_partial_clone.py L1-318

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>
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>
@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
Brett Heap and others added 3 commits November 8, 2026 13:34
#1180's final head is update-branch's merge of main 50869f3 into
a0e8cde, the tree that squash-landed as 4829418. This is the first of
the three merges that restack #1190 after #1180 landed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1180's squash 4829418 has the same tree as its final head 58a2175,
which the previous merge brought in, so the content it brings for
#1180's own files (adopt-project.py, docs/cli.md,
tests/test_adopt_partial_clone_source.py) is already on this branch.
Those paths, and the paths main brought into 58a2175 (a0e8cde..58a2175),
take this branch's side; the merge changes no file.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main moved past #1180's squash 4829418 by e9a1650 (#1109) and bd3e9c1
(#1235), which touch shape-doctor.py, tests/test_doctor_plan_overrides.py,
tests/test_family_bump_fetch_refspecs.py and tests/test_path_guard_hygiene.py.
None of them is a file #1190 changes. This is the third of the three
merges that restack #1190 after #1180 landed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap changed the title 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) 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) Nov 8, 2026
@brettheap brettheap changed the title 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) 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) Nov 8, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-11-08T13:41:45Z) Lane 1's check of #1190's restack: READY carries to e42134e1, the new H0, once tests and SonarCloud are green there.

Lane: openreposhape-1 (openRepoShape-1)

@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review November 8, 2026 15:41
@sourcery-ai

sourcery-ai Bot commented Nov 8, 2026

Copy link
Copy Markdown

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 6 days and 22 hours by commenting @sourcery-ai review. Upgrade to get a review now.

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