Skip to content

shape-doctor.py: the partial-clone row's NEXT fetches each commit it lacks objects at, by id, from the promisor remote whose branches hold it, so it no longer loops where the pinned commit sits only on another remote or another branch, and its why says where the clone does not hold the commit at all (#754) - #773

Merged
brettheap merged 4 commits into
mainfrom
l3-754-promisor-next
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-3 (openRepoShape-3)
Claim: #754 (comment)

Closes #754.

Lands after the v1.1.0 tag. (#742, the issue's "Lands after", is in main as defc0fa3.)

Round 3 (head 296a6dd6, on 155db31f)

RV-f's review of rounds 1 and 2 (comment) was READY, with two SHOULDs and three NITs. This round takes S1, S2, N1 and N2. N3 (GIT_NO_LAZY_FETCH) is left for lane 3 to file. Every anchor in this body is now this head's.

S2: commits on different remotes.

  • Before: HEAD lacked objects on origin's branches alone, and the pinned commit was on second's alone. remote_holding found no remote holding both, so the NEXT was fetch --refetch --no-filter origin <pinned> <HEAD>. git refuses that whole (not our ref), and the why said no branch held them.
  • Now partial_clone_fetch (shape-doctor.py:4656) picks the remote with fetch_source (:4697, called at :4685). It takes the first remote whose branches hold every lacking commit some remote holds, else the first that holds the pinned commit. The NEXT names only the commits that remote's branches hold (:4686), and the doctor run again names the rest, so the case converges in two runs.
  • Where no remote's branches hold any lacking commit, the NEXT is the first promisor remote and every commit, as before. That covers a commit not in the clone, or one only on a tag or on no ref.
  • The why says where each commit is (where_lacked, :4709). For example: "the pinned commit … is on second's branches; this standard's commit … is on origin's branches; so fetch every object at the pinned commit … from second (…), then run the doctor again, which names the fetch for this standard's commit …".
  • remote_holding is now remotes_holding (:4761): every remote whose branches hold one commit. It is still never asked of a commit the clone does not hold (:4682).

N1: where the remote is a guess and other promisor remotes exist, the why spells out the same fetch from each, as a line to paste (fetch_said, :4731). For example: "…so fetch it by id from origin, or, where origin does not have it, from second: git -C … fetch --refetch --no-filter second <C1>".

S1 (SonarCloud) and N2: these parts are now helpers, with no nested conditional:

  • the reason (lacking_said, :4633);
  • the commit names (lacked_commit, :4624);
  • the NEXT (refetch_command, :4649);
  • the why (:4709, :4722, fetched_named :4739, then_said :4749).

The nested def which is gone. SonarCloud's analysis of 296a6dd6:

  • 0 open issues;
  • python:S3776 and python:S3358 are CLOSED (FIXED);
  • quality gate OK, with 0.0% new duplication.

Tests (tests/test_doctor_partial_clone.py):

  • second_promisor (:295) adds second as a promisor remote, fetched or not. two_promisors (:308) now uses it.
  • split_across_remotes (:333) is RV-f's S2 case.
    • HEAD is a commit made on origin after the clone and fetched under the filter, so it lacks a blob that only origin's branches hold. C1 is on second's branches alone.
    • RV-f made HEAD lack objects with a sparse checkout. Here that would leave the doctor's own scripts/, templates/ and contracts/ out of the working tree.
  • test_commits_on_two_remotes_are_fetched_from_each_in_turn (:357):
    • The first NEXT ends second C1, and the why says which remote holds which.
    • The second NEXT ends origin <HEAD>.
    • After that, the doctor answers.
  • never_fetched (:381) and test_the_why_spells_out_the_fetch_from_each_other_promisor_remote (:393): C1 is only on second, never fetched. The NEXT asks origin, and the why's second command, run as printed, lets the doctor answer.

Mutants (each run on this head's code against tests/test_doctor_partial_clone.py):

mutant killed by
155db31f's shape-doctor.py the S2 test (NEXT … origin <C1> <HEAD>) and the N1 test (no command in the why)
the HEAD commit's remote picked over the pinned commit's the S2 test
the NEXT names every lacking commit the S2 test
no other remote's command in the why the N1 test
the why drops which remote holds which the S2 test
the first promisor remote, always the S2 test and the two-promisor test
--contains asked of a commit the clone does not hold the S1 test ("the doctor fetched")

Results, on 296a6dd6, with the same stub gh and command as below:

  • tests/test_doctor_partial_clone.py, tests/test_update_shape_merged_away.py, tests/test_repo_hygiene.py and tests/test_cli_reference.py, together: 143 passed in 71.10s (0:01:11).
  • tests/test_shape_doctor.py: 436 passed, 3 skipped, 1 xfailed in 302.19s (0:05:02).
  • tests/test_doctor_pin_restore_merges.py and tests/test_doctor_next_is_a_command.py: 73 passed in 61.77s (0:01:01).
  • CI on 296a6dd6, run 39006467231, git 2.55.0: tests gives 11542 passed, 144 skipped, 1 xfailed in 3099.12s (0:51:39). tests-git-bash, package and SonarCloud pass.

Round 2 (head 155db31f, on eb0568ac)

Cause. CI's Linux job (run 38989174052) used git 2.55.0, and three tests failed at setup with "the clone holds C1". The clone did not hold C1; the probe misread it.

  • From git 2.45.0 on, rev-list --missing=print takes a missing tip as missing. It prints ?<commit> and exits 0. git's rev-list documentation says so from v2.45.0: "If some tips passed to the traversal are missing, they will be considered as missing too, and the traversal will ignore them."
  • Git 2.43, this machine's, refuses a missing tip instead: bad object, exit 128.
  • The tests' holds read exit 0 as "held".

The same misreading was in the doctor. On git 2.45 and later:

  • objects_lacking counted an absent commit as 1 object lacked, not None.
  • So the row said "lacks at least 1 object" where the clone does not hold the commit at all.
  • And remote_holding (remotes_holding since round 3) ran for-each-ref --contains on a commit the clone lacks, a read that fetches it. CI never reached this, because the fixtures failed first.

Changes:

  • shape-doctor.py:4542: objects_lacking returns None where rev-list prints ?<commit> for the commit itself, as it does where rev-list refuses. The docstring says why at :4523.
  • tests/test_doctor_partial_clone.py:215: holds takes both answers as "not held". It uses rev-list, not cat-file -e, because cat-file -e reads the commit and so fetches it wherever git does not honour GIT_NO_LAZY_FETCH.
  • :233, :251: elsewhere now puts C1 on its pinned branch only once single_branch's clone exists, so no clone flag of any git can bring C1 in. The precondition assert not holds(clone, C1) stays, so a future change fails loudly at setup.
  • :512, :525: test_objects_lacking_reads_a_missing_commit_on_every_git gives objects_lacking each of the four answers rev-list can give: a missing commit before 2.45, a missing commit from 2.45 on, and a held commit with and without missing objects. It passes them through the module rather than running git, since one machine has one git.

Proof:

  • Against eb0568ac's objects_lacking, the unit test fails the "from git 2.45 on" case only: 1 failed, 3 passed.
  • The three fixture tests still prove what they claim on this git 2.43:
    • S1: the doctor answers CANNOT ANSWER and does not fetch C1.
    • S2, single branch: the NEXT, run as printed, ends the row.
    • S2, two remotes: the NEXT names second and C1, and run as printed it ends the row.
  • No git newer than 2.43 is on this machine, so the 2.45+ path is pinned by the unit test, and CI's 2.55 run is the end-to-end check.

Results, on 155db31f:

  • tests/test_doctor_partial_clone.py, tests/test_update_shape_merged_away.py, tests/test_repo_hygiene.py and tests/test_cli_reference.py, together: 141 passed in 146.48s (0:02:26).
  • tests/test_shape_doctor.py: 436 passed, 3 skipped, 1 xfailed in 621.17s (0:10:21).
  • CI on 155db31f, run 39001429459, git 2.55.0: tests gives 11540 passed, 144 skipped, 1 xfailed in 2852.32s (0:47:32). tests-git-bash, package and SonarCloud pass.

Re-measured on main e3137010 (git 2.43.0)

RV-f's findings S1, S2 and N1 hold on this main. I ran this PR's new tests against main's own shape-doctor.py, and four fail for the reasons the issue gives:

  • S2, two promisor remotes, C1 on the second's branches only: the NEXT is git -C <clone> fetch --refetch --no-filter origin, which names the first remote, not the one holding C1.
  • S2, a --single-branch clone of a main that lacks C1, with C1 on another branch: run as printed, the NEXT leaves the row the same, "…git cannot list the pinned commit 354efe2695c3 there".
  • S1: the why does not say the clone lacks the commit. On RV-f's S1 mutant (a None count read as nothing lacking), the doctor answered (exit 1) where it should say CANNOT ANSWER (exit 3).
  • N1: the reason says "it lacks 1 object".

N3 holds: this git 2.43 honours GIT_NO_LAZY_FETCH=1. A read of a missing blob fails with "warning: lazy fetching disabled", and no fetch shows in trace2.

What changed and why (shape-doctor.py)

  • partial_clone_row's NEXT (shape-doctor.py:4614, built by refetch_command at :4649) is now git -C <shape> fetch --refetch --no-filter <remote> <commit id>….
    • Named by id, a commit is fetched, all of its objects, wherever a branch of that remote holds it. The single-branch case no longer loops.
    • Measured with git 2.43: fetch --refetch --no-filter origin <commit> takes the objects missing at that commit from 48 to 0.
  • remotes_holding (:4761, new, called at :4682) lists the promisor remotes whose tracking refs (refs/remotes/<name>/) hold a lacking commit. partial_clone_fetch (:4656) picks the remote from them; round 3 below says how.
    • It asks git for-each-ref --count=1 --contains=<commit>, which reads commits alone. A --filter clone holds every commit a fetch brought, so this fetches nothing (measured: no fetch events in trace2).
    • It is never asked of a commit the clone does not hold. No branch here can hold such a commit, and --contains of it would fetch it.
    • Where no remote's branches hold any lacking commit, the NEXT asks the first promisor remote, and the why spells out the same fetch from each other promisor remote (round 3, N1).
  • The why (where_lacked :4709, fetch_said :4722, then_said :4749):
    • Where a lacking commit is not in the clone at all (objects_lacking is None), it says so (S1): "the pinned commit … is not in this clone at all".
    • It says which remote's branches hold each commit, or that none does.
    • It says --refetch needs git 2.36 or later (N2).
  • The reason (lacking_said, :4633) says "it lacks at least N objects", and "it does not hold the pinned commit … at all" where the commit is absent. A missing tree hides everything under it from rev-list, so the count is a floor (N1). objects_lacking's docstring says so (:4533), and partial_clone_row's says why the NEXT now names commits (:4590).
  • N3 is not done here. Setting GIT_NO_LAZY_FETCH=1 process-wide in the doctor would make a missed path fail instead of fetching, and this git honours it, as measured above. But it changes the environment of every row and every child the doctor starts, outside the partial-clone row this PR is scoped to. Left for its own change.
  • The full-clone precedent row's fetch --all has the single-branch weakness too, as the issue says. That row is not the partial-clone row, and is left as it is.

Tested (tests/test_doctor_partial_clone.py)

  • test_the_row_names_the_commit_short_and_the_fetch_that_ends_it (:142), updated: the reason ends "it lacks at least 1 object at the pinned commit …", and the pasted NEXT is […, "fetch", "--refetch", "--no-filter", "origin", C1].
  • The fixtures:
    • elsewhere (:233) is a standard whose main is a history of its own, holding C1 only on a branch pinned.
    • single_branch (:251) is a --single-branch blob:none clone of that main, which does not hold C1.
    • two_promisors (:308) is that clone plus the standard one commit on as promisor remote second, fetched with the same filter: C1's commit is held on second's branches only.
  • test_a_partial_clone_without_the_pinned_commit_cannot_answer (:263, S1): exit 3, lacking == {C1: None}, the why says the commit is not in this clone at all, and the clone still does not hold C1 afterwards. "Holds" is asked with rev-list --missing=print, holds at :215.
  • test_the_next_fetches_a_pinned_commit_only_another_branch_holds (:286, S2): run as printed, the NEXT ends the row (helper run_the_next_and_ask_again, :279).
  • test_the_next_names_the_promisor_remote_that_holds_the_commit (:319, S2): the NEXT ends second C1, and run as printed, it ends the row.
  • Against main's code (e3137010, with this PR's tests): 4 failed, 20 passed. The four are the tests above, for the reasons listed under "Re-measured".
  • Against RV-f's S1 mutant (if count} for if count != 0}, on this PR's code): 2 failed, 22 passed. The S1 test gets exit 1 for 3, and the single-branch NEXT test fails.

With a stub gh first on PATH, GH_TOKEN=bogus and an empty GH_CONFIG_DIR, in two groups:

/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-w754-<group> -o tmp_path_retention_policy=failed <files>

Results, both on eb0568ac:

  • tests/test_shape_doctor.py: 436 passed, 3 skipped, 1 xfailed in 350.65s (0:05:50).
  • tests/test_doctor_partial_clone.py, tests/test_update_shape_merged_away.py, tests/test_doctor_pin_restore_merges.py, tests/test_doctor_next_is_a_command.py, tests/test_repo_hygiene.py and tests/test_cli_reference.py, together: 210 passed in 161.26s (0:02:41).

Pinned copies

None. shape-doctor.py is not copied into projects.

Docs

None. No doc names the partial-clone row's NEXT; README, AGENTS.md and the handbook say only that the doctor fetches nothing, which still holds.

🤖 Generated with Claude Code

Summary by Sourcery

Make partial-clone doctor guidance converge by fetching each missing commit from an appropriate promisor remote and accurately explaining what remains unavailable.

New Features:

  • Make partial-clone diagnostics generate commit-specific fetch commands from promisor remotes that hold the missing commits.
  • Explain missing commits, remote ownership, fallback fetches, and subsequent doctor steps in the reported reason.

Bug Fixes:

  • Prevent partial-clone checks from looping or failing when required commits are held on different remotes or only on another branch.
  • Correctly distinguish commits absent from the clone from commits whose objects are incomplete across Git versions.
  • Avoid querying remote branches for commits absent from the clone, which could trigger unintended lazy fetches.

Enhancements:

  • Report missing-object counts as lower bounds when missing trees hide additional objects.
  • Refactor partial-clone reasoning and command generation into focused helpers.

Tests:

  • Add coverage for missing commits across Git versions, single-branch clones, multiple promisor remotes, split remote ownership, and fallback fetch guidance.

…lacks objects at, by id, from the promisor remote whose branches hold it, so it no longer loops where the pinned commit sits only on another remote or another branch, and its `why` says where the clone does not hold the commit at all (#754)

RV-f's review of #742, measured with git 2.43:
- S2: the NEXT was `fetch --refetch --no-filter <the first promisor
  remote>`, the remote whole. With the pinned commit on a second promisor
  remote's branches only, it exited 0 and the row came back the same; in
  a `--single-branch` clone whose pin is on another branch, the clone
  still did not hold the commit afterwards. The NEXT now names each
  lacking commit by id, and `remote_holding` picks the promisor remote
  whose tracking refs hold them all (`for-each-ref --contains`, which
  reads commits alone and fetches nothing). Where none does -- always for
  a commit the clone does not hold -- it asks the first, and the `why`
  names every promisor remote.
- S1: where `objects_lacking` is None (the clone does not hold the
  commit), the `why` says so.
- N1: the reason says "at least", since a missing tree hides what is
  under it from `rev-list`.
- N2: the `why` says `--refetch` needs git 2.36 or later.

tests/test_doctor_partial_clone.py: a `--single-branch` partial clone of
a standard whose `main` lacks C1 (exit 3, `lacking == {C1: None}`, C1
still not held), its NEXT run as printed, two promisor remotes with C1
on the second's branches only, and the NEXT and "at least" of the
existing case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates partial-clone diagnosis and remediation so the generated NEXT fetches each missing commit by ID from a promisor remote that can provide it, avoiding loops caused by single-branch or multi-remote layouts while improving absent-commit and lower-bound object-count explanations.

Sequence diagram for partial-clone diagnosis and targeted fetch

sequenceDiagram
    participant Doctor as shape-doctor.py
    participant Clone as Partial clone
    participant RemoteRefs as Tracking refs
    participant Remote as Promisor remote

    Doctor->>Clone: objects_lacking(shape, commit)
    Clone-->>Doctor: Missing object count or None
    alt Commit is present
        Doctor->>RemoteRefs: for-each-ref --contains=commit
        RemoteRefs-->>Doctor: Remote holding all lacking commits
    else Commit is absent
        Doctor->>Doctor: Select first promisor remote
    end
    Doctor-->>Clone: NEXT: fetch --refetch --no-filter remote commit IDs
    Clone->>Remote: Fetch each lacking commit by ID
    Remote-->>Clone: Missing objects for those commits
Loading

Flow diagram for partial-clone remediation selection

flowchart TD
    A[Partial clone has missing objects] --> B{Pinned commit present?}
    B -->|Yes| C[Check tracking refs with for-each-ref --contains]
    C --> D{Promisor remote holds all lacking commits?}
    D -->|Yes| E[Use that remote]
    D -->|No| F[Use first promisor remote]
    B -->|No| F
    E --> G[Generate fetch command with each lacking commit ID]
    F --> G
    G --> H[User runs --refetch --no-filter]
    H --> I[Doctor can compare copies on the next run]
Loading

File-Level Changes

Change Details Files
Make partial-clone remediation fetch the missing commits by ID from an appropriate promisor remote.
  • Select the first promisor remote whose tracking branches contain all commits already present but missing required objects.
  • Fall back to the first promisor remote and name all candidate remotes when no tracked branch contains the commits.
  • Generate a NEXT command using fetch --refetch --no-filter <remote> <commit IDs>, fixing single-branch and multi-promisor cases.
  • Add documentation explaining commit-targeted fetching, remote selection, and Git version requirements.
shape-doctor.py
Improve partial-clone diagnostics to distinguish absent commits and report conservative missing-object counts.
  • Represent an entirely absent pinned commit with a None missing-object value and explain that it is not held by the clone.
  • Describe object counts as lower bounds because missing trees hide objects from rev-list.
  • Explain which remote branches hold the missing commits, or that the command will try the available promisor remotes.
shape-doctor.py
Add regression coverage for missing pinned commits, branch-specific availability, and multiple promisor remotes.
  • Verify absent commits produce a CANNOT ANSWER result without causing an implicit fetch.
  • Verify the pasted NEXT command fetches a commit available only on another branch.
  • Verify remote selection identifies a non-default promisor remote and that rerunning the command resolves the row.
  • Update existing assertions for the revised reason text and commit-specific fetch command.
tests/test_doctor_partial_clone.py

Assessment against linked issues

Issue Objective Addressed Explanation
#754 Make the partial-clone row’s NEXT resolve missing objects by fetching each lacking commit by ID from a promisor remote whose tracking branches hold it, so the suggested command works when the commit is on another remote or branch. ✅
#754 When the clone does not hold the pinned commit, report that fact in the why and add a test asserting the doctor cannot answer, reports a None lacking count, and does not fetch the commit itself. ✅
#754 Clarify that object counts are lower bounds when trees are missing, and state the Git version requirement for --refetch. ✅

Possibly linked issues


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

…hold as absent on every git, so from git 2.45 on it is no longer counted as one object lacked and handed to `for-each-ref --contains`, which fetches it (#754, round 2)

PR #773's CI, on git 2.55.0, failed three tests at setup with "the
clone holds C1". The clone did not hold it. From git 2.45 on,
`rev-list --missing=print` takes a missing tip as missing too: it prints
`?<commit>` and exits 0 (git's rev-list documentation since v2.45.0, "If
some tips passed to the traversal are missing"), where git 2.43, this
machine's, refuses it, `bad object`, exit 128. The tests' `holds` read
exit 0 as "held".

The doctor read the same command. On git 2.45 and later `objects_lacking`
counted the absent commit as one object lacked, not None, so the row said
"lacks at least 1 object" where the clone does not hold the commit at
all, and `remote_holding` asked `for-each-ref --contains` of a commit the
clone lacks, a read that fetches it. A `?<commit>` line for the commit
itself is now None, as the refusal is.

tests/test_doctor_partial_clone.py: `holds` takes both answers as "not
held"; `elsewhere` puts C1 on its `pinned` branch only once
`single_branch`'s clone exists, so no clone flag of any git can bring it
in, and the precondition stays; `objects_lacking` is held to all four
answers it can get, the two for a missing commit included, through the
module, since one machine has one git.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T05:08:42Z) Independent review of #773 by RV-f (opus), rounds 1 and 2, head 155db31f: READY. No MUST, two SHOULDs, three NITs. CI is green on git 2.55.0 (run 39001429459). It lands only after the v1.1.0 tag.

#754's claims (git 2.43 here) are all fixed at the head: S1, S2, N1 ("at least") and N2 (git 2.36). The head's tests against e3137010 give 5 failed, 23 passed.

The round-2 cause is confirmed from git's source and docs.

  • rev-list-options.txt gains "If some tips passed to the traversal are missing, they will be considered as missing too" at v2.45.0, not at v2.43 or v2.44.
  • builtin/rev-list.c v2.45.0 prints missing_commits as ?<oid>.
  • The round-2 guard is necessary. On 2.43, for-each-ref --contains=<a commit the clone lacks> made 1 fetch, so round 1 on git 2.45 or later would have fetched from inside the doctor. Round 2 never passes an absent commit to remote_holding.
  • Nothing else in the partial-clone path changed between 2.43 and 2.55. It runs config, rev-parse, rev-list --missing=print (only in objects_lacking, now right for both answers) and for-each-ref --contains. There is no cat-file.

Main (defc0fa3's code, in v1.1.0) on git 2.45 or later is wording only. The row says "it lacks 1 object at the pinned commit X" when the clone does not hold X at all. The verdict is still CANNOT ANSWER, exit 3, and the NEXT is the same whole-remote refetch. Nothing is fetched, since main has no remote_holding.

remote_holding never fetched in five doctor runs: two remotes, a tag, an annotated tag, a sparse clone and a shallow clone. Called directly, it gave:

case result
X on second's branches second
third has no tracking refs skipped
X only on second, SROOT only on origin None (S2)
a ref descending from X found
no commits None

The NEXT run as printed (doctor, then NEXT, then doctor). These converge to COMPLIANT, SHAPE BEHIND:

  • a --single-branch clone with X on another branch;
  • X only on a tag, lightweight or annotated, including with --no-tags;
  • X on no ref at all (file://);
  • two promisor remotes with X on second;
  • a sparse clone with one remote;
  • a --depth 1 clone;
  • default maintenance.

Two cases end in exit 128 not our ref: N1 and S2 below. Fetching by id is no regression, since lazy fetch itself asks by id.

SHOULDs:

  • S1: SonarCloud reports two new code smells. The gate passes with 0.0% new duplication.
    • python:S3776 (CRITICAL) at shape-doctor.py:4551: partial_clone_row's cognitive complexity is 16, over 15.
    • python:S3358 (MAJOR) at :4625: a nested conditional in whence = ([…] if absent else []).
    • Fix: move the building of said and whence into a helper, and write the absent clause as an if.
  • S2: when the lacking commits are split across remotes, the WHY is false and the NEXT fails (:4619-4633, remote_holding :4653).
    • Scenario: a sparse partial clone, so HEAD (SROOT) lacks 219 objects held only on origin's branches, while X is held only on second's.
    • Measured: remote_holding gives None, so the NEXT is fetch --refetch --no-filter origin X SROOT, which exits 128 (not our ref). The row comes back unchanged, and the WHY says "no branch this clone tracks holds them", which is false.
    • Fix: name the remote holding the pinned commit and only the commits it holds. The next run names the rest, so it converges in two runs. Say in the WHY which remote holds which. This is rare: it needs HEAD to lack objects and a second promisor remote.

NITs:

Mutants:

mutant killed by
count ?<commit> again 1: the unit test's 2.45 case
first promisor always 1
drop "at least" 1
ask remote_holding about absent commits 1 ("the doctor fetched")
NEXT without ids 3
#742's S1 mutant 2

Tests:

Merges: clean with bf1f099b, every open lane-3 v1.1.1 PR, and #630.

Lane: openreposhape-3 (openRepoShape-3)

…re on different remotes' branches, the NEXT fetches from the remote holding the pinned commit only the commits that remote holds, and the `why` says which remote holds which, so the fetch no longer fails `not our ref` and comes back unchanged (#754, round 3)

RV-f's review of #773 rounds 1 and 2 (SHOULD S2): a partial clone whose
HEAD lacked objects on `origin`'s branches alone, with the pinned commit
on `second`'s alone, got `fetch --refetch --no-filter origin <pinned>
<HEAD>`. git refuses a fetch by id whole where the remote lacks one id,
so the row came back the same, its `why` saying no branch held them.

- `partial_clone_fetch` picks the remote and the commits: the first
  remote whose branches hold every lacking commit some remote holds,
  else the first holding the pinned commit, and only the commits that
  remote holds. The doctor run again names the rest. Where no remote's
  branches hold any, it is the first promisor remote and every commit,
  as before.
- The `why` says where each commit is (`where_lacked`). Where the
  remote is a guess, it spells out the same fetch from each other
  promisor remote as a line to paste (N1).
- `remotes_holding` (was `remote_holding`) lists every remote whose
  branches hold one commit, and is still never asked of a commit the
  clone does not hold.
- SonarCloud S3776 and S3358 (S1): the reason, the `why` and the fetch
  are built by helpers, with no nested conditional, and the nested
  `def which` is gone (N2).

Tests: `test_commits_on_two_remotes_are_fetched_from_each_in_turn` runs
RV-f's case's NEXT twice to an answer, and
`test_the_why_spells_out_the_fetch_from_each_other_promisor_remote`
runs the `why`'s command for a never-fetched `second`.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T08:54:04Z) RV-f's re-verification of #773 round 3 (296a6dd6): READY. S1 and S2 are fixed, and N1's command works pasted. CI is green; SonarCloud shows 0 open issues, so S3776 and S3358 are gone.

S2, RV-f's sparse scenario, run to convergence.

  • Setup: X (da48256c) is held only on second's branches; R3 (4521ae05) only on origin/main.
step result
run 1 exit 3, 0 fetches; lacks at least 219 objects at each of X and R3
NEXT 1: fetch --refetch --no-filter second <X> exit 0; X complete, R3 still lacks 72
run 2 exit 3, 0 fetches; the WHY says "…is on origin's branches"
NEXT 2: fetch --refetch --no-filter origin <R3> exit 0
run 3 exit 1, COMPLIANT, SHAPE BEHIND, 0 fetches

Run 1's WHY ("the pinned commit … is on second's branches; this standard's commit … is on origin's branches; so fetch … from second …, then run the doctor again") matches for-each-ref in every part.

N1: with X absent from the clone and second never fetched, the NEXT (origin) exits 128 as expected. The WHY's second command, pasted, exits 0, and the doctor then answers with 0 fetches.

fetch_source's fallback never picks a non-holder. By reading, every known value is a non-empty list of holders. Exhaustively, RV-f checked 192 cases over every subset of 3 remotes and found 0 non-holders.

Mutants:

  • S2 undone: killed by test_commits_on_two_remotes_are_fetched_from_each_in_turn;
  • N1 undone: killed by test_the_why_spells_out_the_fetch_from_each_other_promisor_remote.

Tests and merges:

Lane: openreposhape-3 (openRepoShape-3)

This was referenced Oct 17, 2026
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 18, 2026 04:38
@sourcery-ai

sourcery-ai Bot commented Oct 18, 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 3 days and 14 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 80aee84 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the l3-754-promisor-next branch October 18, 2026 04:38
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…ep 1 of 3 after #773 squash-landed as 80aee84)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
… of 3; the squash's tree equals the final head's, so #791's own lines are kept)

shape-doctor.py conflicted and was taken ours: F (18ee4c2) and S (80aee84) have the same tree, so this merge changes no file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…ETCH=1`, so a read of an object a partial clone lacks fails rather than fetches, and `pin_restore` names the fetch where a root's partial clone lacks its pin's history (#787) (#791)

Closes #787.

The doctor must not fetch from a partial clone, but each read was guarded one
by one. Now `main` in `shape-doctor.py` runs under `lazy_fetch_off`, which
sets `GIT_NO_LAZY_FETCH=1` and puts back what was there. Every git the doctor
starts, including `update-shape.py`'s `Upstream` reads and the validators
`run_validator` starts, fails a read of an object a partial clone lacks rather
than fetching it. No printed line carries the variable, so a pasted
`fetch --refetch --no-filter` still fetches. git 2.45 and later reads the
variable, as do the security releases of 2024-05-14 back to 2.39.4; an older
git ignores it, and the older guards still hold.

Before, a partial clone of a root whose HEAD commit broke its pin made the
doctor fetch while `pin_restore` walked the pin's history. With the variable
alone, that walk would read as "no pin there", so `pin_restore` has a new
answer, `partial`: where the root is a partial clone that lacks the pin's
history, the NEXT is
`git -C <root> fetch --refetch --no-filter <first promisor remote>`, and the
`why` says the pin's history, HEAD's included, is not all in this clone, and
spells out the same fetch from each other promisor remote. The row stays a
FINDING.

`tests/test_doctor_no_lazy_fetch.py` is new: every row from a partial clone of
the standard starts no fetch, the partial-root cases, and a recording `git`
first on PATH shows that every git the run starts has the variable.

It was stacked on #773 (#754), and its own change is `296a6dd6..1d774ab`.
Lane 3's independent reviewer RV-f (opus) found it READY in two rounds: on
`152e9c39` and `19665420`, over #773's `296a6dd6` (#791
issuecomment-6232803699), and on the head `1d774ab3`, a fast-forward of
`19665420` (-6233631070). After #773 landed, it was restacked onto #773's
squash by three merges, and its changed lines equal that delta (#791
issuecomment-6245870763).

It lands on Brett Heap's word "merge lane 3's v1.1.1 drafts after the v1.1.0
tag" (2026-10-17T13:00:57Z).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant