Skip to content

siblings.py: a file, or a folder make siblings cannot list or enter, at ../<Project> is NOT A CLONE, reported and left as it is, so the run no longer ends in a traceback at iterdir before the members after it are placed, and make park no longer ends in one over a folder it cannot enter (#735) - #772

Merged
brettheap merged 2 commits into
mainfrom
l3-735-sibling-slot
Oct 17, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-3 (openRepoShape-3)
Claim: #735 (comment)

Closes #735.

Lands after the v1.1.0 tag. The issue's own "Lands after: #705" is met: #705 landed as 3ceabef0.

What changed, and why

make siblings ended in a traceback on three slots at ../<Project>, exit 1, and never placed the members after them. #705's doctor calls each of these slots not-a-clone. Measured with this PR's tests against main e3137010's siblings.py: all 7 new cases fail.

  • A FILE: any(target.iterdir()) at siblings.py:558 raised NotADirectoryError.
  • A folder with mode 0311 (it can be entered but not listed; 711 is common for another user's directory): the same line raised PermissionError.
  • A folder with mode 000: the traceback comes one line earlier on Python 3.12. (target / ".git").exists() at :557 raises PermissionError, because Path.exists ignores only ENOENT, ENOTDIR, EBADF and ELOOP. The issue puts all three at :558; for mode 000 it is :557.

templates/family-root/scripts/siblings.py:

  • holds_git (:460) asks whether a path holds a .git, and reads an OSError as "no". Two places use it:

    • place's .git question (:596);
    • dispatch's .git question (:768), the --make path that make park and make resume run. Python 3.12 raised there in a folder that cannot be entered, so make park ended in the same traceback. That member is now SKIPPED no clone, as any member with no working clone is.
  • not_a_clone (:471) gives the NOT A CLONE finding's words for each slot nothing is cloned into:

    • a folder with something in it keeps its existing finding, byte for byte;
    • a file: "is a file, not a folder" (:491);
    • a folder it cannot list or enter: "is a folder that cannot be listed" (:496).

    These are the words the doctor's members row uses for the same slots. git clone refuses a folder it cannot read (measured: destination path ... is not an empty directory, exit 128), so nothing could be cloned there.

  • place (:596-605) reports each such slot as NOT A CLONE, in the finding sentence the folder of files already had: "<who>: <at> <what>, so this command will not write into it. Move it aside or name the <file|directory> something else; nothing here overwrites a <file|directory> somebody made." The slot is left exactly as it is, and the members after it are placed.

shape-doctor.py: comments only, at :8012, :8163 and :8182. Its output is unchanged. For these slots, the row says what stands there and nothing of what make siblings does with it. That stays true of every holder, because a holder whose pinned siblings.py predates this change still ends in the traceback. The comments now say so, where they said make siblings "ends in a traceback ... today".

What is tested

  • tests/test_shape_doctor.py: the agreement test test_make_siblings_dry_run_gives_the_rows_word_for_each_slot (:4152) loses its strict xfail on a-file.
    • It now runs over UNREADABLE_SLOTS (:4063): a file, and the 0o311 and 0o000 folders, which are skipped on Windows and as root.
    • The holder's own siblings.py --dry-run prints NOT A CLONE for each, the word the doctor's not-a-clone maps to.
    • The row test (:4119) shares the same list.
  • tests/test_family.py:
    • test_siblings_reports_a_slot_it_cannot_read_as_not_a_clone (:1948) runs over the same three slots, on a real run in a correct layout. It asserts:
      • no traceback, and exit 1;
      • the layout row sibling IRRS NOT A CLONE -;
      • the finding, word for word;
      • IRSS, the member after it, still placed;
      • the slot's file unchanged, and no .git written into the slot.
    • test_a_make_verb_skips_a_folder_it_cannot_enter (:1981) runs --make park --dry-run over a mode-000 folder: no traceback, and sibling IRRS SKIPPED no clone -.
  • Red first: against main's siblings.py, all 7 new cases fail (7 failed, 12 passed). They pass on this branch.

Commands, at 1d149933 on main e3137010, with a stub gh (exit 97) first on PATH, GH_TOKEN=bogus and an empty GH_CONFIG_DIR:

/usr/bin/python3 -m pytest -q -rsx -p no:cacheprovider --basetemp=<scratch>/bt-a -o tmp_path_retention_policy=failed tests/test_family.py tests/test_family_siblings_placement.py tests/test_siblings_family_name.py tests/test_family_makefile_siblings.py tests/test_park_resume_targets.py tests/test_windows_paths.py tests/test_family_printed_quoting.py tests/test_update_shape_add.py tests/test_update_shape_kept_helper.py tests/test_repo_hygiene.py tests/test_cli_reference.py
721 passed in 161.89s (0:02:41)

/usr/bin/python3 -m pytest -q -rsx -p no:cacheprovider --basetemp=<scratch>/bt-b -o tmp_path_retention_policy=failed tests/test_shape_doctor.py tests/test_doctor_members_spelled.py tests/test_doctor_gitmodules_not_utf8.py
471 passed, 3 skipped in 405.86s (0:06:45)   (skips: tests/test_shape_doctor.py:10424, the PowerShell line, Windows host only)

Pinned shape copies

templates/family-root/scripts/siblings.py is a family-root copy carried by digest. A holder pinned before this change shows an upstream-changed row for it until update-shape.py brings it forward. No other pinned copy is touched.

Coordination

#690 changes the same file. Its branch fix/690-siblings-inputs is a WIP checkpoint with no PR yet.

No docs change: no doc quotes the NOT A CLONE finding or describes these slots, and siblings.py's --help is unchanged, so docs/cli.md is too.

🤖 Generated with Claude Code

Summary by Sourcery

Make sibling and make operations safely handle files and unreadable directories by reporting them as non-clones and continuing without overwriting them.

Bug Fixes:

  • Handle files and unreadable directories as non-clone sibling slots without traceback, preserving them and continuing to place later members.
  • Prevent make park and resume operations from failing when a sibling path cannot be inspected for a working clone.

Enhancements:

  • Align sibling placement and doctor reporting for unreadable or non-directory slots with consistent NOT A CLONE findings.

Tests:

  • Add coverage for files and unreadable directories during sibling placement and make operations.
  • Expand doctor agreement tests to cover all unreadable sibling slot types.

…, at `../<Project>` is NOT A CLONE, reported and left as it is, so the run no longer ends in a traceback at `iterdir` before the members after it are placed, and `make park` no longer ends in one over a folder it cannot enter (#735)

`place` asked only whether a folder had something in it: `iterdir` raised
`NotADirectoryError` for a file and `PermissionError` for a folder with
mode 0311 or 000, a traceback, exit 1. `not_a_clone` now gives the
finding's words for each slot nothing is cloned into: the folder of files
keeps its finding byte for byte, a file "is a file, not a folder", and a
folder it cannot list "is a folder that cannot be listed", the words the
doctor's `members` row says of the same slots. `holds_git` asks for a
`.git` and reads an `OSError` as none, for `place` and for `dispatch`
(`--make`), which Python 3.12 answered with `PermissionError` in a folder
that cannot be entered.

The doctor's agreement test (#705) now passes for a file and for both
locked folders, so its strict xfail is gone; the doctor's output is
unchanged, and its comments now say a holder pinned before this change
still ends in the traceback.

`siblings.py` is a family-root copy carried by digest: a holder pinned
before this change shows an `upstream-changed` row for it.

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

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes make siblings and related --make verbs resilient to files and directories that cannot be listed or entered by safely handling filesystem errors, reporting such slots as NOT A CLONE or SKIPPED no clone, preserving their contents, and continuing processing; comprehensive tests and doctor commentary are updated accordingly.

Sequence diagram for resilient sibling placement

sequenceDiagram
    participant Make as make siblings
    participant Place as place
    participant FS as Filesystem
    participant Next as Later members

    Make->>Place: place(root, row, urls, prefix)
    Place->>FS: holds_git(target)
    FS-->>Place: false or OSError
    Place->>FS: not_a_clone(target)
    alt file or unreadable directory
        FS-->>Place: occupied slot
        Place-->>Make: NOT A CLONE
        Place->>FS: preserve existing target
    else empty or absent target
        FS-->>Place: cloneable target
        Place->>FS: git clone
    end
    Place->>Next: continue placing later members
Loading

Sequence diagram for safe park or resume dispatch

sequenceDiagram
    participant Make as make park or resume
    participant Dispatch as dispatch
    participant FS as Filesystem

    Make->>Dispatch: dispatch(root, row, urls, target)
    Dispatch->>FS: holds_git(path)
    alt path cannot be entered or has no .git
        FS-->>Dispatch: false after OSError handling
        Dispatch-->>Make: SKIPPED no clone
    else working clone
        FS-->>Dispatch: true
        Dispatch->>FS: run requested make action
    end
Loading

Flow diagram for unreadable sibling slot handling

flowchart TD
    A[Inspect sibling target] --> B{"holds_git(target)?"}
    B -->|yes| C[Use existing clone]
    B -->|no or filesystem error| D{"not_a_clone(target)"}
    D -->|file or unreadable directory| E[Report NOT A CLONE]
    E --> F[Leave target unchanged]
    F --> G[Continue with later members]
    D -->|absent or empty directory| H[Clone member]
Loading

File-Level Changes

Change Details Files
Harden sibling placement and make-verb dispatch against files and unreadable directories, treating them as non-clones without aborting processing.
  • Added guarded .git detection that converts filesystem access errors into “no clone.”
  • Added classification for files, non-empty directories, and directories that cannot be inspected.
  • Updated placement to emit the appropriate NOT A CLONE finding, preserve the existing occupant, and continue placing later members.
  • Updated --make dispatch to skip inaccessible non-clones instead of raising.
  • Kept existing behavior and wording for ordinary non-empty directories.
templates/family-root/scripts/siblings.py
Expanded regression coverage for unreadable sibling slots and verified continued placement and make-verb behavior.
  • Tested file, unlistable directory, and unenterable directory cases with platform/root skips.
  • Verified no traceback, exit status, exact findings, unchanged occupants, no unwanted .git, and placement of subsequent members.
  • Verified make park --dry-run reports an inaccessible slot as SKIPPED no clone.
tests/test_family.py
Aligned shape-doctor agreement tests and explanatory comments with the new runtime handling while preserving doctor output. shape-doctor.py
tests/test_shape_doctor.py

Assessment against linked issues

Issue Objective Addressed Explanation
#735 Handle a file or a directory that cannot be listed at a sibling slot without a traceback, report it as NOT A CLONE, and leave it untouched while continuing to place later members. ✅
#735 Ensure inaccessible sibling paths do not cause make operations such as make park to traceback when checking for a working clone. ✅
#735 Remove the strict xfail for the doctor/siblings agreement test and cover file, mode-0311, and mode-000 slots, with permission-mode cases skipped on Windows and as root. ✅

Possibly linked issues


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

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T01:10:10Z) Independent review of #772 by RV-d (opus), on head 1d149933: READY. No MUST and no SHOULD; three NITs. It lands only after the v1.1.0 tag, once its Linux tests is green.

Each slot on a real two-member holder (IRRS, then IRSS), measured with make siblings, make park and make resume. The slot is byte-identical before and after every verb.

slot at ../IRRS main: make siblings head: make siblings head: park / resume doctor
a file traceback, IRSS never placed NOT A CLONE, "is a file, not a folder", IRSS cloned SKIPPED no clone not-a-clone
a symlink to a file traceback NOT A CLONE, file wording SKIPPED no clone not-a-clone
a dangling symlink CLONE FAILED same SKIPPED no clone absent (N2)
a symlink to a folder with files NOT A CLONE same SKIPPED no clone not-a-clone
folder, mode 0311 traceback NOT A CLONE, "cannot be listed" SKIPPED no clone not-a-clone
folder, mode 000 traceback, park/resume too NOT A CLONE, "cannot be listed" SKIPPED no clone not-a-clone
a clone whose .git is mode 000 WRONG ORIGIN same WRONG ORIGIN wrong-origin
a folder with files (control) NOT A CLONE same, byte-identical SKIPPED no clone not-a-clone

Answers:

  • The issue's claims hold on main, and so does the writer's correction. Mode 000 tracebacks at :557 (.git.exists() raises); mode 0311 at :558 (iterdir).
  • The doctor agrees with make siblings in every slot.
  • The doctor should keep its neutral wording. A holder pinned before this change still tracebacks.
  • dispatch's new SKIPPED is right for every --make verb. It fires before any make and reuses the existing "no working clone" skip.
  • Pinned copy, docs, Windows:
    • the body names the family-root copy (scripts/shape_materialize.py:533, :558 list it);
    • no doc quotes the finding;
    • the code is pathlib only, and the mode tests skip on Windows and as root;
    • SonarCloud passes.

NITs:

  • N1 siblings.py:768-771 (optional): for a mode-000 folder, make park says "no working clone at ; run make siblings first". If that folder is a clone, it is there. Use not_a_clone's "cannot be listed" words in the skip when they apply.
  • N2 siblings.py:488 (follow-up): a dangling symlink still reads as an empty slot. The dry run says "would clone" and the doctor says absent, while the real run ends CLONE FAILED. This is not a traceback and main behaves the same. Fix: treat is_symlink() and not exists() as NOT A CLONE, "is a link to nothing".
  • N3 tests/test_shape_doctor.py:4063 (optional): add a symlink to a file to UNREADABLE_SLOTS. The doctor and make siblings agree on it today.

Mutants: RV-d tried seven mutants of siblings.py and all seven are killed:

  • holds_git raises;
  • either wrong wording for a file;
  • place stops after the first bad slot;
  • only FileNotFoundError is caught;
  • dispatch's unguarded check;
  • an unlistable folder read as empty.

Tests:

  • 7 family and siblings files: 487 passed in 134.64s;
  • tests/test_shape_doctor.py -k (siblings rows): 41 passed;
  • the slot probe: 10 passed on the head and on main.

Merges. All clean:

Lane: openreposhape-3 (openRepoShape-3)

This was referenced Oct 17, 2026
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 17, 2026 23:42
@sourcery-ai

sourcery-ai Bot commented Oct 17, 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 35 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 112eeed into main Oct 17, 2026
5 checks passed
@brettheap
brettheap deleted the l3-735-sibling-slot branch October 17, 2026 23:42
brettheap pushed a commit that referenced this pull request Oct 18, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
… four files #772 and #788 both changed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…trap_after_bump.py` imports both `appended_to_command_scope` and `needs_a_byte_name` (#715)

Main moved from `0dee22b3` to `ca47b71d` while round 3 was paused (#846, #779, #772, #738, #842, #814, #790, #820, #774, #830 and #841). `git merge-tree` of `8b4dbcf6` with it stopped in one path:

- `tests/test_bootstrap_after_bump.py`, its imports. Main's `aec3b38f` (tests append what they tell git to the command scope) added `appended_to_command_scope` to the `from conftest import (...)` list. This branch added `from test_bootstrap_spelling import needs_a_byte_name` below it (#715, round 1). Both are kept.

`scaffold-project.py`, `scripts/bump-leg.py` and `scripts/shape_materialize.py` merged without a conflict.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…is NOT A CLONE, reported and left as it is, so the dry run no longer says `would clone` where the real run ends CLONE FAILED, and the doctor's `members` row no longer calls the slot `absent` (#784) (#788)

Closes #784.

A dangling symbolic link at `../<Project>` read as an empty slot, because
`exists` follows a link. `make siblings --dry-run` said `would clone`, the
real run ended `CLONE FAILED` (git cannot clone onto a link to nothing, exit
128), and the doctor's `members` row called the slot `absent` and said
`make siblings` places the member there.

Now `not_a_clone` in `templates/family-root/scripts/siblings.py` answers "is a
link to nothing", so `place` reports `NOT A CLONE`, in the finding the other
such slots get, and leaves the link as it is. `make park` and `make resume`
keep `SKIPPED no clone`. In `shape-doctor.py`, `unclonable` answers
`dangling`, so `sibling_slot` says `not-a-clone` and the row says "is a link
to nothing", and the doctor and `make siblings` agree again.
`tests/test_shape_doctor.py` and `tests/test_family.py` hold a new slot,
`a-link-to-nothing`, skipped on Windows.

Lane 3's independent reviewer RV-d (opus) found it READY on `e966a684`, then
stacked on #772 (#788 issuecomment-6230059337). Its optional NIT N2, that a
dangling Windows junction still reads as an empty slot, is #793, in #794.

After #772 landed as `112eeed2`, lane 3 restacked the branch as `bec71bb5`
(#788 issuecomment-6243557591), in three merges: `f204abe1` takes #772's final
head `bcb8e54a`; `5f7ea9b3` takes the squash `112eeed2`, ours for #772's four
files, on which `git diff bcb8e54 112eeed` is empty; `bec71bb5` takes main
`e62b5e17`. Against main the diff is #788's four files, the delta RV-d
reviewed, `1d149933..e966a68`, patch for patch (+120/-53). On `bec71bb5`, the
nine family and siblings files of its gate set pass (607 passed) and
`tests/test_shape_doctor.py` passes (441 passed, 3 skipped). The restack
carries lane 3's own check, not a reviewer's verdict.

It lands on Brett Heap's word "merge lane 3's v1.1.1 drafts after the v1.1.0
tag" (2026-10-17T13:00:57Z).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 18, 2026
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