Skip to content

family.py: bump to the commit a member is already pinned at says nothing to do and exits 1, so a re-run of a bump that landed is no longer REFUSED a git or gh command failed, exit 2, around git's nothing to commit (#210) - #219

Merged
brettheap merged 3 commits into
mainfrom
fix/family-bump-nothing-to-do
Oct 11, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

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

What

family.py bump to the commit a member is already pinned at no longer ends in git's nothing to commit surfaced as REFUSED a git or gh command failed, exit 2. cmd_bump now asks git, after its members: rewrite and before _commit_one, whether the commit would record anything: _nothing_to_record runs git diff --quiet --ignore-submodules=dirty HEAD -- <member path> family.yaml, reads its exit 1 as "there is a difference" and lets any other failure propagate.

  • Nothing to record: bump prints its usual old -> new tree line, then bump-leg.py's sentence on stdout, nothing to do: <org>/<Project> is already pinned at <sha12> and every fact agrees., says nothing on stderr and exits 1, which is what the module docstring says nothing is. No commit, nothing staged.
  • family.yaml goes back byte for byte as it was found. An uncommitted hand edit the rewrite happened to overwrite is not this run's to discard (on main the same run threw the edit away before refusing).
  • git is asked, and was == --to is not read, so three cases keep committing: a gitlink that moved alone and a digest that does not recompute (validate-family.py names bump --member <Project> --to <commit> as the repair for both), and a bump an earlier run wrote and staged but never committed. Each has a test.
  • The member's gitlink is re-staged (git add -- <member path>) before git is asked, as _commit_one does first anyway, so a stray gitlink somebody staged and never committed is not a difference either. validate-family.py reads the index first, reports member-gitlink-mismatch and names bump as the repair, and without the re-stage that remedy, run with the pinned commit, still ended in the refusal this PR closes (fix round 1, found by the independent review). It stages the gitlink and nothing else: family.yaml's index state is left as found.
  • --ignore-submodules=dirty is what git commit itself ignores: files the member holds untracked or modified do not make a bump to the pinned commit look like one with something in it.
  • The module docstring's EXIT CODES says which bump is nothing.
  • The sentence names the repository (InkRouter/IRRS) as bump-leg.py does, not the bare <Project> the issue sketched, so a lane reads one wording from either tool.

No README, AGENTS.md or handbook change: neither describes bump's exit codes (or bump-leg.py's nothing to do); the exit-code contract lives in the module docstring only, which this updates. --help text is unchanged (docs/cli.md is not touched), README stays at its 1416-line cap and AGENTS.md at 464.

Why

Measured with a throwaway family (two scaffolded members, local bare remotes, bump/irrs branch), on main a5b09c3 and on this head, driver and raw output kept by the lane. The staged-gitlink row was measured on main 54aa69a and on head c7e8f5d; a5b09c3..54aa69a touches only adopt-project.py and two adopt tests, so it cannot change any of these rows.

Case main a5b09c3 this head
bump --member IRRS --to <the pinned commit> (the issue's repro) exit 2; stderr REFUSED a git or gh command failed ... git commit -q -F - -- members/IRRS family.yaml ... exit 1 ... nothing to commit, working tree clean; stdout empty exit 1; stdout IRRS <sha12> -> <sha12> tree <sha12>… and nothing to do: InkRouter/IRRS is already pinned at <sha12> and every fact agrees.; stderr empty; HEAD, gitlink and git status --porcelain identical before and after
the same bump run again after it landed, with --trailer exit 2, REFUSED; family.yaml byte-identical exit 1, nothing to do; family.yaml byte-identical
an uncommitted edit to the member's id: row, then a bump to the pinned commit exit 2 and the edit gone (git status clean) exit 1 and the edit kept ( M family.yaml before and after)
a stray gitlink STAGED (git -C members/IRRS commit --allow-empty, then git add members/IRRS, not committed; validate-family.py rc 1, member-gitlink-mismatch), then bump --to <the pinned commit>, the validator's own remedy exit 2, REFUSED ... git commit -q -F - -- members/IRRS family.yaml ... nothing to commit; the index ends at the pinned gitlink and the validator at rc 0 exit 1; stdout the IRRS <sha12> -> <sha12> tree <sha12>… and nothing to do: InkRouter/IRRS is already pinned at <sha12> and every fact agrees. lines; stderr empty; git status --porcelain empty, index gitlink at the pinned commit, validator rc 0
ordinary bump to a new commit exit 0, one commit, members/IRRS and family.yaml output and exit code byte-identical to main (diffed, 12-hex shas and scratch paths normalized)
gitlink moved alone, then bump back to the commit the row already names validate-family.py rc 1 (member-gitlink-mismatch) before; bump exit 0 and one commit; rc 0 after byte-identical to main (diffed, normalized)

Tests

Targeted set on head c7e8f5d40940cc1c648fcb119915234fa0b9c091 (two commits on base a5b09c3: 66ace9b and the fix round): tests/test_family.py tests/test_shape_doctor.py tests/test_repo_hygiene.py tests/test_cli_reference.py give 328 passed in 315.35s (0:05:15), exit 0 (84 + 181 + 50 + 13). On 66ace9b the same set was 327 passed in 582.45s (0:09:42). Eight new tests in tests/test_family.py (the first is parametrized plain and with --trailer): bump to the pinned commit is nothing to do with no commit, nothing staged, family.yaml byte-identical and validate-family.py green; it ignores what the member holds untracked or modified; it leaves an uncommitted family.yaml edit alone; it unstages a stray gitlink and says nothing (fix round 1: fails on 66ace9b with the REFUSED text, passes here); it finishes a bump an earlier run staged and never committed; it repairs a gitlink that moved alone; it repairs a digest that does not recompute. One existing test gained an assertion that an ordinary bump does not say nothing to do. No new test carries a Windows skip marker (WINDOWS_SKIP), so the Windows job runs all of them; they use git, run_script and git status --porcelain the way tests/test_bump_leg.py does. The full suite was not run here; CI runs it on three OSes.

Composes with

Re-run on head c7e8f5d: git merge-tree --write-tree <sibling> HEAD, then the whole of tests/test_family.py in the merged tree.

Sibling Sha Trial merge tests/test_family.py merged
fix/family-add-member-case (#206, same file, also touches cmd_bump) 9725bd269d47 local (6c917d60774c pushed: also CLEAN) CLEAN 99 passed
origin/fix/decode-git-output-utf8 (#217, touches _commit_one) b6f7cfdcfbd6 CLEAN 84 passed
origin/fix/family-add-check-before-mount (lane 4's #203) d25dd4d1a40d CLEAN 84 passed
fix/plan-report-stdout-encoding (local branch, not pushed, touches scripts/family.py) 300630329549 CLEAN not run
origin/main 54aa69a370f3 (adopt-project.py and adopt tests only) CLEAN not run

No sibling's assertions break this change, and this change breaks none of theirs.

Left out

  • family.py bump has no --dry-run, though its docstring says a dry run prints and exits 0 and bump-leg.py has one; a nothing to do answer still costs the member fetch and checkout bump always did.

Closes #210

🤖 Generated with Claude Code

Summary by Sourcery

Make family.py bump distinguish completed no-op reruns from actual repairs, reporting consistent pins as nothing to do without creating commits or discarding user changes.

New Features:

  • Treat rerunning family.py bump at an already consistent pinned commit as a no-op with exit code 1 and a clear nothing to do message.

Bug Fixes:

  • Avoid surfacing Git's empty-commit failure as a bump refusal when there is nothing to record.
  • Preserve pre-existing manifest edits and correctly handle staged or inconsistent gitlinks and digests while determining whether a bump has work to commit.

Enhancements:

  • Document the bump no-op exit-code behavior and ensure ordinary repair bumps still create commits when the manifest, gitlink, or digest differs.

Tests:

  • Add coverage for repeated bumps, trailers, member working-tree changes, preserved manifest edits, staged gitlinks, incomplete prior bumps, gitlink repairs, and digest repairs.

…othing to do` and exits 1, so a re-run of a bump that landed is no longer `REFUSED a git or gh command failed`, exit 2, around git's `nothing to commit` (#210)

`cmd_bump` checked the member out at `--to`, rewrote the `members:` block with
the row it already had and handed `git commit -- <member path> family.yaml`
nothing. git's own `nothing to commit` (exit 1) reached `main`'s generic
handler as `REFUSED a git or gh command failed`, exit 2 - where this file's
docstring documents exit 1 for nothing and `bump-leg.py` answers the same
re-run with `nothing to do`, exit 1. A lane or script re-running a landed bump
read a refusal to act on.

After that rewrite and before `_commit_one`, `cmd_bump` now asks git whether
the commit has anything to record: `_nothing_to_record` runs
`git diff --quiet --ignore-submodules=dirty HEAD -- <member path> family.yaml`,
reads its exit 1 as "there is a difference" and lets any other failure
propagate.

- When there is nothing to record, `bump` prints its usual `old -> new tree`
  line, then `bump-leg.py`'s sentence - `nothing to do: <org>/<Project> is
  already pinned at <sha12> and every fact agrees.` - on stdout, says nothing
  on stderr and exits 1. It writes no commit and stages nothing.
- `family.yaml` goes back byte for byte as it was found. The rewrite recorded
  nothing, and an uncommitted hand edit it happened to overwrite is not this
  run's to discard (on main the same run threw the edit away before it
  refused).
- git is asked, and `was == --to` is not read. A row that names `--to` is not
  by itself a bump with nothing in it, and three cases keep committing:
  a gitlink that moved alone and a digest that does not recompute
  (`validate-family.py` names `bump --member <Project> --to <commit>` as the
  repair for both), and a bump an earlier run wrote and staged but never
  committed.
- `--ignore-submodules=dirty` is what `git commit` itself ignores: files the
  member holds untracked or modified are not recorded by this commit, its HEAD
  is, so they do not make a bump to the pinned commit look like one with
  something in it.
- The module docstring's EXIT CODES says which bump is `nothing`.

Measured with a throwaway family in local bare remotes, main a5b09c3 versus
this head:

- `bump --member IRRS --to <the pinned commit>` (the issue's repro): exit 2
  and `REFUSED ... git commit -q -F - -- members/IRRS family.yaml ... nothing
  to commit` on stderr, then exit 1 and the two stdout lines above with an
  empty stderr; HEAD, gitlink and `git status --porcelain` identical before
  and after in both.
- The same bump run again after it landed, with `--trailer`: exit 2, then
  exit 1; `family.yaml` byte-identical before and after in both.
- An uncommitted edit to the member's `id:` row, then a bump to the pinned
  commit: exit 2 and the edit gone from the working copy, then exit 1 and the
  edit still there (` M family.yaml` before and after).
- An ordinary bump to a new commit, and a bump back to the commit a row
  already names after the gitlink moved alone: output and exit code diffed
  against main after normalizing the 12-hex shas and the scratch paths,
  byte-identical (exit 0, one commit, `validate-family.py` green after).

Unchanged: every flag and `--help` text (`docs/cli.md` is not touched), the
commit message and trailers of a bump that commits, `add` and `remove`, and
the README and AGENTS.md, which do not describe `bump`'s exit codes.

Left out: `family.py bump` has no `--dry-run`, though the docstring promises
one prints and exits 0 and `bump-leg.py` has one; a nothing-to-do answer still
costs the member fetch and checkout it always did.

Lane: openreposhape-3 (openRepoShape-3)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-11T18:56Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 11, 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 1 day and 4 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@sourcery-ai

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

family.py bump now asks Git whether its member and manifest rewrite would record anything, cleanly reports an already-consistent pin as nothing to do with exit 1 while preserving user edits, and still commits corrective or previously staged changes; tests cover the no-op behavior and edge cases.

Sequence diagram for family bump no-op detection

sequenceDiagram
    participant User
    participant Family as family.py
    participant Git
    participant Files as Member and family.yaml

    User->>Family: cmd_bump
    Family->>Files: write_members
    Family->>Git: git diff --quiet --ignore-submodules=dirty HEAD -- <member path> family.yaml
    alt No differences
        Git-->>Family: exit 0
        Family->>Files: write_bytes(original)
        Family-->>User: print nothing to do
        Family-->>User: exit 1
    else Differences exist
        Git-->>Family: exit 1
        Family->>Git: _commit_one
        Git-->>Family: commit changes
        Family-->>User: exit 0
    end
Loading

File-Level Changes

Change Details Files
Detect no-op bumps before attempting a commit and return the documented nothing result.
  • Compare the rewritten member path and family.yaml against HEAD with git diff --quiet --ignore-submodules=dirty.
  • Treat only git diff exit 1 as a no-op; propagate other git failures.
  • Restore family.yaml byte-for-byte when no changes would be committed.
  • Print the existing bump summary plus the repository-qualified nothing to do message and exit 1 without staging or committing.
scripts/family.py
Clarify the bump exit-code contract and add coverage for no-op and repair scenarios.
  • Document that a bump recording no changes is exit 1 and creates no commit.
  • Test reruns, trailer reruns, member working-tree changes, preservation of uncommitted manifest edits, staged-but-uncommitted bumps, gitlink repairs, and digest repairs.
  • Assert ordinary and corrective bumps do not incorrectly report nothing to do.
scripts/family.py
tests/test_family.py

Assessment against linked issues

Issue Objective Addressed Explanation
#210 When family.py bump is run for a member already pinned at --to with matching gitlink and digest, report that there is nothing to do and exit with code 1 instead of surfacing Git's empty-commit failure as a refusal with exit code 2. ✅
#210 Ensure an already-completed bump creates no commit or staged changes and preserves the existing family.yaml contents, including pre-existing uncommitted edits. ✅
#210 Retain normal bump behavior for cases where a commit is still needed, including repairing a moved gitlink, correcting a digest, or completing previously staged changes, and document/test the updated exit-code behavior. ✅

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

…hether there is anything to record, so a stray staged gitlink and a bump to the pinned commit say `nothing to do` and no longer end in `REFUSED ... nothing to commit`, exit 2 (#210)

Fix round 1 on #219, from the independent review.

`validate-family.py` reads the INDEX first. A member somebody moved and
`git add`ed without committing is reported as `member-gitlink-mismatch`, and
the validator names `family.py bump --member <Project> --to <commit>` as the
repair. `bump` checks the member out at `--to`, which for the pinned commit is
HEAD's gitlink, but the index still held the stray one, so
`git diff --quiet HEAD` found a difference, `_commit_one`'s own `git add`
re-staged the pinned gitlink and `git commit` found nothing: the refusal this
branch closes, exit 2, reached through the validator's remedy.

`cmd_bump` now runs `git add -- <member path>` immediately before
`_nothing_to_record`, as `_commit_one` does first anyway. The checkout already
moved the member to `--to`, so this stages that gitlink and nothing else: on a
submodule path `git add` stages the gitlink and never the member's files, and
`family.yaml`'s own index state is left as found. The commit path is unchanged,
because `_commit_one` stages the same path again.

- A stray gitlink staged in the index and a bump to the pinned commit: exit 1,
  the `nothing to do` line, `git status --porcelain` empty, the index back at
  the pinned gitlink, `family.yaml` byte-identical and `validate-family.py`
  green. On the previous head the same run was exit 2 with `REFUSED ... git
  commit -q -F - -- members/IRRS family.yaml ... nothing to commit`, as on main.
- A new test, `test_bump_to_the_pinned_commit_unstages_a_stray_gitlink_and_says_nothing`,
  stages a stray gitlink, checks the validator reports
  `member-gitlink-mismatch`, bumps to the pinned commit and asserts the line
  above. It fails on 66ace9b with the refusal and passes here.

Unchanged: every other bump path (the ordinary bump, a gitlink that moved
alone, a digest that does not recompute and a bump an earlier run staged and
never committed all still commit), every flag and `--help` text.

Lane: openreposhape-3 (openRepoShape-3)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-11T19:31Z) fix round 1 (independent review, codex is out of quota): bump now re-stages the member's gitlink before asking git whether there is anything to record, so a stray gitlink that was staged and never committed (validate-family.py reports member-gitlink-mismatch and names bump as the repair) no longer makes a bump to the pinned commit end in REFUSED ... nothing to commit, exit 2; it says nothing to do, exit 1, with the index back at the pinned gitlink and the validator green, and a new test fails on 66ace9b and passes here; targeted set 328 passed (test_family 84, test_shape_doctor 181, test_repo_hygiene 50, test_cli_reference 13) on c7e8f5d.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-11T19:34:42Z)

This lane reviewed PR #219 at 66ace9b 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 c7e8f5d (19:21Z), which this review did not cover.

MERGEABLE WITH NITS, no sequencing constraint.

The issue's repro on main gives exit 2, REFUSED ... nothing to commit. On head it gives exit 1 with nothing to do: InkRouter/IRRS is already pinned at 466412c2e9d5 and every fact agrees., empty stderr, and HEAD, index, status and family.yaml unchanged. The same holds with two --trailer lines, with an upper-case --to, and with a CRLF family.yaml under core.autocrlf, whose bytes are put back.

Findings:

  • P3, scripts/family.py:948-961. The restore keeps an uncommitted family.yaml edit, and the run still says "every fact agrees" and exits 1 while the holder's validator is red:

    • an uncommitted id: edit gives member-id-mismatch before and after;
    • a hand-edited pin, bumped back, prints 214c6e26100d -> 466412c2e9d5 above the sentence and gives member-gitlink-mismatch.

    Main exits 2 in both. Keeping the edit is right, but the sentence should name it, or refuse an uncommitted manifest as bump-leg.py's _ensure_root_clean does. tests/test_family.py:747 asserts the sentence in that red state.

  • nit, scripts/family.py:893. A stale staged gitlink, with the checkout back at the pin, still exits 2 REFUSED. git's --quiet names the gitlink with an empty patch. Same as main; c7e8f5d's subject says it addresses this.

  • nit, scripts/family.py:935. The decision follows the member checkout, so a checkout moved off the pin is moved back and the run still says nothing to do. Main moved it too.

  • nit. Mutant M5 (any git failure read as a difference) survives all 83 tests.

  • Separate, pre-existing, no open issue: bump sweeps any uncommitted family.yaml edit into its commit. A header edit with a bump to the pinned commit exits 0 on main and head with a Bump IRRS commit holding only that edit, the holder twin of bump-leg.py rewrites and commits every workflow file it finds on disk, so an untracked draft workflow that names the leg is added whole to the lockstep commit, though the clean-root check skips untracked files because they "cannot reach a commit made with explicit pathspecs" #207.

Tests:

  • Head: test_family, test_commit_trailers, test_repo_hygiene and test_cli_reference, 271 passed. render-cli-reference.py --check ok.
  • Head's test_family.py on main: 4 failed (the nothing-to-do cases, assert 2 == 1) and 79 passed.
  • Mutants killed: no exit, no --ignore-submodules=dirty, no restore, was == --to, and the issue's was == --to and digest equal.

Merges, ls-remote 19:2xZ:

Report: opensoft/brett-wip handoffs/xFactory/attachments/lane-openRepoShape-2/review219-66ace9b/final-report.md.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit d89fffa into main Oct 11, 2026
5 checks passed
@brettheap
brettheap deleted the fix/family-bump-nothing-to-do branch October 11, 2026 23:05
brettheap added a commit that referenced this pull request Oct 12, 2026
…, #252 via #257, #195 via #226, #210 via #219, #211 via #220, #248 via #265, #212 via #223) into fix/adopt-record-submodule-placement

Brings in 06b5204 (#214), 60d08a1 (#217), 2727b08 (#209), 4b2add8 (#252),
aaa40eb (#195), d89fffa (#210), eb6df75 (#211), c3f4987 (#248) and
5751460 (#212) on top of 39ac59c. No rebase. 5751460 landed while the
merge of c3f4987 was being checked, and it changes adopt-project.py,
scripts/shape_materialize.py and tests/test_adopt_submodules.py, as this
branch does, so this one merge takes it too.

No conflicts. Both sides change adopt-project.py, scripts/repo_shape.py,
scripts/shape_materialize.py, shape-doctor.py, tests/test_adopt_submodules.py
and tests/test_shape_doctor.py. Every top-level definition only one side
changed is that side's text in the merge, byte for byte. The one thing
both changed is shape_materialize.py's imports, and both are kept: main's
`codecs` and this branch's `path_spelling_fault` and `PATH_*` codes.
Checked:

- adopt-project.py: main calls `escape_what_stdout_cannot_encode()` first
  in `main` (#214). It makes a `.gitmodules` git's submodule reader cannot
  read a finding where the tip holds no submodule and the history does, or
  where `ignore`, `update` or `branch` has no value (#195). And every path
  a finding names is now `_spelled` (#212). This branch's
  `submodule_record`, `_resolution_text` and the record's preamble are
  unchanged by the merge, and so are `_answered_legs`, `_kept_in_a_leg`,
  `_commit_the_split`, `_refuse_what_check_finds`, `cmd_execute` and `Plan`.
  The record and #195's finding read the kept submodules the same way,
  `_kept_in_a_leg(_answered_legs(...))` over the tip's gitlinks. `execute`
  refuses #195's finding in `_refuse_what_check_finds`, before any leg
  exists and before `_commit_the_split` writes the record. So a source
  #195 refuses gets no record, and so does a source whose tip holds no
  submodule.
- scripts/repo_shape.py: main refuses an explicit key in a flow mapping
  and reads an empty flow-mapping value as null (#252). A block scalar's
  record is now a (key, body) pair, and `_BLOCK_SENTINEL` is gone (#248);
  nothing on this branch names it. The record is a block sequence of
  double-quoted scalars, and it reads back through the merged reader with
  0 mismatches (`submodule_record`, then `parse_yaml`, path and
  resolution):
  - 4,525 names, plus 35 more;
  - 240 aimed at #252 and #248 (`? a`, `{a: }`, `|`, `a: |`, a NUL, the
    old sentinel).
- scripts/shape_materialize.py: main adds `escape_what_stdout_cannot_encode`,
  `run()` reading UTF-8 (#217), and `first_and_more(paths, spell)` (#212).
  This branch's `LEG_SEGMENT_REASONS` and `_spelling_reason` are untouched.
- shape-doctor.py: main adds the `manifest kinds` finding for a
  `family.yaml` beside `project.yaml` (#211), and the stdout escaping.
  This branch's placement row and `recorded_submodules` are untouched.
- tests: #212 respells the finding text that the earlier tests in
  test_adopt_submodules.py assert. None of this branch's tests quotes a
  finding.
- 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, minus what
  either side removed. No side added a name the other side also added,
  in the tests either.
- AGENTS.md is untouched by main: 472 lines, under the cap of 472.
- The branch's diff against main is still #189 alone: 12 files, +1670
  -156, file by file the same as against 39ac59c.

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
…, #209, #252, #195, #210, #211, #248, #212, #251 and #206 merge with one hunk, the materializer import list, kept on both sides, and `_entry_findings` still hands `_leg_findings` the tree and ends with the root-symlink findings

origin/main moved 39ac59c -> 60588d5: 06b5204 (#214 via #246), 60d08a1
(#217 via #230), 2727b08 (#209 via #218), 4b2add8 (#252 via #257),
aaa40eb (#195 via #226), d89fffa (#210 via #219), eb6df75 (#211 via
#220), c3f4987 (#248 via #265), 5751460 (#212 via #223), 23e16ae (#251
via #271) and 60588d5 (#206; its subject names no pull request).

Conflict, resolved by keeping both sides:
- adopt-project.py, the `shape_materialize` import list. This branch
  adds BOOTSTRAP, SYMLINK_HOPS and template_files; #214 adds
  escape_what_stdout_cannot_encode. The resolution is the union.

Auto-merged: scripts/shape_materialize.py, where #214's escaping, #217's
UTF-8 `run()` and #212's `first_and_more(paths, spell=None)` sit apart
from this branch's #183 section, and adopt-project.py's other hunks.
#195 adds `import os` at the line where this branch added it, and the
merge keeps it once.

Checked on the merged tree:
- `_entry_findings` still calls `_leg_findings(plan, tree)` and ends
  with `_root_symlink_findings(plan, source, tree)`. `main` still calls
  `escape_what_stdout_cannot_encode()` first.
- 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
  (AST). `first_and_more` gives its new `spell` a default, so this
  branch's call is unchanged.
- Both sides are intact by +/- line sets: each side's change against the
  base reappears against the other side, apart from the import hunk and
  the shared `import os`.
- #212 says this branch's `plan-root-symlink` names its paths raw, and
  that whichever pull request lands second should spell them. That was
  04792d2. Since d11cf9f every path the finding names goes through
  `_spelled`.

One clash the textual merge does not show is fixed in the next commit:
#217's tests/test_git_output_decoding.py refuses a shipped text-mode
`subprocess` call that names no codec, and this branch's
`_checks_out_symlinks` probe names none.

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
…e-notice

It merged with no conflict. Main brings nine commits since 39ac59c:

- 06b5204, six tools escape a name their stdout cannot encode (#214
  via #246), with tests/test_stdout_encoding.py;
- 60d08a1, `shape_materialize.run()` and 26 other git and gh calls
  read the child's output as UTF-8 (#217 via #230), with
  tests/test_git_output_decoding.py;
- 2727b08, update-shape.py's `gh pr create --base` (#209 via #218),
  with tests/test_update_shape_pr_base.py;
- 4b2add8, scripts/repo_shape.py's flow mappings (#252 via #257), with
  tests/test_yaml_flow_mappings.py;
- aaa40eb, adopt-project.py refuses a `.gitmodules` the history's
  gitlinks make unreadable (#195 via #226), with
  tests/test_adopt_history_gitlinks.py;
- d89fffa, family.py `bump` to the current pin (#210 via #219);
- eb6df75, shape-doctor.py's family.yaml beside a project.yaml (#211
  via #220);
- c3f4987, scripts/repo_shape.py's block-scalar records (#248 via
  #265), with tests/test_yaml_block_scalar_keys.py;
- 5751460, adopt-project.py spells every path a FINDING or a refusal
  names (#212 via #223), with tests/test_adopt_finding_spelling.py.

Three files changed on both sides: adopt-project.py,
tests/test_adopt_ignored_shape_files.py and tests/test_adopt_verify.py.
In each, `git diff -U0` of this merge against 5a8b649 equals main's
diff against 39ac59c, and this merge against 5751460 equals the
branch's diff against 39ac59c, apart from the hunk headers. Neither
side's lines are changed.

How main's changes meet this branch's failure handling:

- Every definition the late failures go through is byte-identical to
  5a8b649's: LATE_FAILURES, the notice (`_nothing_rolled_back`),
  `_failed_loudly`, `_report_late`, the three builders' handlers,
  `_verify` and `cmd_execute` among them. Three definitions differ, each
  by main's hunk alone.
- `main()` gains `escape_what_stdout_cannot_encode()` as its first
  statement (06b5204). The branch writes every REFUSED block and notice
  to stderr, which Python opens with `backslashreplace` whatever the
  locale.
- `run()` names its codec (60d08a1). Output that is not UTF-8 now
  reaches the branch's handlers as `CommandFailed`, not as
  `UnicodeDecodeError`. LATE_FAILURES keeps `UnicodeError`, because
  `git_out` in scripts/repo_shape.py still decodes strictly.
- `Source.registered_submodules` reads the history (aaa40eb). Its one
  caller in `execute` is `_refuse_what_check_finds`, before
  `_create_leg_remotes`, so the new refusal comes before any leg
  repository exists and needs no notice.
- 5751460 spells the paths of the verification's findings. The
  branch's footer and notice follow those findings unchanged.

No top-level name is bound twice in adopt-project.py,
scaffold-project.py, scripts/repo_shape.py or
scripts/shape_materialize.py, and a `symtable` check finds no name
they read that nothing binds. On the merged tree, ten test files give
439 passed and 1 skipped (Windows' own stdout default): the two shared
ones, main's four new ones that read adopt-project.py
(finding_spelling, history_gitlinks, stdout_encoding,
git_output_decoding), and test_adopt_late_failure,
test_adopt_filter_repo_failure, test_adopt_execute_refusals and
test_adopt_plan_escapes.

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
…`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)

`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/`) refused it only at git's last `git add`, after the clone.
`bump` died the same way under `members` or `*`: it stages the gitlink
twice, at the `git add` #219 put before its nothing-to-record check and in
`_commit_one`, and both read the excludes. This is #174 (adopt, fixed
by #200) and #229 (scaffold) on the family side.

Every reader is forced now. The mount passes `--force`, and every stage
`add`, `bump` and `remove` make is `shape_materialize.force_stage` (#233),
`git --literal-pathspecs add -f -- <paths>`: `-f` reaches only the named
paths, each taken literally, so no ignored neighbour a glob character would
match is staged with them (#202). One definition: `_commit_one` and `bump`'s
re-stage both call it.

`--force` turns off more than the ignore check, so each other thing it
changes is refused before the member is read, the holder left exactly as it
was, and a comment at the mount names each guard:
- a `.git/modules/<path>` left by a removed member, which `--force`
  reactivates: `member-git-dir-cached` (#236), at the store `git rev-parse
  --git-path` names, a linked worktree's included;
- an index entry at the path or under it with nothing on disk, which
  `--force` accepts and replaces with the member's gitlink:
  `member-path-taken` now asks the index too, and names the
  `git rm -r --cached` that clears it; on disk it asks `lexists`, so a
  dangling link is refused as well;
- git's own dry run, which `--force` skips (a symlinked `members` had the
  member cloned through the link, outside the holder; a `members` that is a
  submodule had it cloned into that submodule's tree): `member-path-unaddable`
  makes the same dry run first, with `-f`, so an ignore rule passes it and
  nothing else does; the refusal quotes git's line, and its remediation
  presumes neither a link nor a submodule (a held `index.lock` fails it
  too);
- a name another submodule already has, which git 2.51 and later rename the
  mount past and git 2.43 answers by rewriting the other section:
  `member-name-taken`, reading `.gitmodules` where git reads it (the working
  tree, else the index's copy, else HEAD's).
The PRIVATE/access sentence is printed only for the probe's failed clone of
the member, which no ignore rule reaches.

tests/test_family_add_excludes.py: under `members`, `members/`, `*`, `IRQS`,
`IRQS/` and none, `add` is ONE commit of `.gitmodules`, `family.yaml` and a
160000 gitlink at the member's tip, read with `git ls-tree`, that validates
under the same excludes and clones recursively for an operator with none;
`bump` under `members` moves the gitlink in one commit and `remove` is the
control; each guard above is refused before the member is read with the
holder unchanged. Mutations: without `--force` at the mount every rule's row
fails; without `-f` in `_commit_one` the `members`, `members/` and `*` rows
and the bump test fail; with `bump`'s re-stage a plain `git add` the bump
test fails; dropping the index check, the dry run, the name check, its
index/HEAD fallback or `lexists` fails exactly that guard's tests.

Closes #242

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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