Repository navigation
family.py bump --to an annotated tag's object id, which is 40 hex and passes COMMIT_RE, writes the TAG's id into the member's pin.commit while git checkout puts the commit the tag points at in the gitlink, so bump exits 0 and commits a holder whose own validate-family.py reports member-gitlink-mismatch, exit 1, where add --at pins the commit the same tag names #405
Description
Lane: openreposhape-4 (openRepoShape-4)
Found by this lane's adversarial review of #326 part 1 (PR #345, head 875d802), the first item of its "Out of scope (named, not fixed)": "bump --to <a 40-hex annotated TAG object id> exits 0 and commits a row whose commit: is the TAG oid while the gitlink records the peeled commit." #345 landed as 3ae0109 without it, by design: it previews the real run, and "the dry run previews it faithfully". Re-verified on main 3ae0109, with local bare remotes, an empty global git config, GIT_ALLOW_PROTOCOL=file and a gh that exits 97 (never called). Not claimed: macOS and Windows, which were not run. Nothing below depends on the platform.
What is wrong.
cmd_bump(scripts/family.py:2233at 3ae0109) asks only whether--tois 40 hex (COMMIT_RE,:2253). An annotated tag's own object id is 40 hex too.- It then checks the member out at that id (
:2282). Git peels a tag to the commit it points at, so the member's HEAD is that commit, and so is the gitlinkforce_stagerecords from the HEAD. - The row, though, is built from the argument as typed:
tree_digest(member, commit)(:2291), then_bumped_rows(family, row, project_id, commit, digest)(:2292). The tag's id lands inpin.commit. bump --dry-run(family.pybump(andadd,remove) take no--dry-run, though the file's exit-code paragraph says a dry run prints and exits 0 andbump-leg.pyhas one, so the only way to see what a bump would pin is to run it: fetch, check the member out at--toand rewritefamily.yaml#326) resolves--towith^{commit}intoresolved(:2187). It reads the manifest atresolved, then builds its row fromcommit(:2197-2198), so it prints the same split:
U=<openRepoShape checkout> # a clean checkout at 3ae0109
# Runs in bash on Linux (GNU coreutils). Nothing leaves the machine: every repository is a local bare one under $R, git
# reads an empty global config, GIT_ALLOW_PROTOCOL=file makes any accidental https/ssh clone fail, and a `gh` that exits
# 97 stands first on PATH.
R=$(mktemp -d); mkdir $R/bin; printf '#!/bin/sh\nexit 97\n' > $R/bin/gh; chmod +x $R/bin/gh; touch $R/gitconfig
export PATH=$R/bin:$PATH GIT_CONFIG_GLOBAL=$R/gitconfig GIT_CONFIG_NOSYSTEM=1 GIT_ALLOW_PROTOCOL=file \
GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t.invalid
for P in Atlas Nova; do
python3 $U/scaffold-project.py --org testorg --project $P --elected-by 'Test Human' --elected-on 2026-10-12 \
--local-remote-dir $R/remotes --work-dir $R/work >/dev/null
done
python3 $U/scripts/family.py init --org testorg --family Fam --created-by 'Test Human' --created-on 2026-10-12 \
--local-remote-dir $R/fam --work-dir $R/h >/dev/null 2>&1
H=$R/h/Fam; cd $H
python3 $U/scripts/family.py add --family-root $H --member testorg/Atlas --local-remote-dir $R/remotes >/dev/null
# Atlas releases: one commit, an ANNOTATED tag on it, both pushed
(cd $R/work/Atlas && echo moved > NOTES.md && git add NOTES.md && git commit -qm 'moved on' \
&& git tag -a v2 -m 'release 2' && git push -q origin main v2)
TAG=$(git -C $R/work/Atlas rev-parse v2); PEELED=$(git -C $R/work/Atlas rev-parse 'v2^{commit}')
echo "--to $TAG is a $(git -C $R/work/Atlas cat-file -t $TAG) of commit $PEELED"
python3 $U/scripts/family.py bump --family-root $H --member Atlas --to $TAG --dry-run | grep -E '^(commit|gitlink) '
python3 $U/scripts/family.py bump --family-root $H --member Atlas --to $TAG | grep -E 'committed'; echo "bump: exit ${PIPESTATUS[0]}"
echo "row pin.commit: $(grep -A6 'project: Atlas' family.yaml | grep 'commit:' | tr -d ' "' | cut -d: -f2)"
echo "HEAD's gitlink: $(git rev-parse HEAD:members/Atlas)"
python3 scripts/validate-family.py 2>&1 | grep -E '^(FINDING|REFUSED|family ok)' | cut -c1-150; echo "validate-family.py: exit ${PIPESTATUS[0]}"
python3 $U/scripts/family.py bump --family-root $H --member Atlas --to $PEELED | grep -E 'committed'; echo "bump --to the peeled commit: exit ${PIPESTATUS[0]}"
python3 scripts/validate-family.py 2>&1 | grep -E '^(FINDING|REFUSED|family ok)' | cut -c1-150; echo "validate-family.py: exit ${PIPESTATUS[0]}"Output at 3ae0109:
--to 1f17cb89971400096727e55bf50190f51b027379 is a tag of commit 4d4cac88f24035b15d8fe8c170044add893c44e2
commit f5222ce5b704 -> 1f17cb899714
gitlink members/Atlas f5222ce5b704 -> 4d4cac88f240
committed fdbba21ccea6: members/Atlas, family.yaml
bump: exit 0
row pin.commit: 1f17cb89971400096727e55bf50190f51b027379
HEAD's gitlink: 4d4cac88f24035b15d8fe8c170044add893c44e2
FINDING member-gitlink-mismatch: members/Atlas: gitlink 4d4cac88f24035b15d8fe8c170044add893c44e2 != family.yaml pin 1f17cb89971400096727e55bf50190f51b
validate-family.py: exit 1
committed 3eda2c05e571: members/Atlas, family.yaml
bump --to the peeled commit: exit 0
family ok: Fam (fam), 1 member(s)
validate-family.py: exit 0
The commit bump made is the one its pull request carries, and the holder's own gate, which CI runs, is red on it. Nothing in the run says the id was a tag. The dry run's two lines show the split, but only to a reader who compares the two ids. A second bump --to the peeled commit repairs it (the last four lines).
Why. add was made to peel in #236. _probe_commit (:1457) resolves --at with ^{commit}: "an annotated tag's own sha resolves to the commit it tags, which is what checking it out pinned before". tests/test_family_add_refusals.py::test_an_at_that_is_a_tag_of_a_commit_pins_the_commit_it_tags holds it. bump kept the older shape: the argument is the pin, and the checkout is trusted to agree. For a commit's id they agree. For a tag's id the checkout peels and the row does not.
bump-leg.py also writes the tag's id into contracts/<role>-pin.yaml when given one (probed on 3ae0109 with a tag on a spec leg's commit). Its own validate-pins.py then goes red before the commit, so it rolls back and exits 2, bump-leg-validators-red. That is a refusal, but it comes from its gate and does not name the tag. family.py bump has no gate before its commit.
What it should do. A bump given a tag's id either pins the commit the tag names, as add --at does, or refuses the tag by name before anything moves. The decisions are the author's:
- Peel or refuse.
- Peeling matches
add(family.py:addasks every refusal of the holder and of a throwaway clone of the member before it mounts anything, and unwinds and names whatever fails after the mount, so a refused add (a leg, a short or unknown--at, an empty member) no longer leaves a staged gitlink and.gitmodulessection that the next add commits under its own title (#203) #236) and whatgit checkoutalready does with the id. - Refusing matches the remediation
member-to-not-a-commitalready prints ("A tag can be moved and a commit cannot; a pin is a commit or it is nothing"). It asks the person for the commit by name. - Either way, a tag of a tree or a blob needs
add's answer,member-at-unknown.
- Peeling matches
- Where. Resolve
--toonce, in the member after the fetch (rev-parse --verify <to>^{commit}). Use that one id for the checkout, the digest, the row, the commit message and thenothing to dotest, in the real run and in_bump_dry_run, which already computesresolved. bump-leg.py. The same question for its--to, decided once for both tools.
How to test. Build a tag with tests/test_family_add_refusals.py's tagged_remote, then run bump --to <the tag's id>:
- Peel: exit 0,
pin.commitand HEAD's gitlink both the peeled commit, andvalidate-family.pyexit 0. - Refuse: exit 2, with the holder's snapshot unchanged (HEAD,
ls-files -s -z,family.yaml's bytes, the member's HEAD). - Under
--dry-run: thecommitandgitlinklines name one commit, or the dry run refuses as the real run does.
Verified at main 3ae0109 (scripts/family.py sha256 bf059598…).
Related: #326 / PR #345 (the review that named it; bump --dry-run), #236 (add --at peels a tag), #365 (bump's rollback, in progress in this lane, in the same function; independent of this), #407 (the same review's other real-run item, in remove).
Activity
brettheap commentedon Oct 12, 2026
CLAIMED — lane openRepoShape-3, session 43f4ac63-0116-41de-977d-afc23cdf7fdf@Eagle, 2026-10-12T19:28:42Z, for #405
Logged in
lanes/log/openRepoShape-3.mdatddf158af969302dc222c6ee99e2ab02134e89efd.The three reads (lane-collision-protocol Rule 1):
brettheap commentedon Oct 12, 2026
(lane openRepoShape-3, 2026-10-12T19:50:57Z) A v1.0.0 scope notice. Brett Heap scoped openRepoShape's first release today: every procedure in AGENTS.md works end to end for ordinary repositories on Linux, macOS and Windows. The tag waits for every in-scope issue to close and a full test run to pass on all three OSes on the release commit. This issue is #405 (P1, parent #459): in scope, in the
v1.0.0milestone. Nothing changes in how it is worked. Known-limit issues stay open, without the milestone, under their area's parent.tests/test_family_bump_to_a_tag.py: a release tag that reaches the me…
family.py: `bump` refuses a `--to` that names an annotated tag, a tre…
Merge main 965420b into fix/adopt-plan-quote-yaml-keywords (#181)