Skip to content

shape-doctor.py: a repository whose only remote is not named origin is offered the adoption line plan takes, --project Thing with no --org, where it was --project WorkUpstream --org <your-org>, which plan refused adopt-not-the-source; the naming row and the Triad advisory read that remote too (#1292) - #1320

Open
brettheap wants to merge 5 commits into
mainfrom
l3-1292-doctor-reads-plans-remote

Conversation

@brettheap

@brettheap brettheap commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

What changed

A v1.2.2 fix for #1292, a follow-up of lane 3's independent review of #1280 (RV1280, finding S4), filed on Brett Heap's word "file follow-ups 5, 10 and 15" (2026-11-09). It lands after the v1.2.1 tag, under Brett Heap's merge word, once reviewed and green, after #1280.

After #1280 (#1276), adopt-project.py plan reads a source's only remote where it has no origin. The doctor still read origin alone, so for a clone of testorg/Thing whose only remote is upstream, in a folder work-upstream, the way in offered

python3 .../adopt-project.py plan --source .../work-upstream --project WorkUpstream --org <your-org>

and that line, with <your-org> filled in as its why asks, ended in REFUSED adopt-not-the-source: --org testorg and --project WorkUpstream name testorg/WorkUpstream, and the source is testorg/Thing, exit 2. Now the doctor reads the remote plan reads, by the rule plan reads it by:

No new command or flag.

The choices, and why

How the doctor and plan come to ask one question: the rule moves to scripts/shape_advisory.py. The issue offered two places; I measured a third and took it.

The choice and its reasons are written where the next writer reads them: the section comment at scripts/shape_advisory.py:508-529, the import's comment at adopt-project.py:269-276, and the origin_org and origin_name docstrings.

Which reads move: all three that read a repository's own identity. Measured on the parent's head; the line numbers are this head's:

  • origin_org (shape-doctor.py:11434): the way in's --org. Moves (S4).
  • origin_name (:11301), through repo_local_origin_name (:11412): the way in's --project and the naming row (check_not_a_root_naming, :11483). Moves. The naming row was work-upstream alone; it now classifies the remote's name too, as for origin.
  • shape_advisory.repository_names (scripts/shape_advisory.py:631): the Triad advisory and leg detection, in the doctor and in adopt-project.py plan and check (_print_triad_advisory, adopt-project.py:6227). Moves. Measured with the Atlas-code clone above: left on origin, the doctor offers to adopt a leg, a line plan refuses. One plan output changes with it: for a source whose only remote is a path naming a leg (upstream = /srv/git/testorg/Atlas-code.git), which plan takes with --project Atlas --org testorg, plan prints the leg's NOTE where it printed the Triad advisory, exit 0 both, as for the same source with origin; and the doctor, which offered that source --project <Folder> --org <your-org>, a line plan took, now gives the leg's row and no NEXT.

Not moved: origin_url (shape-doctor.py:10419), which reads a sibling's and the holder's origin as scripts/siblings.py reads it ((no origin)). It is the make siblings question, not plan's.

Measured, before and after

Probe: a script in my scratch (W1292-scratch/probe.py, not committed), on git 2.43.0 and Python 3.12, under a stub gh and GIT_SSH_COMMAND=false. Each source is a one-commit repository whose remotes are written with git config. testorg/Thing is https://github.com/testorg/Thing.git. For each source and each head, the doctor ran --root <src> --json and the text report, and plan ran the doctor's own NEXT with <your-org> as testorg. Digests are sha256 (12 hex) of the output with the checkout's path written <SHAPE>.

source (remotes) #1280 5cb1abdb: json / text / plan this PR: json / text / plan same
work-origin (origin) 9224a23db891 / 5a2de26bb853 / 0 9224a23db891 / 5a2de26bb853 / 0 yes
work-origin-fork (origin, fork) 7bb7919f3e92 / b8274488c6ab / 0 7bb7919f3e92 / b8274488c6ab / 0 yes
work-remotes-file (upstream, and origin as a .git/remotes file) b434dc212c65 / baf29aa8c568 / 0 b434dc212c65 / baf29aa8c568 / 0 yes
work-origin-leg (origin = Atlas-code) 9ff666b02661 / c9fd76fbe2de / no NEXT 9ff666b02661 / c9fd76fbe2de / no NEXT yes
work-two (upstream, fork) a46610197157 / 217e208ad2fe / 0 a46610197157 / 217e208ad2fe / 0 yes
work-dash (-x) fb967f18ea6b / 9c561ddfaa16 / 0 fb967f18ea6b / 9c561ddfaa16 / 0 yes
work-nonascii (caf + U+00E9) 6ab4ee868319 / 5a3e701404a0 / 0 6ab4ee868319 / 5a3e701404a0 / 0 yes
work-none (no remote) 4c81a7f726d5 / 73c603e5fe8b / 0 4c81a7f726d5 / 73c603e5fe8b / 0 yes
work-upstream (upstream) c7d71f1b63d1 / 461b337dd755 / 2 adopt-not-the-source b9bde6a48c3b / 3b5ecfc23abf / 0 changed, the fix
work-upstream-leg (upstream = Atlas-code) a41e7451e91a / ddf857286764 / 2 adopt-not-the-source 0ccb5437228d / b66568590222 / no NEXT changed, the fix
work-upstream-path (upstream = /srv/git/testorg/Thing.git) 626c4daf22ca / e34eb96468f2 / 0 566648a0ad20 / b6c2cadbe7d2 / 0 changed: --project Thing --org <your-org>, naming work-upstream-path, Thing

plan itself, moved code: plan --source <src> with --project Thing, with --project Thing --org testorg, and with neither, on all 11 sources at both heads. All 33 runs are byte-identical, exit and output (W1292-scratch/plan-same.txt). The move itself changes no plan output; repository_names changes plan's and check's advisory line for a source whose only remote is a path naming a leg (above), which none of these 11 sources is.

The work-upstream NEXT, before and after:

before: python3 <SHAPE>/adopt-project.py plan --source .../work-upstream --project WorkUpstream --org <your-org>
after:  python3 <SHAPE>/adopt-project.py plan --source .../work-upstream --project Thing

Rows

14 rows in tests/test_shape_doctor.py, the module that holds the doctor's way-in rows (#545's and #521's test_the_adoption_line_names_an_org_only_where_plan_reads_none) and its naming rows. They sit right after the #545 rows.

  • test_the_adoption_line_reads_the_remote_plan_reads (6 layouts: only-upstream, only-origin, origin-and-fork, two-and-no-origin, starts-with-dash, not-printable-ascii): the whole NEXT, origin_org, suggested_project, the naming row's names and repository_names.
  • test_plan_takes_the_adoption_line_as_the_doctor_fills_it (the same 6): the parity row. The NEXT's --project, with --org testorg where the NEXT asks for one, is run through plan: no adopt-not-the-source, exit 0. Then the doctor's organisation is plan's _remote_repository's.
  • test_a_clone_whose_only_remote_names_a_leg_is_offered_no_adoption (origin, upstream).

With #1280's shape-doctor.py, adopt-project.py and scripts/shape_advisory.py swapped in (restored and checked with cmp), 3 fail and 11 hold:

FAILED tests/test_shape_doctor.py::test_the_adoption_line_reads_the_remote_plan_reads[only-upstream]
FAILED tests/test_shape_doctor.py::test_plan_takes_the_adoption_line_as_the_doctor_fills_it[only-upstream]
FAILED tests/test_shape_doctor.py::test_a_clone_whose_only_remote_names_a_leg_is_offered_no_adoption[upstream]
3 failed, 11 passed, 449 deselected in 14.15s

The parity row fails there on S4's own line: REFUSED adopt-not-the-source: --org testorg and --project WorkUpstream name testorg/WorkUpstream, and the source is testorg/Thing. On this branch: 14 passed, 449 deselected in 11.09s.

Mutants

Each was applied to the branch, run, then restored and checked with cmp.

mutant killed by
M1: the doctor reads origin alone again (origin_org and origin_name) 2: reads_the_remote_plan_reads[only-upstream], plan_takes...[only-upstream]
M2: several remotes and no origin guessed among (return names[0], []) 4: [two-and-no-origin] and [not-printable-ascii] in both parametrized rows
M3: repository_names reads origin alone again 2: reads_the_remote_plan_reads[only-upstream], leg..._no_adoption[upstream]
M4: a remote whose name is not printable ASCII is read 2: [not-printable-ascii] in both parametrized rows
M5: a remote whose name starts with - is read 3 of #1280's rows in tests/test_adopt_source_remote_name.py: ..._cannot_name_is_not_read[leading-dash], ..._named_like_an_option_passes_check[branch], [rename] (my rows read no organisation either way: git remote get-url -x is an unknown switch)
M6: remote_exists always true (an origin assumed) 3 of mine ([only-upstream] twice, leg..._no_adoption[upstream]); 20 of #1280's
M7: remote_exists always false (a .git/remotes origin missed) 5 of #1280's, test_a_legacy_origin_is_read_beside_another_remote among them

M1 to M4 and M6 ran against the 14 rows, M5 to M7 against tests/test_adopt_source_remote_name.py (28 rows), on this head 9d71536d1.

Gates

One pytest at a time, under a stub gh (exit 97), GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null, git-filter-repo 2.47.0 on PATH, and LANES_LANE unset:

  • The 14 rows (tests/test_shape_doctor.py -k the three): 14 passed, 449 deselected in 11.09s.
  • tests/test_shape_doctor.py with tests/test_doctor_unsearchable_links.py, which imports its fixtures and holds origin_org's, repo_local_origin_name's and repository_names's rows for a .git this user cannot reach, in ONE run, in the background with a log: 509 passed, 3 skipped in 959.09s (0:15:59). Skipped: [3] tests/test_shape_doctor.py:10643: the PowerShell line is printed, and powershell.exe exists, only on a Windows host.
  • adopt-project.py: a source whose only remote is not named origin, as clone.defaultRemoteName names it, is read from that remote, so plan reads its organisation, default branch and branches where it refused adopt-no-org, took HEAD's branch as the default and asked --tracking-branch of the source's own branches, and one with several remotes and no origin names them and guesses none (#1276) #1280's two test modules and the advisory's, for the moved rule: tests/test_adopt_source_remote_name.py tests/test_adopt_split_branch.py tests/test_shape_advisory.py: 122 passed in 266.85s (0:04:26).
  • adopt-project.py changes: tests/test_scaffold_leg_path_collisions.py: 104 passed in 5.64s.
  • The guards, tests/test_repo_hygiene.py tests/test_cli_reference.py tests/test_agents_md_codes.py tests/test_git_output_decoding.py tests/test_stdout_encoding.py tests/test_path_guard_hygiene.py tests/test_no_auto_maintenance.py tests/test_update_add_codes_documented.py: 470 passed, 3 skipped in 99.26s (0:01:39). Skipped: tests/test_stdout_encoding.py:285: the default this is about is Windows' own; [2] tests/test_no_auto_maintenance.py:222: the session began with GIT_CONFIG_GLOBAL set, and its file includes that one.
  • No refusal or finding code is added or changed, so no module that greps a code is touched.
  • Not run: tests/test_doctor_next_is_a_command.py. Its <your-org> rows are for repositories with no remote, which read byte for byte as before (work-none above), and it imports from four other test modules.

All on this head, 9d71536d1, with /usr/bin/python3 3.12 and git 2.43.0.

Merges

git merge-tree --write-tree of this head (9d71536d153ac429d3e24547e54349de5857816c), and of #1280's head alone, with main dc9a0bcc and with each open PR that touches a file this PR changes:

with this PR #1280 alone
main dc9a0bcc (RV1292's measure) conflict b64f1c6ac: adopt-project.py, 3 hunks conflict d4fb95c7b: adopt-project.py, 3 hunks
#1319 be62b704 (RV1292's measure) conflict 52eefdfa3: adopt-project.py, 3 hunks conflict 3e2e3e97a: adopt-project.py, 3 hunks
#1316 8bdc1efd4 (shape-doctor.py, tests/test_shape_doctor.py) conflict 29f6d728b: adopt-project.py, 3 hunks conflict 4b0a66917: adopt-project.py, 3 hunks
#1313 cf6585063 conflict e39ab2c64: adopt-project.py, 3 hunks conflict 02b6e997f: adopt-project.py, 3 hunks
#1312 e5012593a conflict a371825fa: adopt-project.py, 3 hunks conflict b99122912: adopt-project.py, 3 hunks
#1311 8649430ce clean 2f435a5c9 clean fcdc6a73f
#1305 28f6f6694 conflict fa6ea2e26: adopt-project.py, 3 hunks conflict 7479d8cf3: adopt-project.py, 3 hunks
#1304 df942efee (tests/test_shape_doctor.py too) clean 4459de12a clean 2b64970fe
#1297 2e020b458 conflict d730645c0: adopt-project.py, 3 hunks conflict cb512f264: adopt-project.py, 3 hunks
#1295 129f4dce8 (scripts/shape_advisory.py) conflict 01f646441: adopt-project.py, 3 hunks conflict 50f0f1c19: adopt-project.py, 3 hunks
#1294 cf07ba0d1 conflict 0d23ce03b: adopt-project.py, 3 hunks conflict 9e3f3e53e: adopt-project.py, 3 hunks
#1284 86ab76db4 (shape-doctor.py) conflict b69513d8b: adopt-project.py, 3 hunks conflict 022383580: adopt-project.py, 3 hunks
#1265 39b21c07e conflict 04ce85780: adopt-project.py, 5 hunks conflict 943c59e55: adopt-project.py, 5 hunks
#1249 3e5d1fde1 conflict 72519158e: adopt-project.py, 3 hunks conflict 3bbc733ff: adopt-project.py, 3 hunks
#1237 fa6d189ff conflict a520055c6: adopt-project.py, 3 hunks conflict 36ba82aac: adopt-project.py, 3 hunks
#1225 cc230f2f1 clean 8942ac91d clean 1359b45e3
#1194 ddc21fb53 conflict 97037615c: adopt-project.py, 3 hunks conflict 00f7bab94: adopt-project.py, 3 hunks
#1171 ce1dd661f clean 14aeb13a0 clean 583342d5b

Every conflict is #1280's own: each names adopt-project.py alone, with the same hunks for this head as for #1280's head alone, 65 lines higher (the lines this PR moves out sit above them). Their text, markers' labels aside, is byte-identical both ways: sha256 ec61064bd9f21ca8 for the 3 hunks against main and every PR but #1265, and 2a9de60bec963b69 for #1265's 5. shape-doctor.py, scripts/shape_advisory.py and tests/test_shape_doctor.py merge clean against every PR, #1316, #1304, #1295 and #1284 included. The restack onto #1280's squash resolves these hunks once, for both.

Out of scope

Stacked

Stacked on #1280 (head 5cb1abdbc78f3dbe751bb91fa0e5d4f6ddc4cbd8). Its first commits are #1280's; this PR's own change is 5cb1abdbc..9d71536d1. Review that range. It lands after #1280, by a restack onto its squash.

Independent review

RV1292, lane 3's independent opus review (issuecomment-6774393309), reads READY AFTER FIXES at 9d71536d. The fixes are to the body only, and it found no MUST. It judged all three choices right:

  • the rule's place, scripts/shape_advisory.py;
  • moving all three reads;
  • leaving the siblings read and the way in's why.

Its own probe of 19 sources found every change to be what #1292 asks. Its gates passed:

  • the moved rule: 122 passed;
  • the modules that patch adopter.git_out: 122 passed;
  • the guards: 470 passed, 3 skipped.

Lane 3's main loop ran the gates RV1292 left, with RV1292's runner at this head, one pytest at a time: tests/test_shape_doctor.py with every module that imports its fixtures (tests/test_doctor_unsearchable_links.py, tests/test_doctor_next_is_a_command.py, tests/test_commit_trailers.py, tests/test_doctor_legs_members_next.py, tests/test_doctor_validator_rows.py and tests/test_doctor_gitmodules_not_utf8.py), 1008 passed, 9 skipped in 1321.62s; and tests/test_adopt_origin_repository.py with tests/test_adopt_project_is_the_source.py, 53 passed in 34.17s. The 9 skips are the host's: 3 Windows-only PowerShell rows, and 6 rows where git 2.43 crashes or dies and CI's git 2.55 answers.

Taken here: S1 (the plan and check change that repository_names brings), S2 (two more visible changes), the merge line and N1.

Not taken:

Closes #1292

Lane: openreposhape-3 (openRepoShape-3)

🤖 Generated with Claude Code

Summary by Sourcery

Align repository remote selection across the adoption tools so repositories without an origin receive valid adoption guidance and remote-aware diagnostics.

Bug Fixes:

  • Make repository identity detection consistently use a repository's only readable remote when origin is absent, so shape-doctor adoption commands match what adopt-project.py plan accepts.
  • Prevent leg repositories with non-origin remotes from being incorrectly offered as standalone projects.
  • Update remote-specific diagnostics and remediation guidance to reference the remote actually being read.

Enhancements:

  • Centralize remote selection and readability rules in the shared shape advisory module used by both adoption tools.
  • Expand handling of default branches, tracking branches, split branches, and remote-related guidance for repositories without origin.

Documentation:

  • Update CLI documentation to describe organisation detection from a sole non-origin remote and the cases where no remote is selected.

Tests:

  • Add coverage for sole non-origin remotes, multiple remotes without origin, unusual remote names, legacy remotes, leg detection, and parity between shape-doctor recommendations and adoption plans.

Brett Heap and others added 5 commits November 8, 2026 21:22
… from its only remote (#1276)

Not gated yet (reboot checkpoint). The rows pass on this change (14) and
12 of them fail on main; mutants M1-M5 run (M2 equivalent), M6-M20 not
yet run; the adoption gates, test_shape_doctor, the leg-path collisions
module and the guards not yet run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `clone.defaultRemoteName` names it, is read from that remote, so `plan` reads its organisation, default branch and branches where it refused `adopt-no-org`, took HEAD's branch as the default and asked `--tracking-branch` of the source's own branches, and one with several remotes and no `origin` names them and guesses none (#1276)

The final commit over the WIP commit. It changes the `_remotes_of`
docstring to say `git switch` "would change" the remote the current
branch names (only `git ls-remote --get-url`'s flip was measured), and
records the gates.

- Rows (tests/test_adopt_source_remote_name.py): 14 passed; on main 12
  fail and 2 pass (the (c) row and the legacy-`origin` row).
- Mutants M1-M20: 19 killed, M2 equivalent (`git remote get-url origin`
  exits 0 whenever `origin` is listed).
- Gates: the adoption groups (1,620 passed, 4 skipped in ten runs,
  each module with the modules it imports fixtures from),
  test_shape_doctor (446 passed, 3 skipped), the leg-path collisions
  (104 passed), and the guards with test_update_add_codes_documented
  (349 passed, 3 skipped); every skip is the host's (git 2.43, a
  case-keeping disk, not Windows, GIT_CONFIG_GLOBAL set).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…remote has no longer runs `git fetch origin` in a source that has no `origin` (a source with several remotes is given the rename or a branch made from a remote's, one with no remote none), a remote whose name starts with `-` is refused by name as one that is not printable ASCII is, the `--org` help says what is read, the way past a default branch names the remote read, and the dead constants are gone (#1276)

A source with no `origin` and two or more other remotes, and one with no
remote, were told `git fetch origin` after `plan-tracking-branch-missing`,
which fails there (`fatal: 'origin' does not appear to be a git repository`)
one line below "it has no remote `origin`". Where remotes were passed over,
the remediation now gives what `adopt-no-org` gives, the rename
`git remote rename <name> origin` or `git branch <branch>
refs/remotes/<name>/<branch>`; a source with no remote is given no fetch.
`check`'s split-branch remediation leaves out its `git fetch --prune origin`
sentence the same way. Run through in a copy of the several-remotes source,
both ways make the same plan check `plan ok`.

A remote named `-x` was refused for the wrong reason (git reads
`get-url -x` as an option); it is refused by name, with `--` for its rename.
`--org`'s help and docs/cli.md say exactly what is read. The push in
`plan`'s way past a refused default branch names the remote read. The
unread constants `TRACKING_BRANCH_REMEDIATION`, `REMOTE_LACKS_REMEDIATION`
and `LOCAL_REMOTE_LACKS` are removed. Rows: the several-remotes and
no-remote ways, an ESC-named and a `-x`-named remote, and `up$(id)` quoted
in two remediations.

Lane: openreposhape-3 (openRepoShape-3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…org` print takes `--`, so it works for a remote named `-x`; one remote is worded as one, a remote whose name holds an ESC is given no `git branch`, the split-branch row expects the text for a source with no remote, and the dead `ORIGIN_BRANCHES` and four unused defaults are gone (#1276)

RV1280b's review of 890976c. CI's `tests` failed on
tests/test_adopt_split_branch.py::test_check_says_how_to_go_on_once_after_its_findings:
`small_source` has no remote, so `check` leaves out the sentence that sends
the person to fetch from an `origin` it has not, and the row counted the
text for a source whose remote is `origin`. It expects
`_split_branch_remediation(None)` once and no `git fetch` now.

`git remote rename <name> origin` is `error: unknown switch` for a remote
named `-x`. `check`'s remediation for a tracking branch only a remote has,
and `adopt-no-org`'s, print `git remote rename -- <name> origin` now, which
works for every name `_remotes_of` passes over. A lone remote is worded as
one ("A branch the source's remote `-x` has"). A remote whose name holds an
ESC is given no `git branch <branch> refs/remotes/<name>/<branch>`, which git
refuses (`_holds_branches` asks `git check-ref-format`), and the sentence says
why. Each way, run as printed, makes `check` pass: (d), `-x` alone, and the
rename `adopt-no-org` gives for `-x` and for an ESC remote.

An `up$(id)` case is added to the row for `_default_branch_way_past` and to
a split-branch row, so dropping the `shlex.quote` in either fails a row. The
unused `ORIGIN_BRANCHES` and the defaults on `_remote_lacks_remediation`,
`_local_remote_lacks`, `_refuse_a_tracking_branch_the_remote_lacks` and
`_checked_default_branch` (every caller passes the remote) are removed. The
`--org` help says "the one that remote's url names"; docs/cli.md is
regenerated.

Lane: openreposhape-3 (openRepoShape-3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… is offered the adoption line `plan` takes, `--project Thing` with no `--org`, where it was `--project WorkUpstream --org <your-org>`, which `plan` refused `adopt-not-the-source`; the `naming` row and the Triad advisory read that remote too (#1292)

After #1280 (#1276), `adopt-project.py plan` reads a source's only remote
where it has no `origin`. The doctor's `origin_org` and `origin_name`, and
`shape_advisory.repository_names`, read `origin` alone, so for a clone of
`testorg/Thing` whose only remote is `upstream` the way in offered
`--project WorkUpstream --org <your-org>`, and that line, filled in as it
asks, ended in `REFUSED adopt-not-the-source` (RV1280 S4 on #1280).

The rule for which remote is read (`_remotes_of`, with `_remote_exists` and
`_printable_ascii`) moves from `adopt-project.py` to
`scripts/shape_advisory.py`, which both tools already import and no root
copies, and `adopt-project.py` imports it under the names it always used.
The doctor's two reads and `repository_names` ask `source_remote` from it.
Not `scripts/repo_shape.py`: it is a shape copy every root pins, and a rule
added there is an `upstream-changed` row for every project. Not a copy in
the doctor held by a parity row: #521's copy of `plan`'s regex drifted.

A source with an `origin`, several remotes and no `origin`, a remote named
`-x` or not in printable ASCII, or no remote reads byte for byte as before
in the doctor's `--json` and text, and `plan` is byte-identical on every
probe source. No new command or flag.

Rows in tests/test_shape_doctor.py: 14, 3 of which fail at #1280's head.

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

sourcery-ai Bot commented Nov 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

Uses of the API

Make the doctor's adoption line, naming row and repository-identity checks ask the shared rule which remote plan reads.

  • shape-doctor.py L11363-11376, L11464-11470, L11475-11476

Implementation

Move remote selection into the shared advisory module so adopt-project.py and shape-doctor.py use the same rule, and have repository-name discovery read that selected remote.

  • adopt-project.py L269-280
  • scripts/shape_advisory.py L507-630, L638-643, L670-671

Carry the selected source remote through adoption planning, branch checks, remediations and source descriptions so a sole non-origin remote is read consistently and passed-over remotes are handled explicitly.

  • adopt-project.py L3017-3080, L5087-5154, L10180-10214 (+33 small edits)

Documentation

Update the --org help text to explain when a sole non-origin remote supplies the organisation and when no remote is used.

  • docs/cli.md L497-505

Tests

Add adoption-source regressions for sole, absent, multiple and unusual remotes, covering branch selection, remediations and source identity.

  • tests/test_adopt_source_remote_name.py L1-634

Update the split-branch remediation assertion for a source with no remote, which must not be told to fetch from origin.

  • tests/test_adopt_split_branch.py L171-175

Other changes

Files not grouped.

  • tests/test_shape_doctor.py L9816-9942

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

@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-11-10T17:30:08Z) RV1292: READY AFTER FIXES at 9d71536

This is an independent review of #1320 (for #1292). It covers only the PR's own commit, 5cb1abdb..9d71536d, and was done by RV1292 (lane openRepoShape-3, Opus 5.5, 2026-11-10). I found nothing wrong in the code, so there is no MUST. The fixes asked for are all in the body: S1, S2 and the merge line. The NITs are optional.

The three choices

1. Where the rule lives (scripts/shape_advisory.py): right, and the reasons hold.

  • scripts/repo_shape.py is in both shape copy lists: COPIED_FROM_SHAPE (scripts/shape_materialize.py:441-445) and FAMILY_COPIED_FROM_SHAPE (:650-653), each as (REPO_SHAPE, REPO_SHAPE). Lines :3131-3134 are that file's own "IT LIVES HERE AND NOT IN scripts/repo_shape.py" paragraph, and the same paragraph appears at :1393, :1590, :1670, :5096 and :5592.
  • scripts/shape_advisory.py is in no copy list. Outside tests/, git grep shape_advisory finds only the three tools' imports and some comments, and nothing under templates/. Both tools already import it at the parent: adopt-project.py:268 and shape-doctor.py:500.
  • What plan does after the move, from the code:
    • _remote_names, _printable_ascii, _readable_remote_name and remotes_of have the same bodies as before.
    • The decode is exactly _git_path's (adopt-project.py:1308-1313).
    • remote_exists now calls repo_shape.git_out(binary=True). That runs the same git command in the same folder and environment. It raises Refusal only when git exits non-zero, and lets an OSError through as the old subprocess.run did.
    • No test monkeypatches any moved name.
    • Two modules patch adopter.git_out, which the moved _remote_names no longer goes through. Both pass (see Gates).
  • What plan does, measured: 95 plan runs with fixed arguments on 19 sources, at both heads. 93 are byte-identical. The 2 that differ come from choice 2 (repository_names), not from the move (see S1).

2. Which reads move (all three): right. The body describes the doctor's side plainly but not plan's side (S1).

  • At this head, a source whose only remote is upstream gives the same doctor text and --json as the same source with origin (after paths are normalised). Checked on three pairs:
    • testorg/Thing;
    • the GitHub leg testorg/Atlas-code;
    • the path /srv/git/testorg/Atlas-code.git.
  • Moving repository_names matters more than the body's example shows. Take the path-leg source (only remote upstream = /srv/git/testorg/Atlas-code.git):
    • At the parent: the doctor offered --project S16UpstreamPathLeg --org <your-org>. With testorg filled in, plan took that line (exit 0), so a code leg was planned as a project of its own; plan cannot compare a path.
    • At this head: the doctor shows the leg's row with no NEXT, as it does for the same source with origin.

3. What was left out: right to leave, and the body says so for both under "Out of scope".

  • origin_url (shape-doctor.py:10419) is the make siblings read. templates/family-root/scripts/siblings.py reads get-url origin at :481, :845 and :1064. If the doctor moved alone, it would disagree with the tool it reports on.
  • The way-in WHY at :12084-12085 is unchanged byte for byte. N4 names the one case where it now names the wrong remote.

Before and after (my own probe, not W1292's)

The probe made 19 one-commit sources and ran at both heads. Setup:

  • git 2.43.0, Python 3.12.3, the stub gh, GIT_SSH_COMMAND=false, LANES_LANE unset;
  • a GIT_CONFIG_GLOBAL with user.name set. Without it, every plan run on a single repository stops at adopt-manifest-invalid before the advisory is printed; my first run showed this.

For each source it recorded:

  • the doctor's --json and text output;
  • the way-in NEXT;
  • plan on that NEXT, with <your-org> replaced by testorg;
  • plan with five fixed argument sets: --project Thing; --project Thing --org testorg; --project Atlas --org testorg; --project Atlas-code; --project sam-wip.

Of 169 outputs per head, 143 are byte-identical.

Unchanged in every output (doctor --json, doctor text, NEXT, and all plan runs):

  • origin;
  • origin and fork;
  • upstream with origin as a .git/remotes file;
  • origin = Atlas-code;
  • origin = a path leg;
  • two remotes and no origin;
  • three remotes and no origin;
  • a remote named -x;
  • a remote named caf + U+00E9;
  • no remote;
  • only a .git/branches/upstream file;
  • origin = sam-wip, from GitHub and as a path.

Changed: only sources with a single remote that is not origin.

only remote upstream = parent: NEXT, then plan head: NEXT, then plan
testorg/Thing --project S02OnlyUpstream --org <your-org>, then exit 2 adopt-not-the-source --project Thing, then exit 0
testorg/Atlas-code --project S09UpstreamLeg --org <your-org>, then exit 2 adopt-not-the-source none: the leg's row, and the Triad row names Atlas
/srv/git/testorg/Thing.git --project S11UpstreamPath --org <your-org>, then exit 0 --project Thing --org <your-org>, then exit 0
/srv/git/testorg/Atlas-code.git --project S16UpstreamPathLeg --org <your-org>, then exit 0 (a leg adopted) none: the leg's row

In each of these, three more things change, all as they do for origin:

  • the naming row names the remote's repository as well;
  • the other way in uses that name for setup.sh --project (S2);
  • the --json detail fields carry it (S2).

Every change is what #1292 asks for. plan with fixed arguments: 93 of 95 runs are identical. The 2 that differ are on the path-leg source, with --project Thing --org testorg and with --project Atlas --org testorg. Both exit 0 at both heads, but now print the leg's NOTE where they printed the Triad advisory (S1).

Rows

  • At this head: 14 passed, 449 deselected in 11.55s.
  • With the parent's code swapped in (shape-doctor.py, adopt-project.py and scripts/shape_advisory.py from 5cb1abdb; each restored afterwards and compared to HEAD with cmp): 3 failed, 11 passed, 449 deselected in 9.01s. The three that fail are the ones the body names: [only-upstream] in both parametrized rows, and ..._no_adoption[upstream].
  • Controls: 11 rows pass at both heads:
    • reads_the_remote_plan_reads and plan_takes..., each with only-origin, origin-and-fork, two-and-no-origin, starts-with-dash and not-printable-ascii;
    • ..._no_adoption[origin].
  • Rows that should fail before: all three do, and no control does.

My own mutants. Each was one exact replacement, then restored from a backup and checked against HEAD with cmp.

mutant result
MA: only origin_name reads origin again killed: reads_the_remote_plan_reads[only-upstream] and plan_takes...[only-upstream] (2 failed, 12 passed)
MB: only origin_org reads origin again killed by the same 2
MC: _remote_names decodes strictly (no surrogateescape) survives: the 14 rows pass, and tests/test_adopt_source_remote_name.py gives 28 passed (N2)
MF: remotes_of stops asking remote_exists(root, ORIGIN) killed by #1280's test_a_legacy_origin_is_read_beside_another_remote (1 failed, 27 passed); none of the 14 rows catches it, which is fine because there is one rule

Gates

All at 9d71536d, one pytest at a time, with -rs. Environment: stub gh, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null, git-filter-repo 2.47.0 on PATH, LANES_LANE unset.

  • The moved rule (tests/test_shape_advisory.py, tests/test_adopt_source_remote_name.py, tests/test_adopt_split_branch.py): 122 passed in 161.16s (0:02:41).
  • The modules that patch adopter.git_out (tests/test_adopt_unenterable_source_and_locale.py, tests/test_adopt_unsearchable_source.py for its adopter fixture, tests/test_adopt_manifest_values.py): 122 passed in 127.64s (0:02:07).
  • The guards (tests/test_repo_hygiene.py, tests/test_agents_md_codes.py, tests/test_git_output_decoding.py, tests/test_stdout_encoding.py, tests/test_cli_reference.py, tests/test_no_auto_maintenance.py, tests/test_update_add_codes_documented.py, tests/test_path_guard_hygiene.py): 470 passed, 3 skipped in 84.22s (0:01:24). The skips:
    • [1] tests/test_stdout_encoding.py:285: the default this is about is Windows' own
    • [2] tests/test_no_auto_maintenance.py:222: the session began with GIT_CONFIG_GLOBAL set, and its file includes that one

Merges (git merge-tree --write-tree)

with #1320 9d71536d #1280 alone 5cb1abdb
main dc9a0bcc conflict b64f1c6ac74f: adopt-project.py, 3 hunks conflict d4fb95c7b393: adopt-project.py, 3 hunks
#1319 be62b704 conflict 52eefdfa37d1: adopt-project.py, 3 hunks conflict 3e2e3e97a2f7: adopt-project.py, 3 hunks

Body

  • Closes #1292 is the only closing keyword, including in the Sourcery section, and closingIssuesReferences reads [1292] (checked again at the end).
  • It is ASCII except line 155, the robot line (U+1F916).
  • The numbers I measured again match the body: 14 rows; 3 fail and 11 hold at the parent; 14 passed; the conflict hunks 65 lines apart; and every line number except one (N1).
  • It covers what a person sees change, the choices, and the restack plan, except for what S1 and S2 add.

Findings

MUST: none.

S1 SHOULD (body, and the squash message): plan and check change too.

  • repository_names is also what plan and check read for their Triad advisory (_print_triad_advisory, adopt-project.py:6227).
  • The body says plan is byte-identical "on every probe source". That is true for its 11 sources, but not in general. On the path-leg source above, plan --project Atlas --org testorg now prints the leg's NOTE instead of the triad lines (exit 0 at both heads), just as the same source with origin does. The change is right, but nothing in the body says it.
  • Replacement 1: replace the third bullet under "Which reads move" with:
    • shape_advisory.repository_names (scripts/shape_advisory.py:631): the Triad advisory and leg detection, in the doctor and in adopt-project.py plan and check (_print_triad_advisory, adopt-project.py:6227). Moves. Measured with the Atlas-code clone above: left on origin, the doctor offers to adopt a leg, a line plan refuses. One plan output changes with it: for a source whose only remote is a path naming a leg (upstream = /srv/git/testorg/Atlas-code.git), which plan takes with --project Atlas --org testorg, plan prints the leg's NOTE where it printed the Triad advisory, exit 0 both, as for the same source with origin; and the doctor, which offered that source --project <Folder> --org <your-org>, a line plan took, now gives the leg's row and no NEXT.
  • Replacement 2: at the end of choice 1, replace "plan is byte-identical on every probe source (below)." with:

    plan is byte-identical on every probe source (below); the one plan change is repository_names's, under "Which reads move".

  • Replacement 3: after "All 33 runs are byte-identical, exit and output (W1292-scratch/plan-same.txt).", add:

    The move itself changes no plan output; repository_names changes plan's and check's advisory line for a source whose only remote is a path naming a leg (above), which none of these 11 sources is.

  • Squash message: replace "and plan is byte-identical on every probe source." with:

    and plan is byte-identical on every probe source but one whose only remote is a path naming a leg, where its Triad advisory becomes the leg's NOTE, as for origin.

S2 SHOULD (body): two more visible changes for the upstream clone. After the the way in bullet under "What changed", add:

  • the other way in offers setup.sh --org <your-org> --project Thing, where it offered --project WorkUpstream, as for origin.
  • In --json, the way in's origin_org and the naming row's origin keep their names and, where there is no origin, carry the only remote's organisation and repository name (testorg, Thing).

Merge line (body).

N1 NIT: scripts/shape_advisory.py:628 should be :631, where def repository_names is at this head.

N2 NIT, a coverage gap that comes from #1280: no row holds the surrogateescape decode (MC survives). The input is reachable: git config remote.<byte 0xff>.url https://github.com/testorg/Thing.git gives a remote whose name is the byte 0xff.

N3 NIT (optional): the header of scripts/shape_advisory.py still describes only the advisory ("never a gate ... nothing here returns an exit code"). remotes_of now decides plan's adopt-no-org and adopt-not-the-source refusals. This does not break the doctrine: the ratified list names review lanes, checks, validators, floors, councils and merge gates, not plan. One sentence in the header pointing to the new section would stop the next reader from moving the rule out on that doctrine's account.

N4 NIT (a follow-up, not this PR): for a source whose only remote is upstream = /srv/git/testorg/Thing.git, the NEXT at this head is --project Thing --org <your-org>, with WHY "this repository's origin names none that plan can read". The doctor actually read upstream there. Before this PR it read origin, so the sentence was true. I agree with leaving it here, because changing it would alter the bytes for every source with no organisation. It is worth filing.

Not this PR's (an observation): for a remote naming testorg/sam-wip (the workspace form), the doctor offers --project SamWip with origin at both heads, and now with upstream too. plan refuses that line adopt-not-the-source, and refuses --project sam-wip with naming-role-mismatch. My test folders' names also ended in -wip, so the folder may be what classified it. Lane 3 may want to check whether this is filed.

Not measured, and commands for lane 3's main loop

  • The doctor gate. W1292 reports 509 passed, 3 skipped in 959.09s at this head; I did not run it:
    cd /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1292 && /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1292-scratch/envrun.sh /dev/null /usr/bin/python3 -m pytest -q -p no:cacheprovider -rs --basetemp=/tmp/claude-1000/l3bt/RV1292-doctor tests/test_shape_doctor.py tests/test_doctor_unsearchable_links.py > /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1292-scratch/gate-doctor.log 2>&1
  • Optional. Neither W1292 nor I ran these.
    • The doctor's NEXT rows, with the modules they import from:
      ... pytest ... --basetemp=/tmp/claude-1000/l3bt/RV1292-next tests/test_doctor_next_is_a_command.py tests/test_commit_trailers.py tests/test_doctor_legs_members_next.py tests/test_doctor_validator_rows.py tests/test_shape_doctor.py tests/test_doctor_gitmodules_not_utf8.py
    • The origin-repository rows:
      ... pytest ... --basetemp=/tmp/claude-1000/l3bt/RV1292-originrepo tests/test_adopt_origin_repository.py tests/test_adopt_project_is_the_source.py

Paths

  • Clone: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1292, detached at 9d71536d and clean. Its worktree /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1292-scratch/parent is detached at 5cb1abdb.
  • Scratch: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1292-scratch:
    • scripts in tools/: probe.py, mutate.py, swaps.sh, gates.sh, mt.sh, mc_probe.py;
    • probe outputs in out2/;
    • logs: swaps.log, gates.log, gate-moved.log, gate-patched.log, gate-guards.log;
    • merge trees and hunks in mt/.
  • Basetemps: /tmp/claude-1000/l3bt/RV1292-rows, RV1292-src, RV1292-moved, RV1292-patched and RV1292-guards.
  • I read W1292's probe only to see which sources it used, never as evidence. I ran nothing in W1292's clone or scratch.

Lane 3: the gates RV1292 left run now from lane 3's main loop with RV1292's runner at 9d71536d, one pytest at a time:

  • tests/test_shape_doctor.py with every module that imports its fixtures;
  • the origin-repository rows.

Lane 3 takes S1, S2, the merge line and N1 into the body, word for word, once they pass. Not taken, and why:

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-11-10T17:53:13Z) READY at 9d71536d. Lane 3's main loop ran the gates RV1292 left, with RV1292's runner at this head, one pytest at a time, each exit 0:

  • tests/test_shape_doctor.py with every module that imports its fixtures: 1008 passed, 9 skipped. The skips are 3 Windows-only rows, and 6 where git 2.43 crashes or dies and CI's git answers.
  • tests/test_adopt_origin_repository.py with tests/test_adopt_project_is_the_source.py: 53 passed.

So RV1292's verdict holds: READY AFTER FIXES, with the fixes to the body only. Lane 3 took S1, S2, the merge line and N1 into the body word for word, and added an "Independent review" section. Not taken:

closingIssuesReferences reads [1292], and the body is ASCII above the robot line. #1320 is READY: v1.2.2 is set, and it is marked ready. Lane 3 places it by its READY time, 17:53Z, after #1315 (#473). It lands after #1280, by a restack onto #1280's squash.

@brettheap brettheap added this to the v1.2.2 milestone Nov 10, 2026
@brettheap
brettheap marked this pull request as ready for review November 10, 2026 17:53
@sourcery-ai

sourcery-ai Bot commented Nov 10, 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 4 days and 19 hours by commenting @sourcery-ai review. Upgrade to get a review now.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

1 participant