Skip to content

validate-pins.py and validate-family.py: a leg's or member's git-directory HEAD that is a FIFO, a socket or a device, or a symlink to one, is refused by name before git is asked, where a FIFO left the gate with no answer at all (#1290) - #1319

Open
brettheap wants to merge 15 commits into
mainfrom
l3-1290-fifo-head-refused

Conversation

@brettheap

@brettheap brettheap commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

What changed

A v1.2.2 fix for #1290, filed by lane 3 on Brett Heap's word "file follow-ups 5, 10 and 15" (2026-11-09) from RV1239's review of #1239 (issuecomment-6385532185) and W1256's "Out of scope" in #1263. It lands after the v1.2.1 tag and after #1263, which it is stacked on, once reviewed and green, in the order lane 3 posts for v1.2.2.

What a person sees. A leg's or a member's git directory whose HEAD is a FIFO made the assembly gate (templates/assembly-root/scripts/validate-pins.py) and the family holder's gate (templates/family-root/scripts/validate-family.py) give no answer at all: #1222's and #1256's guard does not open a FIFO, but git's own open of HEAD, in the pinned commit's git rev-parse, waits for a writer, and git_out has no timeout. Now each gate refuses it by name, exit 2, before git is asked, in the words it already gives a HEAD this user cannot open:

REFUSED pin-submodule-unreadable: .git/modules/spec/HEAD: cannot be read (a FIFO, not a file)
REFUSED member-unreadable: .git/modules/members/IRRS/HEAD: cannot be read (a FIFO, not a file)

with the remedy for a cause that is no permission ("put a regular file this user can open back in its place, or take away one nobody wants"), not chmod.

A socket, a device, or a symlink to a FIFO, a socket or a device, which git does not wait on but cannot read as a HEAD, ended the run in pin-commit-unresolvable or member-commit-unresolvable ("the pinned commit is not in that ... object store"); each is named the same way now, (a socket, not a file), (a symlink to a FIFO, not a file). The assembly root's own .git/HEAD, which the same function asks (#1146), is git-repo-unreadable the same way: a FIFO there gave no answer, and a socket or a symlink to a FIFO there ended pin-gitlink-absent ("this repository does not record it as a submodule"), about a root that records it. A regular HEAD, one at 000, a directory, a symlink that leads nowhere and no HEAD answer as they did.

The two choices #1290 left open, settled by measurement

Where: (a), a by-name refusal in both gates, not (b), a timeout in git_out, nor (c). Measured with the probe below, at #1263's head 1962f749, with scripts/repo_shape.py's git_out changed in the scaffolded root's own copy (the gate's repo_shape is the root's copy):

(b) as written assembly gate, FIFO HEAD family gate, FIFO HEAD
timeout=5 in git_out's subprocess.run, nothing else exit 1, a traceback ending subprocess.TimeoutExpired: Command '['git', 'rev-parse', '--verify', '--quiet', '<commit>^{commit}']' timed out after 5 seconds the same, exit 1
the same, the timeout made a git-failed Refusal exit 2, REFUSED pin-commit-unresolvable: spec: the pinned commit <commit> is not in that submodule's object store (git rev-parse ... timed out after 5 s) exit 2, REFUSED member-commit-unresolvable: members/IRRS: the pinned commit <commit> is not in that member's object store (...)

A link: covered, with every kind that is no file and no directory. The guard asks stat, which follows a link, so the refusal covers a HEAD that stat finds neither a regular file nor a directory: a FIFO, a socket, a device, and a symlink to any of them. Measured at 1962f749:

The parity #1263 holds stands. The family gate's line for each of these HEADs equals the assembly gate's (the parity row below), and the family gate's helper is a copy of the assembly gate's, not an import, as #1221 and #1256 copied theirs.

Measured, before and after

The probe (W1290-scratch/probe_1290.py, a pytest file loaded with -p conftest so it uses the suite's own project and holder fixtures) scaffolds each gate from the tree under test and runs the root's own scripts/validate-pins.py or the holder's scripts/validate-family.py as a subprocess in a session of its own, bounded at 25 s, killing the whole session (git included) at the bound. Git 2.43.0, Python 3.12.3, uid 1000, offline (stub gh, GIT_SSH_COMMAND=false, LANES_LANE unset). Before is #1263's head 1962f749; after is this head. Each cell is the exit and the first line; <commit> and the temporary paths are shortened. Both shapes (a gitfile leg or member, under .git/modules/, and one with its own .git directory) gave the same answer in every row but where noted.

HEAD of the leg's or member's git directory Assembly gate, before Assembly gate, after Family gate, before Family gate, after
a regular file (the control) exit 0, passes exit 0, passes exit 0, passes exit 0, passes
a FIFO no answer in 25 s exit 2, pin-submodule-unreadable: .git/modules/spec/HEAD: cannot be read (a FIFO, not a file) (own .git: names <path>/.git/HEAD) no answer in 25 s exit 2, member-unreadable: .git/modules/members/IRRS/HEAD: cannot be read (a FIFO, not a file) (own .git: names <path>/.git/HEAD)
a symlink to a FIFO exit 2, pin-commit-unresolvable exit 2, pin-submodule-unreadable: .git/modules/spec/HEAD: cannot be read (a symlink to a FIFO, not a file) (own .git: names <path>/.git/HEAD) exit 2, member-commit-unresolvable exit 2, member-unreadable: .git/modules/members/IRRS/HEAD: cannot be read (a symlink to a FIFO, not a file) (own .git: names <path>/.git/HEAD)
a socket exit 2, pin-commit-unresolvable exit 2, pin-submodule-unreadable: .git/modules/spec/HEAD: cannot be read (a socket, not a file) (own .git: names <path>/.git/HEAD) exit 2, member-commit-unresolvable exit 2, member-unreadable: .git/modules/members/IRRS/HEAD: cannot be read (a socket, not a file) (own .git: names <path>/.git/HEAD)
a symlink to a device (/dev/null) exit 2, pin-commit-unresolvable exit 2, pin-submodule-unreadable: .git/modules/spec/HEAD: cannot be read (a symlink to a device, not a file) (own .git: names <path>/.git/HEAD) exit 2, member-commit-unresolvable exit 2, member-unreadable: .git/modules/members/IRRS/HEAD: cannot be read (a symlink to a device, not a file) (own .git: names <path>/.git/HEAD)
a symlink to a regular file exit 2, pin-commit-unresolvable exit 2, pin-commit-unresolvable exit 2, member-commit-unresolvable exit 2, member-commit-unresolvable
a directory exit 2, pin-commit-unresolvable exit 2, pin-commit-unresolvable exit 2, member-commit-unresolvable exit 2, member-commit-unresolvable
a symlink that leads nowhere exit 2, pin-commit-unresolvable exit 2, pin-commit-unresolvable exit 2, member-commit-unresolvable exit 2, member-commit-unresolvable
(not HEAD) the git directory's config a FIFO, gitfile shape no answer in 25 s no answer in 25 s no answer in 25 s no answer in 25 s
(not a leg's) the root's or holder's own .git/HEAD a FIFO no answer in 25 s exit 2, git-repo-unreadable: .git/HEAD: cannot be read (a FIFO, not a file) no answer in 25 s no answer in 25 s
(not a leg's) the root's own .git/HEAD a socket, or a symlink to a FIFO exit 2, pin-gitlink-absent exit 2, git-repo-unreadable: .git/HEAD: cannot be read (a socket, not a file) or (a symlink to a FIFO, not a file) exit 2, gitlinks-unreadable exit 2, gitlinks-unreadable (the holder's own HEAD, #1172's)

Where the before answer is *-commit-unresolvable, git said fatal: not a git repository: .../.git/modules/spec for a gitfile leg, and for a leg with its own .git directory it walked up to the root's repository (git rev-parse --verify --quiet HEAD in the leg exited 0 there) and rev-parse of the pinned commit exited 1 with nothing. The two (b) rows are in the table under "The two choices".

Byte for byte. Of the probe's 54 lines, with the temporary paths and the per-scaffold commit masked, 19 changed: the four kinds above in both shapes and both gates (16), the assembly root's own FIFO HEAD (1), and the two emulated (b) runs, which now never reach git (2). The other 35 are identical before and after: every control state, the FIFO config, the holder's own FIFO HEAD, and the bare git rev-parse lines. RV1290 re-measured it with 66 states, comparing the exit and the whole stdout and stderr with commits and temporary paths masked: 27 changed and 39 are identical, every change one named above.

No git was left waiting: each probe and row that timed out killed its session, and ps showed no git rev-parse of this run after its bound.

Rows

Both gates' rows are in the modules that already hold that area's rows, beside #1222's and #1256's own HEAD rows: tests/test_validate_pins_spelled_and_named.py (the leg gate's "#1222: a leg git directory's HEAD" section) and tests/test_validate_family_unreadable.py (#1256's section and its parity rows). The family module imports bounded, NOT_HEADS and FIFO_HEADS from the leg gate's module (functions and parameters, no fixture), so both run in one pytest.

Mutants

Each applied to the committed file, the 28 rows run, the file restored from HEAD and checked with cmp (all ok, tree clean after).

Mutant Result
M1 validate-pins.py: the new _a_head_git_cannot_read(head) call dropped killed: 21 failed, 7 passed (every new leg row but the directory control, the root row, and every parity row; the family gate's own rows pass)
M2 validate-family.py: the same call dropped killed: 12 failed, 16 passed (every member bounded row and every parity row)
M3 validate-pins.py: a directory HEAD refused too (S_ISREG alone) killed: 3 failed, 25 passed (the three directory controls)
M4 validate-family.py: a symlink not named as one (link = "") killed: 6 failed, 22 passed (the symlink rows, bounded and parity)

Gates

One pytest at a time, -rs, with a stub gh first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null, git-filter-repo 2.47.0 on PATH and LANES_LANE unset, on the committed head be62b704 with a clean tree:

  • The rows, the parent's changed module, and every module they take a fixture from, in ONE run: tests/test_validate_pins_spelled_and_named.py tests/test_validate_family_unreadable.py tests/test_validate_pins_without_git.py tests/test_family.py tests/test_family_add_refusals.py tests/test_family_head_lost_gitmodules.py: 535 passed in 429.85s (0:07:09).
  • Every module that greps a code this change can print (pin-submodule-unreadable, member-unreadable, git-repo-unreadable), with their fixture sources: tests/test_validate_pins_named_and_spelled.py tests/test_validate_pins_without_git.py tests/test_family_gitmodules_unreadable.py tests/test_family_hand_mount_git_rm_lines.py tests/test_family_py_unreadable.py tests/test_family_siblings_bootstrap_unreadable.py tests/test_family_siblings_placement.py tests/test_validate_family_git_not_runnable.py tests/test_family.py tests/test_family_add_refusals.py tests/test_family_head_lost_gitmodules.py tests/test_pinned_root_git_unreadable.py tests/test_root_git_unreadable.py: 1113 passed in 772.84s (0:12:52).
  • The guards, and the rows that read a template's text: tests/test_repo_hygiene.py tests/test_cli_reference.py tests/test_agents_md_codes.py tests/test_git_output_decoding.py tests/test_stdout_encoding.py tests/test_path_guard_hygiene.py tests/test_no_auto_maintenance.py tests/test_update_add_codes_documented.py tests/test_pinned_copy_text.py tests/test_link_source_chain_defaults.py tests/test_template_text_placeholders.py: 725 passed, 3 skipped in 226.98s (0:03:46). The skips are the host's: test_stdout_encoding.py:292 ("the default this is about is Windows' own") and test_no_auto_maintenance.py:222 x2 ("the session began with GIT_CONFIG_GLOBAL set", which the lane's gate environment sets to /dev/null).

tests/test_shape_doctor.py and tests/test_scaffold_leg_path_collisions.py were not run: adopt-project.py is not changed. The two root modules take fixtures from tests/test_shape_doctor.py and ran without it for that reason; neither changes a fixture.

Merges

git merge-tree --write-tree, with main at f39c6707 (and at dc9a0bcc, #1225) and every open PR that touches these four files (gh pr list --state open): #1263 itself and #1310 (head daaa5ad8, which holds 6b9a4ece; validate-family.py).

Merge This head be62b704 #1263's head 1962f749 alone
with main f39c6707 clean, tree 9265d65d clean, tree 6d487f1c
with main dc9a0bcc clean, tree 1d3f8d0a clean, tree 1b0353b1
with #1310 daaa5ad8 clean, tree 49b19ec5 clean, tree e43b74a6
with #1310 6b9a4ece clean, tree 954a17f3 clean, tree ce4391b5

This head merged with main f39c6707, then with #1310 daaa5ad8: clean, tree 112620e7. No conflict, the parent's or this PR's. (RV1290's measure.)

Stacked

Stacked on #1263 (head 1962f7498171f6cf481bbc169cb915c42700ef05). Its first commits are #1263's; this PR's own change is 1962f749..be62b704. Review that range. It lands after #1263, by a restack onto its squash.

Out of scope

Independent review

RV1290, lane 3's independent opus review (issuecomment-6774950982), reads READY AFTER FIXES at be62b704. The fixes are to the body only, and it found no MUST. It judged all three choices right:

  • (a), a refusal by name in both gates, not a timeout in git_out;
  • refusing every kind stat finds neither a file nor a directory;
  • covering the root's own HEAD, with the family gate's copy held by a parity row.

Its own probe of 66 states found every change to be what #1290 asks. Its mutants M5 to M8 were all killed. Its gates passed:

  • the rows' modules: 535 passed;
  • the guards: 725 passed, 3 skipped;
  • the code-grepping modules with tests/test_shape_doctor.py: 1559 passed, 3 skipped.

Taken here: SHOULD 1 (the merges, at main f39c6707), SHOULD 2 (the root's socket and symlink-to-FIFO change) and NIT 4.

Not taken:

  • NIT 1 to NIT 3: test and docstring text that would move the reviewed head. They are a small follow-up candidate for after this PR lands.
  • NIT 5: the two hangs that remain, a FIFO config and the holder's own FIFO HEAD. A follow-up candidate for Brett Heap.

Closes #1290

Lane: openreposhape-3 (openRepoShape-3)

🤖 Generated with Claude Code

Summary by Sourcery

Reject non-file, non-directory Git HEAD objects by name before Git runs so validation exits with actionable refusals instead of hanging.

New Features:

  • Refuse leg and member Git directories whose HEAD is a FIFO, socket, device, or symlink to one before invoking Git, with clear diagnostic names and remediation guidance.

Bug Fixes:

  • Prevent validation gates from hanging when Git attempts to read a FIFO HEAD.
  • Apply equivalent unreadable-HEAD handling to the assembly root's repository and preserve parity between assembly and family validation gates.

Enhancements:

  • Leave existing handling unchanged for regular files, directories, and other previously supported HEAD states while distinguishing special filesystem objects in refusal messages.

Tests:

  • Add bounded subprocess, in-process guard, parity, control, and mutation coverage for special HEAD filesystem objects across both validation gates.

Brett Heap and others added 15 commits October 23, 2026 21:40
… under `.git/modules/<leg>` or in a `.git` directory, is refused `pin-submodule-unreadable` by name, where it ended `pin-commit-unresolvable` (#1222)

`_refuse_an_unreadable_git_dir` asked the `HEAD` of a leg's git directory
only by `stat`, which a `HEAD` at mode 000 passes, so git, which could not
read it, ended the run in `pin-commit-unresolvable` under THE LOCKSTEP
RULE. A `HEAD` that is there is now opened as `_gitfile_opens` opens a
gitfile: only where it is a regular file, and an open that finds nothing
is a git directory with no `HEAD`, left to git. One this user cannot open
is named, `.git/modules/spec/HEAD` or `spec/.git/HEAD`, with the leg
checkout's remedy.

The gate half of #1222; the doctor half waits for #1200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… or whose git directory (its own `.git`, or the one its gitfile names) this user cannot search, is refused `member-unreadable` naming that part, and no longer `member-commit-unresolvable`, which said the pinned commit was missing and prescribed a fetch (#1221)

`_check_member_tree` only `stat`ed the member's `.git` before running
`git rev-parse` in it. A gitfile at mode 000 stats; a `.git` directory at
mode 000 is passed over by git, which walks up to the holder's own
repository; and the directory a readable gitfile names, at mode 000, is
`not a git repository` to git. Each ended `member-commit-unresolvable`.

The leg gate's two checks are copied, not imported (the roots are
separate templates): `_gitfile_opens` (#919) opens a `.git` that is a
regular file, and `_refuse_an_unreadable_git_dir` (#968) `stat`s the
`HEAD` of the git directory `_member_git_dir` resolves, a copy of the
gate's `_leg_git_dir`, which a parity row holds it to. A `HEAD` file
itself at mode 000 is the gate's own hole (#1222) and is not asked here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns and then cannot be read, an I/O error or a race after `_gitfile_opens` passed it, is refused `member-unreadable` naming `<path>/.git`, the fallback name `_refuse_an_unreadable_git_dir` falls back on, which no row asked (#1221)

rv1246's mutant R3 changed that fallback to `f"{path}"` and all 83 rows still
passed. One in-process row refuses `Path.read_bytes` for the member's `.git`
with EIO after the open succeeds and expects `members/IRRS/.git: cannot be
read (Input/output error)` with the other-reason cure. With R3 applied the row
fails; on main's bytes of validate-family.py it fails too. No code in
validate-family.py changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…56-member-head-unreadable

#1256 builds on #1246's member helpers, which have not landed yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…6-member-head-unreadable

#1256 mirrors #1239's open of a git directory's HEAD, which has not landed yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…annot open, under `.git/modules/<path>` or in a `.git` directory, is refused `member-unreadable` by name, where it ended `member-commit-unresolvable` with a fetch (#1256)

A `HEAD` at mode 000 stats, so #1221's `stat` of it passed, and git,
which could not read it, ended the run in `member-commit-unresolvable`,
"the pinned commit ... is not in that member's object store", with a
fetch that cannot help. `_refuse_an_unreadable_git_dir` now opens a
`HEAD` that is there through `_gitfile_opens`, as validate-pins.py opens
a leg's since #1222 (#1239): only a regular file is opened, an open that
finds nothing is left to git, and a refused open is `member-unreadable`
naming `.git/modules/<path>/HEAD`, `<path>/.git/HEAD`, or the whole path
of a linked worktree's `HEAD` outside the holder, with the member
checkout's remedy. A git directory with no `HEAD` stays git's.

The copied helpers stay word for word with validate-pins.py's:
`_gitfile_opens` and `_member_git_dir` (`_leg_git_dir`) compare equal by
`ast.unparse` with docstrings dropped.

Rows in tests/test_validate_family_unreadable.py: mode rows for a gitfile
member, a `.git`-directory member and a linked worktree (skipped under
root and on Windows), in-process stand-ins for each cause, a race that
finds no `HEAD`, a directory or FIFO `HEAD` never opened, a NUL name left
to git, and a parity row holding the line to the gate's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l3-1256-member-head-unreadable (restack)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… l3-1256-member-head-unreadable (restack)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tack; ours for templates/family-root/scripts/validate-family.py and tests/test_validate_family_unreadable.py, whose squash equals #1246's final head)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ctory `HEAD` that is a FIFO, a socket or a device, or a symlink to one, is refused by name before git is asked, where a FIFO left the gate with no answer at all (#1290)

#1222's and #1256's guard opens a `HEAD` only where it is a regular file,
since the open of a FIFO waits for a writer; but git's own open of it, in
the pinned commit's `git rev-parse`, waited, and `git_out` has no timeout,
so the assembly gate and the family holder's gate gave no answer (RV1239
on #1239, W1256 on #1263). A `HEAD` that `stat` finds neither a file nor
a directory is now `pin-submodule-unreadable` or `member-unreadable`,
`<HEAD>: cannot be read (a FIFO, not a file)`, with the remedy for a cause
that is no permission. A socket, or a symlink to a FIFO or a device,
which git does not wait on but cannot read as a `HEAD`, ended the run in
`*-commit-unresolvable` and is named the same way. The assembly root's own
`.git/HEAD`, asked by the same function (#1146), is `git-repo-unreadable`.

Choice (a) of #1290, not a timeout in `git_out`: measured, a timed-out
call is a traceback (exit 1) or, made a `git-failed` refusal,
`*-commit-unresolvable` again, and `scripts/repo_shape.py` is a shape copy
a root may hold older than its gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Nov 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Read the two gates’ matching HEAD checks together: they now refuse a git-directory HEAD that resolves to neither a regular file nor a directory, naming the file type before Git can hang or report the pinned commit missing.

  • templates/assembly-root/scripts/validate-pins.py L473-474, L484, L489-529
  • templates/family-root/scripts/validate-family.py L707-719, L730-738, L743-791 (+3 small edits)

Tests

Review the assembly-gate cases together: bounded FIFO runs, unopened special-file checks, the root repository’s HEAD, and controls ensuring directories and other unaffected states retain their prior behavior.

  • tests/test_validate_pins_spelled_and_named.py L54-63, L993-1000, L1005-1129 (+2 small edits)

Review the family-gate cases and parity checks together: they cover member git-directory shapes, bounded FIFO runs, refusal naming, and agreement with the assembly gate.

  • tests/test_validate_family_unreadable.py L46-62, L972-1092, L1165-1273 (+1 small edits)

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-11-10T17:54:45Z) RV1290: READY AFTER FIXES at be62b70

I reviewed this PR's own range, 1962f749..be62b704, as lane 3's independent reviewer. I did not write any of #1319. The code and the rows are ready as they stand, and I found no MUST. The two SHOULDs are fixes to the body only. The NITs are optional.

The three choices

1. WHERE: (a) is right, and it settles what #1290's "To settle" asks.

2. WHICH KINDS: refusing every kind that stat finds neither a file nor a directory is right, and so are the words.

3. BEYOND THE ISSUE: covering the root's own HEAD is in scope and right, and so is the family copy with a parity row.

Findings

MUST: none.

SHOULD 1 (body): the Merges section is stale. Main has moved twice since W1290 measured, to dc9a0bcc (#1225) and then f39c6707 (#1316). #1310's head is now daaa5ad8, which contains 6b9a4ece. Replace the section's first line and table with:

git merge-tree --write-tree, with main at f39c6707 (and at dc9a0bcc, #1225) and every open PR that touches these four files (gh pr list --state open): #1263 itself and #1310 (head daaa5ad8, which holds 6b9a4ece; validate-family.py).
>

Merge This head be62b704 #1263's head 1962f749 alone
with main f39c6707 clean, tree 9265d65d clean, tree 6d487f1c
with main dc9a0bcc clean, tree 1d3f8d0a clean, tree 1b0353b1
with #1310 daaa5ad8 clean, tree 49b19ec5 clean, tree e43b74a6
with #1310 6b9a4ece clean, tree 954a17f3 clean, tree ce4391b5

>

This head merged with main f39c6707, then with #1310 daaa5ad8: clean, tree 112620e7. No conflict, the parent's or this PR's.

SHOULD 2 (body): the root's socket and symlink-to-FIFO change is not stated.

  • In "What a person sees", replace
    The assembly root's own .git/HEAD, which the same function asks (#1146), is git-repo-unreadable the same way.
    with
    The assembly root's own .git/HEAD, which the same function asks (#1146), is git-repo-unreadablethe same way: a FIFO there gave no answer, and a socket or a symlink to a FIFO there endedpin-gitlink-absent ("this repository does not record it as a submodule"), about a root that records it.
  • Add this row to the before/after table, after the root's FIFO row:
    | (not a leg's) the root's own .git/HEADa socket, or a symlink to a FIFO | exit 2,pin-gitlink-absent| exit 2,git-repo-unreadable: .git/HEAD: cannot be read (a socket, not a file)or(a symlink to a FIFO, not a file)| exit 2,gitlinks-unreadable| exit 2,gitlinks-unreadable(the holder's ownHEAD, #1172's) |

NIT 1 (test text): the comment says git waits on a symlink to a FIFO, which it does not (measured). At tests/test_validate_pins_spelled_and_named.py:1060, replace #: The two of them git waits on. with #: The two of them that are a FIFO or lead to one, each run through the gate in a bound: git waits on the FIFO itself (#1290), not on a symlink to it.

NIT 2 (test text): the bounded rows' docstrings say the same thing.

  • At tests/test_validate_pins_spelled_and_named.py:1080-1082, replace #1222's guard does not open it, but git rev-parse did, and waited for a writer: the gate gave no answer. It is with #1222's guard opens neither. Over the FIFO, git rev-parsewaited for a writer and the gate gave no answer; over the symlink, git did not wait, and the run endedpin-commit-unresolvable. Each is
  • At tests/test_validate_family_unreadable.py:1046-1048, make the same change, ending ... the gate gave no answer in 60 s; over the symlink, git did not wait, and the run ended member-commit-unresolvable. Each is

NIT 3 (code docstring): validate-family.py's _a_head_git_cannot_read ends "Its stat, then its own lstat, as that gate's _what_stands asks." But _what_stands asks lstat first. The answers are the same for a HEAD the guard has just found. Suggested text: Its statnames the kind, and itslstatwhether it is a symlink, which that gate's_what_stands asks the other way round.

NIT 4 (body, optional): "Of the probe's 54 lines" leaves out the two timeout=5-only (b) runs, which W1290 measured in a separate run. The claims still hold. If lane 3 wants a sentence for "Byte for byte": RV1290 re-measured it with 66 states, comparing the exit and the whole stdout and stderr with commits and temporary paths masked: 27 changed and 39 are identical, every change one named above.

NIT 5 (follow-up, lane 3's call): two hangs remain, as the body says. A FIFO config in a leg's or member's git directory still gives no answer in either gate. The holder's own .git/HEAD as a FIFO does too (#1172's area). Only (c) would bound them. A follow-up issue could be filed; it is not this PR's.

Measured

Git 2.43.0, Python 3.12.3, uid 1000. Every run used a stub gh, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null, git-filter-repo 2.47.0 and LANES_LANE unset.

Before and after, with my own probe.

  • My probe has 66 states. Each gate was scaffolded from the tree under test and run as a subprocess in a session of its own, bounded at 15 s, with the whole session killed at the bound.
  • I compared the exit code and the whole stdout and stderr, with SHAs, short hashes and temp paths masked. Result: 27 changed, 39 identical. After every run, ps showed 0 git rev-parse --verify --quiet processes left.

Leg's or member's git-directory HEAD, in both shapes and both gates:

State Before (1962f749) After (be62b704)
a FIFO no answer in 15 s exit 2, pin-submodule-unreadable / member-unreadable: <HEAD>: cannot be read (a FIFO, not a file)
a socket exit 2, *-commit-unresolvable exit 2, by name, (a socket, not a file)
a symlink to a FIFO exit 2, *-commit-unresolvable exit 2, by name, (a symlink to a FIFO, not a file)
a symlink to a socket exit 2, *-commit-unresolvable exit 2, by name, (a symlink to a socket, not a file)
a symlink to a device (/dev/null) exit 2, *-commit-unresolvable exit 2, by name, (a symlink to a device, not a file)
regular, at 000, absent, a directory, a symlink to a file, a symlink to a directory, a dangling symlink, a symlink loop as before byte-identical
  • What the after line names: .git/modules/spec/HEAD or spec/.git/HEAD for a leg, and .git/modules/members/IRRS/HEAD or members/IRRS/.git/HEAD for a member.
  • The whole stderr after: the refusal line plus the remedy for a cause that is not a permission. There is no traceback and no chmod.

Other states:

State Before After
assembly root's .git/HEAD a FIFO no answer git-repo-unreadable: .git/HEAD: cannot be read (a FIFO, not a file)
assembly root's .git/HEAD a socket pin-gitlink-absent git-repo-unreadable by name
assembly root's .git/HEAD a symlink to a FIFO pin-gitlink-absent git-repo-unreadable by name
assembly root's .git/HEAD at 000 git-repo-unreadable (Permission denied) identical
holder's own .git/HEAD a FIFO no answer no answer (identical)
holder's own .git/HEAD a socket, a symlink to a FIFO, or at 000 gitlinks-unreadable identical
FIFO config, both gates no answer no answer (identical)
(b) timeout only, both gates exit 1, traceback exit 2, by name; git is never asked
(b) timeout as a refusal, both gates exit 2, *-commit-unresolvable (... timed out after 5 s) exit 2, by name; git is never asked

Every changed output is what #1290 asks. Each cell of the PR body's own table matches my measurement.

Rows. I selected the PR's 28 new and changed rows with -k 'fifo_head_is_refused or head_git_cannot_read or head_that_is_a_directory_is_left or root_s_own_fifo_head'.

  • At the head: 28 passed, 212 deselected in 13.37s. The body says 28 passed, 212 deselected.
  • With the parent's two gate files swapped in: 25 failed, 3 passed, 212 deselected in 164.45s. The body says 25 failed, 3 passed.
    • The 3 rows that pass are the three directory controls.
    • Five rows failed with "gave no answer in 30 s": 3 for validate-pins.py and 2 for validate-family.py.
    • I restored both files from HEAD and checked them with cmp: ok, tree clean.

My mutants, M5 to M8. Each was applied with an exact-once text replacement, then the file was restored and checked with cmp (ok). All four were killed.

Mutant What it changes Result Rows that failed
M5 validate-family.py refuses a directory HEAD (S_ISDIR dropped) killed, 2 failed both test_a_member_head_that_is_a_directory_is_left_to_git
M6 validate-family.py calls a socket "a FIFO" killed, 2 failed the two parity socket-* rows
M7 validate-pins.py lets every symlinked HEAD through (if head.is_symlink(): return) killed, 10 failed 2 bounded link-to-FIFO rows, 4 in-process link rows, 4 parity link rows
M8 validate-family.py raises a PermissionError, which gives the chmod remedy killed, 4 failed the 4 family bounded rows

Gates. One pytest at a time, -rs, at the committed head with a clean tree:

  • The rows' modules and their fixture sources: tests/test_validate_pins_spelled_and_named.py tests/test_validate_family_unreadable.py tests/test_validate_pins_without_git.py tests/test_family.py tests/test_family_add_refusals.py tests/test_family_head_lost_gitmodules.py: 535 passed in 335.76s.
  • The guards: W1290's eleven modules: 725 passed, 3 skipped in 174.15s. The skips are the host's: test_stdout_encoding.py:292, and test_no_auto_maintenance.py:222 twice.
  • The code-grepping modules and their fixture sources, plus tests/test_shape_doctor.py: 1559 passed, 3 skipped in 1357.55s. The skips are test_shape_doctor.py:10516, which runs only on a Windows host.
  • Does leaving out tests/test_shape_doctor.py matter? Not for this PR.

Body.

  • It says what a person sees change, the choices and the restack plan.
  • It is ASCII except the robot line. The bot's section is ASCII too.
  • Closes #1290 is its only closing keyword, the bot's section included, and closingIssuesReferences reads [1290].
  • The numbers I re-measured match: 28 passed, 25 failed and 3 passed, every probe cell, and the (b) results. The merges with 6b9a4ece give W1290's trees, 954a17f3 and ce4391b5. Only the main merges are stale (SHOULD 1).

CI at 17:52Z: SonarCloud, package and tests-git-bash pass. tests is still pending.

Not measured, and paths

  • I did not re-run W1290's M1 to M4. The parent-gates swap above stands in for M1 and M2: it puts the hang back, and the rows fail on it.
  • Windows and macOS were not measured. CI is Linux-only, and the rows skip on Windows (POSIX_NODES).
  • All gates were run, so none is left for lane 3. Main may move again before landing.
  • Clone: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1290, detached at be62b704, tree clean.
  • Scratch: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1290-scratch. It holds:
    • the probe and its results: probe_rv1290.py, probe-parent.jsonl, probe-head.jsonl, compare.txt;
    • the rows and mutant logs: rows-*.log, mutate.py;
    • the gate logs: gates.log, gate-*.log;
    • CHECKPOINT.md;
    • a read-only worktree of the parent at parent/.
  • Basetemps are under /tmp/claude-1000/l3bt/RV1290-*.

Lane 3: RV1290 ran every gate itself, so none is left. Lane 3 takes SHOULD 1, SHOULD 2 and NIT 4 into the body, word for word. Not taken, and why:

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-11-10T17:55:45Z) READY at be62b704. RV1290 ran every gate itself (issuecomment-6774950982), so its verdict stands: READY AFTER FIXES, with the fixes to the body only. Lane 3 took them word for word:

  • SHOULD 1: the merges, at main f39c6707;
  • SHOULD 2: the root's socket and symlink-to-FIFO change, in "What changed" and as a table row;
  • NIT 4.

It added an "Independent review" section too. Not taken:

  • NIT 1 to NIT 3: test and docstring text that would move the reviewed head. They are a small follow-up candidate.
  • NIT 5: a follow-up candidate for Brett Heap.

closingIssuesReferences reads [1290], and the body is ASCII above the robot line. #1319 is READY: v1.2.2 is set, and it is marked ready. Lane 3 places it by its READY time, 17:55Z, after #1320 (#473). It lands after #1263, by a restack onto #1263's squash.

@brettheap brettheap added this to the v1.2.2 milestone Nov 10, 2026
@brettheap
brettheap marked this pull request as ready for review November 10, 2026 17:56
@sourcery-ai

sourcery-ai Bot commented Nov 10, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 4 days and 19 hours by commenting @sourcery-ai review. Upgrade to get a review now.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant