Repository navigation
adopt-project.py: --tracking-branch and the plan's tracking_branch: and adopt_branch: are held only to checked_value's alphabet and never asked of git, so a name git refuses (~, a..b, x.lock, a trailing /) passes plan and check and execute dies at the first git command that takes it, after a leg repository exists (git init --bare -b, git checkout -B, or a push to GitHub after both gh repo create calls), and the corrected plan is then refused leg-remote-exists #420
Description
(lane openRepoShape-2, 2026-10-12T17:23Z)
Found by the writer of lane-2 PR #355 (the fix for #283), which asks git about --tracking-branch in scaffold-project.py and family.py and says in its "Left out" section that adopt-project.py takes the flag the same way and has the same defect, as its own change; widened by the review of lane-2 PR #356, whose follow-up FU1 found the same gap in the plan's adopt_branch:. Reproduced on origin/main 92e29d9 (git 2.43.0, Python 3.12.3), local remotes only, plus the GitHub route with a stub gh that records its calls. Not claimed.
What is wrong. adopt-project.py holds two branch names to checked_value's alphabet (SAFE_ARG_RE, ^[A-Za-z0-9._/@+~-]{1,255}$, scripts/repo_shape.py:94, the check at :1827) and never asks git whether either is a branch name. Two names, one gap:
tracking_branch(the flag--tracking-branch, defaultmain):planrunschecked_value("--tracking-branch", ...)(_checked_plan_inputs,adopt-project.py:1710) andemitwrites the value into the plan (:1403);checkreads notracking_branch:at all, and its findings (_entry_findings,:3202) are the leg paths (_leg_findings,:2146), the submodule questions and the valuesexecutewrites into a file (_value_findings,:4418);executeruns the samechecked_value("tracking_branch", ...)in_checked_plan_values(:3787, called at:4247).adopt_branch(the split branch; no flag,planwrites the constantADOPT_BRANCHat:1404, a person or an AI may edit it):executereads it throughchecked_value("adopt_branch", ...)in the same function (:3785);checknever reads it.
The alphabet is wider than the names git accepts: ~, a..b, x.lock, a/, a//b and .a are all inside it and git refuses each. So plan exits 0 and writes the name, check says plan ok, and execute dies at the first command that gives the name to git, after a leg repository exists:
- the tracking branch, under
--local-remote-dir:git init -q --bare -b <name>(_create_leg_remotes,:3455), exit 2,fatal: invalid initial branch name, withNorthwind-spec.gitleft behind. Against GitHub_create_leg_remotesrunsgh repo createfor both legs first (:3443) and the name first reaches git atgit push <url> HEAD:refs/heads/<name>(_extract_leg,:3505; the seeded leg's:3553) aftergit filter-repohas run: both repositories exist on GitHub when it dies, and theRULESET_HINTabout organisation rulesets is printed afterfatal: invalid refspec. - the split branch:
git checkout -q -B <name>in_extract_leg(:3487), after both leg remotes exist._build_the_legsswallows theCommandFailedon the premise thatrunalready said why (:4008), so the run exits 2 with NOTHING on stderr (adopt-project.pyexecuteexits 2 with nothing on stderr whengit filter-repofails, after both leg remotes exist:_build_the_legsswallowsCommandFailedon the premise thatrunalready reported it, andrunonly raises #171, PR adopt-project.py:executeprints the command and its stderr when a leg cannot be built, so agit filter-repothat fails after both leg remotes exist no longer exits 2 with an empty stderr (#171) #184).
In both cases the corrected plan, run against the same --local-remote-dir, is refused leg-remote-exists (:3450-3452), which has no --force.
# $U: a checkout of openRepoShape at 92e29d9; git-filter-repo on PATH. $SRC: a three-file git repository (src/app.py, docs/spec.md, README.md), one commit. Local; no network.
cp -a "$SRC" "$D/src" # a fresh source per run: `execute` adds a branch to it
python3 "$U/adopt-project.py" plan --source "$D/src" --project Northwind --org testorg --elected-by 'T H' --elected-on 2026-09-02 \
--tracking-branch 'a..b' --out "$D/plan.yaml" # exit 0; plan has `tracking_branch: a..b`
python3 "$U/adopt-project.py" check --plan "$D/plan.yaml" # exit 0, `plan ok`
python3 "$U/adopt-project.py" execute --plan "$D/plan.yaml" --yes --local-remote-dir "$D/remotes" --work-dir "$D/work"
# exit 2
# REFUSED a git or gh command failed. THE EXACT COMMAND WAS:
# git init -q --bare -b a..b <D>/remotes/Northwind-spec.git
# exit 128
# fatal: invalid initial branch name: 'a..b'
ls "$D/remotes" # Northwind-spec.git
# the split branch: a plan written with main, then `adopt_branch: "a..b"` typed in (quoted: `~` alone is YAML null)
sed -i 's|^adopt_branch:.*|adopt_branch: "a..b"|' "$D/plan.yaml"
python3 "$U/adopt-project.py" check --plan "$D/plan.yaml" # exit 0, `plan ok`
python3 "$U/adopt-project.py" execute --plan "$D/plan.yaml" --yes --local-remote-dir "$D/remotes2" --work-dir "$D/work2"
# exit 2, stderr empty; ls "$D/remotes2" -> Northwind-code.git Northwind-spec.git| the value | git check-ref-format --branch |
plan |
check |
execute --local-remote-dir |
left behind |
|---|---|---|---|---|---|
--tracking-branch a..b, ~, x.lock, a/, a//b, .a |
128 each | exit 0 | exit 0, plan ok |
exit 2, fatal: invalid initial branch name |
Northwind-spec.git |
plan's tracking_branch: hand-edited to "a..b", "x.lock", "~" or "a/" |
128 | n/a | exit 0, plan ok |
exit 2, the same fatal |
Northwind-spec.git |
plan's adopt_branch: hand-edited to "a..b", "x.lock", "~" or "a/" |
128 | n/a | exit 0, plan ok |
exit 2, nothing on stderr | Northwind-spec.git and Northwind-code.git |
main and adopt/ok (controls) |
0 | exit 0 | exit 0, plan ok |
exit 0, adoption verified |
both legs, as intended |
the plan corrected to a good name, the same --local-remote-dir |
0 | n/a | n/a | exit 2, REFUSED leg-remote-exists |
what the first run made |
--tracking-branch a..b, no --local-remote-dir, gh a stub that makes a local bare repository per gh repo create |
128 | exit 0 | n/a | exit 2 after two gh repo create calls, at git push https://github.com/testorg/Northwind-spec.git HEAD:refs/heads/a..b: fatal: invalid refspec, then the RULESET_HINT |
both repositories |
Why. checked_value exists to keep an option and a shell word off a command line (its docstring says argument injection is the threat), and it answers that. It was never a question to git, and the alphabet it allows is a superset of the names git creates. The leg paths have the question (leg_path_problems, asked at all three sites since #169); the two branch names have none. PR #355 puts the question into one helper, branch_problem(what, value) and refuse_bad_branch(refusal, what, value), in scripts/shape_materialize.py (on its head, not yet on main), which asks git check-ref-format --branch of NO repository and never copies a rule, and calls it from the scaffold and the family holder only.
What it should do. Ask git, at the three sites, with the one helper, for both names, and refuse nothing git accepts:
planrefuses--tracking-branch, right afterchecked_value(:1710), before any plan is written: exit 2, a code of its own (adopt-bad-tracking-branch, asadopt-bad-leg-pathis the flag's code for the leg paths), git's own words frombranch_problem, and the helper's remediation. Nothing is written.checkreports a finding for the plan'stracking_branch:(mainwhen the key is absent, asexecutereads it) and for itsadopt_branch:(the constant when absent),FINDING plan-bad-tracking-branch: <problem>andFINDING plan-bad-adopt-branch: <problem>or one code naming the key (the author's choice), in_entry_findingsbeside_leg_findings(:3239), soplan okis no longer said for a planexecutewill refuse, and_refuse_what_check_finds(:3846) refuses the same findings.executerefuses both before any leg exists: in_checked_plan_values(:3785-3788), which runs before_create_leg_remotes(call at:4262), underplan-bad-tracking-branch/plan-bad-adopt-branch, asplan-bad-leg-pathis the plan-value code beside them (:3782). Every bad value is listed in one run, not the first.- Reuse scaffold-project.py and family.py:
--tracking-branchis asked ofgit check-ref-format --branchbefore anything exists, so~,a..b,x.lockanda/are refused under--dry-runand no longer die at the firstgit init -baftergh repo createhas made the repositories (#283) #355's check, do not write a second. If scaffold-project.py and family.py:--tracking-branchis asked ofgit check-ref-format --branchbefore anything exists, so~,a..b,x.lockanda/are refused under--dry-runand no longer die at the firstgit init -baftergh repo createhas made the repositories (#283) #355 lands first, importrefuse_bad_branchandbranch_problemin thefrom shape_materialize import (blocks (adopt-project.py:121,:134,:141); if not, this issue and scaffold-project.py and family.py:--tracking-branchis asked ofgit check-ref-format --branchbefore anything exists, so~,a..b,x.lockanda/are refused under--dry-runand no longer die at the firstgit init -baftergh repo createhas made the repositories (#283) #355 share the one helper and whichever lands second imports it. The helper lives inscripts/shape_materialize.py, which no project pins (COPIED_FROM_SHAPEholdsscripts/repo_shape.pyand two naming files), so no project seesupstream-changed. - The question is git's. A name git accepts is accepted (
release/1.2,trunk,lane-3/shape+fix@eagle; scaffold-project.py and family.py:--tracking-branchis asked ofgit check-ref-format --branchbefore anything exists, so~,a..b,x.lockanda/are refused under--dry-runand no longer die at the firstgit init -baftergh repo createhas made the repositories (#283) #355's own list), and git's rules are asked, never copied here.
How to test. A new tests/test_adopt_branch_names.py beside tests/test_adopt_leg_paths.py, which is the precedent for "ONE helper asked in three places" (its docstring, :1-30) and has the helpers to reuse: the adopter fixture (:169) that loads adopt-project.py as a module, world (:180) and edited (:213) for an answered plan and a private copy of it, needs_filter_repo (:43), and make_source_repo, write_plan (its extra= takes ("--tracking-branch", name)), run_script, ADOPT from conftest. Take the refused and accepted names from #355's tests/test_tracking_branch_ref.py (REFUSED, ACCEPTED) once it lands, or from the table above.
- (a)
plan --tracking-branch <name>for each refused name: exit 2,adopt-bad-tracking-branch, git's verdict in stderr, no plan file (comparetest_plan_refuses_a_non_canonical_path_and_writes_nothing,:358); - (b)
checkon a plan edited totracking_branch: "<name>", and on one edited toadopt_branch: "<name>": exit 1, aFINDINGnaming the key, noplan ok; a plan with neither key still checks ok; - (c)
_checked_plan_valuesonPlan(Path("plan.yaml"), {"kind": "adoption-plan", "mode": "in-place", "tracking_branch": name})and theadopt_branchtwin:Refusalwith the plan-value code (compare the leg-path case in the same file); - (d) end to end, with
needs_filter_repo:execute --yes --local-remote-diron each hand-edited plan exits 2 with the plan-value code, the--local-remote-diris empty or absent andsource_branches(world.source) == ["main"], and the same plan corrected then adopts into that same directory (theleg-remote-existsrow); - (e) controls: the accepted names plan and check ok, and one of them adopts end to end.
Reverting any one site fails its own cases. A targeted run of changes toadopt-project.pyincludestests/test_shape_doctor.py(it feeds placement plans throughPlanand_leg_findings) andtests/test_scaffold_leg_path_collisions.py(it guards the module's attributes).
Not in this issue. scaffold-project.py and family.py (#283, PR #355). Names git accepts that are odd for a branch (refs/heads/x, @, FETCH_HEAD): the question is git's, as #355 decides. A leading + is a name git accepts and a forced refspec: adopt pushes HEAD:refs/heads/<name> (:3505, :3553, :4224) and is not affected. Whether tracking_branch is also a YAML scalar the project's reader would read back as another type (true, 007) is #258 and PR #314, asked of the manifest's values, and a name must satisfy both questions. A branch that already exists at or in git's way of the split branch is #320 and PR #382, which reads the SAME adopt_branch value in check and execute: the two changes should share the read.
Related: #283 and PR #355 (the scaffold and the family holder; names this tool as its follow-up); #169 (the leg paths asked at three sites with one helper, the shape this follows); #258 and PR #314 (the YAML-type question on the same value); #320 and PR #382 (the split branch's collision with an existing ref); #171 and PR #184 (a failed leg build exits 2 with nothing on stderr); #324 and PR #373 (RULESET_HINT names main).
Lane: openreposhape-2 (openRepoShape-2)
Activity
docs/release-notes/v1.0.0.md: brought up to main d41cc29, so section …
brettheap commentedon Oct 14, 2026
CLAIMED — lane openRepoShape-1, session 300f8f46-004a-4e72-8587-4f192be2e108@Eagle, 2026-10-14T22:28:57Z, for #420
Logged in
lanes/log/openRepoShape-1.mdat1778a737ce8dd3ab08518769cb5ef6a103f09c74.The three reads (lane-collision-protocol Rule 1):
Merge origin/main 2e6c6fa into fix/520-adopt-check-branch-names
Merge origin/main (de428f5) into fix/530-adopt-nested-gitattributes
Merge origin/main (de428f5) into fix/418-adopt-submodule-url-answers
docs/release-notes/v1.1.0.md: refreshed at main 4cf5fb0 (49 pull requ…