Skip to content

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

Closed

Description

@brettheap

(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, default main): plan runs checked_value("--tracking-branch", ...) (_checked_plan_inputs, adopt-project.py:1710) and emit writes the value into the plan (:1403); check reads no tracking_branch: at all, and its findings (_entry_findings, :3202) are the leg paths (_leg_findings, :2146), the submodule questions and the values execute writes into a file (_value_findings, :4418); execute runs the same checked_value("tracking_branch", ...) in _checked_plan_values (:3787, called at :4247).
  • adopt_branch (the split branch; no flag, plan writes the constant ADOPT_BRANCH at :1404, a person or an AI may edit it): execute reads it through checked_value("adopt_branch", ...) in the same function (:3785); check never 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:

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:

  1. plan refuses --tracking-branch, right after checked_value (:1710), before any plan is written: exit 2, a code of its own (adopt-bad-tracking-branch, as adopt-bad-leg-path is the flag's code for the leg paths), git's own words from branch_problem, and the helper's remediation. Nothing is written.
  2. check reports a finding for the plan's tracking_branch: (main when the key is absent, as execute reads it) and for its adopt_branch: (the constant when absent), FINDING plan-bad-tracking-branch: <problem> and FINDING plan-bad-adopt-branch: <problem> or one code naming the key (the author's choice), in _entry_findings beside _leg_findings (:3239), so plan ok is no longer said for a plan execute will refuse, and _refuse_what_check_finds (:3846) refuses the same findings.
  3. execute refuses both before any leg exists: in _checked_plan_values (:3785-3788), which runs before _create_leg_remotes (call at :4262), under plan-bad-tracking-branch / plan-bad-adopt-branch, as plan-bad-leg-path is the plan-value code beside them (:3782). Every bad value is listed in one run, not the first.
  4. Reuse scaffold-project.py and family.py: --tracking-branch is asked of git check-ref-format --branch before anything exists, so ~, a..b, x.lock and a/ are refused under --dry-run and no longer die at the first git init -b after gh repo create has made the repositories (#283) #355's check, do not write a second. If scaffold-project.py and family.py: --tracking-branch is asked of git check-ref-format --branch before anything exists, so ~, a..b, x.lock and a/ are refused under --dry-run and no longer die at the first git init -b after gh repo create has made the repositories (#283) #355 lands first, import refuse_bad_branch and branch_problem in the from shape_materialize import ( blocks (adopt-project.py:121, :134, :141); if not, this issue and scaffold-project.py and family.py: --tracking-branch is asked of git check-ref-format --branch before anything exists, so ~, a..b, x.lock and a/ are refused under --dry-run and no longer die at the first git init -b after gh repo create has made the repositories (#283) #355 share the one helper and whichever lands second imports it. The helper lives in scripts/shape_materialize.py, which no project pins (COPIED_FROM_SHAPE holds scripts/repo_shape.py and two naming files), so no project sees upstream-changed.
  5. 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-branch is asked of git check-ref-format --branch before anything exists, so ~, a..b, x.lock and a/ are refused under --dry-run and no longer die at the first git init -b after gh repo create has 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 (compare test_plan_refuses_a_non_canonical_path_and_writes_nothing, :358);
  • (b) check on a plan edited to tracking_branch: "<name>", and on one edited to adopt_branch: "<name>": exit 1, a FINDING naming the key, no plan ok; a plan with neither key still checks ok;
  • (c) _checked_plan_values on Plan(Path("plan.yaml"), {"kind": "adoption-plan", "mode": "in-place", "tracking_branch": name}) and the adopt_branch twin: Refusal with 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-dir on each hand-edited plan exits 2 with the plan-value code, the --local-remote-dir is empty or absent and source_branches(world.source) == ["main"], and the same plan corrected then adopts into that same directory (the leg-remote-exists row);
  • (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 to adopt-project.py includes tests/test_shape_doctor.py (it feeds placement plans through Plan and _leg_findings) and tests/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

  1. added a commit that references this issue on Oct 13, 2026
  2. added this to the v1.1.0 milestone on Oct 14, 2026
  3. brettheap commented on Oct 14, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-1, session 300f8f46-004a-4e72-8587-4f192be2e108@Eagle, 2026-10-14T22:28:57Z, for #420

    Logged in lanes/log/openRepoShape-1.md at 1778a737ce8dd3ab08518769cb5ef6a103f09c74.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#420:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#420"`, kept where the row or its body names `#420`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `420` as a whole token:
       none
    
  4. added a commit that references this issue on Oct 15, 2026
  5. added 3 commits that reference this issue on Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions