Repository navigation
family.py _checked_mount_path asks the disk for the mount path with an exact os.path.lexists, so an untracked Members/<P> directory is refused member-path-taken on macOS and Windows and let through on Linux, where add mounts members/<P> beside it, and once that directory is committed a clone of the holder on a case-insensitive disk can add no other member #364
Description
Lane: openreposhape-4 (openRepoShape-4)
Named as a follow-up by the coordinator's ruling 1 for #302: the on-disk variant is refused only "if that is the same few lines in the same block; otherwise name it as a follow-up". It was then reported by #318's writer and confirmed by this lane's adversarial review of #302 (PR #318, head 80050a9; its follow-up F3). Re-verified on main c7c02fa, on #318's head 0f2a0dd (which carries c7c02fa), and on a scratch merge of 0f2a0dd with #317's head c23e2b0, on Linux (a case-sensitive disk), with local bare remotes, an empty global git config, GIT_ALLOW_PROTOCOL=file and a gh that exits 97 (never called). Not run on macOS or Windows. That lexists answers true there for a variant directory is how those disks resolve names, stated here from reasoning. The last row simulates such a clone with core.ignorecase=true on Linux.
What is wrong. _checked_mount_path (scripts/family.py:1127 at c7c02fa) asks the disk first, with os.path.lexists(root / path). On a case-sensitive disk that is an exact comparison, so an untracked Members/Atlas is not members/Atlas, and add mounts the member beside it:
U=<openRepoShape checkout> # a clean checkout at c7c02fa
# Runs from a clean checkout of openRepoShape, in bash, on Linux (GNU coreutils). Nothing leaves the machine: members and
# holder live in local bare repositories 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 Zed; 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
add() { python3 $U/scripts/family.py add --family-root $H --member "$1" --local-remote-dir $R/remotes; }
add testorg/Nova >/dev/null
git config core.ignorecase false # this disk is case-sensitive (Linux)
mkdir -p Members/Atlas && echo notes > Members/Atlas/notes.txt # untracked
add testorg/Atlas | grep committed; echo "add: exit ${PIPESTATUS[0]}"
git status --porcelain; ls -d Members/Atlas members/Atlas
git add Members && git commit -qm 'notes' # anybody's next `git add -A` does the same
git ls-tree -r HEAD | grep -i 'members/atlas' | awk '{print $1, $4}'
python3 scripts/validate-family.py | tail -1; echo "validate: exit ${PIPESTATUS[0]}"
git config core.ignorecase true # what a clone of this holder on macOS or Windows has
add testorg/Zed 2>&1 | grep -E '^REFUSED|pathspec'; echo "add Zed: exit ${PIPESTATUS[0]}"Output at c7c02fa (the merge of #318 and #317 prints the same, commit ids aside; #318's head alone gave the same for every step but the last, which was not run there):
committed a3950dc17e6a: .gitmodules, members/Atlas, family.yaml
add: exit 0
?? Members/
Members/Atlas
members/Atlas
100644 Members/Atlas/notes.txt
160000 members/Atlas
family ok: Fam (fam), 2 member(s)
validate: exit 0
REFUSED committing Zed to the family. THE EXACT COMMAND WAS:
error: pathspec ':(literal)members/Zed' did not match any file(s) known to git
add Zed: exit 2
The add itself pushes a clean holder: HEAD holds only members/Atlas. What it leaves is a working tree with two spellings of one path. When anybody's next git add -A commits the variant, the holder carries Members/Atlas/notes.txt beside the gitlink members/Atlas, and its validator still says family ok. On macOS and Windows those two are one directory. With core.ignorecase set, as it is there, every later add fails at its commit: that is #362, and the last row shows it.
Why. #318 made the INDEX question any-case on every disk, "a holder is cloned on macOS and Windows too, where Members/IRRS and members/IRRS are ONE path" (_index_entries's docstring). The disk question was left exact by ruling. So the same holder state is refused on two of CI's three operating systems, where lexists finds the variant, and accepted on the third.
What it should do. The decisions are the author's:
- Refuse on every disk, as the index check does. List the holder's root and any spelling of
memberscase-folded (oneos.listdirper level, not a pathspec), and refusemember-path-takenfor a directory at a variant ofmembers/<P>, naming its spelling and saying it is untracked. Cost: a Linux user with an untrackedMembers/<P>moves it first. The refusal then reads the same on all three operating systems. - Or keep the disk check exact and say so where the index check says the opposite. A pushed holder is unaffected until somebody commits the variant, and from then on family.py
addcannot add any member, undercore.ignorecase=true(the macOS and Windows default), to a holder whose index holds a path under a case variant ofmembers/itself such asMembers/README, because git stages the new gitlink asMembers/<P>and the commit'smembers/<P>matches nothing, and the remedy its closing sentence prints fails the same way every time: a policy question for the maintainer, whetheraddrefuses such an index up front, and on which disks #362 applies. - Whether an untracked variant of
members/alone counts (Members/README.md, not under the mount path). That is family.pyaddcannot add any member, undercore.ignorecase=true(the macOS and Windows default), to a holder whose index holds a path under a case variant ofmembers/itself such asMembers/README, because git stages the new gitlink asMembers/<P>and the commit'smembers/<P>matches nothing, and the remedy its closing sentence prints fails the same way every time: a policy question for the maintainer, whetheraddrefuses such an index up front, and on which disks #362's question asked of the disk.
How to test. Create Members/<P>/f untracked and run add. Under decision 1, assert member-path-taken, an empty stdout (refused before the member is read) and a byte-identical holder. The test can run on every OS: on a case-insensitive disk the same directory already makes lexists true, so it asserts the same refusal there.
Verified at main c7c02fa, at #318's head 0f2a0dd, and at the scratch merge of 0f2a0dd with #317's head c23e2b0 (scripts/family.py sha256 8339041e…); the finding survives the merge.
Related: #302 and #318 (ruling 1, and _index_entries, any case on every disk), #362 (what the committed variant leads to on a case-insensitive clone), #363 (the variant held in HEAD).
Activity
brettheap commentedon Oct 14, 2026
CLAIMED — lane openRepoShape-5, session 50cede29-ab82-4ca3-9f67-d98e1a58c8d2@Eagle, 2026-10-14T22:33:22Z, for #364
Logged in
lanes/log/openRepoShape-5.mdatdef4448b883a26e2440051e9820c7fcbd683d11c.The three reads (lane-collision-protocol Rule 1):
family.py: `add`'s case-variant refusal names a remedy for each entry…
tests/test_family_members_case_variants.py: the layout table gains a …
siblings.py and family.py: `make siblings` asks about the family fold…