Skip to content

adopt-project.py execute clones the source with the operator's init.templateDir applied, so a template holding modules/spec and modules/code makes the leg mounts' --force (#200) reactivate those planted stores: both gitlinks pushed on the split branch are a stranger's commit, the VERIFICATION still says adoption verified because it checks a mount is present and not which commit, and the recursive clone of the result exits 128 #243

Closed

Description

@brettheap

Lane: openreposhape-4 (openRepoShape-4)

Found by this lane's #229 fix-round writer while verifying the #229 review's out-of-scope note O2 (review of 5878305, PR #235), re-verified on main 54aa69a today. It is pre-existing on main (the mount's --force came in with #200) and not a regression of any open branch. Not claimed. It is contrived: an operator has to have a git template that carries modules/<leg path>, and no git command puts one there, though a template made by copying a working project's .git would. But when it happens the tool says adoption verified over a branch that cannot be cloned.

What is wrong. _commit_the_split builds the assembly root with git clone -q <source> <assembly> (adopt-project.py:2725). git clone applies the operator's init.templateDir to the new .git, so a template that holds a non-bare store at modules/spec and modules/code leaves .git/modules/spec and .git/modules/code in the "fresh" clone. _mount_the_legs then runs git submodule add --force -q <leg working tree> <path> (:2393-2394). With a store already at .git/modules/<name>, --force takes git's "reactivate the local git directory" branch instead of refusing: the mount uses the planted store and checks out ITS HEAD, not the leg. The recorded URL is rewritten to the canonical remote and git remote set-url origin is run inside the mount (:2395-2398), but the commit the gitlink names is the stranger's. execute pushes the split branch (against GitHub it then opens the pull request on it, :2612), and its VERIFICATION table prints adoption verified: every source path is in exactly one place: it accounts for every source blob and checks that each mount path holds a gitlink (adopt-mount-missing, #173), but never asks which commit the gitlink names. The result cannot be cloned by anyone: git clone --recurse-submodules of the split branch exits 128, because the commit is in no leg.

# $U: a clean checkout at 54aa69a; git-filter-repo on PATH; gh is a PATH shim that logs its argv and exits 97 (never called: its log was never created).
# Every repository is a bare repository under $R/remotes; no network. $S is a small source repository (the adoption tests' `make_source_repo`:
# 27 files, three commits). The operator's global config (GIT_CONFIG_GLOBAL, a file of our own) says `init.templateDir = $R/template`; $R/template/modules/spec
# and $R/template/modules/code are each a copy of a stranger's non-bare `.git` (one empty commit, $PLANTED; a remote `origin`).
python3 $U/adopt-project.py plan --source $S --project Northwind --org testorg --elected-by 'Test Human' --elected-on 2026-09-02 --out $R/adoption-plan.yaml
#   exit 0   (the three review_required entries answered in the plan: examples/ -> spec, .claude/ and release.yaml -> root)
python3 $U/adopt-project.py check --plan $R/adoption-plan.yaml
#   exit 0   plan ok
python3 $U/adopt-project.py execute --plan $R/adoption-plan.yaml --yes --local-remote-dir $R/remotes --work-dir $R/work
#   exit 0   VERIFICATION — every source path at 841099d46b56, by blob sha
#              spec 6 of 27 source paths ... code 10 of 27 ... root 11 of 27 ... drop 0 of 27 ... added 19 new paths
#            adoption verified: every source path is in exactly one place
git -C $S ls-tree adopt/three-repo-shape -- spec code
#   160000 commit $PLANTED	code
#   160000 commit $PLANTED	spec
for r in spec code; do git --git-dir $R/remotes/Northwind-$r.git rev-parse refs/heads/main; done
#   83aec281cf30…  89283a3cdb58…     (the legs' pushed tips; neither is $PLANTED)
git clone --recurse-submodules -b adopt/three-repo-shape $S $C       # a clean config: no template
#   exit 128   fatal: git upload-pack: not our ref $PLANTED
#              fatal: Fetched in submodule path 'code', but it did not contain $PLANTED. Direct fetching of that commit failed.

The control, the same run with no init.templateDir (on 54aa69a): execute exit 0, the split branch's gitlinks are the legs' pushed tips (code 5579520ee872…, spec 0ae419e8820d…), the clean recursive clone exits 0, and in the clone validate-manifest.py prints manifest ok: Northwind (northwind), 3 legs, validate-pins.py prints pins ok and bootstrap.py prints bootstrap ok.

A variant: if the planted store has NO origin remote, execute still gets as far as both legs pushed, then dies at git remote set-url origin inside the mount (exit 2, error: No such remote 'origin'), with the legs pushed and no split commit. That one is loud; the silent one needs a planted store that has an origin, which a copy of a working clone's .git does.

The operator's own clones are no better under that template: git clone --recurse-submodules of the SOUND control branch, run under the same template, also exits 128 (Fetched in submodule path 'code', but it did not contain 5579520ee872…), because git reuses the planted store. That is the template's doing and not this tool's. What is this tool's is that the planted commit is PUSHED, so the failure reaches everyone who clones the branch, and that the verification vouches for it.

Why. adopt-project.py:2725 clones with the operator's configuration, template included, and nothing there or in _mount_the_legs asks whether .git/modules/<name> exists before --force is passed. The comment at :2387-2391 argues that "the two paths are distinct, so the second mount's name has no .git/modules/<name> in this fresh clone for --force to reactivate"; that is true of a clone with no template and is the premise this breaks. In _verify (:2929-2989), _land_the_root_tree (:3041-3095) passes the two mount gitlinks over by PATH (:3083-3085, mounted.add(path) and continue) and reports a missing one (:3089-3094), and _verify already reads each leg's own tree at HEAD (:2960), but no line compares a mount's oid with that leg's tip. check cannot see any of this: it never builds the assembly. This is the scaffold's MUST-1 on the #229 review, where the same template planted the store through git init; PR #235's head (9bb8bd0) cures it with git init --template= (scaffold-project.py:813). Adopt's counterpart is git clone, and the review named it as unverified (its O2). The other git calls adopt makes that create a repository (git init --bare at :2106, the leg's work clone at :2137, the seeded leg's git_init_commit at :2185) take the template too; none of them is a mount target for --force, and this issue did not look further at them.

What it should do. Two remedies; the author decides, and they are not alternatives.

  1. --template= on the root clone (:2725), the cure PR scaffold-project.py: the leg mount passes --force to git submodule add, so a leg path an ignore rule matches (__pycache__, .DS_Store, or the default spec under an operator's own excludes) scaffolds instead of dying at the mount after all three repositories exist and both legs are pushed (#229) #235 applies to the scaffold's git init: an empty template is git's documented way to use none, so the assembly's .git/modules is empty when the mounts run and --force has nothing to reactivate. On a throwaway copy of 54aa69a with "--template=" added to that one command, the same input gives: execute exit 0, gitlinks equal to the legs' pushed tips (code 8ec31da4bbd8…, spec 527ef250538f…), a clean recursive clone that exits 0, and manifest ok, pins ok, bootstrap ok in it. The comment at :2387-2391 should then say why no store can be in the way.
  2. The verification compares each mounted gitlink with the leg's pushed tip, so a wrong commit can never read adoption verified, whatever put it there (this template, GIT_TEMPLATE_DIR, a leftover store, a git that renames or reuses a name differently). On a throwaway copy of 54aa69a with only that comparison added after _land_the_root_tree (each mount's gitlink against git rev-parse HEAD of the leg's working tree, which is what was pushed), the template input gives: execute exit 1, two findings (spec is a gitlink to e56fa5e1c6ac in the root tree, where the spec leg was pushed at 6b759e184754, and code likewise) and no adoption verified. That is a probe and not the fix; the finding's name and wording are the author's. It does not undo the push: the message every finding ends with says "The legs and the branch exist ... NOTHING was deleted from the source", so 2 turns a lie into a refusal and leaves the clean-up to the operator, and only 1 prevents the harm.

How to test. A new test file modelled on tests/test_adopt_mount_ignored.py, with the adoption tests' local-bare fixture (make_source_repo, run_script and resolve in tests/conftest.py), --local-remote-dir, no gh, and GIT_CONFIG_GLOBAL written as excluding() does in tests/test_scaffold_ignored_shape_files.py (the path POSIX-style and quoted): [init] templateDir pointing at a directory that holds modules/spec and modules/code, each a copy of a stranger's .git made in the test, with an origin. (a) check and execute exit 0 and the split branch's git ls-tree gitlinks equal each leg's pushed tip, never the exit code alone; a recursive clone under a clean config succeeds and the clone's validate-pins.py passes. (b) The verification on its own, with no template: a split commit whose mount gitlink names another commit makes _verify return 1 with the new finding and never print adoption verified, built by hand so it does not depend on remedy 1. (c) Removing --template= makes (a) fail; removing the comparison makes (b) fail. (d) The no-origin variant, if it is to stay loud: exit 2 or, with remedy 1, exit 0. (e) Windows: copy the stranger's .git with shutil.copytree and remove the trees with conftest.rmtree, since git writes read-only object files; and no text=True stdin to git.

Related: #200 (#174, the mount's --force), #229 (PR #235, the scaffold's counterpart and the cure), #173 (#165, the verification that checks a mount is present and not which commit)

Activity

  1. brettheap commented on Oct 11, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-2, session 224cb7ce-54c8-4501-bfa8-7a7e398984a0@Eagle, 2026-10-11T22:09:44Z, for #243

    Logged in lanes/log/openRepoShape-2.md at c3a9908740a6c7cd6107ff0b0eeb0027e16e8cfb.

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

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#243:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#243"`, kept where the row or its body names `#243`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `243` as a whole token:
       none
    
  2. added 7 commits that reference this issue on Oct 12, 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

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions