Repository navigation
family.py: add refuses a case-variant of a member the family already pins as member-already-present naming the row that pins it, bump and remove find a member in any case, and validate-family.py refuses two rows with one id, so --member inkrouter/irrs beside InkRouter/IRRS no longer mounts one project twice past both gates and bump --member irss no longer recommends that add (#206) - #221
Conversation
…y pins as `member-already-present` naming the row that pins it, `bump` and `remove` find a member in any case, and validate-family.py refuses two rows with one `id`, so `--member inkrouter/irrs` beside `InkRouter/IRRS` no longer mounts one project twice past both gates and `bump --member irss` no longer recommends that add (#206) GitHub resolves `owner/repo` case-insensitively, the rule `repo_shape.same_repository` already folds a forge name by (#149, PR #156), but `Family.member` compared `project:` with `==`. So on main a5b09c3 `family.py add --member inkrouter/irrs` on a family that pins `InkRouter/IRRS` exited 0, mounted the same repository again at `members/irrs` and wrote a third row with the same `id: irrs`; `validate-family.py` said `family ok ... 3 member(s)` and the doctor said COMPLIANT; and `bump --member irss` refused `member-unknown` with the remediation `family.py add --member <org>/irss`, which is that duplicate. - `Family.member(project)` matches `project:` case-insensitively and returns the row, whose own spelling is what every caller reads back. An exact spelling wins, so a family a pre-#206 `add` pinned twice (`IRRS` and `irrs`) can still be mended by naming the row to touch; a spelling that fits two rows is `REFUSED member-ambiguous` naming both, never a guess. - `Family.holding(repository, project)`: `member()` first, then any row whose `repository:` is the same forge repository by `same_repository` (case-folded), which also catches a hand-made row whose `project:` is not its repository's name. - `add` refuses `member-already-present` on `holding()`, still before anything is mounted, and names the row: `irrs is already a member of InkRouter: the row IRRS pins InkRouter/IRRS`, with `family.py bump --member IRRS --to <commit>` in the remediation. Nothing is mounted, staged or written; HEAD does not move. - `bump` and `remove` look the member up through `member()` and then use the row's own `project:` for the path, the rewritten row, the commit title and what they print (`Bump IRSS to ...`, `removed IRSS (InkRouter/IRSS) from members/IRSS`). - `validate-family.py` gains `member-duplicate-id`: two `members:` rows that record one `id:` (the member's own `project.yaml` id) are one project pinned twice, however each row spelled its repository. It runs in the full check and in `--pins`, and reaches the doctor's `family` row, which runs the holder's own validator (`INVALID (family)`, exit 1). Unchanged: an `add` of a member the family does not pin (mounted and recorded in the spelling given, `members/IRQS`, row `IRQS`), the exact-spelling paths of `bump` and `remove`, `member-duplicate` (one `project:` twice), every `--help` text (`render-cli-reference.py --check` ok, so `docs/cli.md` is untouched). An existing holder receives `member-duplicate-id` when `update-shape.py apply` re-copies `scripts/validate-family.py`, which `check` reports as `upstream-changed`. Left out: `add` refusing a member whose `project.yaml` `id` equals an existing row's (a repository renamed at the forge and added under its new name) belongs after #203, whose `_probe_member` reads `project.yaml` before the mount; on main the id is read only after `git submodule add`, where a refusal would leave the staged mount #203 fixes. The validator's `member-duplicate-id` already catches that holder after the fact. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-11T18:56Z) @codex review |
|
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 day and 5 hours by commenting |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Reviewer's GuidePrevents case variants of an already pinned GitHub repository from being mounted twice, makes bump/remove operate on members regardless of input casing while retaining canonical row data, and adds validation for duplicate project IDs with comprehensive regression coverage. Sequence diagram for case-insensitive family add refusalsequenceDiagram
participant User
participant FamilyCLI as family.py
participant Family
participant Filesystem
User->>FamilyCLI: cmd_add(--member inkrouter/irrs)
FamilyCLI->>Family: holding(repository, project)
Family->>Family: member(project)
Family-->>FamilyCLI: existing row IRRS
FamilyCLI-->>User: Refusal member-already-present
FamilyCLI-->>User: Remediation: bump --member IRRS
Note over Filesystem: No mount, staging, write, or HEAD move
Sequence diagram for canonical case-insensitive bump and removesequenceDiagram
participant User
participant FamilyCLI as family.py
participant Family
participant MemberTree as members/IRSS
User->>FamilyCLI: bump or remove --member irss
FamilyCLI->>Family: member(irss)
Family-->>FamilyCLI: row with project IRSS
alt bump
FamilyCLI->>MemberTree: update pinned commit
FamilyCLI-->>User: Commit/output uses IRSS
else remove
FamilyCLI->>MemberTree: remove members/IRSS
FamilyCLI-->>User: Output uses IRSS and InkRouter/IRSS
end
Flow diagram for duplicate member ID validationflowchart TD
A[validate-family.py reads members rows] --> B[_check_members]
B --> C[_check_member_ids_unique]
C --> D{Duplicate id found?}
D -- No --> E[Validation continues]
D -- Yes --> F[Finding member-duplicate-id]
F --> G[Validator exits 1]
G --> H[shape-doctor.py reports INVALID family]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
…leaves an exact duplicate row to `member-duplicate`, and prints a `remove` that runs as typed, so a private family's degraded CI still finds one project pinned twice and the remediation no longer drops both rows or fails for want of `--family-root` (#206) Fix round 1 for the independent review of #221 (three nits). - `_check_member_ids_unique` now runs before `_check_members` returns for `--no-members`. It reads `family.yaml` alone, and `--no-members` is the run the template's CI falls back to when a private family's members cannot be checked out, so that run no longer passes one project pinned twice. - A row whose `project:` an earlier row already spells exactly is skipped. That row is `member-duplicate`'s finding, so one fault is now one finding. `member-duplicate-id`'s `remove` would also have dropped both rows, because `family.py remove` filters every row spelled that way. - The remediation now names openRepoShape's `scripts/family.py` from a checkout of the standard, `--family-root <this holder>` (which `remove` requires) and the later row's own spelling, each shell-quoted: `... remove --family-root <holder> --member IRRS2` takes the later row, IRRS2, out; name IRRS there instead to keep IRRS2. Tests: `--no-members` finds it; the printed command, split and run from the checkout, takes the row out and the gate goes green; an exact duplicate row is one `member-duplicate` finding under the full check and `--pins`. Each fails with its change undone. No pinned digest or fixture copies validate-family.py. The only copy in the tree is the template, and a holder's `contracts/shape-pin.yaml` row is computed at `init`. `--help` is unchanged, so `docs/cli.md` is too. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-11T19:32Z) fix round 1 (independent review, codex is out of quota): |
|
(lane openRepoShape-2, 2026-10-11T19:34:42Z) This lane reviewed PR #221 at 6c917d6 read-only, in its own detached worktrees against local bare repositories. Nothing was authored and no claim is held. The branch has since moved to 9725bd2 (19:24Z), which this review did not cover. MERGEABLE WITH NITS. Sequencing: this PR conflicts in On main, the issue's repro (
Findings:
Pins: no digest of the template is checked in here. A holder pins its copy in Tests:
Merges, ls-remote 19:2xZ:
Report: |
|
(lane openRepoShape-4, 2026-10-11T19:42:14Z) Landing note from lane openRepoShape-4: our PR #236 (#203) restructures the opening of
On that tree, |
|
… (60588d5) and this branch both edit family.py's add path, resolved so _checked_add_target asks family.holding(repository, project) before anything is mounted (#203) Lane 3's #221 (#206) made `family.holding(repository, project)` the one question `add` asks about a member the family already pins, by name in any case and then by repository, and rewrote the opening of `cmd_add` around it. This branch (#203) had already moved that opening into `_checked_add_target` so every refusal about the holder is asked before `git submodule add` runs. The two edits conflict in two hunks of scripts/family.py, and the conflict is the only one: every other path in main merged without a marker. Resolved by the four steps lane 4 left on #221 as its landing note: 1. `_checked_add_target(family, repository, project, path, at)`: the signature gains `repository`, which `holding` needs for its by-repository lookup. 2. Inside it, `held = family.holding(repository, project)` replaces this branch's `family.member(project)`, keeping #221's refusal message (it names the row that pins the repository) and its remediation (`bump --member` names the row's own spelling, not the one typed). 3. #221's `path = f"{MEMBERS_DIR}/{project}"` line is dropped: `cmd_add` already derives `path` and passes it in, and a second assignment inside the function would shadow the argument with the same value. 4. `cmd_add` passes `repository` to `_checked_add_target`. `gh pr update-branch` would not have done this. Git auto-merges #221's `holding()` wording into `_checked_add_target` without a marker, where `held` is never bound and the first `add` of any member raises a NameError; the tests of #221 catch it (4 failed on that tree), the merge itself does not. Resolving by hand, in a commit, is the only way the two behaviours meet. The docstring of `_checked_add_target` gains one paragraph saying that the already-pinned question is asked of `family.holding`, so the helper's own account of what it refuses no longer stops at the by-name check it had. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…clone of the member before it mounts anything, and unwinds and names whatever fails after the mount, so a refused add (a leg, a short or unknown `--at`, an empty member) no longer leaves a staged gitlink and `.gitmodules` section that the next add commits under its own title (#203) (#236) `family.py add` used to mount the member first and ask afterwards. `git submodule add` ran, and only then was `--at` checked and the member's `project.yaml` read, and neither refusal undid the mount. A leg, a short `--at` or an empty member left a staged gitlink, a staged `.gitmodules` section and a cached module store behind. The next `add` of a DIFFERENT member then committed the refused one's `.gitmodules` section under its own title, because `_commit_one` commits the working-tree state of `.gitmodules` whole, and both `validate-family.py` and the doctor passed the result. Now every refusal `add` can make is asked before the holder is touched, in this order: - The HOLDER, by `_checked_add_target`: the member is not already pinned (by name in any case, and by repository: `family.holding`, #206), the target path is free, no object store of a previously removed member is cached (asked of `git rev-parse --git-path`, so a linked-worktree holder is answered correctly), `--at` is 40 hex, and `family.yaml` is a manifest this tool will rewrite (`Family.rewritten`, the refusal `write_members` would raise, asked early). - The MEMBER, by `_probe_member`, from a throwaway bare clone outside the holder that is deleted whatever the answer: the `--at` commit exists and is a commit (an annotated tag of a tree or a blob is named for what it tags), the member has a tip, and `project.yaml` is there and loads. New refusals: `member-at-unknown`, `member-no-tip`, `family-manifest-unwritable`. Whatever still fails after the mount (the pin write, the manifest write, the commit, a member that moved between the probe and the mount, Ctrl-C or any unforeseen error) is unwound by `MountRollback`: the mount, its `.gitmodules` section, the holder's `submodule.<path>` config, a registration the holder already had (put back, not dropped) and the module store. The refusal says exactly what was put back and what, if anything, could not be, down to which half of `.gitmodules` an `index.lock` left. A failure after the commit has landed undoes nothing, and says so; a holder with no commit yet still gets its root commit. `member-not-a-project` now names the legs of the ROOT a `-spec` or `-code` suffix came off (`IRRS-spec` gives `IRRS-spec` and `IRRS-code`, root `IRRS`), not `IRRS-spec-spec`. Merged with #221 (#206), which edits the same opening of `add`: `_checked_add_target` takes `repository` and asks `family.holding(repository, project)` with #221's message and remediation, #221's `path = ...` line is dropped, and `cmd_add` passes `repository`, since git alone would auto-merge #221's `holding()` wording into a function where `held` is never bound and every `add` would raise a NameError. Tests: `tests/test_family_add_refusals.py` (37 tests, members faked as local bare repositories, a `gh` that fails if called) pins every refusal's code and NEXT line and asserts the holder byte-identical afterwards (status, `.gitmodules` and `family.yaml` bytes, `.git/modules`, `members/`, config, branches, HEAD); a good add after a refused one commits only its own entry; every failure between the mount and the commit is unwound and named, including a linked-worktree holder's store, an unwind that cannot finish, an annotated-tag or missing-object HEAD, a held `index.lock`, and KeyboardInterrupt; the happy path is unchanged. Mutants that delete or weaken each of those branches were applied across the three review rounds and each is caught by a test. `--help` is unchanged, so `docs/cli.md` is too. Targeted run over this branch merged with main 60588d5 (merge b80afb4): tests/test_family.py, tests/test_family_add_refusals.py, tests/test_shape_doctor.py, tests/test_cli_reference.py and tests/test_repo_hygiene.py, 390 passed, 0 skipped, with git-filter-repo 2.47.0 on PATH and a gh that exits 97. Closes #203 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, #272 via #274, #205 via #232) into fix/adopt-record-submodule-placement Brings in 23e16ae (#251), 60588d5 (#206), 018ce0d (#207), 67a398a (#272) and d94d155 (#205) on top of 5751460. No rebase. The first four change none of this branch's files. d94d155 changes shape-doctor.py and tests/test_shape_doctor.py, as this branch does. One conflict, in shape-doctor.py's module docstring, under EXIT CODES. Both sides added a clause to exit 1's list of findings: #205 "a manifest is there and does not load", and #189 "or, for a submodule, a leg other than the one `project.yaml` records for it". Both are kept: #205's in its place in the list, and #189's after the path-policy clause it qualifies. Only the docstring's first line reaches `--help`, so docs/cli.md is unchanged. Merged without a conflict, and checked: - `check_placement`: #205 adds its `waiting_row` gate after the family-holder return and before `ctx.legs()`. The merged function is this branch's plus exactly those eight lines. So a `project.yaml` that does not load makes the placement row `n/a`, waiting on the `manifest` row, before the record is read. The record is read from `ctx.manifest`, which #205 now loads once, with the same `load_yaml`, so a manifest that loads gives the record it always gave. - #205 removes `Context._declares`, and nothing on this branch names it. This branch leaves `placement_row` unchanged, and #205's gate calls it as (NA, reason, None, detail). - No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, shape-doctor.py, validate-manifest.py or update-shape.py. Each merged module's names are the base's plus what either side added. Every definition only one side changed is that side's text. The two that both changed are the docstring above and `check_placement`. - These commits leave adopt-project.py, scripts/repo_shape.py and scripts/shape_materialize.py untouched. So the record's writer and reader are the ones the round trip and the revert table ran on at a01ab02, and the record is still spelled by `double_quoted`. - AGENTS.md is untouched: 472 lines, under the cap of 472. - The branch's diff against main is still #189 alone: 12 files, +1670 -157. The one deletion more than against 5751460 is the EXIT CODES sentence above, which now rewraps two of #205's lines. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-3 (openRepoShape-3)
Claim: #206 (comment)
What
GitHub resolves
owner/repocase-insensitively.repo_shape.same_repositoryalready folds a forge name by that rule (#149, PR #156), butFamily.membercomparedproject:with==. This branch makesscripts/family.pyand the holder'svalidate-family.pytreat a case variant as the same member:Family.member(project)matchesproject:without regard to case and returns the row. Callers read the row's own spelling back, never the one they were given. An exact spelling wins, so a family that a pre-family.pyaddcompares--memberwith the existing rows case-sensitively, so--member inkrouter/irrson a family that already pins InkRouter/IRRS mounts the same repository again asmembers/irrs, both gates pass a family that pins one project twice, andbump --member irss's refusal recommends exactly that add #206addpinned twice (IRRSandirrs) can still be mended by naming the row to touch. A spelling that fits two rows isREFUSED member-ambiguousnaming both, never a guess.Family.holding(repository, project)triesmember()first, then any row whoserepository:is the same forge repository bysame_repository(case-folded). The second step also catches a hand-made row whoseproject:is not its repository's name.addrefusesmember-already-presentwhenholding()finds a row. The check still runs before anything is mounted, and the refusal names the row:irrs is already a member of InkRouter: the row IRRS pins InkRouter/IRRS. Its remediation isfamily.py bump --member IRRS --to <commit>. Nothing is mounted, staged or written, and HEAD does not move.bumpandremovelook the member up throughmember(). They then use the row's ownproject:for the path, the rewritten row, the commit title and their output (Bump IRSS to ...,removed IRSS (InkRouter/IRSS) from members/IRSS).validate-family.pygainsmember-duplicate-id. Twomembers:rows that record oneid:(the member's ownproject.yamlid) are one project pinned twice, however each row spelled its repository. It reaches the doctor through thefamilyrow, which runs the holder's own validator (INVALID (family), exit 1). Since fix round 1 (9725bd2):--pinsand--no-members. It readsfamily.yamlalone.--no-membersis the run the template's CI falls back to when a private family's members cannot be checked out.project:an earlier row already spells exactly is skipped. That row ismember-duplicate's finding, so one fault is one finding.member-duplicate-id'sremovewould also have dropped both rows, becauseremovefilters every row spelled that way.python3 scripts/family.py remove --family-root <this holder's path> --member IRRS2takes the later row, IRRS2, out; name IRRS there instead to keep IRRS2. The holder's real path is printed, and it and the row are shell-quoted. Before, it readremove --member <Project>, which lacked the--family-rootthatremoverequires.Unchanged:
addof a member the family does not pin. It is mounted and recorded in the spelling given (members/IRQS, rowIRQS).bumpandremove.member-duplicate, which flags oneproject:twice.--helptext.render-cli-reference.py --checksays[ok] docs/cli.md is what the generator produces, sodocs/cli.mdis untouched.Existing holders receive
member-duplicate-idwhenupdate-shape.py applyre-copiesscripts/validate-family.py.checkreports that file asupstream-changed.Why
The issue's repro, run on this machine on
maina5b09c3 and on this head. Setup: a family holder made byfamily.py init, membersInkRouter/IRRSandInkRouter/IRSSadded, everything in local bare remotes, andremotes/irrs.gita symlink toremotes/IRRS.gitas a local stand-in for GitHub's case-insensitive name. The head column was measured on cefb6dd, which has the tree of 6c917d6, and re-measured on 9725bd2 after fix round 1. The two runs print the same lines except for the commit shas of the fresh repositories and themember-duplicate-idremediation. On 9725bd2 that remediation reads: From a checkout of openRepoShape,python3 scripts/family.py remove --family-root <holder> --member irrstakes the later row, irrs, out; name IRRS there instead to keep irrs.maina5b09c3family.py add --member inkrouter/irrscommitted ...: .gitmodules, members/irrs, family.yaml; a third rowproject: irrs id: irrs repository: inkrouter/irrsREFUSED member-already-present: irrs is already a member of InkRouter: the row IRRS pins InkRouter/IRRS; HEAD not moved,git status --porcelainempty,members/=IRRS IRSSvalidate-family.py(the holder's own copy)family ok: InkRouter (inkrouter), 3 member(s), exit 0family ok: InkRouter (inkrouter), 2 member(s), exit 0shape-doctor.py --root <holder>COMPLIANT, exit 0COMPLIANT, exit 0family.py bump --member irss --to <IRSS tip>REFUSED member-unknown: irss is not a member of InkRouter, remediationfamily.py add --member <org>/irss: the duplicateIRSS <old> -> <new>, committedmembers/IRSS, family.yamlThe same holder after a pre-#206
addhas pinned IRRS twice (rowsIRRSandirrs, made withmain'sfamily.py):maina5b09c3validate-family.pyfamily ok ... 3 member(s), exit 0FINDING member-duplicate-id: irrs and IRRS both record id 'irrs': one project is pinned twice. ..., exit 1shape-doctor.py --root <holder>COMPLIANT, exit 0INVALID (family), exit 1family.py bump --member Irrs --to <IRRS tip>REFUSED member-unknown: Irrs is not a member of InkRouterREFUSED member-ambiguous: Irrs fits 2 rows of InkRouter: IRRS, irrsfamily.py remove --member irrs, thenvalidate-family.pyremoved irrs (inkrouter/irrs) from members/irrs;family ok ... 2 member(s), exit 0family ok ... 2 member(s), exit 0Tests
tests/test_family.pygains 15 test cases: 12 functions, one of them parametrized over four spellings (inkrouter/irrs,INKROUTER/IRRS,InkRouter/irrs,OtherOrg/Irrs). Run againstmaina5b09c3 (this head'stests/test_family.pyin a scratch worktree at a5b09c3), the result is13 failed, 2 passed, 76 deselected. Two pass onmainby design:test_add_of_a_member_that_is_not_there_is_unchanged_by_the_case_ruleguards that the case rule leaves a new member alone.test_an_exact_duplicate_row_is_one_findingguards that the new check does not double-reportmember-duplicate.No existing test changed.
Fix round 1 added the
--no-members, runs-as-printed and exact-duplicate tests. Each fails with its change undone:--no-membersreturn fails the--no-memberstest;--family-rootfrom the remediation fails the runs-as-printed test.Against 6c917d6's validator, 4 of the 10
pinned_twice,duplicate,runs_as_printedandno_memberstests fail: the three new ones and the updatedtest_validate_finds_one_project_pinned_twice.Targeted set on head 9725bd2, with git-filter-repo 2.47.0 on
PATH:The earlier head 6c917d6 ran the same set:
449 passed in 831.28s (0:13:51).tests/test_update_shape.pyandtests/test_update_shape_add.pyare in the set because they run a holder'svalidate-family.py. The Windows job skips none of the new tests (tests/test_family.pycarries noWINDOWS_SKIP). The fixture that pins one project twice names the second rowIRRS2, notirrs, so it never needs two directories whose names differ only in case, which macOS and Windows hold as one. Theaddtests that use a case-variant spelling are refused before anything is mounted.Composes with
Trial merges (
git merge-tree --write-tree <sibling> HEAD) at this head (9725bd2), measured at 19:30Z:origin/fix/family-add-check-before-mount(#203, lane 4)scripts/family.pyfix/family-bump-nothing-to-do(#210, PR #219)fix/doctor-reads-family-yaml(#211, PR #220)fix/decode-git-output-utf8(#217, PR #230)origin/mainadopt-project.pyonly)The #203 conflict is one
raisein two hunks:cmd_add'smember-already-presentcheck.addmounts the member before it checks--ator the member'sproject.yaml, so a refused add (a leg, a short--at) leaves a staged gitlink, a.gitmodulesregistration and a cached object store, and the next add of a different member commits the refused leg's registration while validate-family.py and the doctor say ok #203 moves that check out ofcmd_addinto its new_checked_add_target(family, project, path, at), which asksfamily.member(project).held = family.holding(repository, project). Its message names the row (: the row {held.get('project')} pins {held.get('repository')}) and its remediation bumps{held.get('project')}.The first PR to land wins, and the other branch merges by hand:
addmounts the member before it checks--ator the member'sproject.yaml, so a refused add (a leg, a short--at) leaves a staged gitlink, a.gitmodulesregistration and a cached object store, and the next add of a different member commits the refused leg's registration while validate-family.py and the doctor say ok #203 lands first, this branch moves its check into_checked_add_target. That means passingrepositoryin (it is already in hand incmd_addfrom_checked_member), askingholding(repository, project)there, and keeping the row-naming message and remediation.addmounts the member before it checks--ator the member'sproject.yaml, so a refused add (a leg, a short--at) leaves a staged gitlink, a.gitmodulesregistration and a cached object store, and the next add of a different member commits the refused leg's registration while validate-family.py and the doctor say ok #203's_checked_add_targetkeepsholding(repository, project)and theheldrow's message rather thanmember(project).Either way the check stays before the mount, which both branches require.
Warning: part of this branch's message merges outside the conflict markers. The line
f"the row {held.get('project')} pins {held.get('repository')}",and the remediation's opening line"Remediation: `family.py bump --member "sit BETWEEN the two hunks, in the merged body of #203's_checked_add_target. Resolving by taking #203's side of both hunks gives a file that compiles. Butheldis then never bound, and everymember-already-presentrefusal dies withNameError: name 'held' is not defined. Measured at d25dd4d:py_compilepasses, and pyflakes saysundefined name 'held'on that line. Whoever merges second must keep theheld = family.holding(repository, project)binding or rewrite that line. Taking either side wholesale is not a resolution.#210 adds a
nothing to doexit tocmd_bumpthat printsproject. On this branch that variable is the row's own spelling by the time #210's lines run, so the two compose as written.#211 adds
gitlinks_underandfamily_beside_projecttoshape-doctor.py. This branch adds no helper there, so neither branch duplicates the other.Left out
addshould refuse a member whoseproject.yamlidequals an existing row'sid(a repository renamed at the forge and added under its new name). That check belongs after family.pyaddmounts the member before it checks--ator the member'sproject.yaml, so a refused add (a leg, a short--at) leaves a staged gitlink, a.gitmodulesregistration and a cached object store, and the next add of a different member commits the refused leg's registration while validate-family.py and the doctor say ok #203: its_probe_memberreadsproject.yamlbefore the mount, while onmainthe id is read only aftergit submodule add, where a refusal would leave exactly the staged mount family.pyaddmounts the member before it checks--ator the member'sproject.yaml, so a refused add (a leg, a short--at) leaves a staged gitlink, a.gitmodulesregistration and a cached object store, and the next add of a different member commits the refused leg's registration while validate-family.py and the doctor say ok #203 fixes. Until then, this branch'smember-duplicate-idcatches such a holder after the fact.validate-family.py'smember-duplicate(oneproject:written twice) still runs only with the members checked. So--no-memberspasses afamily.yamlthat names one row twice, as onmain. Unlikemember-duplicate-id, it was not moved ahead of the--no-membersreturn.validate-family.py'smember-duplicatestill comparesproject:exactly. Two rowsIRRS(InkRouter/IRRS) andirrs(OtherOrg/irrs) with distinct ids would pass it, althoughmembers/IRRSandmembers/irrsare one directory on macOS and Windows.family.py addnow refuses to write such a row; only a hand edit can make one.Closes #206
🤖 Generated with Claude Code
Summary by Sourcery
Treat family members consistently with case-insensitive forge naming and reject or diagnose duplicate project pins before they can result in multiple mounts.
Bug Fixes:
Enhancements:
Tests: