Repository navigation
family.py: add mounts with --force and _commit_one stages with git --literal-pathspecs add -f, so an operator whose own excludes name members, members/, * or the member gets one commit instead of exit 2, bump moves a pin under them too, and each refusal --force turns off is made before the member is read (#242) - #308
Merged
Merged
Conversation
…`git --literal-pathspecs add -f`, so an operator whose own excludes name `members`, `members/`, `*` or the member gets one commit instead of exit 2, `bump` moves a pin under them too, and each refusal `--force` turns off is made before the member is read (#242) `add` ran `git submodule add` with no `--force` and `_commit_one` a plain `git add`, and both read a global `core.excludesFile`. `members/` is a fixed path, so an operator whose excludes named `members`, `members/`, `*` or the member's name had every add of every family refused: git's dry run refused the first four before the clone, and a directory-only rule on the member's name (`Atlas/`) only at git's last `git add`, after it. With the mount alone forced, `members`, `members/` and `*` still died at `_commit_one`, and so did every `bump`, which stages `[path, family.yaml]` through it with the gitlink already tracked. This is #174 (adopt, fixed by #200) and #229 (scaffold) on the family side. Both readers are forced now. `_commit_one` stages `git --literal-pathspecs add -f -- <paths>`: `-f` reaches only the named paths, and each is taken literally, so `-f` stages no ignored neighbour a glob character would match (#202). That is the spelling PR #233 gives `shape_materialize.force_stage`; #233 is not on this branch, and once it lands the line can become a call to `force_stage`. `bump` rides with this issue: the same line closes it. `--force` turns off more than the ignore check, so each other thing it changes was checked against cc83d15 (#236's head, which this branch is stacked on) by running `add` on it and on cc83d15 with only the two `--force` edits, on git 2.43: - a `.git/modules/<path>` left by a removed member, which `--force` REACTIVATES: already refused before the mount by `member-git-dir-cached`, at the store `git rev-parse --git-path` names, a linked worktree's included. No change. - an index entry at the path with nothing on disk: NOT refused before the mount. git refused it at the mount, and with `--force` it accepted a staged gitlink and replaced it with the member's tip, exit 0. `member-path-taken` now asks the index too, for any entry at the path or under it, and names the `git rm -r --cached` that clears it. - a repository already checked out at the path, which `--force` REUSES: already refused by `member-path-taken`, on disk. It now asks `lexists`, so a dangling link there is refused first too instead of failing at the clone (the same with or without `--force`). - the dry run `--force` skips: a symlinked `members` and a `members` that is a submodule were refused by git before the clone, and with `--force` the member was CLONED through the link (outside the holder) or into the submodule's tree before git refused it and the unwind deleted it. `member-path-unaddable` now makes that same dry run first, with `-f`, so an ignore rule passes it and nothing else does, as adopt's `_refuse_an_unmountable_path` does for its leg mounts. - a name another submodule already has: git 2.51 and later refuse it without `--force` and, given it, mount the member under a new name (`members/Atlas1`) whose store and registration `MountRollback` would not find; git 2.43 rewrites the other submodule's `.gitmodules` section to point at the member, with or without `--force` (probed: exit 0, the other submodule's section gone). `member-name-taken` refuses it on every git, reading `.gitmodules` where git reads it, and before `member-git-dir-cached`, which would otherwise blame a "previously removed" member for the other submodule's store. The mount carries a comment naming each of these and the check that guards it. The PRIVATE/access sentence (`UNREADABLE_HINT`) is printed in one place, the probe's failed clone of the member, which no ignore rule can reach; no excludes row prints it, before or after this change. tests/test_family_add_excludes.py: under `members`, `members/`, `*`, `IRQS` and `IRQS/` (the issue's `Atlas`, as this family's spare is named) and none, `add` exits 0 with ONE commit of `.gitmodules`, `family.yaml` and a 160000 gitlink at the member's tip, read with `git ls-tree`; `validate-family.py` passes under the same excludes; a recursive clone of the pushed holder, with no excludes, has the member at its pin. `bump` under `members` moves the gitlink in one commit, and `remove` is the control. Each guard is refused before the member is read with the holder exactly as it was (the suite's `snapshot`). Mutations: without `--force` at the mount all five rules fail (`IRQS/` at git exit 128, after the clone); without `-f` in `_commit_one` `members`, `members/`, `*` and `bump` fail and `IRQS` and `IRQS/` pass, as the issue found; either one also fails the guard tests whose setup or follow-up `add` runs under `members`. Dropping the index check, the dry run, the name check or `lexists` fails exactly that guard's tests. Dropping `--literal-pathspecs` fails nothing, and no test is added for it: `MEMBER_RE` and the `--member` check (`SAFE_ARG_RE`) admit no glob character, so no member name can hold one. Replayed onto main 6d8d88b after #236 landed (#221's `holding()` kept). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re is anything to record, through `shape_materialize.force_stage`, which `_commit_one` now calls instead of spelling it, so on main a `bump` under an operator's excludes naming `members` or `*` commits instead of exiting 2 at the `git add` #219 added, and a refused dry run of the mount path quotes git instead of blaming a symlink or a submodule (#242) Main's #219 put `run(["git", "add", "--", path])` into `cmd_bump` before `_nothing_to_record`, to re-stage the gitlink so a stray staged one is not a difference the commit then does not find. That is a third reader of the operator's excludes, which #242 never saw while it was stacked on #236. Replayed onto main, `bump` under `members` exited 2 at that line with `The following paths are ignored by one of your .gitignore files: members`, and `test_bump_moves_the_gitlink_past_the_operators_excludes` failed (the review of 289e542, M1). The line is `force_stage(family.root, [path])` now: `-f` over the one path named, taken literally, which stages the gitlink and never the member's files, as before. `_commit_one`'s hand-written `git --literal-pathspecs add -f --` is a call to `shape_materialize.force_stage` (#233, on main since 150c41d), which runs exactly that command, imported beside the other names family.py takes from shape_materialize. One definition of the forced, literal stage, and the same call closes M1 (the review's S1). `member-path-unaddable`'s remediation no longer presumes the cause (N1). git's dry run takes `index.lock`, so a held one refuses it, and the refusal quoted git's `Unable to create '.../index.lock': File exists` and then told the human to replace a symbolic link or take a submodule out, neither of which was there. It now points at git's line and names the link and the submodule as the cases they are. tests/test_family_add_excludes.py: the held lock, refused before the member is read with the holder exactly as it was and git's line quoted; and `member-name-taken` with `.gitmodules` off the disk and the name given only by the index's copy or HEAD's, which `_submodule_paths` falls back to as `git submodule add` does (N4: a mutant without that fallback passed every test). Mutations: with `cmd_bump`'s stage back to a plain `git add`, the bump test fails again; without the fallback, both new `member-name-taken` rows fail. Left as follow-ups, as the review names them: N2 (the index check matches case-sensitively where adopt uses `--icase-pathspecs`), N3 (second copies of adopt's index helpers and dry run), N5 (`bump` and `remove` commit the row's `path:` non-literally), N6 (a `.gitmodules` section under another name already at the mount path). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 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 day and 13 hours by commenting |
Reviewer's GuideThe PR makes family member mounting and gitlink commits robust against operator excludes by using forced, literal path-scoped Git operations, while adding equivalent preflight checks for all safety conditions that Sequence diagram for forced family member add with preflight refusalssequenceDiagram
participant Operator
participant Family as family.py
participant Git
participant Member as Member repository
Operator->>Family: add member
Family->>Git: _index_entries(path)
Git-->>Family: index entries at or below path
Family->>Git: _submodule_paths()
Git-->>Family: .gitmodules from working tree, index, or HEAD
Family->>Git: git add --dry-run --ignore-missing -f -- path
alt mount path refusal
Git-->>Family: refusal reason
Family-->>Operator: member-path-taken or member-path-unaddable
else preflight passes
Family->>Git: git submodule add --force -- url path
Git->>Member: clone member
Member-->>Git: cloned repository
Git-->>Family: mounted gitlink
Family->>Git: force_stage(root, paths)
Git-->>Family: literal forced staging
Family-->>Operator: one commit
end
Flow diagram for literal forced staging of family changesflowchart TD
A["family add, bump, or remove"] --> B["force_stage(root, paths)"]
B --> C["git --literal-pathspecs add -f -- named paths"]
C --> D["ignored neighbors remain unstaged"]
D --> E["git commit"]
F["bump re-stage before nothing-to-record check"] --> B
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
brettheap
added a commit
that referenced
this pull request
Oct 12, 2026
…s `_entry_findings` order sentence composed with #255's #313 (71b58a1, #262 and #263) makes `check` find a kept root path where the shape writes a directory or its `shape/` copy, through `_root_shape_findings` called from `_leg_findings`, and makes `shape_materialize` refuse a copy whose own path is taken. This branch appends each value `execute` writes into a file that no file can hold to `_entry_findings`. The one conflict is that function's docstring, the sentence naming the order of its findings: it keeps #313's words (each entry's `leg:`, the two mount paths and what the root keeps where the shape writes, then the source's own submodules) and appends this PR's clause, ", then each value `execute` writes into a file that no file can hold (#255)". Nothing else in the file conflicts: the return still reads `(findings + submodule_findings + unwritable_findings, unsafe_remediation + remediation + unwritable_remediation)`, and `_leg_findings` carries #313's findings inside `findings`. Main's c0ed615 (#308, family.py) comes with it and touches none of this branch's files. Main has since moved on to c7c02fa (#315, family.py); this merge stops at 71b58a1. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 12, 2026
…263's root-shape findings join `_leg_findings`, so the one list reads #196's unsafe paths first, the root's shape paths with the legs, and #190's relative urls last Main since b421e6b: c0ed615 (#242 via #308, family.py `add --force`: family.py and its own test only) and 71b58a1 (#262, #263 via #313: a path kept in the root where the shape writes a directory or its `shape/` copy is a `check` finding and an `execute` refusal before any leg exists). The one file changed on both sides is adopt-project.py. TWO CONFLICT HUNKS, both docstrings, each the union of both sides: 1. The module docstring's `check` paragraph: 71b58a1's "no path kept in the root where the shape writes (...)", then the submodules clause with #190's "each relative url naming the same repository from the leg as from the source". 2. `_entry_findings`' first paragraph: #190's "and the NOTEs it prints beside them", and 71b58a1's "the two mount paths and what the root keeps where the shape writes". Auto-merged, 71b58a1's: the `shape_materialize` import (`CASE_ASIDE as _CASE_ASIDE`); `_leg_findings`' docstring and its `_root_shape_findings(plan, tree)` call; the root-shape block from `ROOT_BLOCKS_SHAPE` to `_root_shape_findings`; and `_refuse_what_check_finds`' remediation sentence. scripts/shape_materialize.py and tests/test_adopt_root_shape_paths.py are main's blobs. Proofs: - Each side's +/- lines are unchanged by the merge except at the two unioned docstrings. - `_leg_findings(plan, tree)` keeps `tree`. - `_entry_findings` reads #196's unsafe paths, then coverage, then `_leg_findings` (the root-shape findings among them), then `_submodule_findings`, whose #190 relative urls come last. It returns `(findings, unsafe_remediation + remediation, notes)`. - No top-level name is bound twice, and no line over 79 columns is added (42 on main, 42 merged). Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 12, 2026
…, #242 via #308, #262 and #263 via #313) into fix/adopt-record-submodule-placement Brings in ede15ce (#238), 6f45bf9 (#196), b421e6b (#254), c0ed615 (#242) and 71b58a1 (#262, #263) on top of 6d8d88b. No rebase. ede15ce changes shape-doctor.py. 6f45bf9 and 71b58a1 change adopt-project.py and scripts/shape_materialize.py. b421e6b changes scripts/shape_materialize.py too, giving a project's validators an explicit PYTHONIOENCODING. All of these are files this branch changes. c0ed615 changes scripts/family.py alone. No conflicts. Checked: - adopt-project.py: #196 gives `check` and `execute` one test for an entry's path, `entry_path_problem`, as the finding `plan-unsafe-path`. #262 and #263 add `_root_shape_findings` to `_leg_findings`, for a kept root path where the shape writes. Both reach `execute` through `_refuse_what_check_finds`, before any leg exists and before `_commit_the_split` appends the record. This branch's `submodule_record`, `_resolution_text`, the record's preamble, `_answered_legs`, `_kept_in_a_leg`, `_commit_the_split`, `cmd_execute`, `submodule_plan_problems`, `_submodule_findings`, `Plan`, `y`, `emit` and `walk` are unchanged by the merge. `_root_shape_findings` is asked only beside a `legs:` mapping, so the doctor's placement plan, whose `legs:` is a list, still goes through `_leg_findings(plan, None)`. - shape-doctor.py: #238 changes `validator_row` and `run_validator`. This branch's `check_placement`, `recorded_submodules`, `audit_leg`, `placement_row`, `placement_verdict` and `Context` are unchanged by the merge. - scripts/repo_shape.py is untouched by these commits, so the record's writer and reader, and `double_quoted` in `submodule_record`, are 3e86200's. - Both sides added lines to the module docstrings of adopt-project.py and shape-doctor.py. Every line either side added is in the merge. - 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, update-shape.py, scripts/family.py or scripts/bump-leg.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. - 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, file by file the same as against 6d8d88b. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 12, 2026
brettheap
added a commit
that referenced
this pull request
Oct 12, 2026
Main brings five commits since 6d8d88b: - ede15ce (#238 via #287): shape-doctor.py's validator rows; - 6f45bf9 (#196 via #231): `entry_path_problem`, one test of an entry's path for `check` and `execute`; - b421e6b (#254 via #281): bump-leg.py and update-shape.py give the validators an explicit PYTHONIOENCODING (scripts/shape_materialize.py gains `captured_child_environment` and `inherited_child_environment`); - c0ed615 (#242 via #308): family.py's `add` and `bump` under excludes; - 71b58a1 (#262, #263 via #313): `check` finds, and `execute` refuses, a kept root path where the shape writes a directory or its `shape/` copy, and the materializer never writes over a copy. It merged with no conflict. Only adopt-project.py changed on both sides. Every other file the merge changes against this branch is main's, and every file it changes against main is this branch's. In adopt-project.py the merge's diff against this branch is main's own diff (559 lines), and its diff against main is this branch's own diff (784 lines). Main's twenty hunks there, by where they land: 1-2. the module docstring: `check` validates each entry's path and keeps no path in the root where the shape writes (6f45bf9, 71b58a1); 3. the `from shape_materialize import` block: `CASE_ASIDE`, `MOVE_THE_LEG`, `first_and_more` (71b58a1); 4-6. `_leg_findings`: a docstring line, the call to `_root_shape_findings`, and the new block after it (`ROOT_BLOCKS_SHAPE`, `ROOT_BLOCKS_COPY`, `_KeptTree`, `_shape_blocks`, `_blocked_sentence`, `_fix_the_kept_path`, `_root_shape_findings`) (71b58a1); 7. after `_submodule_findings`: `NOT_EXTRACTED`, `entry_path_problem`, `_misread`, `_refused_by_policy` and `_unsafe_path_findings` (6f45bf9); 8-11. `_entry_findings`: its docstring, and the entries' path findings first (6f45bf9); 12-13. `_extract_leg`: `literal:` on every `--paths-from-file` line (6f45bf9). This branch changed `_extract_leg` as well, further down: it reads the commit count and the digest before `_push_the_leg`. The two sets of lines do not touch, and the merged function has both; 14-15. `_mount_the_legs`: `git --literal-pathspecs rm` (6f45bf9); 16-17. `_leg_paths`: collected, not judged (6f45bf9); 18-20. `_refuse_what_check_finds`: its docstring and remediation (6f45bf9, 71b58a1). The new findings of 6f45bf9 and 71b58a1 are `_entry_findings`, which `cmd_execute` reaches only through `_refuse_what_check_finds`, before `_create_leg_remotes`. So they are refused before any leg exists, and no notice is due. The materializer's new `materialize-copy-blocked` is raised after both legs are pushed, inside `_commit_the_split`, when a run reaches the materializer without `check`'s finding. That makes it a late failure, which this branch reports with the notice and exit 2. Main's `test_with_check_skipped_execute_stops_at_the_copy_and_overwrites_nothing` expects it to escape `cmd_execute`, so this merge breaks that test, as 9092fda's merge of #200 broke the mount-guard tests. The next commit makes the test read the reported refusal. No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py or tests/conftest.py. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 12, 2026
…, #238, #196, #254, #242 and #262/#263 merge with three adopt-project.py hunks kept on both sides, and #262's `_CASE_ASIDE` import kept as the name's one binding origin/main moved b30595c -> 71b58a1: - be2790e (#204 via #227), update-shape.py; - 6d8d88b (#203 via #236), family.py; - ede15ce (#238 via #287), shape-doctor.py; - 6f45bf9 (#196 via #231): adopt-project.py `entry_path_problem`, scripts/shape_materialize.py; - b421e6b (#254 via #281): scripts/shape_materialize.py, bump-leg.py, update-shape.py; - c0ed615 (#242 via #308), family.py; - 71b58a1 (#262 and #263 via #313): adopt-project.py `_root_shape_findings` and scripts/shape_materialize.py `copy_in_the_way`. It fixes what this branch's pull request listed as follow-ups (i) and (ii): a source's own `shape/` where the shape writes its copy, and a plain FILE where the shape needs a directory. Conflicts, all in adopt-project.py, each resolved by keeping both sides: - The module docstring's `check` paragraph. 71b58a1 adds "no path kept in the root where the shape writes ..." before the submodule clause, and this branch adds the symlink clause after it. Both are kept, in that order. - `_entry_findings`' docstring. 6f45bf9 and 71b58a1 give the order "each entry's path, then coverage, then each entry's `leg:`, the two mount paths and what the root keeps where the shape writes, then the source's own submodules". This branch adds "then each symlink the plan keeps in the root that a shape file cannot be written through". - `_entry_findings`' return line. 6f45bf9 returns `unsafe_remediation + remediation`, and this branch appends `_root_symlink_findings(plan, source, tree)` to the findings. The merge returns both. Found after the textual merge, and resolved here so that no commit binds a name twice: 71b58a1 (through 65a9262) imports #198's phrase as `_CASE_ASIDE` in the "#172's shape half" import block, and b37159a had imported it on a line of its own. Main's import is kept; this branch's line and its five-line comment go. scripts/shape_materialize.py merges with no conflict. `place` gets 71b58a1's `copy_in_the_way` inside its collision block and this branch's `_refuse_unwritable` after it, as 71b58a1's message foresaw. Checked on the merged tree: - `_entry_findings` still hands `_leg_findings` the tree, and the symlink findings still come last in its list. - AST: no top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py or scripts/shape_materialize.py, and none is loaded but bound nowhere. Main changed the signature of no function this branch calls. - Both sides are intact by +/- line sets, apart from the three hunks and the import above. The next two commits fix what this merge breaks, each with a revert line. #262's rule and this branch's both name a symlink kept AT the copy under `shape/`. And where the shape's copy lands is now `copy_in_the_way`'s question, asked before this branch's own check of it. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 12, 2026
…s state the contract, that a caller writing into a checkout somebody works in calls `force_stage` alone and never after a sweep, instead of counting two callers, so a change to the helper is no longer checked against `update-shape.py` alone with `family.py`'s `add`, `bump` and `remove` commits missing (#369) (#398) `force_stage`'s docstring said TWO CALLERS, AND ONLY ONE OF THEM SWEEPS FIRST, and named `update-shape.py apply --add` as the one that calls it alone; `stage_written`'s closing sentence said the same. Since #242 (PR #308) `family.py`'s `_commit_one` (every `add`, `bump` and `remove` commit) and `cmd_bump`'s re-stage call it alone too, for exactly the reason the docstring gave for `update-shape.py`: they write into a holder somebody may be working in, where `-A` would stage their work beside the shape's. Both docstrings now state the contract without a count: a caller that writes into a checkout somebody works in calls `force_stage` alone, never after a sweep, and `force_stage` never sweeps itself. The reason the old text gave is kept, and so is its history: the rule began with `update-shape.py apply --add` (#202), and `family.py`'s commits took it up (#242), each stage named exactly. The next alone-caller is bound by the rule without being named, so it does not make the text stale again. The opening list of tools that share the helper names `family.py`'s `add`, `bump` and `remove` commits. `stage_written`'s closing paragraph is rewritten the same way and drops its own "THREE CALLERS" count. Docstring-only: the module's AST with its docstrings stripped is identical before and after. Closes #369 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Lane: openreposhape-4 (openRepoShape-4)
Claim: #242 (comment)
What changes
family.py addmounts the member withgit submodule add --force, and every stageadd,bumpandremovemake goes throughshape_materialize.force_stage(git --literal-pathspecs add -f -- <paths>). An operator whose own excludes (a globalcore.excludesFile) namemembers,members/,*or the member gets one commit instead of exit 2, andbumpmoves a pin under them too.members/is a fixed path, so the operator had nothing to change but their own excludes. git's dry run refusedmembers,members/,*andAtlasbefore the clone. A directory-onlyAtlas/was refused only at git's lastgit add, after the clone. With the mount alone forced,_commit_one's plaingit addstill refused the first three.bumpstages the gitlink twice. Once at thegit addmain's family.py:bumpto the commit a member is already pinned at saysnothing to doand exits 1, so a re-run of a bump that landed is no longerREFUSED a git or gh command failed, exit 2, around git'snothing to commit(#210) #219 put before its nothing-to-record check, and once in_commit_one. Both areforce_stagenow: one definition, called from both places.-freaches only the named paths, each taken literally. No ignored neighbour a glob character would match is staged with them (update-shape.pyapply --addstages the new file with a plaingit add, so a path the operator's excludes ignore fails after the copies, the re-pin and the branch are written, nothing is rolled back, andcheckthen exits 0 over a re-pin nobody committed #202).--forceturns off more than the ignore check, so each other thing it changes is refused before the member is read. The holder is left exactly as it was, and a comment at the mount names each guard:.git/modules/<path>, which--forcereactivates:member-git-dir-cached(family.py:addasks every refusal of the holder and of a throwaway 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.gitmodulessection that the next add commits under its own title (#203) #236), at the store git's--git-pathnames, a linked worktree's included.--forceaccepts and replaces with the member's gitlink:member-path-takennow asks the index too. On disk it askslexists, so a dangling link is refused as well.--forceskips:member-path-unaddablemakes it first, with-f. Skipping it let a symlinkedmembershave the member cloned through the link (outside the holder), and amembersthat is a submodule have it cloned into that submodule's tree. The refusal quotes git's line, and its remediation presumes neither a link nor a submodule, because a heldindex.lockfails the dry run too.member-name-taken. It reads.gitmoduleswhere git reads it: the working tree, else the index's copy, else HEAD's.The review, M1/S1, and the replay
289e542was written stacked on family.py:addasks every refusal of the holder and of a throwaway 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.gitmodulessection that the next add commits under its own title (#203) #236's headcc83d15. It was reviewed adversarially (review242-289e542): the diff was right on its own base, but the verdict was NOT MERGEABLE on M1. Merged with main,bumpundermembersexited 2 at main's own unforcedgit addfrom family.py:bumpto the commit a member is already pinned at saysnothing to doand exits 1, so a re-run of a bump that landed is no longerREFUSED a git or gh command failed, exit 2, around git'snothing to commit(#210) #219, which the stacked branch never saw, and its own bump test failed. S1: once update-shape.py:apply --addforce-stages its additions past the operator's own excludes, and a branch, stage or commit that fails before the commit lands now puts back the copies, the pin, the README line, the index and the branch and names the step, socheckno longer exits 0 over an uncommitted re-pin (#202) #233 landed,_commit_one's hand-written forced add should become a call toforce_stage.addasks every refusal of the holder and of a throwaway 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.gitmodulessection that the next add commits under its own title (#203) #236 squash-landed as6d8d88b, after family.py:addrefuses a case-variant of a member the family already pins asmember-already-presentnaming the row that pins it,bumpandremovefind a member in any case, and validate-family.py refuses two rows with oneid, so--member inkrouter/irrsbesideInkRouter/IRRSno longer mounts one project twice past both gates andbump --member irssno longer recommends that add (#206) #221.289e542was then cherry-picked onto that main on this new branch (fix/family-add-force-past-excludes-on-main), and the old branch is left where it is. There were 2 conflict hunks. family.pyaddrunsgit submodule addwithout--force(and_commit_onea plaingit add), so under an operator's global excludes namingmembersor the member's own name it exits 2 at the mount for every member, and the remediation it prints blames access (gh auth status, your SSH key) instead of the ignore rule #242's side was taken for the new helpers and the_checked_mount_pathcall. Main's side was taken for family.py:addrefuses a case-variant of a member the family already pins asmember-already-presentnaming the row that pins it,bumpandremovefind a member in any case, and validate-family.py refuses two rows with oneid, so--member inkrouter/irrsbesideInkRouter/IRRSno longer mounts one project twice past both gates andbump --member irssno longer recommends that add (#206) #221'sholding()pieces: therepositoryargument and theheld-based remediation. The replayed commit's+/-lines against6d8d88bare identical togit diff cc83d15 289e542(654 lines each). The two diffs differ only in blob ids, hunk line numbers and the context lines at family.py:addrefuses a case-variant of a member the family already pins asmember-already-presentnaming the row that pins it,bumpandremovefind a member in any case, and validate-family.py refuses two rows with oneid, so--member inkrouter/irrsbesideInkRouter/IRRSno longer mounts one project twice past both gates andbump --member irssno longer recommends that add (#206) #221's pieces. The test file's blob is identical.cmd_bump's re-stage isforce_stage(family.root, [path])._commit_onecallsforce_stage.member-path-unaddable's remediation no longer blames a symlink or submodule when git refused for another reason, such as a heldindex.lock._submodule_paths's index/HEAD fallback, which a mutant removing it used to survive.Follow-ups the review named, none in this PR:
--icase-pathspecs.bumpandremovecommit the row'spath:with a non-literal pathspec..gitmodulessection under another name already at the mount path.Tests
env GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t GIT_CONFIG_GLOBAL=<empty file> GIT_CONFIG_NOSYSTEM=1 PATH=<gh shim that exits 97>:<git-filter-repo 2.47.0>:$PATH /usr/bin/python3 -m pytest tests/test_family_add_excludes.py tests/test_family.py tests/test_family_add_refusals.py tests/test_shape_doctor.py tests/test_cli_reference.py -q -p no:cacheprovider: 380 passed in 499.31s, no failure and no skip, over the final tree (15c080c).tests/test_family_add_excludes.py covers each excludes rule (
members,members/,*,IRQS,IRQS/) and none:addmakes ONE commit of.gitmodules,family.yamland a 160000 gitlink at the member's tip, read withgit ls-tree.bumpundermembersmoves the gitlink in one commit, andremoveis the control.Mutations on this head:
cmd_bump's re-stage back to a plaingit add,test_bump_moves_the_gitlink_past_the_operators_excludesfails atgit add -- members/IRRS("The following paths are ignored ... members").member-name-takenrows fail: the add blames a "previously removed" member for the other submodule's store instead.NOTE: the symlink cases skip on Windows. Several cases run on macOS and Windows for the first time on this PR: the linked-worktree store path, a submodule at
members, the heldindex.lock, and.gitmodulesread from the index or HEAD. This head is green only whentests,tests-macosANDtests-windowsall pass.Closes #242
Summary by Sourcery
Make family member operations resilient to operator excludes while preserving safe, atomic refusal behavior for unsafe mount conditions.
New Features:
Bug Fixes:
Enhancements:
Tests: