Repository navigation
shape-doctor.py: a leg or member whose git directory this user cannot read is named in the gate's words, spec: .git/modules/spec: cannot be read (<reason>), so the legs and members rows never count it as drift, placement never calls it "not a git repository", and no HEAD is read for it (#1174) - #1200
Conversation
…cannot read gives the gate's advice as its WHY, and each branch #975 added for a mount this user cannot reach is built by a test (#1175) `shape_file_state` keeps the `OSError` of a refused `stat` or read beside its words, `leg_shape_comparison` hands each one back, and `check_leg_shape_files` gives the note `cannot_read_why` of them: the permission advice or the other one, ending "Then run this doctor again". Beside a missing file's line the advice follows that line's why. The NEXT stays None and the row a note, so the verdict does not change. A whole mount gets no why here: the `legs` row names it and gives its advice. tests/test_doctor_unreadable_paths.py builds the five branches rv975's mutants MI, MJ, MK1, MK2 and ML found unbuilt: a leg with no gitlink over a mount it cannot list, and a holder with two members, one unreachable beside one a checkout cures, by the holder's own bootstrap and by the git line, in process and with a real mode 000. ML's term is not dead: the `rerun_sentence` it adds is not the advice's own "run this doctor again", and the cases tell the two apart. The note's why is asserted word for word, for a permission and for an I/O error, for a refused `stat` and a refused read, beside a missing file, and under a real mode 000. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… read is named in the gate's words, `spec: .git/modules/spec: cannot be read (<reason>)`, so the `legs` and `members` rows never count it as drift, `placement` never calls it "not a git repository", and no HEAD is read for it (#1174) `mount_read`, which the `legs`, `leg shape files`, `placement` and `members` rows share, now asks the mount's git directory as `validate-pins.py` asks it (`_leg_git_dir` and `_refuse_an_unreadable_git_dir`, #968): the `.git` itself where it is no regular file, else the directory its gitfile's `gitdir: ` line names (`git_dir_of`), and then the `stat` of that directory's `HEAD`. A refused read of the gitfile, or a refused `stat` of the `HEAD`, is the mount's `mount_cannot_be_read` line, named as the gate names it (`git_dir_named`): `spec/.git`, or, where the directory is not under the mount, `spec: .git/modules/spec`, so every row's line starts with the mount. The `legs`, `placement` and `members` rows then say what they say of a mount this user cannot reach: this doctor again as the NEXT, the gate's advice as the WHY, no drift, and the verdict is the `pins` (or `family`) row's INVALID. The `leg shape files` row runs no git, so it asks with `git_dir=False` and still compares the files of such a leg, as it did before. The `members` row's note on what the holder's bootstrap checks out recursively still skips a member whose mount line is set, now one whose git directory cannot be read too: git can move nothing in it. Before, a leg at its pin was `populated, but git cannot read a HEAD in it`, sent to the lockstep gate and counted as drift; a member was `checked out at ?`; `placement` said "it is not a git repository"; and a `.git` directory this user cannot search was passed over by git, which read the ROOT's HEAD as the leg's and named a checkout over it. Each verdict was DRIFTED. A gitfile that names no directory, or a git directory that is not there, is left to git as before. Also pins that the `leg shape files` note for a mount this user cannot reach has no WHY (RV1193's NIT 1 on #1193). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideUpdates shape-doctor.py to identify unreadable leg or member git directories using gate-compatible resolution and HEAD checks, report the exact refusal across relevant rows, avoid false drift and HEAD reads, and retain independent file-shape comparisons; extensive tests cover permissions, naming, parsing, controls, and regressions. Sequence diagram for unreadable mount git-directory detectionsequenceDiagram
participant Doctor as shape-doctor.py
participant Mount as Mount
participant GitDir as Git directory
participant Head as HEAD
Doctor->>Mount: mount_read(root, rel, git_dir)
Mount->>Mount: is_a(.git)
alt git_dir is enabled
Mount->>GitDir: git_dir_of(.git)
Mount->>Head: is_a(git_dir/HEAD)
alt read refused
GitDir-->>Mount: OSError
Mount-->>Doctor: mount_cannot_be_read
Doctor->>Doctor: skip head_of and drift counting
else readable
Head-->>Mount: readable
Mount-->>Doctor: own_git=True
Doctor->>Head: head_of
end
else leg shape files
Mount-->>Doctor: mount result without git-directory read
Doctor->>Mount: compare files with templates
end
Flow diagram for unreadable mount verdict and reportingflowchart TD
A[mount_read] --> B{Git directory readable?}
B -->|No| C[mount_cannot_be_read: gate-compatible path and reason]
C --> D[legs and members skip drift counting]
C --> E[placement reports refusal]
D --> F[No HEAD read]
E --> G[Verdict INVALID from pins or family]
B -->|Yes| H[Normal HEAD comparison]
I[leg_shape_comparison] --> J[Calls mount_read with git_dir=false]
J --> K[Compares files independently]
K --> L[Shape-files row remains OK when files match]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-3, 2026-10-23T10:07:25Z) Independent review (RV1200, opus, ruling 8) of #1200 at Measured with an own probe, real modes restored in a
Rulings:
Tests at the head, one pytest at a time, under a stub gh:
Mutants, own script, each KILLED, restored and checked with
Merges: SonarCloud: gate OK, 0 new issues, 0.0% new duplication. CI: SHOULD 1 (process, no code here),
NITs, nothing to change:
Body and commit: draft, no milestone, base #1193's branch; "Stacked on #1193" in the second paragraph; the only closing keyword is Lane: openreposhape-3 (openRepoShape-3) |
Ours for the parent files shape-doctor.py and tests/test_doctor_unreadable_paths.py: S carries F, which this branch already holds. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
|
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 1 hour and 36 minutes by commenting |
…ory is inside the root's own the root's line ahead of `mount_read`'s refusal of that directory, so a root's `.git` this user cannot read is said once again, and a leg's own `.git` directory keeps its line (#1146) Merged with main at 50869f3, #1200's `mount_read` asks each leg's git directory, and `audit_leg` returned on its refusal, `code: .git/modules/code: cannot be read (<reason>)`, before #1147's `root_line` was reached. Where the root's own `.git` cannot be read, the legs' git directories under it cannot either, so 5 of the 47 rows of tests/test_root_git_unreadable.py failed on the `placement` reason: the row named a symptom once per leg instead of the root's line once (lane 3's note on #1147). `audit_leg` now gives the root's line first, where `root_line` is set, `mount_read` refused the leg, and `read_through_the_root` says the leg's git directory is inside the root's own: under the root's `.git`, or under the directory a root's gitfile names, which is where git keeps a submodule's for a root that is a linked worktree or a submodule itself (measured: `../../main/.git/worktrees/wt/modules/spec` and `../../.git/modules/root/modules/spec`). Every other refusal keeps `mount_read`'s line: a mount, a `.git` or a gitfile this user cannot read, and a leg's own `.git` directory, whose repository is not the root's. With the root's `.git` readable, `root_line` is None and #1200's refusal stands. validate-pins.py already refuses the root before any leg, and validate-manifest.py asks no leg's git directory, so neither changes. tests/test_root_git_unreadable.py: 3 rows, in process, on every platform and as root, with every `HEAD`'s `stat` refused: the root's line once for legs under the root's `.git` and under the directory its gitfile names, and the control, a leg's own `.git` directory keeping `spec/.git: cannot be read`. Each fails on the merge. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ose own `.git` this user cannot read is named `.git: cannot be read (<reason>)`, so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) (#1147) #1146's case, found by r1144: an assembly root whose own `.git` is a link into a folder at mode 000, so this user cannot search it. On base `038dafba` the doctor read it as `DRIFTED (2 leg(s) not at the pin)`, with `no gitlink recorded` for each leg, and its `manifest` and `pins` rows said each validator `crashed before reaching a verdict (exit 1): PermissionError`. Run directly, `validate-pins.py` and `validate-manifest.py` each ended in a `PermissionError` traceback, exit 1, on Python 3.12, and in `REFUSED not-a-git-repo`, exit 2, under a 3.14-like shim in which `Path.is_file`, `is_dir`, `exists` and `is_symlink` answer False to any `OSError`. Both raised inside `find_repo_root` of `scripts/repo_shape.py`, at `(candidate / ".git").exists()`. - The validators each call `_refuse_an_unreadable_root(start)` first in `main`. With the file's own `_is_a` (`scripts/repo_shape.py` is a pinned copy too, and a pinned validator must not rely on a newer one, #370 and #610), it asks each `.git` that `find_repo_root` would ask, in the same order, from the real path of the `--root` given, and stops at the first one that is there, a directory or a gitfile. One that answers absent (ENOENT, ENOTDIR, EBADF, ELOOP, and Windows 21, 123 and 1921) is passed over, as `exists()` passes it. A refused `stat` ends in the new code `git-repo-unreadable`, the sibling of `not-a-git-repo`: `REFUSED git-repo-unreadable: .git: cannot be read (Permission denied)`, exit 2, with the cause's advice (the `chmod` cure for a permission). A `.git` above the `--root` given is named whole. `find_repo_root` is not changed and answers as before after it. - `validate-pins.py` also asks the git directory of that `.git`, as #968 asks of a leg's: a gitfile it cannot open, a git directory whose `HEAD` it cannot reach. On base git said of each that the root records no gitlink, and the gate refused `pin-gitlink-absent`. `validate-manifest.py` runs no git and asks only the `stat`, so over a readable `.git` and an unreadable git directory it passes, a true answer about `project.yaml`. - The doctor's `root_git_read` asks what `validate-pins.py` asks and names the git directory as the gate does (relative to the root inside it, whole outside it, a gitfile ending in LF or CR LF). The `legs` row says the gate's line once, as a FINDING, with the doctor itself as NEXT and the gate's advice as WHY. Each leg's entry has `gitlink: null`, the line in `gitlink_cannot_be_read`, and `pin`, `populated`, `head`, `own_git` and `unborn` null, so a reader of `--json` gets no `KeyError`. `drifted_reasons` counts no such leg, and still counts a readable leg beside it that is off its pin. - `placement` audits every leg git can read, and gives the root's line to a leg whose `.git` is there and leads to a repository git names no top level for; its row says the root's line once, `n/a`, only where it is every leg's. The verdict follows the ladder (an unfinished merge, DRIFTED, MISPLACED, INVALID, SHAPE BEHIND): no leg is compared with its pin while the root's `.git` cannot be read, so none is drift, and a path in the wrong leg of a leg git reads is `MISPLACED (1 path)`. - Over #1200 (`a2fc1da8`): #1200's `mount_read` refused a leg's git directory before the root's line was reached, so `placement` named `<leg>: .git/modules/<leg>: cannot be read` once per leg instead of the root's line once. `audit_leg` now gives the root's line first where the leg's git directory is inside the root's own, under its `.git` or under the directory its gitfile names (where git keeps a submodule's for a root that is a linked worktree or a submodule itself, measured with git 2.43.0). Every other refusal keeps `mount_read`'s line: a mount, a `.git` or a gitfile, and a leg's own `.git` directory. With the root readable, #1200's line stands. - The S1 round (`4d299dd3`): `audit_leg` asks `populated`, which is True only where `mount_read` got as far as the mount's `.git`, before `read_through_the_root`. A spec mount this user can search and not list (mode 0100 or 0300) had been given the root's line, because `spec/.git` could still be read through the root, against the text above. It now keeps `spec: cannot be listed (Permission denied)`. The verdict stays `INVALID (pins)`, so only the text of the `placement` row changes. - Both validators are in `COPIED_VERBATIM`, and no file is added to or taken from a pinned list. Against a project one commit past its pin, `update-shape.py check` says `upstream-changed` for the two and `unchanged` for the other 9; a project that holds the old copies keeps `crashed ... PermissionError` in the validator rows until `update-shape.py apply`. No `--help` text changes, so `docs/cli.md` is unchanged, and no refusal code is added to `AGENTS.md`. Not touched: #1144's eight sites, the doctor's gitmodules-on-disk code (#1109), and `scripts/repo_shape.py`. - Measured in a scaffold's recursive clone, on Python 3.12.3 and under the shim, with base `038dafba` against `b75babee`: the issue's case went from the `DRIFTED` above to `INVALID (manifest, pins)`, the `legs` row `.git: cannot be read (Permission denied), so no gitlink is read and no leg is compared with its pin`, and each validator to `REFUSED git-repo-unreadable`, exit 2, no traceback. A `.git` that is a directory at 000 or 444, or a gitfile at 000, went from `DRIFTED` to `INVALID (pins)` (`validate-manifest.py` passes). A spec leg with a repository of its own and a misplaced path, beside a root `.git` at 000, went from `DRIFTED (1 leg(s) not at the pin)` to `MISPLACED (1 path)`. The controls (a readable `.git`, a link to nothing, a link to itself) give the base's reports byte for byte, with paths and commit SHAs normalized, on both Pythons. - `tests/test_root_git_unreadable.py` is new and holds 51 rows (47 at `b75babee`, 50 at `a2fc1da8`). Each round was run base against head, with the new file laid over base. At `b75babee`, on 3.12.3 and under the shim, base failed 37 and passed 10 (controls, and the walk rows that stop at a gitfile), and the head passed 47. For the order round, over 54 ids with #1200's 4 rows, base failed 8 and passed 46, and the head passed 54. For the S1 row, `a2fc1da8`'s `shape-doctor.py` failed it and the head passes it. Mutants: 49 of 50 killed at `b75babee` (the survivor, r1147's Da, differs only in a race between the `stat` and the read), 7 of 7 at `a2fc1da8`, and 5 of 7 at `4d299dd3`, where M6 and rv1147c's K4 are equivalent on a real filesystem. - Known limits: - Four other pinned callers of `find_repo_root` keep the defect, a `PermissionError` traceback on 3.12 and `REFUSED not-a-git-repo` under the shim: the assembly root's `bootstrap.py`, and the family root's `validate-family.py`, `bootstrap.py` and `siblings.py`. They need the same local walk in four more pinned copies, filed as #1148. - The `chmod` cure says "every directory above it inside the root", but in the issue's case the locked folder is outside the root and the line names `.git`, a link, so `chmod u+rx .git` is refused. These are the gate's words, unchanged, as r1111's N5 left them. - A root with no `.git` at all (a link to nothing or to itself, which #430's rule calls absent, or none under a parent whose `.git` cannot be read) still reads `DRIFTED (2 leg(s) not at the pin)`; under such a parent `validate-pins.py` walks up and refuses, and the doctor asks only the root's own `.git`. - While the root's `.git` cannot be read, the `legs` row says that alone, so the verdict can read `MISPLACED (1 path)` where the run after it is mended reads `DRIFTED (1 leg(s) not at the pin)`. - `placement` also gives the root's line to a leg whose gitfile names a folder that does not exist and to a leg whose `.git` folder has no `HEAD`; the run after the root is mended gives git's answer for both. - A root whose `.git` is a link, with legs whose gitfiles name the link's target and not `.git/modules/<leg>`, keeps `mount_read`'s line for each leg in `placement`, since `read_through_the_root` follows no link. The `legs` row still says the root's line once. - A family holder whose own `.git` cannot be read, and the `members` row, are not changed (#1109's code is avoided); #1148 asks for them to be measured. - Only what #968 asks of a leg is asked: the `stat` of `.git`, the open of a gitfile, the `stat` of the git directory's `HEAD`. Review: lane 1's independent reviewers reviewed the first two rounds, lane 3 noted the clash with #1200, and lane 5 re-reviewed the order round. r1147 found `e86fb8f4` READY on condition, with no MUST, two SHOULDs and eight NITs (#1147 issuecomment-6332417236): S1, no rows for a relative `--root` or a walk that stops at a gitfile, and S2, `placement` turning down legs it could still read. It killed 13 of its 21 mutants; the survivors were the rows S1 and the NITs asked for, and Da, which is equivalent. The fix round `b75babee` answered both SHOULDs and the NITs, and r1147b found it READY, with no MUST or SHOULD and three NITs (issuecomment-6333323566): each named mutant was killed by its named row, 20 of r1147's 21 are killed (Da is equivalent), N1 and N2 are answered in the text above, and N2's docstring and N3 are optional and left. Lane 3's note (issuecomment-6362488848), from its review of #1200 (issuecomment-6362479557), found that on a merge of #1200 with `b75babee` 5 of the 47 rows failed, all on `placement`'s reason, and named the order that `a2fc1da8` gives. Lane 5's re-review of `a2fc1da8`, by rv1147c (rv1147 and rv1147b hung before they measured anything), found it READY, with no MUST, one SHOULD and four NITs (issuecomment-6723841826): the SHOULD, S1, was a mount this user can search and not list that got the root's line; the NITs were K2, K4 and K7 surviving, rv1248's note, which the text does not contradict, and the draft, to be marked ready. rv1147c killed 8 of its 11 mutants, and found the tree of the merge `569ea056` to be exactly that of `git merge-tree --write-tree b75babe 50869f3`, with no hand edit. Lane 1 took the code for S1, not a narrower text, as `4d299dd3` (issuecomment-6724251407). Lane 5's delta check of `4d299dd3`, by rv1147d, found it READY, with no MUST or SHOULD and three NITs: M6 has no row (an optional stand-in row), the PR was a draft, and CI's `tests` was still running (issuecomment-6724684319). Tests, under a stub `gh` with a bogus token: at `a2fc1da8`, lane 1's gate, on tree `50a97fd7`, gave 1,018 passed and 5 skipped over 8 calls: `tests/test_root_git_unreadable.py` 50 passed, `tests/test_doctor_unreadable_paths.py` 68 passed, `tests/test_path_guard_hygiene.py` and `tests/test_no_auto_maintenance.py` with `tests/test_scaffold_leg_path_collisions.py` 283 passed and 2 skipped, the doc trio (`test_agents_md_codes`, `test_cli_reference`, `test_repo_hygiene`) 171 passed, and `tests/test_shape_doctor.py` in 4 shards 446 passed and 3 skipped (the PowerShell line that is printed only on a Windows host). Lane 5's rv1147c ran the first two files with `tests/test_shape_doctor.py` in one pytest at `a2fc1da8`, on git 2.43.0 and on 2.47.1: 564 passed and 3 skipped each time. The wider gate at `b75babee`, with the 37 other test files that name `placement` and the validator files, gave 6,740 ids: 6,737 passed and 3 skipped; the validators have not changed since `e86fb8f4`. At `4d299dd3`, w1147s's gate (Python 3.12.3, git 2.43.0) gave `tests/test_root_git_unreadable.py` with `tests/test_doctor_unreadable_paths.py` 119 passed; `tests/test_shape_doctor.py`, 4 shards, 446 passed and 3 skipped; the scans with `tests/test_scaffold_leg_path_collisions.py` 283 passed and 2 skipped; the doc trio 171 passed. That is 1,024 ids: 1,019 passed and 5 skipped. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its H0 `4d299dd3` is green on Linux and SonarCloud. The branch was never rebased. After H0, the only commit is update-branch's clean merge of main, giving 95ea564, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: 8 pytest calls over `tests/test_root_git_unreadable.py`, `tests/test_doctor_unreadable_paths.py`, `tests/test_path_guard_hygiene.py`, `tests/test_no_auto_maintenance.py`, `tests/test_scaffold_leg_path_collisions.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`, `tests/test_repo_hygiene.py` and `tests/test_shape_doctor.py` in 4 shards: 1,019 passed, 5 skipped. Closes #1146 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



What changed
A follow-up outside v1.2.0 (Brett Heap, 2026-10-21T10:32:04Z). It lands only after the v1.2.0 tag, on a later merge word.
Stacked on #1193 (#1175, branch
l3-1175-unreadable-mount-rows-tested, headf3247e54), and based on that branch. Lane 3 asked for the stack after RV1193's review of #1193: this PR changes code that #1193 adds, and it takes RV1193's NIT 1 on #1193. Its own diff is one commit on top off3247e54.shape-doctor.pynow asks a leg's or a member's git directory the wayvalidate-pins.pyasks it. Where this user cannot read that directory, thelegs,membersandplacementrows say the gate's line. Thelegsandmembersrows no longer count that mount as drift,placementno longer calls it "not a git repository", and the verdict is the gate's INVALID instead of DRIFTED.The change:
mount_readasks the git directorymount_readis the one question that thelegs,leg shape files,placementandmembersrows share. Where the mount has a.git, it now also asks two things, as the gate's_refuse_an_unreadable_git_dirdoes (#968):git_dir_of(dotgit), a copy of the gate's_leg_git_dir. It gives the.gititself where that is not a regular file. For a gitfile it gives the directory thegitdir:line names, joined to the gitfile's own directory as git joins it, or None where the gitfile names no directory;statof that directory'sHEAD, throughis_a.If either read is refused, that refusal becomes the mount's
mount_cannot_be_readline.own_gitis then False, sohead_ofnever runs in the mount. The line names the path the way the gate names it (git_dir_named): normalized and relative to the root,spec/.gitor.git/modules/spec. Where the directory is outside the mount, the line starts with the mount's path:spec: .git/modules/spec: cannot be read (Permission denied).From there the
legs,membersandplacementrows do what #975 already made them do for a mount this user cannot reach. The NEXT is this doctor again, the WHY is the gate's advice for the cause, anddrifted_reasonsskips the leg or member. The verdict falls to thepinsrow's INVALID, or thefamilyrow's for a holder.A gitfile that names no directory, or a git directory that is not there, is not a refused read. Both are left to git, as before (
populated, but git cannot read a HEAD in itand the lockstep gate), the same way the gate leaves them to git.Decided where the Direction left it open
leg shape filesrow still compares the files of such a leg. RV1193 asked for this to be ruled on purpose. That row compares a leg's files on disk with the templates and runs no git, and a git directory nobody can search stops none of those files from being read. Soleg_shape_comparisonasksmount_read(..., git_dir=False): the mount'sstat, its listing and its.git'sstat, exactly as before shape-doctor.py: a leg whose git directory this user cannot read (.git/modules/specat mode 000) is counted as off its pin, so the verdict is DRIFTED for a leg at its pin, andplacementcalls it not a git repository #1174, and not the git directory. Where the files match, the row staysokwith no WHY, as it was at main. Thelegsrow names the directory and gives the advice. Pinned byassert_comparedin every mode cell and stand-in case, and by mutants M16 and M17.membersrow does not ask such a member what it carries. shape-doctor.py: five branches of the unreadable-mount rows (#975) have no test, so mutants MI, MJ, MK1, MK2 and ML survive, and theleg shape filesnote for a file this user cannot read gives no cure #1175's guard on the holder's bootstrap note (if MOUNT_UNREAD not in entry, which RV1193 calls MJ) now also skips the.gitmodulesof a member whose git directory cannot be read, even though that file is readable. This is right: git can move nothing in a member whose repository it cannot read, so no keep line is owed for that member's legs. Only thecarriednote is affected.test_a_member_whose_git_directory_is_refused_is_not_asked_what_it_carriespins it: the why is the same with that file and without it, and it carries nogit -C LEG branch NAMEline. Dropping the guard (MJ) fails that test.placementandmembersrows print the line as that mount's half of the reason, and their code assumes it starts with the mount's path (leg_audit_summary,member_problem). Without the prefix, the leg's half would read.git/modules/spec: .... The gate's own line is the suffix, word for word: the mode cells check it against thepinsrow, and a parity case checks it against the gate's_named.str(), as the gate's_namednames it. On POSIX that is the same text asas_posix().RV1193's NIT 1 on #1193 is folded in:
test_beside_a_checkout_line_the_unread_mount_gets_the_advicenow asserts that theleg shape filesnote for a mount has no WHY, and RV1193's mutant N10 is KILLED (see Mutants).Measured
Before is main
961640c6; after is this PR's head. Both come from one probe, which was copied intotests/for one run and then moved back out, with every mode restored in afinally. Python 3.12.9e9303b6b1f8in the before block is the ROOT's HEAD (git rev-parse HEADin the root).Before, main
961640c6:After, this PR (
shape-doctor.pyas at5fb38294):The
pinsrow is unchanged in every cell (REFUSED pin-submodule-unreadable: <the same path>: cannot be read (Permission denied)), and so is every other row butmachine, which is n/a under the stubghin both. Theleg shape filesrow isokand compares the files before and after.Out of scope
Nothing in the issue is headed "Needs a ruling" or "Documented limit".
docs/release-notes/v1.2.0.mdstill lists this as a v1.2.0 known limit. That file records v1.2.0's state, and this PR does not touch it.validate-family.py, the gate behind thefamilyrow, refuses a member whose git directory is at 000 asmember-commit-unresolvable("not in that member's object store"). It does not name the directory the wayvalidate-pins.pyhas named a leg's since validate-pins.py and repo_shape.py after #919: gh's stderr that is not UTF-8 ends in a traceback, a leg's git directory this user cannot read is called an unresolvable pin with no reason, and two gh details still name the repository raw #968. That is the family gate's problem and outside this file. The doctor'smembersrow now names the directory, and the verdict is INVALID either way.leg shape filesnote for a leg file this user cannot read gives the gate's advice as its WHY, and each branch #975 added for a mount this user cannot reach is built by a test (#1175) #1193's, and are not taken here.Tests
Each test ran under a stub
ghfirst on PATH (exit 97), withGH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=false,GIT_CONFIG_GLOBAL=/dev/nullandEMAILset, as/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch> -o tmp_path_retention_policy=failed <files> -rs, one at a time, on Python 3.12.At the head,
5fb38294:tests/test_doctor_unreadable_paths.pyalonetests/test_repo_hygiene.pytests/test_path_guard_hygiene.pytests/test_no_auto_maintenance.pytests/test_agents_md_codes.pytests/test_cli_reference.pytests/test_update_add_codes_documented.pyAt
56557756, whoseshape-doctor.pyis the head's byte for byte. The head changes only the test file'sHEADstand-in (refused_head, see Merges), and that file passes alone at the head, above:tests/test_doctor_unreadable_paths.pyandtests/test_shape_doctor.pyin ONE pytest (the file imports that one's session fixtures)tests/test_doctor_unreadable_pin.pytests/test_doctor_next_is_a_command.pytests/test_legs_readers_not_a_list.pytests/test_doctor_legs_members_next.pytests/test_doctor_paste_through_git.pyThe skips are the host's: 3 for a Windows-only PowerShell line, 6 for this host's git 2.43 (CI runs 2.55), and 2 because the session set
GIT_CONFIG_GLOBAL. The mode cases also skip as root and on Windows; every mutant below is also killed by an in-process case, so a run as root or on Windows still kills it.Mutants
Each mutant changes one text in
shape-doctor.py(found exactly once), runs this test file, then #1193's copy of it, and puts the file back by copy, checked withcmp(cmp okafter every one).HEADis notstated.gittaken for a directory<rel>:before a directory outside the mount<rel>:before every lineHEAD's refusal keeps<rel>/.gitown_gitstays True on a refusal (head_ofruns)PermissionErroris caughtHEADis refusedleg shape filesasks the git directorygit_dir=Falseignored[unread[1]]carriedguard droppedshape-doctor.pyat #1193's head (all of #1174 out)Every mutant is KILLED by this file. All but MJ pass #1193's copy of it, so the new cases are what catch them. MJ is #1175's own mutant: #1193's cases kill it, and so does
test_a_member_whose_git_directory_is_refused_is_not_asked_what_it_carries. N10 is killed by the assert added for RV1193's NIT 1 (test_beside_a_checkout_line_the_unread_mount_gets_the_advice).Merges
git merge-tree --write-treeof the head5fb38294with origin/main and with every open PR that touchesshape-doctor.pyortests/test_doctor_unreadable_paths.py(re-listed live):All are clean as text. One is not clean in meaning:
.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147 (shape-doctor.py: a shape root whose own.gitis a link into a folder this user cannot search reads DRIFTED, its legs as having no gitlink, and both of its validators crash with PermissionError #1146, a root whose own.gitthis user cannot read). On the merge of this head with shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147's headb75babee,tests/test_doctor_unreadable_paths.pypasses (68), and 5 of shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147's owntests/test_root_git_unreadable.pyfail. Those 5 pass on shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147's own head. Each is a root.gitat 000 or r--, or linked into a folder at 000. Every leg's git directory then sits inside that.git, so this PR'smount_readnamesspec: .git/modules/spec: cannot be readin theplacementrow, where shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147 names the root's.git: cannot be read. In each, the assertion that fails is on theplacementrow's reason. Whichever of the two lands second should ask the root's line before a leg's git directory inaudit_leg, as shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147'svalidate-pins.pyrefuses the root's.gitbefore any leg's. Not changed here: it needs shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147'sroot_git_read.statof everyHEAD, the root's.git/HEADincluded, which shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147 asks first. The stand-in (refused_head) now spares the root's.git/HEAD, and the 3 pass on the merge.Closes #1174
Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code
Summary by Sourcery
Handle unreadable leg and member Git directories as gate-level invalid findings without treating them as drift or attempting to read their HEADs.
New Features:
Bug Fixes:
Enhancements:
Tests: