Repository navigation
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
Conversation
…` 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>
Reviewer's GuideThe PR makes Sequence diagram for partial-clone source validationsequenceDiagram
participant User
participant Command as check_or_execute
participant Git as Git
participant Source as PartialCloneSource
participant Remote as PromisorRemote
User->>Command: run check or execute
Command->>Command: _lazy_fetch_off()
Command->>Source: open source
Command->>Git: read promisor configuration
Git-->>Command: promisor remotes
Command->>Git: rev-parse --verify -q HEAD
Git-->>Command: HEAD id or unborn
Command->>Git: rev-list --objects --missing=print --branches --tags HEAD --
Git-->>Command: missing-object count or walk failure
alt partial clone has missing reachable objects
Command-->>User: Refusal source-partial-clone
else source is full or complete
Command->>Command: continue check or execute
end
Note over Git,Remote: GIT_NO_LAZY_FETCH=1 prevents lazy fetches into the source
Flow diagram for safe check and execute handlingflowchart TD
A[Run check or execute] --> B[Set GIT_NO_LAZY_FETCH=1]
B --> C[Open source]
C --> D{Promisor remote configured?}
D -->|No| G[Continue command]
D -->|Yes| E[Walk branches tags and reachable HEAD objects]
E --> F{Missing objects or walk failure?}
F -->|Yes| H[Refuse as source-partial-clone]
H --> I[Suggest git fetch --refetch --no-filter]
F -->|No| G
G --> J{Command}
J -->|check| K[Read and validate plan]
J -->|execute| L[Create legs and perform adoption]
K --> M[Restore environment]
L --> M
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-22T21:24:10Z) Independent review r1180 of #1180 at
Lane 1 runs the fix round in a later window. Review of #1180 (r1180, lane openRepoShape-1)Verdict: NOT READY, for one SHOULD. The code does what the PR says, and every count and claim I re-measured matches the body. I'm holding it because one design claim, "why branches and tags", has no test behind it. A mutant that drops State at stop: all my runs had finished before STOP arrived, so nothing was killed. The mutant harness put FindingsSHOULD-1: no row pins
NIT-1: mutant
NIT-2: the known limit is narrower than what happens.
NIT-3: the remedy does not last.
NIT-4: the wording and remedy for a walk that fails do not fit.
NIT-5:
What I measured and confirmedThe new test file, base against head.
Real partial clones (
The decorator restores the environment on every path.
Promisor detection, all three keys and a
The known limit's git versions are right. I read git's own sources with WebFetch:
Overlap with #1169: clean.
Mutants: 17 of 19 killed. The survivors are
PR body form.
Two departures from the rules. In probe repositories I created under the scratch dir, I set Not covered (STOP arrived)
Lane: openreposhape-1 (openRepoShape-1) |
…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>
|
(lane openRepoShape-1, 2026-10-23T02:49:54Z) Independent re-review r1180b of #1180 at Review r1180b of #1180 at
|
| mutant | result |
|---|---|
filter-key-promisor |
killed by 5 rows (the 3 check refusals, two remotes, failing walk) |
filter-key-unspelled |
killed by the same 5 |
later-and |
killed by two remotes |
later-first-only |
killed by two remotes |
refetch-first-only |
killed by two remotes |
none-no-later |
killed by the failing walk |
count-no-later |
killed by 4 rows |
none-worded-as-count |
killed by the failing walk |
none-read-as-zero |
killed by the hand-written branch and the failing walk |
walk-no-tags |
killed by both tag rows |
walk-no-branches |
killed by 8 rows |
ext-not-last |
killed by extension-also-promisor |
count-worded-as-none |
survived |
none-no-said |
survived |
none-fsck-no-where |
survived |
none-no-refetch |
survived |
My equivalents of this round's lane mutants give the body's kill counts exactly: 5, 4, 1, 1 and 1.
The body's other claims all check out:
- title 231 characters;
- exactly one closing form,
Closes #1145, and no other close, fix or resolve form before a number; - the base branch is at
bba168bd, which is adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) #1119's head; adopt-project.py:check's rehearsal and_stored_as_writtentake a parent's-csettings,attr.treetoo, and the source's objects,GIT_ATTR_SOURCEis refused by name before any question, and each throwaway reads its environment first (#1136) #1169's branch is atdcf2751d; - merge-tree
6cd96b8b×b8e63bcf→ exit 0,3f0c984c;6cd96b8b×dcf2751d→ exit 0,99c4cefd;2c8082a2×b8e63bcf→3a74e27a; - both quoted refusals match my p1 and p2 output word for word, apart from the path;
- the row groups add up to 26.
closingIssuesReferencesis[]only because the base is notmain.
Not covered
- git 2.45 or later, which CI likely runs. The PR's own docstring (
tests:422-423) says the hand-written branch is counted there. Its refusal would then lead with the refetch that r1180 found does not cure it. Not measured: this bench has only git 2.43. - Gate files not run: the 4
test_shape_doctor.pyshards,test_scaffold_leg_path_collisions.py,test_adopt_check_throwaway_repos.py, and the tree merged withdcf2751d. The diff since2c8082a2is message text, one helper and comments. - Real CPython 3.14, and a promisor on a network.
Departures from the rules
- Config writes: my probes wrote config only in repositories under my own basetemps, through the PR's test helper (
remote.origin.uploadpack) andgit sparse-checkout set. - Untracked files exist only in my scratch clones (
head,probe,base). - Load rule: one 2.4 s single-row pytest call started while
pgrepread 5. The rule says wait at 4 or more.
The review checkout /workspace/projects/openRepoShape-worktrees/l1-r1180 is clean at 6cd96b8b. Everything I made is under /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1180b (probes, mut_r1180b.py, mut-results.txt, the logs).
Lane: openreposhape-1 (openRepoShape-1)
…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>
check and execute run with GIT_NO_LAZY_FETCH=1 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)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)
…ached`, so it reads `attr.tree` as the split does (r1169b's SHOULD-1); rows for r1169b's P3 and P5 (check, all forms and a no-setting control; execute, three forms) and #1192's case (P7) Measured so far at this commit, worktree, git 2.43, probe environment: the 17 new ids passed; the round's 50 ids plus the 17 gave 67 passed; tests/test_adopt_reencoded_files.py gave 51 passed. Base runs, the shim, mutants, the gate and the #1180 merge are not run yet. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-23T12:09:42Z) Independent re-review r1180c of #1180 at Lane 1 takes the NIT: the walk's argv ends with Review r1180c of #1180 at
|
…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>
…s/test_adopt_check_throwaway_repos.py Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-23T18:31:43Z) Independent review r1180d of #1180 at
Lane 1 takes the 4 NITs in a fix round. The bug the reviewer found outside this PR is filed as #1215. Review r1180d of #1180 at
|
| mutant | killed by |
|---|---|
M01 argv without -- |
1: the new row |
M02 shown without -- |
2: the failing walk, born and unborn |
M03 argv ends with --end-of-options instead of -- |
1: the new row |
M04 -- before the id |
12: the same 12 as the body's dashes-before-head |
M05 argv walks HEAD by name, with -- |
survived: equivalent (P2, P3) |
M06 argv walks HEAD by name, no -- |
1: the file-named-HEAD row |
| M07 HEAD not walked | 3: the HEAD-blob row (check, execute), the ghost HEAD |
M08 head = _head_id(path) or "HEAD" |
3: the failing walk (unborn), the unborn row (check, execute) |
M09 HEAD_ID peeled to HEAD^{commit} |
1: the ghost HEAD |
M10 shown names the id, not HEAD |
1: the failing walk (born) |
| M11 exit 128 read as success | 2: the ghost branch, the ghost HEAD |
M12 no --tags |
2: the tag row (check, execute) |
M13 no --branches |
1: the ghost branch |
M14 --missing=allow-any |
11 |
| M15 git's words dropped | survived (NIT-2) |
M16 if not lacking: (None read as 0) |
4: the ghost branch, the failing walk (born, unborn), the ghost HEAD |
| M17 count capped at 1 | survived (NIT-3) |
The writer's 3 round-3 mutants match M01, M02 and M04, with the same killing rows.
Step 3: merge-tree (merge-tree --write-tree, run in a scratch clone):
| with | merge-base | result |
|---|---|---|
#1169 76c9b767 |
bba168bd |
clean, exit 0, tree c4495b59 (auto-merging adopt-project.py) |
#1190 7f3e1280 |
a4ec51d2 (an ancestor) |
clean, tree 6f740ccb, which is 7f3e1280's own tree |
#1119 195d9936 |
bba168bd |
clean, tree 6971b2c9 |
#1119 e0a98f13 (live) |
bba168bd |
clean, tree 73a3dd1f |
The body's three tree ids are confirmed: a4ec51d2+1e958ca6 gives 71bb1e8c, a4ec51d2+25805102 gives daab3eca, and b316f415+25805102 gives 88269a06.
The body:
- Closing forms (scripted scan): exactly one,
Closes #1145, with no "Covers" or "Part of". - Required lines: the ruling (2026-10-21T10:32:04Z) is named, and the body ends with the Lane line, a blank line, and the generated line.
- Title: 237 characters.
- Stacking: "lands after adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) #1119" is right; only the head id is stale (NIT-4).
Left (not covered)
-
Step 4, the gate at
a4ec51d2, except the 38-row file above. The other calls were not run:tests/test_doctor_partial_clone.py;tests/test_scaffold_leg_path_collisions.py;tests/test_path_guard_hygiene.pywithtests/test_no_auto_maintenance.py;- the doctor's 4 shards.
A clean gate clone is ready at
r1180d/gate, detached ata4ec51d2.r1180d/shards.py <gate> <outdir>does one collect and writesids_00toids_03, asserting each is non-empty. Then pass each file aspytest ... @<outdir>/ids_0N; pytest 9.1.1 reads it one id per line. -
The merged trees (
c4495b59with adopt-project.py:check's rehearsal and_stored_as_writtentake a parent's-csettings,attr.treetoo, and the source's objects,GIT_ATTR_SOURCEis refused by name before any question, and each throwaway reads its environment first (#1136) #1169,73a3dd1fwith adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) #1119's live head) were not tested. -
git 2.45 or later, and real CPython 3.14: neither is on this bench.
Outside this PR (observations, not findings)
adopt-project.py:14412:git rev-list --count HEAD, run in a leg's work clone aftergit filter-repo, has no--. A leg that tracks a root file namedHEADmakes it exit 128. P6 gaverev-list --count HEAD exit=128 ... both revision and filename, whileHEAD --exits 0 andrev-parse HEADexits 0. This is the same class of bug as r1180c's NIT-1, and it predates adopt-project.py check: a partial-clone source missing blobs that check reads is fetched into, so check's own reads run the upload-pack its config names and write a promisor pack into the source #1145. Lane 1 may want an issue for it.shape-doctor.py:5864and:6166: these pass a commit id torev-listwith no--. Not measured.
Departures from the rules
findings.md: not written, because the harness refused it (see above).- Load rule:
- Driver A's start line logged 5 pytest processes, although my check just before read 4. Driver B waited 40 s, until the count was below 5.
- My two concurrent pairs, head with the shim and
b316f415withe0a98f13, started at 2 and 4 processes, so the second call of each pair began at the limit. Each call ran under 40 s.
git config: set only in my own probe repositories underr1180d/p1(uploadpack.allowFilterandremote.origin.uploadpack), and through the PR's fixtures under my basetemps. Never at global or system scope, and never in a shared repository.gh: read-only only, as the brief allowed: oneapiGET of r1180c's comment, andpr viewof adopt-project.py:checkandexecuterun withGIT_NO_LAZY_FETCH=1and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach assource-partial-clone, so neither fetches into the source any more (#1145) #1180, adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) #1119, adopt-project.py:check's rehearsal and_stored_as_writtentake a parent's-csettings,attr.treetoo, and the source's objects,GIT_ATTR_SOURCEis refused by name before any question, and each throwaway reads its environment first (#1136) #1169 and adopt-project.py:planruns withGIT_NO_LAZY_FETCH=1, refuses a partial-clone--sourcethat lacks objects assource-partial-clone, and names a partial--pin-sourceit could read only by fetching and the fetch that makes it whole (#1162) #1190.- Files changed in scratch clones only:
r1180d/dbase/tests/test_adopt_partial_clone_source.py(overwritten with the head's copy);r1180d/pbase/tests/test_adopt_partial_clone_source.py(untracked copy).
Lane: openreposhape-1 (openRepoShape-1)
…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>
|
(lane openRepoShape-1, 2026-10-23T20:49:33Z) Lane 1's delta check of #1180 since r1180d's READY at
Lane: openreposhape-1 (openRepoShape-1) |
|
There was a problem hiding this comment.
Hey - I've found 1 issue
Fixed security issues:
- Command injection from untrusted input passed to OS command execution (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="adopt-project.py" line_range="13013-13019" />
<code_context>
+ `source.cloned: true` plan names."""
+ before = os.environ.get(NO_LAZY_FETCH)
+ os.environ[NO_LAZY_FETCH] = "1"
+ try:
+ yield
+ finally:
+ if before is None:
+ os.environ.pop(NO_LAZY_FETCH, None)
+ else:
+ os.environ[NO_LAZY_FETCH] = before
+
+
+def _config_listed(path: Path, *args: str) -> bytes:
+ """What `git config -z <args>` lists in `path`, as bytes; nothing where
+ it lists nothing (exit 1) or cannot run. Config only: no object is read,
+ so nothing is fetched."""
+ try:
+ listed = subprocess.run(["git", GIT_CONFIG, "-z", *args],
+ cwd=str(path), capture_output=True,
+ check=False)
+ except OSError:
+ return b""
+ return listed.stdout if listed.returncode == 0 else b""
+
+
</code_context>
<issue_to_address>
**Config lookup failure skips guard**
When A `git config` query cannot run or errors, but a later Git invocation in the command succeeds, `_config_listed` treats a failed `git config` invocation as an empty config result, so `_promisor_remotes` returns no remotes and the guard exits. If later Git commands succeed, they can read the partial clone and lazily fetch into its object store without the promised refusal.
Distinguish the normal no-match exit from configuration-query failures and refuse when promisor status cannot be determined.
Also at `adopt-project.py:13162-13163`.
</issue_to_address>Sourcery assessment
Approval pending. 1 finding to address first.
Blocking findings: adopt-project.py:13019
| try: | ||
| listed = subprocess.run(["git", GIT_CONFIG, "-z", *args], | ||
| cwd=str(path), capture_output=True, | ||
| check=False) | ||
| except OSError: | ||
| return b"" | ||
| return listed.stdout if listed.returncode == 0 else b"" |
There was a problem hiding this comment.
🟡 Medium · Config lookup failure skips guard
When A git config query cannot run or errors, but a later Git invocation in the command succeeds, _config_listed treats a failed git config invocation as an empty config result, so _promisor_remotes returns no remotes and the guard exits. If later Git commands succeed, they can read the partial clone and lazily fetch into its object store without the promised refusal.
Distinguish the normal no-match exit from configuration-query failures and refuse when promisor status cannot be determined.
Also at adopt-project.py:13162-13163.
Prompt for AI agents
In `adopt-project.py` at lines 13013-13019:
**Config lookup failure skips guard**
When A `git config` query cannot run or errors, but a later Git invocation in the command succeeds, `_config_listed` treats a failed `git config` invocation as an empty config result, so `_promisor_remotes` returns no remotes and the guard exits. If later Git commands succeed, they can read the partial clone and lazily fetch into its object store without the promised refusal.
Distinguish the normal no-match exit from configuration-query failures and refuse when promisor status cannot be determined.
Also at `adopt-project.py:13162-13163`.


Closes #1145.
Now on main, after #1119. #1119 squash-landed on main as
b7edd881, and GitHub retargeted this PR to main. Its branch was stacked on #1119's headbba168bd, and it was restacked through three merges: #1119's final head9b856443(c3f8a2b7, clean); #1119's squashb7edd881(ef73f75e: no conflict, and it changed no file, sincegit diff 9b856443 b7edd881is empty); then main50ce8ab1(b18d2e8d, clean).git diff --name-only origin/main b18d2e8dlists exactly this PR's three files, and the changed lines ofgit diff -U0 origin/main b18d2e8dequal those ofgit diff -U0 bba168bd a4ec51d2, the reviewed delta (985+/-lines, the same sha256). Main'sadopt-project.py,docs/cli.mdandtests/conftest.pyare byte for bytebba168bd's, so "base" below is main's too.Outside v1.2.0. Brett Heap ruled at 2026-10-21T10:32:04Z: "leave the follow-ups out of v1.2.0". This PR lands after the v1.2.0 tag, on a later word.
Beside #1169 (#1136). Both PRs change
adopt-project.py. #1169 is restacked onto main too, asfe25bb14, through the same three merges after #1119's squash.git merge-tree --write-tree a0e8cded fe25bb14is clean, treeac442d4c. That merged tree is not tested here: whichever of the two lands second takes a merge of main, and lane 1 gates it then.What
checkpromises never to write into the source. A partial clone (git clone --filter) broke that through git itself. Git fetches an object such a clone lacks from its promisor remote on any read that needs one. That fetch runs theupload-packthe source's ownremote.origin.uploadpacknames, and writes what it fetched into the source'sobjects/as a promisor pack.How it was measured. git 2.43.0-1ubuntu7.3 and Python 3.12.3. The source is conftest's synthetic tree: 26 files, 3 commits, cloned from a local
originthat setsuploadpack.allowFilter. The plan was written from a full clone and answered. Then the source was made again at the same path and commit, in each form below.remote.origin.uploadpackwas set to a wrapper that counts its runs, andobjects/was hashed before and after. Base is #1119's headbba168bd. The rows down to the unborn HEAD were first measured at179b58ef, and their test rows pass at this PR's heada0e8cded; the HEAD rows were measured atb316f415with a scratch probe. The commits:3b11751aand2c8082a2added the tests,70f32485added the tag-only and extension rows,6cd96b8bchanged the refusal's words and added the two-remote row,41230f64walks HEAD, names whatgit fsckshows and pins the words of both arms,b316f415adds two assertions and the file-named-HEADrow, anda4ec51d2ends the walk with--and adds the row for a file named as HEAD's own id (r1180c's NIT-1). Then the three merges above restack it on main, anda0e8cded(r1180d's NIT-1 to NIT-3) makes the walk's comments and two row docstrings say what git says with the--, and pins git's own words in the could-not-walk refusal and the exact count in the count refusal.bba168bdcheck--filter=blob:none --no-checkout(28 objects lacking)plan ok. 26upload-packruns;objects/CHANGED;.promisorfiles 1 -> 27REFUSED source-partial-clone. 0 runs;objects/unchanged;.promisor1 -> 1check--filter=blob:none, checked out (2 history blobs lacking)plan ok. 0 runssource-partial-clone. 0 runs; unchangedcheckprotocol.file.allow=alwaysplan ok(the test row)source-partial-clone. 0 runs; unchangedcheckplan ok. 0 runs; unchangedcheckgit fetch --refetch --no-filter origin; lacks nothing)plan ok. 0 runs; unchanged.promisor1 -> 1execute--no-checkoutgit filter-repodied withBlob not found. 26 runs; CHANGEDsource-partial-clone, and no leg remote made. 0 runs; unchangedexecuteBlob not found). 0 runssource-partial-clone, no leg. 0 runs; unchangedexecuteadoption verified. 0 runs;objects/changed by the split's push onlyexecuteadoption verified. 0 runs.promisor1 -> 1checkplan ok. 0 runs; unchangedsource-partial-clone, "lacks at least 1 object(s) its branches, tags and HEAD reach". 0 runs; unchangedexecuteREFUSED extracting the spec leg, the split'sgit cloneexit 128 (unable to read sha1 file,Clone succeeded, but checkout failed). 0 runssource-partial-clone, no leg remote made. 0 runs; unchangedcheck,executecheckexit 0,plan ok;executemakes both legs, thenREFUSED extracting the spec leg. 0 runssource-partial-clone; no leg. 0 runs; unchangedcheck,executegit switch --orphan) (control)checkexit 0,plan ok;executeexit 0,adoption verified. 0 runsThe refusal, for the first row:
HEAD.
execute'sgit clone <source> <work>checks out the source's HEAD beforecheckout -B, and a HEAD detached at a commit no branch or tag reaches can name a blob the clone lacks. r1180b measured it:checksaidplan ok, andexecutemade both legs and then died in that clone's checkout. So_objects_lackingwalks HEAD as well, by its id, wheregit rev-parse --verify -q HEADgives one (rows above). Four shapes it must not get wrong, each a test row:git switch --orphan) gives no id, and HEAD is left out of the walk. A plainHEADin the walk, which ends with--, exits 128 there (fatal: bad revision 'HEAD'; without the--,fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree) and would refuse a clone that lacks nothing.HEADin the source's work tree: withHEADin the argument list and no--, git exits 128,ambiguous argument 'HEAD': both revision and filename. With the--the walk now ends with,HEADby name walks as the id does (measured with git 2.43, and r1180d's M05 is an equivalent mutant), so this row now holds the walk only againstHEADwith no--. The walk keeps the id.ambiguous argument '<id>': both revision and filename, exit 128, andcheckrefused a source that lacks nothing. The walk now ends with--, after the id, so no file of the work tree is a path of it, whatever it is named, and the walk the refusal names ends with--too. A commit cannot hold a file named as its own id, so the row's file is in the work tree only..git/HEADby hand:git update-refwould fetch it). The refusal iscould not walkon git 2.43:rev-list --missing=printexits 128,fatal: bad object <id>, and runs noupload-pack, with the variable ignored too. Asking HEAD for a commit (git rev-parse --verify -q 'HEAD^{commit}') read the object instead, and on a git that ignores the variable it fetched it: 1upload-packrun, measured. UnderGIT_NO_LAZY_FETCH=1it exits 1, as an unborn HEAD does, and HEAD would be left out. So the id is asked without the peel, which printed it and ran nothing in both cases.Where git cannot walk the refs (
lacking is None), the refusal says that and namesgit fsck, with what it shows in a partial clone (r1180b's NIT-1): it names a branch, tag or HEAD whose object the repository lacks (error: refs/heads/ghost: invalid sha1 pointer,error: HEAD: invalid sha1 pointer; noupload-packran), and it does not list what a promisor remote holds. It no longer says the source "lacks what its branches and tags reach, as far as git can tell", and it no longer leads with a refetch, which does not bring in a ref outside the remote's refspec (r1180's hand-written branch). The refusal for a HEAD at a commit the source lacks, on git 2.43:A refetch does not last.
git fetch --refetch --no-filter originleavesremote.origin.partialclonefilter blob:nonein place (r1180: after a cure that brought lacking objects to 0, the next plain fetch left a branch lacking 2). So both refusals end with a sentence saying so. Unsetting the key was not measured, and the sentence does not say to.A commit the plan names, which the source lacks. Two reads take a commit from the plan rather than from the source:
cmd_check, on a stale or edited plan;_not_a_whole_clone(git cat-file -e <commit>^{commit}), for asource.cloned: trueplan, insidePlan.open_source, before the guard.Take a cured partial clone and a plan whose
commit:names a commitoriginholds and the source lacks. On base, each command ranupload-packonce, and the commit was fetched into the source. On head, each runs it 0 times, andobjects/is unchanged.checkgivesFINDING plan-stale, exit 1. Inexecute, the cloneplanmade is no longer whole, so it is cloned again (from a GitHub that does not have it, in the row).check --sourceandexecute --source(SOURCE_OVERRIDE_HELP, anddocs/cli.md, regenerated) namesource-partial-cloneand the fetch that makes the source whole.Why
Two layers, as
shape-doctor.pyhas them (#683, #754, #787):_lazy_fetch_off(), a decorator overcmd_checkandcmd_execute. It setsGIT_NO_LAZY_FETCH=1for the whole command and putsos.environback afterwards. Every git the command starts inherits it: each read of the source,check's throwaway repositories, andexecute's clones andgit filter-repo. A read of an object the source lacks then fails where it fetched before. This holds whatever reads it and whichever commit it reads, the plan's included. Measured with git 2.43 on onels-tree -r -l: exit 128, and noupload-packran. The gits that honour the variable are 2.45.0 and later, the security releases of 2024-05-14 back to 2.39.4 (CVE-2024-32465), and distributions' builds of those, this bench's among them._refuse_a_partial_clone_short(source), called right after the source is opened. Incheckit comes after the open's try/except, so the unwritable-work-dir refusal is not masked. Inexecuteit comes before_require_filter_repo()and before any leg. It asks:_promisor_remotes: the doctor's rule, read from config alone, so no object is read. A remote whosepromisoris true (--type=bool), a remote with apartialclonefilter, and the remoteextensions.partialclonenames, that one last.promisor = falsealone is none. A full clone stops here, unchanged._objects_lacking: it streamsgit rev-list --objects --missing=print --branches --tags, and HEAD's id where_head_id(git rev-parse --verify -q HEAD) gives one, then--(so no file of the work tree is a path of the walk), and counts the?lines. git's words go to a temporary file, so neither pipe can stall the other. This walk never fetches on any git that has--missing: with git 2.43 and the variable unset, noupload-packran, with HEAD at a commit the clone lacks included. A walk that fails isNone, and refused, naming the walk as run and how git exited.Why both layers. The guard alone would leave the plan-named commits fetchable. The variable alone does nothing on a git that ignores it. On such a git, the guard keeps the 26 fetches from happening: the test row stands for that git by setting another name.
Why branches, tags and HEAD. The source's branch is what
checkreads andexecutesplits,execute'sgit filter-reporewrites a clone of the source, which copies every branch and tag, and that clone'sgit clonechecks out the source's HEAD. The checked-out partial clone lacked only history blobs. It passedcheck, and failedexecuteafter both legs existed. Tags too: a cured clone that fetches one annotated tag with the filter it keeps lacks the blob the tag's commit changes, and only the tag reaches it (branches lack 0, tags 1). With--tagsout of the walk,checksaidplan okandexecutemade both legs, then died ingit filter-repowithBlob not found. HEAD too: the same clone with the tag deleted and HEAD left on its commit (branches 0, tags 0, HEAD 1). With HEAD out of the walk,checksaidplan okandexecutemade both legs and died in the clone's checkout. A test row pins each.A refusal, exit 2, rather than a FINDING (lane 1's ruling). A source that lacks objects cannot be read without fetching, so
checkstops before it reads the source.Rejected:
-c protocol.allow=never: measured, the source's ownprotocol.file.allow=alwaysoutranks it, and the lazy fetch ran 4 times;GIT_ALLOW_PROTOCOLover the whole command: it stops the fetch too, butcheckandexecuteneed their transports, for the clone of the source,ls-remoteand the pushes;_a_partial_clone, whichcheckasks of a pin's recorded clone, keeps its older rule.Tests
tests/test_adopt_partial_clone_source.py, 38 rows. Each counts the promisor'supload-packruns through a countingremote.origin.uploadpack(a Python script started withsys.executable), and hashesobjects/:checkrefusesnocheckout,checkoutandfileallow, and fetches nothing; each refusal gives the cure, says a later fetch stays filtered, and gives as the count it lacks at least exactly the?lines of its own walk, which the row counts (git rev-list --objects --missing=print --branches --tags HEAD --) and asserts is more than 1, so a count held at 1 is told (r1180d's NIT-3) (3);checkreadsfullandcuredas before (2);executerefusesnocheckoutandcheckoutbefore the leg directory exists (2);executesplits a cured partial clone,adoption verified, 0 runs (1; skipped wheregit-filter-repois absent);NO_LAZY_FETCHset to a name no git reads (1);check(the advisory) and throughexecute(_not_a_whole_clone,source.cloned: true, gh stood in for). 0 runs on head, then at least 1 with the variable ignored, which proves the row's read fetches. Each skips, with the reason, on a git that fetches underGIT_NO_LAZY_FETCH=1(2);tagged, an annotated tag on a commit no branch reaches that changesspecs/001-feature/spec.md, with its own filter; refused assource-partial-clonethroughcheckand throughexecute, no leg directory, 0upload-packruns (2);update-ref --no-derefand the tag deleted, so branches lack 0, tags 0 and HEAD 1. Refused throughcheckand throughexecute, the refusal naming HEAD in "its branches, tags and HEAD reach" and sayingexecute's clone checks out its HEAD, no leg directory, 0 runs (2);.git/HEAD, withNO_LAZY_FETCHset to a name no git reads: refused, 0 runs,objects/unchanged. Only this row tells the id asked asHEADfromHEAD^{commit}(1);refs/heads/unbornis read as before, throughcheck(plan ok, nothing fetched) and throughexecute(adoption verified, 0 runs; skipped wheregit-filter-repois absent) (2);HEADin the source's work tree is no path of the walk:checksaysplan ok(1);git rev-list <id>alone exits 128, thenchecksaysplan ok, 0 runs,objects/unchanged (1);partialclonefilterkey (1);rev-list --missing=printexits 128; from 2.45 it lists the commit. Refused either way, asserted by its code alone (1);LACKING_WALKwith an option no git has), HEAD born and unborn: refused on any git, naming the walk as run (HEADwhere HEAD has an id, then--last), how git exited and git's own words after it (exited <rc>: <words>, asked of this git by running the same walk: git 2.43 prints its usage, exit 129, which does not name the option; r1180d's NIT-2), saying git could not walk the refs, with no object count, naminggit fsckin the source with what it shows, and the cure (2);git fsckshows in a partial clone, which that refusal names: a ghost branch, tag or HEAD is named, and the blobs a promisor holds are not listed; it runs noupload-pack(4);_promisor_remotes:promisortrue andyes,partialclonefilteralone,extensions.partialclonealone and last, the remote it names last where it is also a promisor (extensions.partialclone = o, thenoandppromisors:['p', 'o']),promisor = falsealone, none (8);_lazy_fetch_offsets the variable and puts it back, unset and set, an exception included (2).Base vs head at main (round 4,
a0e8cded). The 38-row file, in a scratch clone ofa0e8cdedwith main50ce8ab1'sadopt-project.pyanddocs/cli.mdput in, in the probe environment under git 2.43.0 and Python 3.12.3: 27 failed, 11 passed in 63.37 s. The 11 are the controls:checkfullandcured,executecured, the unborn HEAD throughcheckandexecute, the file namedHEAD, the file named as HEAD's own id, and the fourgit fsckrows. The two rows round 4 changes fail on base, as they did: the threecheckrefusals and the failing walk, born and unborn. Heada0e8cded: 38 passed in 24.30 s, and 38 passed in 34.83 s under the 3.14-likesitecustomizeshim (exists,is_fileandis_diranswer False to anyOSError; it loaded 44 times in the run).Base vs head, round 2 (
b316f415). The 37-row file, copied into a scratch clone ofbba168bd(with the base's own conftest), in the probe environment under git 2.43.0 and Python 3.12.3: 27 failed, 10 passed in 34.53 s. Head: 37 passed in 16.32 s (run before the commitb316f415, with the same bytes), and 37 passed in 19.43 s onb316f415under the 3.14-likesitecustomizeshim. The 10 that pass on base are the rows base answers as head does:checkfullandcured,executecured, the unborn HEAD control throughcheckandexecute, the file namedHEAD, and the fourgit fsckrows. Of the 11 rows new in round 2, 4 fail on base: the HEAD alone throughcheck(exit 0,plan ok) and throughexecute(both legs made, thenREFUSED extracting the spec leg, exit 128,unable to read sha1 file); the HEAD at a commit the source lacks (FINDING adopt-git-unasked, exit 1, where 2 is asserted); and the failing walk with an unborn HEAD. The other 7 are the controls. The 26 rows of6cd96b8bwere 23 failed and 3 passed on base (lane 1's run), and 27 = 23 + 4.Base vs head, r1180c's NIT-1 (round 3). The new row
test_a_file_named_as_the_head_id_is_no_path_of_the_walkand the edited failing-walk row (it names the walk as run, so it now expects the--), withb316f415's ownadopt-project.py, in a scratch clone, in the probe environment under git 2.43.0 and Python 3.12.3. Four rows were selected: the new row, the file-named-HEADrow, and the failing-walk row born and unborn. 3 failed, 1 passed. The new row fails with the refusal (exit 2,REFUSED source-partial-clone, thecould not walkarm, where 0 andplan okare asserted); the failing-walk row fails born and unborn, because base names the walk without the--; the file-named-HEADrow passes, as it did. That base is round 3's,b316f415: against #1119's head, and against main, the new row is a control row and passes (r1180d: 27 failed, 11 passed with #1119'se0a98f13; and round 4's run at main above). Heada4ec51d2: the whole file, 38 passed in 22.91 s.tests/test_adopt_plan_partial_clone.pyis #1162's and is not in this PR's tree; in a scratch merge ofa4ec51d2and #1162's tipd4d6e2a7(tree0117a9d7, clean), that file andtest_adopt_partial_clone_source.pytogether: 52 passed in 38.69 s. That file asserts no text of the walk.Mutants. At
b316f415, 44 of 44 killed, againstb316f415'sadopt-project.pyand the 37-row file, in two scratch clones, in four runs of 11, withadopt-project.pyrestored after each mutant in afinally. At the end of each rungit statusshowed the file unchanged, and afterwards the blob of each clone and of the worktree equalled HEAD's,b1e44868. The 44:muts_f1180_all.py): each applies once to this head's text, butlater-dropped-none, whose anchor moved with the wording and was re-pointed atwith --source, and run this command again. {later}. Each killed, by at least 1 row;count-worded-as-none(if lacking is None or lacking:) killed by 5 rows (the threecheckrefusals and the two HEAD rows);none-no-said(the walk andexiteddropped from the refusal) by 2 (the failing walk, born and unborn);none-fsck-no-whereby 2;none-no-refetchby 2;head-not-walked(HEAD left out of the argv)head-unverified(head = "HEAD"always: a plainHEAD)checkandexecute, the file namedHEADhead-peeled(HEAD_IDwithHEAD^{commit})head-by-name(the argv takesHEAD, not its id)HEADhead-shown-never(the walk named withoutHEAD)head-shown-always(named withHEADeven when unborn)none-arm-no-head("branches and tags git could not walk")count-arm-no-head("tags reach")fsck-old-claim(the sentence says fsck "names each object the repository lacks")none-no-exit(exited N:dropped)execute-words-no-head(the sentence aboutexecute's clone checking out HEAD dropped)Round 3's mutants (r1180c's NIT-1), 3 of 3 killed, against
a4ec51d2'sadopt-project.pyand the 38-row file, in a scratch clone,adopt-project.pycopied back from the worktree after each:argv-no-dashes(the walk's argv without--)shown-no-dashes(the named walk without--)dashes-before-head(--before the id, so the refs and HEAD are read as paths)checkrefusals, the twoexecuterefusals, the guard alone, the two-remote row, the tag rows (2), the HEAD rows (2), the HEAD at a commit the source lacksRound 4's mutants (r1180d's NIT-2 and NIT-3), 2 of 2 killed. r1180d ran 17 mutants against
a4ec51d2and 14 were killed. Of the 3 survivors, M05 (the argv walksHEADby name, with the--) is an equivalent mutant: with the--, the name and the id walk the same. So round 2'shead-by-namerow, killed atb316f415, is equivalent froma4ec51d2on, and the file-named-HEADrow now kills onlyHEADby name with no--(r1180d's M06). The other two are killed now. Each was applied toa0e8cded'sadopt-project.pyin a scratch clone, its row run and failed, then the file was restored, its blob checked equal to HEAD's76b860ff, and the row run again and passed:f"{spelled_as_git_out(words)}"->"")lacking = max(lacking, int(line.startswith(b"?"))))checkrefusals: 3 failedHistory: round 1 measured, by the author on
2c8082a2, 11 of 11 killed; the review (r1180) ran 19 and 2 survived,walk-no-tagsandext-not-last, which the tag rows and the extension row now kill; lane 1 ran all 29 against6cd96b8b: 29 of 29; r1180b ran 16 on6cd96b8band 4 survived, the four above, which this round kills.The first round's table, each mutant in a scratch clone of
2c8082a2, restored after each:cmd_checkcheckcmd_executeexecutecmd_checkcheckrefusals, the guard alone, the hand-written branch, the failing walkcmd_executeexecuterefusals_lazy_fetch_offdoes not restorelacking == 0returncheckcured,executecured, the plan-named commit throughcheck_promisor_remotesreturns[]PROMISOR_KEYSwithoutpromisorpromisor,promisor-yes,extension-lastPROMISOR_KEYSwithoutpartialclonefilterfilterPROMISOR_KEYSwithoutextensions.partialcloneextensionNonewalk read as 0 (if not lacking)The gate. On
b316f415, in the probe environment, under 3.12, each call under 9 minutes and 2 at once (the two doctor shards in each pair ran together):test_path_guard_hygiene.py,test_no_auto_maintenance.pytest_no_auto_maintenance.py:222: the probe setsGIT_CONFIG_GLOBALtest_agents_md_codes.py,test_cli_reference.py,test_repo_hygiene.pytest_adopt_check_throwaway_repos.pytest_scaffold_leg_path_collisions.pytest_shape_doctor.py, 4 shards (112 node ids each, passed line by line)test_shape_doctor.py:10510, which runs only on a Windows hostNo call failed.
docs/cli.mdwas regenerated byscripts/render-cli-reference.py(the help now says "its branches, tags or HEAD reach"), and G2 holds it. The full suite and CI's CPython 3.14 did not run here.Round 3's gate (
a4ec51d2), the two calls the change needs, in the probe environment, under 3.12, each under 9 minutes:test_adopt_partial_clone_source.pytest_path_guard_hygiene.py,test_no_auto_maintenance.pyNot rerun at
a4ec51d2: the doctor shards,test_adopt_check_throwaway_repos.py,test_scaffold_leg_path_collisions.pyand the doc trio. They ran atb316f415(G2 to G8); this change touches two lines of_objects_lacking, its comments and one test file, and no flag, code or help text.Round 4's runs, at the restack
b18d2e8dand ata0e8cded, in the probe environment, under 3.12, each under 9 minutes and at most 2 at once:b18d2e8dtest_adopt_partial_clone_source.pyb18d2e8dtest_adopt_check_throwaway_repos.pya0e8cdedtest_adopt_partial_clone_source.pya0e8cdedscripts/render-cli-reference.py --checkata0e8cded:[ok] docs/cli.md is what the generator produces. Round 4 changes comments, two docstrings and the assertions of two rows, and no flag, code or help text. Lane 1 runs the full landing gate.Round 1's gate, on
2c8082a2, with each call under 9 minutes and at most 2 at once:test_path_guard_hygiene.py,test_no_auto_maintenance.pytest_shape_doctor.py, 4 shardstest_scaffold_leg_path_collisions.pytest_adopt_check_throwaway_repos.py,test_adopt_reencoded_files.py,test_adopt_root_symlinks.pytest_adopt_partial_clone_source.pyandtest_adopt_pins.py;test_adopt_plan_yaml_quoting.pyandtest_adopt_single_repository_record.py;test_doctor_no_lazy_fetch.py,test_doctor_pin_restore_merges.pyandtest_doctor_partial_clone.pytest_agents_md_codes.py,test_cli_reference.py,test_repo_hygiene.pyNo call failed in round 1 either. The adopt and doctor files that name partial or promisor (round 1's G7 to G10) were not run again this round: this round changed
adopt-project.pyin the walk, the refusal's words and one help sentence, and the rows above.Known limits
A promisor on a network was not measured (lane 1's ruling: a known limit, measured only by reasoning). The same reads go through the same lazy
git fetch <promisor>, with the transport its url names: https, with the credential helper of whoever runs the command, or ssh, with their keys. The fix stops it at the same place. Under the variable no fetch is started, and the guard reads config and runsrev-list --missing=print, which fetches nothing, before it refuses.On a git that ignores
GIT_NO_LAZY_FETCH(before 2.39.4, and unpatched 2.40 to 2.44 builds), two reads of a commit the plan names can still fetch, and they differ in which clone they reach:_not_a_whole_clone(git cat-file -e <commit>^{commit}) runs insidePlan.open_source, for asource.cloned: trueplan, before the guard, so in ANY partial clone, a cured one or not. r1180 measured an uncurednocheckoutclone, withNO_LAZY_FETCHswapped for another name:checkandexecuteeach ranupload-packonce,objects/changed, thenREFUSED source-partial-clone. Low likelihood:planmakes that clone itself, as a full clone.cmd_check, on a stale plan, runs after the guard, so it reaches a partial clone only where the guard let it pass: a cured one, which lacks nothing its branches, tags or HEAD reach but may lack a commit the plan names. The guard walks branches, tags and HEAD only.The two rows that pin the cured case skip on such a git, with the reason. The uncured case through
_not_a_whole_cloneis pinned by no row.What the guard counts is a least, "at least N object(s)": a missing tree hides what is under it.
git 2.45 or later was not measured (this bench has git 2.43.0). The rows of a branch and of a HEAD at a commit the source lacks assert the refusal's code alone: on 2.43 the walk fails and the refusal is the "could not walk" arm, and from 2.45 the walk lists that commit as lacking and the refusal would be the count arm. The gits that honour
GIT_NO_LAZY_FETCHare as the Why section says.CPython 3.14 did not run here: the bench has 3.12.3. The file ran under a 3.14-like
sitecustomize(exists,is_fileandis_diranswer False to anyOSError): 38 passed ata0e8cded(and 38 ata4ec51d2, r1180d).Not in this PR
plan, which reads its--sourcethe same way: 26upload-packruns and.promisor1 -> 27, measured onbba168bd. It is adopt-project.py plan: a partial-clone--sourceis fetched into, ascheckandexecutewere before #1145 #1162, stacked on this PR, because puttingplanunder the variable also changes how it reads a partial--pin-source._a_partial_clone, the older rulecheckasks of a pin's recorded clone. It is not changed.Lane: openreposhape-1 (openRepoShape-1)
🤖 Generated with Claude Code
Summary by Sourcery
Refuse incomplete partial-clone sources before adoption reads them and prevent
checkorexecutefrom fetching into the source.New Features:
checkandexecutefrom fetching missing objects into partial-clone sources by detecting incomplete history across branches, tags, and HEAD.source-partial-clonerefusals with remediation for restoring a complete source clone.Bug Fixes:
Enhancements:
Documentation:
Tests: