Repository navigation
shape-doctor.py's legs and members NEXT after #354 moves a detached HEAD that holds the only copy of a commit, with no reflog note, in three states: an emptied leg mount whose kept gitdir holds the commit, a holder whose own scripts/bootstrap.py is the NEXT (any commit made in the pinned members/<P>), and a commit in a member's own leg that --recursive moves, so after the paste the commit is held by a reflog entry alone #413
Description
Lane: openreposhape-4 (openRepoShape-4)
Found by this lane's adversarial review of #276 (PR #354, head c30bfb2, its N1): "the reflog note is not printed for every stranded commit". PR #354's body carries it as follow-up N1 at its head cb2a684. #354 is open. Verified at cb2a684. Case (b) is the same on main 3ae0109, where the holder's bootstrap is already the members NEXT. Cases (a) and (c) come with #354's git line: on main the NEXT for both is a bootstrap line that fails (exit 1 and exit 2) and moves nothing. Local bare remotes, an empty global git config, GIT_ALLOW_PROTOCOL=file, and a gh that exits 97 (only the machine row calls it). Not claimed: macOS and Windows, which were not run.
What is wrong. #354 makes the legs and members NEXT a script-free git submodule update --init --recursive --checkout -- <paths> (submodule_update_next, shape-doctor.py:1801 at cb2a684). That line moves HEAD, so #354 adds a sentence for the case where moving HEAD loses something: "A commit made in a <leg|member> on no branch is left only in its reflog by this line. To keep one, run first git -C branch NAME". The sentence is printed for each mount in off_pin. check_legs (:1874) and members_verdict (:3518) put a mount there only when it is populated, its HEAD is detached, and its HEAD is not the pin while the gitlink is (:1962-1968). Three states move a detached HEAD that holds the only copy of a commit, and none of them is in off_pin:
U=<openRepoShape checkout> # a clean checkout of PR #354's head cb2a684
# 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
doctor() { python3 $U/shape-doctor.py --root "$1" --json > "$1.json"; }
show() { python3 -c '
import json, sys
r = [r for r in json.load(open(sys.argv[1]))["rows"] if r["id"] == sys.argv[2]][0]
line, _, why = (r["next"] or "").partition(" # ")
print(" " + r["id"] + ":", r["status"], "|", r["reason"])
print(" NEXT:", line); print(" its sentence:", why)
open(sys.argv[1] + ".next", "w").write(line)' "$1" "$2"; }
paste() { (cd $R && sh -c "$(cat $1.json.next)" >/dev/null 2>&1); echo " pasted: exit $?"; }
kept() { echo " commit ${2:0:12}: HEAD now $(git -C "$1" rev-parse --short=12 HEAD), refs that hold it: $(git -C "$1" for-each-ref --contains $2 | wc -l), reflog entries: $(git -C "$1" reflog --format=%H | grep -c $2)"; }
python3 $U/scaffold-project.py --org testorg --project Atlas --elected-by 'Test Human' --elected-on 2026-10-12 \
--local-remote-dir $R/remotes --work-dir $R/work >/dev/null
echo "== (a) a project: a commit made on a leg's detached HEAD, then the mount emptied (its gitdir kept)"
C=$R/a; git clone -q --recurse-submodules $R/remotes/Atlas.git $C
(cd $C/spec && echo x >> README.md && git commit -qam 'work on a detached HEAD'); K=$(git -C $C/spec rev-parse HEAD)
git -C $C submodule deinit -q -f spec
doctor $C; show $C.json legs; paste $C; kept $C/spec $K
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
python3 $U/scripts/family.py add --family-root $R/h/Fam --member testorg/Atlas --local-remote-dir $R/remotes >/dev/null
git -C $R/h/Fam push -q origin HEAD:main
echo "== (b) a holder that has its own scripts/bootstrap.py: a commit made in the pinned (detached) member"
H=$R/b; git clone -q --recurse-submodules $R/fam/Fam.git $H
(cd $H/members/Atlas && echo y > NOTES.md && git add NOTES.md && git commit -qm 'work in the pinned copy'); K=$(git -C $H/members/Atlas rev-parse HEAD)
doctor $H; show $H.json members; paste $H; kept $H/members/Atlas $K
echo "== (c) a holder without one: commits in the member and in the member's own (detached) spec leg"
H=$R/c; git clone -q --recurse-submodules $R/fam/Fam.git $H
git -C $H rm -q scripts/bootstrap.py && git -C $H commit -qm 'no bootstrap'
git -C $H submodule update -q --init --recursive
(cd $H/members/Atlas && echo y > NOTES.md && git add NOTES.md && git commit -qm 'work in the member'); M=$(git -C $H/members/Atlas rev-parse HEAD)
(cd $H/members/Atlas/spec && echo z >> README.md && git commit -qam 'work in its leg'); K=$(git -C $H/members/Atlas/spec rev-parse HEAD)
doctor $H; show $H.json members; paste $H; kept $H/members/Atlas $M; kept $H/members/Atlas/spec $KOutput at cb2a684:
== (a) a project: a commit made on a leg's detached HEAD, then the mount emptied (its gitdir kept)
legs: FINDING | spec: the gitlink is there and the directory is empty -- an unbootstrapped clone
NEXT: git -C $R/a submodule update --init --recursive --checkout -- spec
its sentence: checks each leg out at the commit this repository records for it, and fetches a mount that is empty. Then make bootstrap puts each leg on its tracking branch, and leaves alone one whose branch is already ahead of the pin
pasted: exit 0
commit 037c3d06add1: HEAD now 739634162782, refs that hold it: 0, reflog entries: 1
== (b) a holder that has its own scripts/bootstrap.py: a commit made in the pinned (detached) member
members: FINDING | members/Atlas: checked out at c947e34d5a4a, pinned at 8c60cb1e957f
NEXT: python3 $R/b/scripts/bootstrap.py --root $R/b
its sentence: what `make bootstrap` runs in a holder: it fetches every member at its pin and leaves it detached there
pasted: exit 0
commit c947e34d5a4a: HEAD now 8c60cb1e957f, refs that hold it: 0, reflog entries: 1
== (c) a holder without one: commits in the member and in the member's own (detached) spec leg
members: FINDING | members/Atlas: checked out at 92997d61c9c3, pinned at 8c60cb1e957f
NEXT: git -C $R/c submodule update --init --recursive --checkout -- members/Atlas
its sentence: checks each member out at the commit this repository records for it, and fetches a mount that is empty. A commit made in a member on no branch is left only in its reflog by this line. To keep one, run first git -C $R/c/members/Atlas branch NAME with a name of your choice. This holder has no scripts/bootstrap.py that can run, so this is the plain git line, with no credential rewrite for a private member
pasted: exit 0
commit 92997d61c9c3: HEAD now 8c60cb1e957f, refs that hold it: 0, reflog entries: 1
commit 519bd430d34e: HEAD now 739634162782, refs that hold it: 0, reflog entries: 1
In (a), in (b), and for the second commit in (c), the commit is now held by no ref, only by a reflog entry that git gc eventually expires, and the sentence said nothing about it. The first commit in (c) is the one the sentence does name; the repro does not run its keep line. How each state escapes off_pin:
- (a) The mount is empty, so
check_legsreads no HEAD (populatedis false). But.git/modules/speckeeps its own HEAD, the detached commit, andsubmodule updateresets it to the gitlink. - (b) Members are detached by design, so any commit made in
members/<P>is on no branch. The holder's ownscripts/bootstrap.pyis the NEXT whenever it can run (:3593). Its sentence says only that it "fetches every member at its pin and leaves it detached there".off_pinis computed for this member and not used on this branch. - (c)
--recursivegoes into the member's own legs and checks each out at the gitlink the MEMBER records. The sentence namesmembers/Atlasand its keep line, and says nothing ofmembers/Atlas/spec, whose commit goes the same way.
Why. off_pin answers "is this mount's HEAD detached off its pin, as this row reads it", which is a narrower question than "does pasting this line move a detached HEAD that holds a commit". The line reaches gitdirs whose mount is empty, and nested submodules the row never reads. The bootstrap branch was kept from main (R276-3) and did not get the sentence.
What it should do. Wherever the NEXT moves a detached HEAD that holds a commit no ref holds, the sentence names it and how to keep it. The decisions are the author's:
- (b) Print the sentence on the bootstrap branch too.
off_pinis already there. - (a) Read the kept gitdir's HEAD for an empty mount and compare it with the gitlink. The keep line cannot be
git -C <mount> branch NAMEhere: the mount is empty, so git walks up and would branch the ROOT. It has to name the gitdir (git --git-dir=<root>/.git/modules/<path> branch NAME, say). - (c) Either look into each populated member's own legs before printing the line, or say in general words that
--recursivemoves the member's own legs too, or take--recursiveoff themembersline. That last would cost the holder's bootstrap its parity (fetch_membersrecurses). - Whether this lands before or after shape-doctor.py: fifteen NEXT builders end the command with
# <comment>, which a stock interactive zsh (macOS's default shell) does not read as a comment, so pasting the line hands the comment's words to the command as arguments, runs whatever follows a;, and runs themake bootstrapthelegsandmemberscomments put in backticks, then exits 127 #277 moves the sentence intowhy.
How to test. The three states in tests/test_doctor_legs_members_next.py. In each, the commit is named before the paste, and the named keep line, run first, leaves it on a branch after the paste. That is the existing …keep_line…keeps_a_detached_commit test's shape.
Verified at PR #354's head cb2a684 (shape-doctor.py sha256 82a5ac78…), and case (b) at main 3ae0109 (f1731ff4…).
Related: #276 / PR #354, #277 (the sentence moves to why; in progress in this lane), #414, #415.
Activity
brettheap commentedon Oct 14, 2026
v1.1.0 (triage of 2026-10-14): #414 can ride along (backlog); it is the same
check_legsand members routing andsubmodule_update_next. Brett Heap ruled on 2026-10-14 that case (b) stays P2: AGENTS.md:427 forbids its precondition.brettheap commentedon Oct 14, 2026
CLAIMED — lane openRepoShape-4, session 11c6c0ef-495a-4a05-94cf-811b7b0a1012@Eagle, 2026-10-14T16:52:44Z, for #413
Logged in
lanes/log/openRepoShape-4.mdata33ad5323f7d4e5b22f1cd3d7a90d60c6b87fc7f.The three reads (lane-collision-protocol Rule 1):
shape-doctor.py: a mount whose `.gitmodules` section lost only its `u…
shape-doctor.py: the `members` NEXT is the git line and not the holde…
tests/test_doctor_legs_members_next.py: the docstring of `test_the_li…