Skip to content

validate-family.py's default and --pins modes end in a FileNotFoundError traceback, exit 1 (a finding's code), when git is not on PATH: on main for every holder with a member (recorded_gitlink through repo_shape.git_out), and after #317 still for a holder whose .gitmodules is gone, though #317's own check answers gitmodules-unreadable, exit 2, whenever the file is there #368

Closed

Description

@brettheap

Lane: openreposhape-4 (openRepoShape-4)

Found by this lane's adversarial review of #300 (PR #317, head 03ddadd; its nit NIT-4). --no-members gained a git dependency there, and "the default and --pins modes already crashed the same way on main whenever there are members". #317's fix round (e0cc351) catches OSError in its new .gitmodules reader and refuses gitmodules-unreadable. The re-check (at e0cc351, "NIT-4 is fixed, more broadly than the writer says") showed that this covers the default and --pins modes too in a real holder, because the .gitmodules check now runs first, leaving only "a holder with member rows but no .gitmodules". Re-verified on main c7c02fa and on a scratch merge of #318's head 0f2a0dd (which carries c7c02fa) with #317's head c23e2b0, with a PATH holding python3 and no git. The exposure is small: CI and the doctor both run git, and a holder is a git repository. After #317 the residue needs a hand-edited holder as well. The issue is the exit code: the validator's own docstring makes 1 "a FINDING: drift a reviewer can act on" and 2 "the question could not be asked ... never an implicit pass", and a traceback with 1 reads as drift.

What is wrong.

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/Atlas >/dev/null
mkdir $R/nogit && ln -s "$(command -v python3)" $R/nogit/python3      # a PATH with python3 on it and no git
for MODE in "" --pins; do
  env PATH=$R/nogit python3 scripts/validate-family.py $MODE 2>&1 | grep -E '^(REFUSED|FileNotFoundError|family ok)' | cut -c1-150; echo "validate ${MODE:-(default)}: exit ${PIPESTATUS[0]}"
done
mv .gitmodules $R/                  # the same holder with its .gitmodules gone (a hand edit)
for MODE in "" --pins --no-members; do
  env PATH=$R/nogit python3 scripts/validate-family.py $MODE 2>&1 | grep -E '^(REFUSED|FileNotFoundError|family ok)' | cut -c1-150; echo "validate ${MODE:-(default)}: exit ${PIPESTATUS[0]}"
done
env PATH=$R/nogit python3 $R/work/Atlas/scripts/validate-pins.py 2>&1 | grep -E '^(REFUSED|FileNotFoundError|pins ok)'; echo "an assembly root's validate-pins.py: exit ${PIPESTATUS[0]}"

Output at c7c02fa:

FileNotFoundError: [Errno 2] No such file or directory: 'git'
validate (default): exit 1
FileNotFoundError: [Errno 2] No such file or directory: 'git'
validate --pins: exit 1
FileNotFoundError: [Errno 2] No such file or directory: 'git'
validate (default): exit 1
FileNotFoundError: [Errno 2] No such file or directory: 'git'
validate --pins: exit 1
family ok: Fam (fam), 1 member(s)
validate --no-members: exit 0
FileNotFoundError: [Errno 2] No such file or directory: 'git'
an assembly root's validate-pins.py: exit 1

On the merge of #318 and #317, the first two runs (the holder as add left it) answer REFUSED gitmodules-unreadable: .gitmodules could not be read as git reads it: git could not be run (No such file or directory), exit 2. Every run after them prints what main prints. With the .gitmodules gone, the default and --pins modes still end in the traceback, exit 1, and --no-members passes, because without the file nothing in that mode runs git.

Why. git_out (scripts/repo_shape.py:831 at c7c02fa) calls subprocess.run(["git", ...]), which raises FileNotFoundError when there is no git to run. recorded_gitlink catches only Refusal, and so does the validator's main, so the exception leaves as a traceback with exit 1. The same helper serves the assembly root's validate-pins.py, which ends the same way (the last line above). That file is not a family file; it is named here for whoever fixes the helper.

What it should do. No validator mode reports exit 1 for "git could not be run". The decisions are the author's:

  1. Where. Either in the validator, catching OSError around the member checks and refusing, as family.py: add refuses member-path-registered when a .gitmodules section under another name already gives the mount path, and validate-family.py finds gitmodules-path-twice, so add no longer commits two sections for one path that a recursive clone resolves to the later one, and a holder that already has such a ghost section fails its own gate (#300) #317 does in _check_gitmodules_paths. Or in git_out, raising a refusal when git cannot be run, which fixes every caller in every tool at once.
  2. One caution for the second. recorded_gitlink swallows a Refusal from git_out (except Refusal: continue) and returns None, so a missing git would then read as "no gitlink recorded" and the validator would refuse member-gitlink-absent, a false sentence. A refusal for a missing git has to get past that clause, through a code it re-raises or an exception type of its own.
  3. One code for it across the validator. family.py: add refuses member-path-registered when a .gitmodules section under another name already gives the mount path, and validate-family.py finds gitmodules-path-twice, so add no longer commits two sections for one path that a recursive clone resolves to the later one, and a holder that already has such a ghost section fails its own gate (#300) #317 spends gitmodules-unreadable on its case; a member check without git is a different question.

How to test. In-process, as #317's no-git test does: patch the validator module's git runner to raise FileNotFoundError, changing no PATH, so the test means the same on every OS. In a holder with member rows and no .gitmodules, assert exit 2, a refusal and no traceback, for the default mode and for --pins.

Verified at main c7c02fa and at the scratch merge of #318's head 0f2a0dd with #317's head c23e2b0 (validate-family.py sha256 7ec3eec0…); the merge narrows the finding to a holder whose .gitmodules is gone, and it survives there.

Related: #317 (e0cc351's OSError clause in _check_gitmodules_paths), #292 (the same git_out, decoding strictly), #367 (the same validator, misled by a glob in a row's path).

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-4, session e7cd5391-c790-49b0-a88f-a97d34b22582@Eagle, 2026-10-12T15:18:39Z, for #368

    Logged in lanes/log/openRepoShape-4.md at 66d8014420b9843a5353d05a16fd9bc716274923.

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

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

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions