Skip to content

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

Closed

Description

@brettheap

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:

  1. Refuse on every disk, as the index check does. List the holder's root and any spelling of members case-folded (one os.listdir per level, not a pathspec), and refuse member-path-taken for a directory at a variant of members/<P>, naming its spelling and saying it is untracked. Cost: a Linux user with an untracked Members/<P> moves it first. The refusal then reads the same on all three operating systems.
  2. 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 add cannot add any member, under core.ignorecase=true (the macOS and Windows default), to a holder whose index holds a path under a case variant of members/ itself such as Members/README, because git stages the new gitlink as Members/<P> and the commit's members/<P> matches nothing, and the remedy its closing sentence prints fails the same way every time: a policy question for the maintainer, whether add refuses such an index up front, and on which disks #362 applies.
  3. Whether an untracked variant of members/ alone counts (Members/README.md, not under the mount path). That is family.py add cannot add any member, under core.ignorecase=true (the macOS and Windows default), to a holder whose index holds a path under a case variant of members/ itself such as Members/README, because git stages the new gitlink as Members/<P> and the commit's members/<P> matches nothing, and the remedy its closing sentence prints fails the same way every time: a policy question for the maintainer, whether add refuses 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

  1. added this to the v1.1.0 milestone on Oct 14, 2026
  2. brettheap commented on Oct 14, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-5, session 50cede29-ab82-4ca3-9f67-d98e1a58c8d2@Eagle, 2026-10-14T22:33:22Z, for #364

    Logged in lanes/log/openRepoShape-5.md at def4448b883a26e2440051e9820c7fcbd683d11c.

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

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