Skip to content

update-shape.py: a push or gh pr create that fails after apply --branch committed is named as that step, keeps the commit and ends on a NEXT line, and a re-run with --pr on the branch opens the missing pull request, so the tool no longer refuses "committing the re-synced shape" and then answers "nothing to do" (#204) - #227

Merged
brettheap merged 8 commits into
mainfrom
fix/update-shape-pr-create-failure
Oct 12, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

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

Seven commits, to land as one squash. The final message is drafted by the lane:

What

_land commits, pushes and opens the pull request. A failed push or gh pr create used to reach cmd_apply's one except CommandFailed arm. That arm called every failure "committing the re-synced shape", printed no NEXT line and did not flush stdout. A missing gh escaped as a traceback. The re-run then said "nothing to do" and exited 0. The commit is now where rolling back stops:

Why

The issue's repro, re-measured on this machine against main 2727b08, which carries #218. Atlas is scaffolded from a clone of main 39ac59c into local bare remotes, with one upstream commit B (25ff23b96682); the tree under test is main 2727b08 against this branch at c874e4f. gh is a PATH shim that logs its argv and exits 97 on pr create while a marker file exists. Every run uses the same path and fixed git dates, so outputs compare byte for byte.

run main 2727b08 this branch (c874e4f)
r1 --branch shape/update-y --push --pr, gh pr create fails exit 2 REFUSED committing the re-synced shape, no NEXT line exit 2 REFUSED opening the pull request for shape/update-y; …origin has it: that commit stands and nothing was rolled back; NEXT = the gh pr create the shim logged
r2 the same command again, gh succeeds exit 0 nothing to do, no gh call exit 0: gh pr list, then the same gh pr create; pull request …/pull/7; 1 commit
r3 a third time exit 0 nothing to do exit 0 …/pull/7 is already open; gh pr list only
r4 = r1 with > log 2>&1 REFUSED on line 13, committed on on 21, pushed on 22 committed on 14, pushed 15, REFUSED 16, NEXT 25
r5 --push --pr, origin refuses the push exit 2 REFUSED committing the re-synced shape, no NEXT line exit 2 REFUSED pushing shape/update-y; NEXT git -C <root> push -u -- origin shape/update-y && gh pr create --repo testorg/Atlas …
r5n --push without --pr, origin refuses the push exit 2 REFUSED committing the re-synced shape, no NEXT line exit 2 REFUSED pushing shape/update-y; NEXT git -C <root> push -u -- origin shape/update-y && gh pr create --repo testorg/Atlas --base main --head shape/update-y; no re-run promised
r5b r5 re-run after the push URL is fixed exit 0 nothing to do; the branch never reaches origin exit 0: pushed, pull request opened, 1 commit
r8 re-run where gh reports the pull request MERGED exit 0 nothing to do exit 0 …/pull/5 for shape/update-y is merged; a re-run opens no second one; gh pr list only, no push
r9 --pr without --push, gh pr create fails exit 2, the same mislabel exit 2 REFUSED opening the pull request …; NEXT pushes first; promises apply --push --pr
g1 PATH=/usr/bin:/bin (git and python3, no gh), --push --pr exit 1 FileNotFoundError: … 'gh' after the commit and the push exit 2 REFUSED opening the pull request for shape/update-y, exit 127; NEXT gh pr create --repo testorg/Atlas …
g2 g1 re-run, still no gh exit 0 nothing to do exit 2 REFUSED asking gh for shape/update-y's pull request, exit 127, the same NEXT line, still 1 commit
--branch= -shape, --upload-pack=touch, shape/a b, shape/x@{1} exit 2 REFUSED unsafe-value after 12 lines of rewrite, rolled back exit 2 REFUSED update-branch-invalid, nothing on stdout, tree untouched
--branch= shape/a..b, shape/x.lock exit 2 REFUSED creating the branch … after 12 lines of rewrite, rolled back by #233 exit 2 REFUSED update-branch-invalid (git's verdict quoted), nothing on stdout, tree untouched
--branch=-shape with nothing to do exit 0 nothing to do exit 2 REFUSED update-branch-invalid
check; --branch and its re-run; --branch --push and its re-run; --branch --push --pr with gh succeeding; --branch=shape/update-y --push --pr byte-identical to main in stdout, stderr, exit code and the gh argv log (cmp)
r10 --branch --push --pr, a pre-commit hook refuses; r10b the same command once the hook is gone byte-identical to main. r10: exit 2 REFUSED committing the re-synced shape on shape/update-y, then #233's ROLLED BACK block; origin has no branch, only main is left, the tree is clean, gh is never called. r10b: exit 0, committed, pushed, one gh pr create, 1 commit

Tests

tests/test_update_shape_landing.py has 33 tests.

Round 4 asserts --repo testorg/Atlas in the landing failed_push[push] line. It also checks both pull_request_line forms in #218's repository test (project, and both family spellings), and the bare form without --repo in #218's no-repository test. Run against the merge commit 87ba8c5, which does not have the change, these give 4 failed, 8 passed: the three repository cases and failed_push[push] fail, and the no-repository guard passes before and after. Its first 15 gave 10 failed, 5 passed on main a5b09c3. The 5 that pass there pin behaviour that must not change.

Round 3 adds two tests:

  • test_a_failure_before_the_commit_under_push_and_pr_is_rolled_back runs on every platform, because gh is never reached.
  • test_the_same_command_after_a_rolled_back_commit_lands_and_opens uses the gh stub.

In the merge, #233's branch-invalid case in tests/test_update_shape_add_ignored.py becomes test_a_branch_git_cannot_name_is_refused_before_any_write. That test asserts update-branch-invalid, empty stdout, and a tree untouched with the operator's staged work in it.

Mutations, each run against the whole file:

Targeted set on c874e4f71058bd6bba94ae100c92603ef8d4a6d1:
tests/test_update_shape.py, tests/test_update_shape_add.py, tests/test_update_shape_add_ignored.py, tests/test_update_shape_landing.py, tests/test_update_shape_pr_base.py, tests/test_shape_doctor.py, tests/test_repo_hygiene.py, tests/test_cli_reference.py → 424 passed in 839.27s (0:13:59). At c96396b, before #218, it was 397 passed.

scripts/render-cli-reference.py --check: [ok]. Merging main did not change any --help text. pyflakes is clean on update-shape.py and both test files.

Base tested: 2727b08. Main has since moved to 4b2add8 (#257, scripts/repo_shape.py flow mappings and its own test); git merge-tree --write-tree 4b2add8 c874e4f is clean.

On Windows:

Composes with

sibling sha result
#224 fix/update-shape-manifest-indent (#208) e7d1720 on main landed; merged in be70651, no conflict
#233 fix/update-shape-add-rollback (#202, lane 4) 150c41d on main landed; merged in be70651, 3 hunks resolved as below
#218 fix/update-shape-pr-base-branch (#209) 2727b08 on main landed; merged in 87ba8c5, no conflict; both pair changes made, see below
#230 fix/decode-git-output-utf8 (#217) 60d08a1 on main landed; merged in 87ba8c5, no conflict; run still raises OSError for a program that cannot start, which _landing_run catches
fix/update-shape-add-dangling-symlink 7bceeca CLEAN (git merge-tree --write-tree 7bceeca c874e4f)

#233, as merged in be70651. These are the steps this body stated before #233 landed. I re-derived them against the landed 150c41d, which is much larger than the c022b64 they were first trialled on:

  1. EXIT CODES apply line: one sentence, "2 a REFUSAL, including a validator that went red or a branch, stage or commit that failed (rolled back); or a push or pull request failing after the commit, which is KEPT, and the NEXT line printed then finishes the job (update-shape.py apply --branch --push --pr: when gh pr create fails after the commit and the push succeeded, it exits 2 with "REFUSED committing the re-synced shape" and no NEXT line, and a re-run on that branch says "nothing to do" and exits 0, so the tool never opens the pull request #204)".
  2. _land docstring: both paragraphs, update-shape.py: apply --add force-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, so check no longer exits 0 over an uncommitted re-pin (#202) #233's ledger sentence first.
  3. The third hunk: update-shape.py: apply --add force-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, so check no longer exits 0 over an uncommitted re-pin (#202) #233's _rolled_back, then this branch's LandingIncomplete … pull_request_target block. Only update-shape.py: apply --add force-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, so check no longer exits 0 over an uncommitted re-pin (#202) #233's old def open_pull_request(root, branch, target, upstream, kind) header is dropped.
  4. The stale sentence in _rolled_back: it now says that a push or pull request failing after the commit never reaches that arm. LandingIncomplete is neither of the arm's classes.
  5. tests/test_update_shape_add_ignored.py: the branch-invalid entry leaves FAILURES and becomes its own test (see Tests). checked_branch refuses shape/update..ignored in main before anything is written. update-shape.py: apply --add force-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, so check no longer exits 0 over an uncommitted re-pin (#202) #233's branch-df case still covers a branch git cannot create after the writes.

Lane 4 resolved the same merge independently (its landing note on this PR, 22:06Z). Its tree has the same code, and the same _land docstring, as be70651. The two differ in three places only: the line wrap in step 1, the wording in step 4, and that lane 4 deletes the branch-invalid case where be70651 keeps it as a test.

#218, as composed in 87ba8c5 and c874e4f. #218's pull_request_repository merged into pull_request_target, where this branch moved the manifest reading open_pull_request used to do. So --pr, the failure NEXT line and the re-run's gh pr list all read one repository and one base. The two changes the second lander owed are made:

  1. test_a_manifest_naming_no_repository_refuses_pr_and_leaves_the_placeholder calls update_shape.pull_request_target(tmp_path, kind) in place of the old five-argument open_pull_request (87ba8c5). update-shape.py apply --branch prints the root's own tracking branch as the gh pr create --base and its assembly repository as the --repo, the two values --pr passes, so a project scaffolded with --tracking-branch trunk is no longer told to open its pull request against main, and the --push line no longer prints a literal <org>/<Project> that a shell reads as a redirection (#209) #218's docstrings that named open_pull_request as the manifest's reader now name pull_request_target.
  2. pull_request_line's bare form carries --repo (c874e4f), so every gh pr create this tool prints or runs names the repository.

Left out

Closes #204

🤖 Generated with Claude Code

Summary by Sourcery

Keep committed shape updates intact after landing failures and make subsequent pull-request completion safe and actionable.

New Features:

  • Allow a later apply --pr run to complete a missing pull request after the branch commit has already been created.

Bug Fixes:

  • Preserve commits when pushing or opening a pull request fails, report the actual failed step, and provide a pasteable recovery command.
  • Handle missing gh installations as actionable landing failures instead of uncaught tracebacks.
  • Validate branch names before any repository changes or commands are performed.

Enhancements:

  • Distinguish failures before the commit, which remain rollback cases, from failures after the commit, which leave the commit intact and return exit code 2.
  • Check existing pull requests before retrying and avoid recreating or repushing branches for open, merged, or closed requests.
  • Keep generated pull-request commands aligned with manifest repository and tracking-branch settings.

Documentation:

  • Update CLI help to describe pull-request completion on a later run.

Tests:

  • Add comprehensive landing tests covering failed pushes, failed pull requests, missing gh, retries, rollback behavior, branch validation, and cross-platform execution.

…ranch` committed is named as that step, keeps the commit and ends on a NEXT line, and a re-run with `--pr` on the branch opens the missing pull request, so the tool no longer refuses "committing the re-synced shape" and then answers "nothing to do" (#204)

`_land` commits, pushes and opens the pull request. A `CommandFailed` from the push or from `gh pr create` used to reach `cmd_apply`'s one `except CommandFailed` arm, which named every failure "committing the re-synced shape", printed no NEXT line and did not flush stdout first. The run after it found the pin already at the target, said "nothing to do" and exited 0, so the tool never opened the pull request `--pr` asked for.

The commit is now where rolling back stops:
- `push_branch` and `open_pull_request` catch their own failure and hand it to `landing_failed`. The refusal names the step (`REFUSED pushing <branch>`, `REFUSED opening the pull request for <branch>`), says the commit stands and nothing was rolled back, and ends on a NEXT line. The exit stays 2.
- The NEXT line is the `gh pr create` that `--pr` runs, title and body included, shell-quoted so it pastes as the same argv. `pull_request_line` builds it before the commit. When this clone's `origin/<branch>` is not HEAD, the line pushes first: `git -C <root> push -u origin <branch> && gh pr create ...`. Under `--push` without `--pr` it ends on the bare `gh pr create --base <tracking> --head <branch>`.
- stdout is flushed before the refusal, so a combined log reads commit, push, REFUSED, NEXT, in that order.
- `LandingIncomplete` carries the failure to `main` as exit 2. It is neither a `Refusal` (that arm rolls the working tree back under a commit holding those bytes) nor a `CommandFailed` (that arm names the commit).
- A re-run with `--pr` and nothing to write finishes the landing when the checkout is on `--branch`, that branch is not the tracking branch, and its `contracts/shape-pin.yaml` differs from `origin/<tracking>`'s. `_finish_landing` asks `gh pr list --state all` before anything else:
  - an open pull request is reported, and no second one is opened;
  - a merged or closed one is reported, and nothing is pushed, so a merged branch the remote deleted stays deleted;
  - with none, `--push` pushes again and the same `gh pr create` runs. There is no second commit.
- The refusal promises that re-run only under `--pr`, and names `apply --push --pr` when origin does not have the branch.
- The `--pr` help says a later run on the branch opens a pull request that is still missing. docs/cli.md is regenerated.

Unchanged, and measured byte-identical to main a5b09c3 in stdout, stderr, exit code and the `gh` argv log:
- `check`;
- `apply --branch`, and its re-run;
- `apply --branch --push`, and its re-run;
- `apply --branch --push --pr` with `gh` succeeding.

A re-run without `--pr`, from a checkout not on `--branch`, with `--branch` naming the tracking branch, or on a branch whose pin is `origin/<tracking>`'s still prints "nothing to do" and exits 0, asking `gh` nothing and pushing nothing.

Left out: the `--base main` that `next_line` prints after a successful `--branch` or `--branch --push` is #209's; a failure inside the commit, staging an `--add` path, is #202's; the adopt-side unflushed log is #188's.

Lane: openreposhape-3 (openRepoShape-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@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 18 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-11T19:02Z) @codex review

@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

The PR changes landing so commits remain intact when pushing or opening a pull request fails, reports the precise failed step with a pasteable recovery command, and allows a subsequent eligible --pr run to discover or create the missing pull request without recommitting; documentation and extensive regression tests cover the new flow.

Sequence diagram for pull request recovery on rerun

sequenceDiagram
    participant User
    participant Apply as cmd_apply
    participant Check as _awaits_its_pull_request
    participant Finish as _finish_landing
    participant GH as gh
    participant Land as _land

    User->>Apply: rerun apply --branch --pr
    Apply->>Check: _awaits_its_pull_request(...)
    Check-->>Apply: eligible, nothing left to write
    Apply->>Finish: _finish_landing(...)
    Finish->>GH: gh pr list --state all --json state,url
    alt Pull request is open
        GH-->>Finish: OPEN
        Finish-->>User: pull request is already open
    else Pull request is merged or closed
        GH-->>Finish: MERGED or CLOSED
        Finish-->>User: no second pull request, no push
    else Pull request is missing
        GH-->>Finish: empty list
        Finish->>Land: push_branch(...) if --push
        Finish->>Land: open_pull_request(...)
        Land->>GH: gh pr create ...
        GH-->>User: pull request opened
    end
Loading

Flow diagram for post-commit landing failures

flowchart TD
    A[Commit re-synced shape] --> B{Push requested}
    B -- Yes --> C[push_branch]
    C -- Failure --> D[landing_failed]
    C -- Success --> E{Pull request requested}
    B -- No --> E
    E -- Yes --> F[open_pull_request]
    F -- Failure --> D
    E -- No --> G[Landing complete]
    F -- Success --> G
    D --> H[Flush stdout]
    H --> I[Print precise REFUSED step]
    I --> J[Print pasteable NEXT command]
    J --> K[Raise LandingIncomplete]
    K --> L[main returns exit 2]
Loading

File-Level Changes

Change Details Files
Preserve post-commit landing state and report the exact failing step with a recoverable NEXT command.
  • Build the pull-request command before committing so failure guidance reproduces the original arguments.
  • Handle push and pull-request failures separately, flush prior output, keep the commit, and return exit code 2 through a dedicated exception.
  • Generate shell-quoted NEXT commands that include a prerequisite push when the branch is not known on origin.
  • Update apply exit-code documentation and CLI help.
update-shape.py
docs/cli.md
Complete missing pull requests on eligible no-write re-runs without creating a second commit.
  • Detect re-runs only for --pr on the checked-out non-tracking branch whose pin proposes a change.
  • Query all pull-request states before pushing; avoid duplicate PRs and avoid restoring merged or closed remote branches.
  • Push when necessary and reuse the original PR creation command, including failure handling.
update-shape.py
Add comprehensive regression coverage for post-commit failures and recovery behavior.
  • Test failed push and PR creation messages, ordering, command quoting, retained commits, and exit codes.
  • Test successful re-runs, existing/open and merged/closed PRs, branch guards, tracking-branch behavior, and no-PR compatibility.
  • Use isolated bare remotes and a controllable gh stub, with an in-process test for cross-platform coverage.
tests/test_update_shape_landing.py

Assessment against linked issues

Issue Objective Addressed Explanation
#204 Ensure failures during the push or pull-request acts are reported as failures of the correct step, preserve the committed changes, flush prior stdout output, and provide an actionable NEXT command while retaining exit code 2. ✅
#204 When a pull-request creation fails after the branch has been committed and pushed, allow a subsequent apply --branch --push --pr run to detect the missing pull request and finish opening it without creating a second commit or pull request. ✅
#204 Update the CLI documentation and exit-code/help text to describe the preserved post-commit failure behavior and the ability of a later --pr run to open a still-missing pull request. ✅

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

…servative alphabet and then `git check-ref-format --branch`, before anything is read, written or run, so a value that reads as an option never reaches `git`, `gh` or a NEXT line (#204)

Fix round 1 for SonarCloud pythonsecurity:S8705, which flagged two places on PR #227 where `--branch` reaches a command with no validation it recognises:
- `_git_answer`'s `subprocess.run`, reached from `landing_failed` on the re-run path, which never passes through the commit's own `checked_value`;
- the printed NEXT line's `git -C <root> push -u origin <branch>`, which an agent pastes and runs.

The fix:
- `checked_branch` runs in `main`, before `cmd_apply`, whenever `--branch` is given. It refuses as `REFUSED update-branch-invalid`, with a remediation line, any value that:
  - `BRANCH_RE` does not fullmatch. That is the alphabet `checked_value` allowed, less `~`, with a letter or digit first, so there is no leading `-`, `/` or `.`;
  - `git check-ref-format --branch` then refuses, or would expand rather than echo. Git is asked only once the alphabet has passed.
- `landing_failed` asks git which remote-tracking refs point at HEAD (`for-each-ref --points-at HEAD refs/remotes/origin/`) and looks for the branch in that answer, so the name is no longer a `git` argument there.
- `--` ends git's options before the positionals: in `git push -q -u -- origin <branch>`, and in the NEXT line's `git -C <root> push -u -- origin <branch>`.
- The two composite assertions Sonar also flagged (S9073) are split.

Measured with the fixture from the first commit (Atlas from a5b09c3 plus upstream commit B), running `apply --branch=<value> --push --pr`:
- On main a5b09c3 and on 6ed4e70, `-shape`, `--upload-pack=touch`, `shape/a b` and `shape/x@{1}` are refused `unsafe-value` only after the 12 lines of the rewrite.
- `shape/a..b` and `shape/x.lock` fail as "REFUSED committing the re-synced shape" at `git checkout -b`, with the rewrite left modified in the tree.
- `-shape` with nothing to do says "nothing to do" and exits 0.
- On this head every one of them is refused `update-branch-invalid` with nothing on stdout and the tree untouched.
- `shape/update-y` lands byte-identical to main: stdout, stderr, exit code, the `gh` argv log and the resulting refs.

`--help` is unchanged, so docs/cli.md is too.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-11T19:19Z) fix round 1: SonarCloud S8705 (2 findings at update-shape.py:957 and :2002) fixed in 8705b27 by checking --branch once in main (allowlist fullmatch, then git check-ref-format --branch, refused as update-branch-invalid before anything runs), reading the pushed state without the branch as a git argument, and -- before the push positionals; targeted set 350 passed on 8705b27.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-11T19:29:51Z)

This lane reviewed PR #227 at 6ed4e70 read-only, in its own detached worktrees. It authored nothing and holds no claim.

MERGEABLE WITH NITS. Whichever of #227 and #218 lands second changes one test call. #233 conflicts in 3 hunks, and keeping both sides resolves them. The branch has since moved to 8705b27, which was not reviewed.

The issue's two runs are fixed. Head prints REFUSED opening the pull request for shape/update-y, keeps the commit, and its NEXT line is the exact argv gh got. The re-run opens the pull request with no second commit. Main printed "committing the re-synced shape", then "nothing to do".

Findings (update-shape.py at 6ed4e70):

  • P3-1: with no gh on PATH, the FileNotFoundError escapes open_pull_request (:2171-2176), _finish_landing (:2085-2088) and push_branch (:2013-2017).
    • Run 1 ends in a traceback, exit 1, no NEXT line, as on main.
    • The new re-run also ends in a traceback; main exited 0.
    • This contradicts the new EXIT CODES text (:83-85). Fix: catch OSError there and hand it to landing_failed.
  • P3-2: the re-run never passes --branch through checked_value (the first run does, at :1510).
    • Probe: checkout on refs/heads/--receive-pack=<script>, then apply --branch=<that> --push --pr. It ran git push -q -u origin --receive-pack=<script>, and with push.default=current the script ran.
    • Low reach, but this is SonarCloud's red S8705. The subject of 8705b27 says it adds this check.
  • Nit: building the gh pr create after the commit (:1884-1887) is a surviving mutant. The promise at :1878-1880 is untested.
  • Nit: a re-run without --yes pushes and opens the pull request, because :1907-1909 runs before confirm at :1916.
  • Nit: re-running a first command that had --add refuses update-add-already-pinned. The promise at :1991-1995 could say to drop --add.
  • Nit: the NEXT line uses POSIX quoting (:2002-2005) and does not paste in cmd.exe.
  • Nit: Sonar S9073 at tests/test_update_shape_landing.py:197 and :225.

Tests:

  • Main a5b09c3 with the new file: 10 failed, 5 passed, each failure for the reason the fix explains.
  • Head, update_shape + add + landing + cli_reference + repo_hygiene + commit_trailers + family -k update_shape: 285 passed.
  • Head, test_shape_doctor.py: 181 passed. render-cli-reference --check: ok.
  • Mutants: 11 of 12 killed.
  • The "byte-identical to main" claims hold.

Merges with 6ed4e70:

The full report is handoffs/xFactory/attachments/lane-openRepoShape-2/review227-6ed4e70/final-report.md in opensoft/brett-wip.

…y --branch` committed is named as that step with exit 127 and the NEXT line, on the first run and on the `--pr` re-run, so a machine without `gh` no longer ends in a `FileNotFoundError` traceback, exit 1, after the commit and the push (#204)

Fix round 2, from the independent review (rv-update-shape) of 8705b27.

- `_landing_run` wraps `run` for the three steps after the commit: the push, the re-run's `gh pr list` and `gh pr create`. `subprocess` raises `OSError` for a program it cannot start rather than returning an exit code, so it escaped every arm. It now becomes `CommandFailed(command, root, 127, "`gh` could not be started (<strerror>): is it installed and on PATH?")`, which `landing_failed` names as the step, keeping the commit, and ends on the NEXT line: the `gh pr create` to run once `gh` is installed.
- Measured with PATH=/usr/bin:/bin (git and python3, no gh), `apply --branch shape/update-y --push --pr`, then the same command again:
  - main a5b09c3: exit 1 `FileNotFoundError: ... 'gh'` after "committed on" and "pushed"; the re-run says "nothing to do", exit 0.
  - 8705b27: the same first run; the re-run is exit 1 with the same traceback.
  - this head: `REFUSED opening the pull request for shape/update-y`, exit 127 in the block, NEXT `gh pr create --repo testorg/Atlas --base main --head shape/update-y --title …`, exit 2. The re-run is `REFUSED asking gh for shape/update-y's pull request`, with the same NEXT line, exit 2, and still one commit.
- Tests:
  - `test_a_gh_missing_from_path_is_the_step_that_failed_on_both_runs` runs with a PATH of git's and Python's directories, and skips where `gh` lives beside one of them (Linux runners);
  - `test_a_gh_that_cannot_be_started_in_process` covers both runs on every platform;
  - `test_a_bad_branch_is_refused_even_with_nothing_to_write` pins fix round 1's one behaviour change.
  - Removing the `OSError` arm fails the two `gh` tests.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-11T19:43Z) fix round 2 (independent review, codex is out of quota): a gh (or git) that cannot be started after the commit is now the named step (REFUSED opening the pull request for <branch> / REFUSED asking gh for <branch>'s pull request, exit 127, NEXT = the exact gh pr create) on the first run and the re-run instead of a FileNotFoundError traceback, with an e2e PATH test and an in-process test (Windows); a test pins that an invalid --branch is refused with nothing to write; the PR body's Composes-with now gives the exact #233 resolution (3 hunks, the stale _rolled_back sentence, and dropping its branch-invalid case: 75 passed on the trial merge) and the #218 one-call change (67 passed); targeted set 353 passed on 60038cc.

brettheap and others added 2 commits October 11, 2026 22:04
…reate-failure

origin/main 39ac59c carries #233 (#202, 150c41d), whose ledger rolls back a
branch, a stage or a commit that fails and closes at the commit. Three hunks
in update-shape.py, both sides kept:
- EXIT CODES, apply 2: one sentence covering the rolled-back branch, stage
  or commit and the push or pull request kept after the commit (#204).
- `_land`'s docstring: #202's ledger sentence, then #204's paragraph.
- #202's `_rolled_back`, then #204's `LandingIncomplete` ...
  `pull_request_target`; only #202's old `open_pull_request` header goes.
  `_rolled_back`'s sentence saying a post-commit push or pull request "is
  #204's, and prints the line it always printed" now says it never reaches
  that arm: `LandingIncomplete` is neither of its two classes.

tests/test_update_shape_add_ignored.py: the `branch-invalid` FAILURES case
(`shape/update..ignored` failing `git checkout -b` after the writes) is now
refused by `checked_branch` in `main` before anything is written, so it
becomes its own test asserting `update-branch-invalid`, empty stdout and an
untouched tree with the operator's staged work; `branch-df` still covers a
branch-creation failure after the writes.

Lane: openreposhape-3 (openRepoShape-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s rolled back as #202 rolls it back, and the same command then lands and opens the pull request on one commit

After merging main (#233, #202), the commit is where rolling back stops
from both sides: #202's ledger puts back a branch, a stage or a commit
that fails and closes at the commit, and #204 keeps the commit when a push
or `gh pr create` fails after it. The post-commit half was already pinned
on the merged tree by the landing tests (`the_commit_stands`: clean tree,
HEAD on the branch with the re-pin) and by #202's
`test_a_failure_after_the_commit_lands_rolls_nothing_back`. Nothing drove
the pre-commit half with the landing flags passed. Two tests do now:
- `test_a_failure_before_the_commit_under_push_and_pr_is_rolled_back`
  (every platform; `gh` is never reached): a pre-commit hook refuses the
  commit under `--branch --push --pr`. Exit 2, `REFUSED committing the
  re-synced shape on <branch>`, #202's ROLLED BACK block (pin, manifest
  and copy restored, HEAD switched back, the branch deleted), and nothing
  of the landing: no push, no `gh` call, no NEXT line, no re-run promised,
  origin without the branch, the tree and refs as they were.
- `test_the_same_command_after_a_rolled_back_commit_lands_and_opens`
  (shell `gh` stub, WINDOWS_SKIP): with the hook gone the same command has
  work to write, so it is the first run again, not the `--pr` re-run: one
  commit, pushed, one `gh pr create` and no `gh pr list`.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, 2026-10-11T22:05:55Z) Landing note: our #233 (#202) landed as 150c41d, and main is now 39ac59c. The "#233: the second to land resolves it like this" section of this PR's body applies as written. We applied its steps 1-5 on 39ac59c + 60038cc, then ran tests/test_update_shape.py tests/test_update_shape_add.py tests/test_update_shape_add_ignored.py tests/test_update_shape_landing.py tests/test_cli_reference.py: 163 passed. docs/cli.md merges to this branch's version, and render-cli-reference.py --check reports ok.

Our own 19:42Z note on #204 was incomplete. It missed two things:

  • the _land docstring hunk;
  • the edit to FAILURES in tests/test_update_shape_add_ignored.py, which drops the branch-invalid entry. This branch's branch check now refuses that name before anything is written, and branch-df still covers a branch git cannot create.

Follow your PR body, not that note. The patch against this branch is on Eagle (py-bench) at /tmp/claude-1000/-workspace-projects-openRepoShape/83338f75-6338-4d51-9749-89179ecaf55f/scratchpad/l4cf/resolved-227-vs-pr.patch.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-11T22:21Z) fix round 3 (independent review, codex is out of quota): main 39ac59c is merged in as be70651; it carries #224 and #233 (#202). The 3 update-shape.py hunks are resolved as this body's #233 steps said, re-derived against the landed 150c41d:

Lane 4's independent resolution (22:06Z note, its patch) gives the same code and the same _land docstring; it differs only in wrap and wording, and in deleting branch-invalid where this branch keeps it as a test.

Semantics checked. LandingIncomplete is neither of the classes cmd_apply hands to _rolled_back, so a failure after the commit keeps it: 12 landing tests and #233's test_a_failure_after_the_commit_lands_rolls_nothing_back kill a mutation that rolls it back. Nothing drove a failure before the commit with --push --pr passed, so c96396b adds two tests: test_a_failure_before_the_commit_under_push_and_pr_is_rolled_back (every platform: #233's ROLLED BACK block, no push, no gh, no NEXT) and test_the_same_command_after_a_rolled_back_commit_lands_and_opens. They are the only tests that kill a mutation reporting such a failure as a landing failure; #233's own cases all survive it.

Re-measured on main 39ac59c: check, --branch / --push and their re-runs, --push --pr with gh ok, and the new pre-commit-refusal run and its re-run are byte-identical to main; the r1-r9 and missing-gh rows differ as intended.

Targeted set (update_shape, _add, _add_ignored, _landing, shape_doctor, repo_hygiene, cli_reference) on c96396b: 397 passed in 635.82s (0:10:35). render-cli-reference.py --check is ok. #218 1acebc6 still merges clean textually, with its one test call and the --repo owed by the second to land (re-measured: 3 TypeError). #230 f4dca0d is clean. Head c96396b.

brettheap and others added 2 commits October 11, 2026 22:57
…reate-failure

origin/main 2727b08 carries #218 (#209), plus 06b5204 (#214) and 60d08a1
(#217). No textual conflict: #218's `pull_request_repository` lands in
`pull_request_target`, where this branch moved the manifest reading
`open_pull_request` used to do, so `--pr`, the failure NEXT line and the
re-run's `gh pr list` all read the one repository and base.

#218's `test_a_manifest_naming_no_repository_refuses_pr_and_leaves_the_placeholder`
called the old five-argument `open_pull_request`, a TypeError here; it now
calls `update_shape.pull_request_target(tmp_path, kind)`, which is where
`--pr` refuses, before the commit. #218's docstrings that named
`open_pull_request` as the reader of the manifest name `pull_request_target`.

Lane: openreposhape-3 (openRepoShape-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the repository with #218's `--repo`, so every `gh pr create` this tool prints or runs says which repository it opens against

With #218 (#209) merged, `next_line`'s `--branch` and `--push` lines carry
`--repo <the manifest's repository>`, and `--pr` always did. The one left
was `pull_request_line`'s bare form, which is the `gh pr create` at the end
of the NEXT line a failed push prints under `--push` without `--pr`
(`git -C <root> push -u -- origin <branch> && gh pr create --base <tracking>
--head <branch>`). Pasted from another directory, its `gh` opened against
whatever repository the shell was in.

- `pull_request_line` without `--pr` reads the manifest once and builds
  `gh pr create --repo <repository> --base <tracking> --head <branch>`
  through #218's `pull_request_repository`, the same answer `--pr` and
  `next_line` use.
- A manifest naming no repository leaves `--repo` off rather than
  refusing: without `--pr` nothing is opened, and `next_line`'s
  `<org>/<Project>` placeholder would be one literal argument in a line
  quoted to be pasted as an argv.
- Tests: the landing `failed_push[push]` case asserts `--repo testorg/Atlas`;
  #218's repository test checks both `pull_request_line` forms for a
  project and both family spellings; its no-repository test checks the bare
  form leaves `--repo` off.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-11T23:14Z) fix round 4 (independent review, codex is out of quota): this is the #218/#227 pair, since #218 landed first as 2727b08.

Merge, 87ba8c5. Main 2727b08 (#218, #230, #246) merged with no conflict. #218's pull_request_repository landed inside pull_request_target, so --pr, the failure NEXT line and the re-run's gh pr list read one repository and one base. Pair change (i): #218's test_a_manifest_naming_no_repository_refuses_pr_and_leaves_the_placeholder now calls update_shape.pull_request_target(tmp_path, kind) in place of the old five-argument open_pull_request. #218's docstrings that named open_pull_request as the manifest's reader now name pull_request_target.

Round 4, c874e4f. Pair change (ii): pull_request_line's bare form gains #218's --repo, so the push-failure NEXT line under --push without --pr ends gh pr create --repo testorg/Atlas --base main --head <branch>. A manifest naming no repository leaves --repo off; a placeholder would be one literal argument in a line quoted as an argv. Tests: the landing failed_push[push] case; both pull_request_line forms in #218's repository test; the bare form in its no-repository test. Fail-first on 87ba8c5: 4 failed, 8 passed.

Measured. Re-measured on main 2727b08: check, --branch / --push and their re-runs, --push --pr with gh ok, and the pre-commit-refusal run and its re-run are byte-identical to main. r5n now carries --repo.

Tests. Targeted set (update_shape, _add, _add_ignored, _landing, _pr_base, shape_doctor, repo_hygiene, cli_reference) on c874e4f: 424 passed in 839.27s (0:13:59). render-cli-reference.py --check is ok; no --help change.

Not chased. Main moved to 4b2add8 (#257, repo_shape.py) after the merge; git merge-tree --write-tree 4b2add8 c874e4f is clean. fix/update-shape-add-dangling-symlink 7bceeca is also clean. Head c874e4f.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit be2790e into main Oct 12, 2026
5 checks passed
@brettheap
brettheap deleted the fix/update-shape-pr-create-failure branch October 12, 2026 00:17
brettheap added a commit that referenced this pull request Oct 12, 2026
… via #236) into fix/adopt-record-submodule-placement

Brings in b30595c (#249, #250), be2790e (#204) and 6d8d88b (#203) on top
of c3a9645. No rebase. b30595c changes scripts/repo_shape.py and
scripts/shape_materialize.py, as this branch does. be2790e changes
update-shape.py and 6d8d88b changes scripts/family.py, neither of which
this branch touches.

One conflict, in scripts/shape_materialize.py's `from repo_shape import`
block. b30595c imports `double_quoted` for `render`, and this branch
imports `PATH_CLIMBS`, `PATH_ITSELF`, `PATH_NOT_CANONICAL` and
`path_spelling_fault` for `_spelling_reason` and `LEG_SEGMENT_REASONS`.
Kept all of them, in the block's order.

Merged without a conflict, and checked:

- scripts/repo_shape.py: b30595c reads a quote as opening a quoted scalar
  only where a scalar begins, and requires the whole text to be that one
  scalar (`_unquoted`, `_quote_end`, `_quoted_scalar`; `_scalar` hands
  them any text that begins with a quote). The record writes every path
  and reason as one whole scalar through `double_quoted`. `double_quoted`,
  `_escaped`, `_ESCAPE_OF`, `_unescape`, `parse_yaml` and
  `path_spelling_fault` are unchanged by the merge. Through the merged
  reader, `submodule_record` then `parse_yaml`, path and resolution, there
  are 0 mismatches in each set:
  - 4,525 names, plus 35 more;
  - 240 aimed at #252 and #248;
  - 195 aimed at #249 and #250: `it's`, `a"b`, `"abc`, `a" b "c`,
    `a' b 'c`, `"a\"`, `a\`, `[it's, a]`, `note "[1]"`, `"Brett Heap`,
    `O'Brien & Co: #1 [draft] {x}`, and more.
- scripts/shape_materialize.py: b30595c spells a YAML template's
  `"{{KEY}}"` value with `double_quoted` in `render`. The record is
  appended after `render` has run (`text += append.get(rel, "")` in
  `_materialize`), so no part of it passes through `render`. This branch's
  `_spelling_reason`, `LEG_SEGMENT_REASONS` and `leg_path_problem` are
  unchanged.
- 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 or
  scripts/family.py. Each merged module's names are the base's plus what
  either side added. The only thing both sides changed is the import block
  above.
- The branch's diff against main is still #189 alone: 12 files, +1670
  -157, file by file the same as against c3a9645.

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
Main brings two commits since b30595c:

- be2790e (#204 via #227): update-shape.py names a push or `gh pr
  create` that fails after `apply --branch` committed, with a NEXT line,
  with docs/cli.md and its tests;
- 6d8d88b (#203 via #236): family.py `add` asks every refusal before it
  mounts anything.

Neither changes a file this branch changes, nor scripts/repo_shape.py or
scripts/shape_materialize.py. Every file the merge changes against this
branch is main's, and every file it changes against main is this
branch's. `render-cli-reference.py --check` still finds docs/cli.md
current.

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 `_entry_findings` composed with #255's, and this PR's pair renamed `unwritable_findings`/`unwritable_remediation`

#231 (6f45bf9) puts each entry's path first in `_entry_findings` and
returns `unsafe_remediation` ahead of the rest; this branch appends each
value `execute` writes into a file that no file can hold. The two hunks
are its docstring's order sentence and its return. The sentence keeps
main's order and appends this PR's clause; the return is
`(findings + submodule_findings + unwritable_findings,
  unsafe_remediation + remediation + unwritable_remediation)`, so the
paths still come first, as #196 needs.

This PR's local pair `value_findings, value_remediation` is renamed
`unwritable_findings, unwritable_remediation`. #261's branch (ca21b57)
binds `value_findings, value_remediation` in the same function, from
`_manifest_findings`: a resolution keeping both lines under one name
would silently drop one PR's findings. With the rename it cannot.

The rest of main (#254/#281, #238/#287, #203/#236, #204/#227,
#249/#250, #241, #215 and the earlier merges) merges with no conflict.
The resolution is the lane's re-verify one (`resolve-main.py`, 385
passed on 6f45bf9), rename aside.

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
…#241, #249/#250, #204, #203 and #238 merge with no conflict, and a `.gitmodules` value git refuses is answered before #190's relative-url check, which reads no url from a file git cannot read

Main since d94d155, the main GitHub's update-branch merged at 8ce5614:
c2ff2b0 (#215 via #240), c3a9645 (#241 via #256), b30595c (#249/#250 via
#284), be2790e (#204 via #227), 6d8d88b (#203 via #236) and ede15ce (#238
via #287).

No conflict hunk. Three files were changed on both sides; `git merge`
auto-merged each, and every hunk of each side lies apart from the other's.
Each side's +/- lines are identical before and after the merge in all
three (`git diff -U0` d94d155..8ce5614 against ede15ce..merge, and
d94d155..ede15ce against 8ce5614..merge). Line numbers are the merged
file's.

adopt-project.py
- main, c2ff2b0 (#215): 384-506, `SUBMODULE_TYPED_KEY_RE` to `_refused_value`
  and `_who_refuses_a_boolean`; 510-515 and 522-524, `_registrations`'
  docstring and its return of `_refused_value`'s words; 791-797,
  `Source.registered_submodules`' docstring; 2181-2185, `_where_git_fails`'
  docstring.
- #190: 36-38 and 51-53, the module docstring; 102-103, the `repo_shape`
  import list; 334-339, `Registrations.url`; 2252-2501, `RELATIVE_URL` to
  `_relative_url_findings`; 2503-2548, `_submodule_findings` and
  `_entry_findings` with `local_remote_dir` and the NOTEs; 2608 and 2630,
  `cmd_check`; 3070-3092, `_repository_urls` and `_remote_urls`;
  3111-3216, `_refuse_what_check_finds` and `_as_resolved_here`; 3443,
  `cmd_execute`.

scripts/repo_shape.py
- main, b30595c (#249/#250): 173-535, the YAML reader's quote boundaries,
  from the comment after `YamlError` to `_split_flow`.
- #190: 1975-1985, `REMOTE_PORT_RE` and `REMOTE_HOST_ALIASES`; 2099-2116,
  `remote_key`'s docstring and body.

tests/test_shape_doctor.py
- #190: 2662-2693, `test_a_port_and_githubs_ssh_host_are_not_part_of_a_remotes_key`.
- main, ede15ce (#238): 5209-5808, the validator-PYTHONPATH tests after
  `test_a_drifted_roots_next_command_runs_when_it_is_pasted`.

Every other file is one side's blob: main's for the 17 main changed, and
8ce5614's for tests/test_adopt_relative_submodule_url.py and
tests/test_adopt_submodules.py. The merge's diff against main is #190's
five files, with the stat it had against d94d155 (1097+, 41-). No
function or class is bound twice at the top level of adopt-project.py,
scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py,
shape-doctor.py, scripts/family.py or the three test files. The helpers
c2ff2b0 moved into tests/conftest.py (`planned`, `findings`, ...) are
plain functions, and tests/test_adopt_relative_submodule_url.py imports
seven named helpers from conftest and defines its own `planned`.

HOW #215 AND #190 MEET. `_submodule_findings` lists
`submodule_plan_problems` first, which holds #215's
`plan-gitmodules-unreadable`, then `_relative_url_findings`. Where git's
submodule reader refuses a value of the source's `.gitmodules`,
`registered_submodules` answers with git's words, a string, and
`_kept_relative_urls` reads no url from a string. So a source with both
problems gets #215's finding alone, and no NOTE either. #190's finding is
not lost: once the value is fixed, `check` reports it, and `execute`
refuses it through the same list. #190 adds lines only after #215's, so it
can hide nothing of #215's. A relative url kept in a leg is always beside a
gitlink kept in a leg, where `_where_git_fails` names a failure, so the
unreadable finding is always printed whenever #190's would be skipped.
Measured on this tree with a source holding `upstream/dependency`,
`url = ../dependency` and `update = bogus`, with `.gitmodules` and
`upstream/` kept in code for testorg:
- `origin` in otherorg: `check` 1 with `plan-gitmodules-unreadable` alone;
  `execute --local-remote-dir` 2, `REFUSED plan-gitmodules-unreadable`,
  with no remotes directory made. With `update = checkout`: `check` 1,
  `plan-submodule-relative-url`.
- `origin` in testorg: the same `check` 1 and `execute` 2, with no NOTE.
  With `update = checkout`: `check` 0 and the NOTE.
- At 8ce5614, before #215, the first source was `plan-submodule-relative-url`.
  The second was `check` 0 with the NOTE, over a file `git fast-export`
  dies on.

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
…`update-shape.py` with is a Python stub the update's child starts through a `sitecustomize` hook rather than a `#!/bin/sh` file on PATH, so the eight tests that carried `WINDOWS_SKIP` run on Windows too and the `--pr` landing and re-run paths (#204) are run by all three CI jobs, not two (#334) (#336)

#227 drove `update-shape.py apply --branch --push --pr` against a `gh` that was a shell script on PATH, so every test that reached it was skipped on Windows: `CreateProcess` reads no shebang. No file the suite can write is a `gh` there either: for a program name with no extension it appends `.exe`, so a `gh.cmd` beside a script is not what `gh` names, and the runner's real `gh.exe` further down PATH would be the one that ran. So the stub is no longer found by searching PATH.

- The stub is a Python program (`GH_STUB`): it logs its argv to `GH_LOG`, one line per call and each argument ended by a unit separator, as bytes, so the log is the same on every OS; `pr list` answers from `GH_STATE/list`; `pr create` exits 97 with the same `HTTP 422` line while `GH_STATE/create-fails` exists, else prints the URL and records the pull request; anything else exits 64.
- Python imports `sitecustomize` at start-up from `PYTHONPATH`, so the update's child interpreter is started with a `sitecustomize.py` (`GH_ON_PATH`) first on it. It does what PATH resolution would have done: a `subprocess.Popen` whose program is named `gh` is started as `python <the stub> <the same arguments>`. The stub is a real process, its exit code and streams cross real pipes and the arguments cross the OS's own command-line quoting; `shape_materialize.run` still builds the `CommandFailed` from that process, and the refusal and the NEXT line still name `gh`, not python. `git` is untouched, and with no `GH_STUB` in the environment the module does nothing.
- The `gh` fixture's interface is unchanged (`env`, `calls`, `failing`, `answer`, `forget`); `env` carries `PYTHONPATH`, `GH_STUB`, `GH_LOG` and `GH_STATE` in place of a `PATH` entry.
- The eight `@WINDOWS_SKIP` marks come off, nine test items: each was skipped for the stub, as the file's own docstring said, and the rest of what they do (`git`, a refusing commit hook, NEXT-line quoting) is also done by tests of this file that were never skipped on Windows. The `WINDOWS_SKIP` and `stat` imports go with them.
- The two in-process tests keep their bodies. Their docstrings no longer call themselves the Windows job's coverage: the first is the same two runs with no process for `gh`, the second is the only coverage of a missing `gh` on a runner that has `gh` beside `git` (the subprocess test skips there).

Unchanged: no `assert` line and no test name moves (`git diff -U0 | grep -E '^[-+]\s*(assert|def test_)'` is empty), so no assertion is weakened; the scripts are untouched.

What this does not exercise: the operating system finding a `gh` by searching PATH. The old stub did on POSIX only, and a missing `gh` stays covered by `test_a_gh_missing_from_path_is_the_step_that_failed_on_both_runs` and the in-process test.

Left out: six other files skip on Windows for the same `#!` `gh` stub (`test_free_plan_hint.py` four times, `test_neutral_pin_validator.py`, `test_scaffold_pin_and_reuse.py`, `test_scaffold_pin_collisions.py`, `test_scaffold_leg_paths.py`, `test_scaffold_leg_path_collisions.py`); `GH_STUB` and `GH_ON_PATH` could move to `tests/conftest.py` for them.

Lane: openreposhape-3 (openRepoShape-3)
Co-Authored-By: Claude Sonnet 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
…orking tree holds what HEAD commits at the pin, the manifest and every pinned copy, and is refused as `update-rerun-uncommitted` otherwise, so an in-place `apply --at C` left uncommitted on a branch that re-pins B no longer gets B pushed under a pull request titled C (#333) (#379)

`cmd_apply` decided "nothing to write" from the working tree (`read_pin`,
`classify`), and `_awaits_its_pull_request` decided that a pull request was
owed from HEAD's `contracts/shape-pin.yaml` against `origin/<base>`'s. Over
a tree that is HEAD's that is one answer; with an uncommitted change in
between it was two, and `_finish_landing` pushed HEAD under a pull request
built from the working tree. Measured on ab623dd with local bare remotes
and a logging `gh` shim: B committed and pushed on the branch, an in-place
`apply --at C` left uncommitted, then `apply --at C --branch --push --pr`
exited 0, said the pin on the branch already named C, pushed the branch
(its pin B) and ran `gh pr create --title "Re-sync the shape copies to
opensoft/openRepoShape @ <C>"`.

- Once `_awaits_its_pull_request` recognises a re-run, and before `gh` is
  asked or anything is pushed, `_refuse_what_head_does_not_commit`
  compares the pin, the kind's manifest and every pinned copy with HEAD by
  bytes (`git hash-object` on the file against `HEAD:<path>`: the clean
  filter runs, and no index flag hides an edit) and refuses
  `update-rerun-uncommitted`, naming each path that differs or that HEAD
  does not commit.
- The refusal names HEAD and the commit the pull request would be titled
  with and, where the two pins differ, both of them ("the pull request
  would name one commit and carry another"). Its remediation: commit those
  changes on the branch, or put them back to HEAD's bytes, then run the
  same command again. Nothing has been written, so nothing is rolled back.
- The index is not asked, and neither is any other path (a README edit,
  an untracked file): nothing on this path commits, and neither decision
  reads them.

Unchanged: a re-run over a clean tree, and every case
tests/test_update_shape_landing.py and tests/test_update_shape_pr_base.py
test (#227, #218); the first-run path and #202's rollback before the
commit, which the new check is not on; `check`; every `--help` text.
Upstream commit C, which changes `scripts/validate-pins.py` alone, is added
to the landing tests' module fixture; no existing test reads the upstream's
tip. The new tests run the update as a child with that file's Python `gh`
stub (#334), so they run on all three operating systems and a `gh` the tool
asked would be heard. They hold the comparison to both of its properties:
the issue's case is still refused with its paths marked
`--assume-unchanged` or `--skip-worktree`, which a `git diff HEAD` would
not see, and a CRLF pin and manifest that the project's `* text=auto
eol=lf` cleans to HEAD's blobs still open the pull request.

Left out: the first-run path over an uncommitted in-place apply still
commits only the paths it wrote, so `apply --at D --branch <new>` over an
uncommitted `apply --at C` exits 0 with a commit whose own
`validate-pins.py` is red (measured); a `--pr` run whose working tree needs
a write HEAD already holds still ends in git's "nothing to commit", rolled
back, rather than a named refusal; and `apply --at C --branch <new> --pr`
over an uncommitted in-place C still says "nothing to do".

Lane: openreposhape-3 (openRepoShape-3)
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