Skip to content

update-shape.py: a --pr re-run finishes the landing only when the working 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

Merged
brettheap merged 3 commits into
mainfrom
fix/update-shape-pr-rerun-pin-source
Oct 12, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

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

What

  • Once _awaits_its_pull_request recognises an apply --branch --pr 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 what HEAD commits, by bytes: git hash-object <file> against HEAD:<path>. The clean filter runs, and no index flag hides an edit.
  • A path that differs, or that HEAD does not commit, gets REFUSED update-rerun-uncommitted. The refusal names those paths, HEAD, and the commit the pull request would be titled with. Where the working tree's pin and HEAD's name different commits, it names both and says the pull request "would name one commit and carry another". The 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 at that point, so nothing is rolled back. The check reads neither the index nor any other path, such as a README edit or an untracked file: neither decision reads them, and nothing on this path commits.

Why

Measured on main ab623dd and on this head with local bare remotes and a gh PATH shim that logs its argv and answers pr list with []. A = the fork point, B changes scripts/validate-manifest.py, C changes scripts/validate-pins.py.

scenario main ab623dd this head
The issue. B committed and pushed on shape/update-landing, then an in-place apply --at C left uncommitted, then apply --at C --branch shape/update-landing --push --pr exit 0. Printed "nothing to write: every copied file matches opensoft/openRepoShape @ C, which the pin on shape/update-landing already names" and pushed. Ran gh pr list, then gh pr create --title "Re-sync the shape copies to opensoft/openRepoShape @ C". The remote branch's pin is B exit 2. REFUSED update-rerun-uncommitted: … does not hold what HEAD commits at contracts/shape-pin.yaml, project.yaml, scripts/validate-pins.py … there the pin names C and HEAD's names B, so the pull request would name one commit and carry another. gh not asked, nothing pushed, tree untouched
The branch's re-pin put back to origin/main's by hand, then --at A exit 0. PR titled @ A while the branch carries B exit 2. Names the pin, project.yaml and scripts/validate-manifest.py, and both commits
Only project.yaml edited, not committed exit 0. PR opened, with its base and repository read from the uncommitted manifest exit 2. Names project.yaml
Pin and manifest committed by hand, the copy left out (test case) exit 0, "nothing to write …" exit 2. Names scripts/validate-manifest.py
Clean re-run exit 0, PR opened same
Uncommitted README edit plus an untracked file exit 0, PR opened same

The issue was right, and is now measured rather than read from the code. Its direction was "a clean pin and manifest". This PR also requires every pinned copy to be clean: otherwise a hand commit of the pin without its copy still pushes a branch whose own validator is red.

Tests

Composes with

git merge-tree --write-tree --name-only <branch> 2575d41:

Left out

Closes #333

🤖 Generated with Claude Code

Summary by Sourcery

Refuse pull-request re-runs when the working tree does not match the branch commit they would finish, preventing a pull request from naming one pin while carrying another.

Bug Fixes:

  • Prevent pull-request re-runs from completing or pushing when the working tree differs from the files committed by HEAD, including the pin, manifest, or pinned copies.
  • Report mismatched paths and conflicting pin commits with an actionable refusal before invoking GitHub or modifying the repository.

Enhancements:

  • Compare relevant working-tree files against HEAD by their Git-cleaned content without relying on index state, while leaving unrelated edits and untracked files unaffected.

Tests:

  • Add landing tests covering in-place updates, hidden index changes, manifest edits, and uncommitted pinned copies, along with clean-filter and unrelated-dirt behavior.

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

`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.

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>
@sourcery-ai

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

@sourcery-ai

sourcery-ai Bot commented Oct 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR closes the re-run inconsistency by validating that the working tree’s pin, manifest, and every pinned copy exactly match what HEAD commits before allowing a no-op pull-request landing to proceed; mismatches are named and refused before GitHub or push side effects, with focused regression coverage for the affected scenarios.

Sequence diagram for validating a pull-request re-run before landing

sequenceDiagram
    participant User
    participant Apply as cmd_apply
    participant Rerun as _awaits_its_pull_request
    participant Check as _refuse_what_head_does_not_commit
    participant Git as Git
    participant GH as gh
    participant Remote as Git remote

    User->>Apply: apply --branch --pr --push
    Apply->>Rerun: _awaits_its_pull_request()
    Rerun-->>Apply: re-run recognised
    Apply->>Check: _refuse_what_head_does_not_commit()
    Check->>Git: _not_what_head_commits()
    Git-->>Check: compare working-tree hashes with HEAD blobs
    alt pin, manifest and pinned copies match HEAD
        Check-->>Apply: allow landing
        Apply->>Remote: push
        Apply->>GH: gh pr list / gh pr create
    else any compared path differs
        Check-->>User: Refusal update-rerun-uncommitted
        Note over Check,GH: No push, GitHub request, or write occurs
    end
Loading

File-Level Changes

Change Details Files
Guard pull-request re-runs against divergence between the working tree and the branch’s HEAD before any push or GitHub operation.
  • Invoke the guard only when the existing re-run/no-op path is recognized.
  • Compare the shape pin, manifest, and all pinned copies on disk with their HEAD blobs using Git hashing, including clean-filter behavior and without consulting the index.
  • Refuse with update-rerun-uncommitted when paths differ or are absent from HEAD, reporting affected paths, HEAD/target commits, and remediation; leave the tree untouched.
  • Keep unrelated tracked edits and untracked files outside the comparison so valid re-runs retain existing behavior.
update-shape.py
Expand landing tests to cover mismatched committed and uncommitted shape state across platforms and verify refusal occurs before side effects.
  • Add upstream commit C that changes a pinned copy and expose it through the fixture.
  • Cover in-place apply, manual re-pin, manifest-only edits, and a committed pin with an omitted copy, asserting exact refusal details and affected paths.
  • Assert no gh calls, push, writes, rollback, or tree/status changes occur on refusal.
  • Preserve and verify successful behavior for unrelated README/untracked dirt and clean re-runs.
tests/test_update_shape_landing.py

Assessment against linked issues

Issue Objective Addressed Explanation
#333 Prevent an apply --branch --pr re-run from pushing the commit in HEAD while creating a pull request titled or configured from uncommitted working-tree state. ✅
#333 Before finishing a recognized re-run, verify that the working-tree pin, manifest, and pinned copies match what HEAD commits; refuse without asking gh or pushing when they differ. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-12T14:50:34Z) @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.

…ed with its uncommitted paths marked `--assume-unchanged` or `--skip-worktree`, and a CRLF pin and manifest that are otherwise HEAD's still open the pull request, so the two properties the byte comparison was chosen for are tested (#333)

Independent review of PR #379 (round 1): `_not_what_head_commits` compares
`git hash-object <file>` with `HEAD:<path>` because the clean filter runs
and no index flag hides an edit, and no test held either property. With
the comparison swapped for `git diff --quiet HEAD -- <path>`, every new
test still passed while the issue's case, its paths marked
`--assume-unchanged`, exited 0 and pushed B under a pull request titled C.

- `test_a_rerun_over_a_tree_head_does_not_commit_is_refused_by_name`
  gains `assume-unchanged` and `skip-worktree`: the in-place `apply --at C`
  with the pin, `project.yaml` and `scripts/validate-pins.py` marked with
  that flag, so `git status --porcelain` is empty and `git diff --quiet
  HEAD` exits 0. Each is refused as the in-place-apply case is, naming the
  same three paths and both commits.
- `test_a_crlf_pin_and_manifest_still_hold_what_head_commits`: the pin
  and `project.yaml` rewritten with CRLF line ends, which the project's
  `* text=auto eol=lf` cleans back to HEAD's blobs. The re-run opens the
  pull request for B as a clean one does, and leaves both files' bytes as
  they were. A CRLF pinned copy cannot show this, because it is
  `locally-modified` before the re-run is recognised.

Both run the update as a child with the file's Python `gh` stub (#334),
on every operating system, with no skip. Against the index-reading
comparison the two new cases fail (exit 0, "pushed"); against
`hash-object --no-filters` the CRLF test fails; on this commit all eight
pass. update-shape.py is unchanged.

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-12T15:36:05Z) fix round 1 (independent review, codex is out of quota):

Finding (should-fix): no test held the property git hash-object was chosen for. Reproduced and fixed in 3465a6a, which changes tests only. update-shape.py is unchanged.

  • test_a_rerun_over_a_tree_head_does_not_commit_is_refused_by_name gains two cases, assume-unchanged and skip-worktree. Each runs the issue's in-place apply --at C, then marks the pin, project.yaml and scripts/validate-pins.py with that flag, and asserts that git status --porcelain is empty and git diff --quiet HEAD exits 0. Each is refused like the in-place-apply case, naming the same three paths and both commits.
  • The optional other half, that the clean filter runs, is covered by the new test_a_crlf_pin_and_manifest_still_hold_what_head_commits. The pin and project.yaml are rewritten with CRLF line ends, which the project's * text=auto eol=lf cleans back to HEAD's blobs. The test asserts that the raw hash differs from HEAD's blob and the cleaned hash equals it. The re-run then opens the pull request for B and leaves both files' bytes as they were.
  • Both use the file's Python gh stub (tests/test_update_shape_landing.py: 8 of its 19 tests drive update-shape.py with a POSIX shell gh stub and are skipped on Windows, so the --pr landing and re-run paths (#204) are run by two of the three CI jobs #334) and carry no skip, so they run on ubuntu, macOS and Windows.

Bite checks, with the tool mutated in a scratch worktree and the eight #333 tests run against it:

  • comparison swapped for git diff --quiet HEAD -- <path>: 2 failed (assume-unchanged, skip-worktree), each assert 0 == 2 after "nothing to write ... pushed shape/update-landing to origin"; 6 passed;
  • hash-object --no-filters: 1 failed (the CRLF test), 7 passed;
  • on 3465a6a: 8 passed.

On 3465a6a: tests/test_update_shape_landing.py plus the four guards give 218 passed, 1 skipped (test_stdout_encoding.py:286, a Windows-only default).

A correction to "Composes with" in the description: #380 (fix/pin-rewrite-indentation) at 73068a3 keeps YamlError and parse_yaml in update-shape.py's repo_shape import, so the NameError measured against its earlier head 7e2230c no longer applies. git merge-tree --write-tree of 73068a3 with 3465a6a is clean.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcery assessment

Approved.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit 87d2eb7 into main Oct 12, 2026
5 checks passed
@brettheap
brettheap deleted the fix/update-shape-pr-rerun-pin-source branch October 12, 2026 17:27
brettheap pushed a commit that referenced this pull request Oct 12, 2026
…plan-manifest-invalid` joins `_entry_findings` after #255's values, and this branch's `plan-root-symlink` stays after the source's own submodules

origin/main moved 92e29d9 -> dd8d0ea (twelve commits), merged by sha:
- f0a8bf6 (#330 via #381), scripts/family.py;
- 87d2eb7 (#333 via #379), update-shape.py;
- cd1fc79 (#327 via #375), shape-doctor.py;
- 45e01f4 (#348 via #396), tests/test_agents_md_codes.py: every
  code-shaped name AGENTS.md spells is a code a tool prints;
- 3ef059c (#258 and #261 via #314), adopt-project.py, scaffold-project.py
  and scripts/shape_materialize.py: the project's YAML reader and its
  `validate-manifest.py` are asked about the manifest's values before
  anything exists; `check` reports `plan-manifest-invalid` through
  `_manifest_findings` in `_entry_findings`; `_repository_urls` refuses
  `adopt-name-too-long`;
- f7f4b14 (#366 via #399), scripts/family.py;
- cb7063f (#301 and #280 via #390), scripts/repo_shape.py and the
  template's validate-manifest.py: a bracket or brace in a plain flow
  scalar is refused;
- 5678f68 (#278, #297 and #298 via #357), shape-doctor.py;
- 53bee5b (#289 via #352), scripts/bump-leg.py;
- f5cb616 (#369 via #398), scripts/shape_materialize.py: the docstrings
  of `force_stage` and `stage_written`;
- a92e60b (#326 via #400), scripts/family.py;
- dd8d0ea (#292 via #353), adopt-project.py: `_elector` keeps a
  `user.name` byte that is not UTF-8, `_remote_repository` reads an
  origin URL that is not UTF-8 as none, and `adopt-no-org` is asked
  before `checked_value`.

scripts/shape_materialize.py, scripts/repo_shape.py and every other file
merge with no conflict. adopt-project.py conflicts in two hunks of
`_entry_findings`, each resolved by keeping both sides:
1. The docstring. Main's order, with #261's manifest findings after
   #255's values and before #190's relative urls; this branch's symlinks
   stay after the source's own submodules.
2. The return. Main's `_manifest_findings(plan)` call, and the findings
   in main's order: each entry's path (#196), coverage, the legs and what
   the root keeps where the shape writes, the source's own submodules
   (#166), this branch's `_root_symlink_findings`, #255's values, #261's
   manifest findings, and #190's relative urls last. The remediation and
   the NOTEs are main's.

How each of main's commits that reach this branch's code meets it:
- 3ef059c: `_manifest_findings` asks the validator the template ships
  about the values `execute` writes; it reads no symlink and no path
  the shape writes. Its materializer half adds the validator's helpers,
  `refuse_unstorable_names` and `MANIFEST_VALIDATOR`, and holds
  `election_date` to the validator's `DATE_RE`; it leaves `place`,
  `_materialize`, `copy_tree`, `template_files` and `_refuse_unwritable`
  as they were.
  In `execute`, its `adopt-name-too-long` (in `_repository_urls`) comes
  before #255's clone-URL refusal, `_refuse_what_check_finds` and this
  branch's `core.symlinks` probe, all before any leg. Its new paragraph
  of the module docstring follows the one 6b53a6d corrected; both merge
  with no conflict.
- f5cb616: docstrings only.
- dd8d0ea: `_remote_repository`, `_checked_plan_inputs`' organisation and
  `_elector`, none of them this branch's.
- cb7063f: the plans `check` and `execute` read go through this reader.
  The plans `plan` writes for this branch's tests hold their entries as
  block mappings, each path `double_quoted` where it needs to be, and
  the rehearsal below read every one of them.

Checked on the merged tree:
- AST (fin183g/names.py, base 92e29d9): no top-level name is bound twice
  in adopt-project.py, scaffold-project.py or scripts/repo_shape.py, and
  none is loaded but bound nowhere. scripts/shape_materialize.py binds
  `YamlError` and `parse_yaml` twice, as main dd8d0ea does (1f90a0f's
  import and 3ef059c's, the same objects); the merge adds none.
- Both sides intact (fin183g/sides_intact.py): the only lines that differ
  from either side's own change are those of the two hunks.
- A rehearsal of this merge (tree d21f8b4, the same as this one) passed
  tests/test_adopt_root_symlinks.py with main's
  test_adopt_{manifest_values,git_reads,relative_submodule_url}.py,
  test_scaffold_manifest_values.py, test_agents_md_codes.py and
  test_yaml_flow_brackets.py: `440 passed`.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 12, 2026
…t: `check_pin` takes the root its `update-pin-row` remediation reads, so #290's refusal survives #267's split instead of a NameError (#290)

Main 580d7df carries #267 (decff87), which split update-shape.py's
`read_pin(root)` into the read and `check_pin(pin, pin_path)`, so that
shape-doctor.py can check the pins in a root's history in memory. The
`update-pin-row` raises moved into `check_pin`, and on this branch they
call `pin_rows_remediation(root)`, #290's `git -C <root> log` advice,
where `root` is not a name. The textual merge is clean and the tree dies
at run time: `NameError: name 'root' is not defined` for a `files:` of
`5`, `true` or `1.5` in `check`, `apply` and shape-doctor.py, which then
prints no report at all.

This commit takes the planned #267 composition, the PR's two attachments
fix1-compose-267-resolution-v2.patch and root-pin-faults-after-267.patch,
applied unchanged; main moved their hunks by offset only (shape-doctor.py
526 lines, tests/test_shape_doctor.py 521, update-shape.py 1):

- update-shape.py: `check_pin(pin, pin_path, root)`, `read_pin` passes
  its root, and the docstring says `root` is the repository whose history
  a refusal's `git log` reads.
- shape-doctor.py `pin_reads_at` passes `ctx.root`. Its docstring says
  `check_pin` refuses `files: 5` as `update-pin-row`, and `TypeError`
  leaves its `except` tuple: no pin's bytes raise one now, so one means a
  caller out of step with `check_pin`, and the doctor fails loudly rather
  than answer that no commit holds a pin to put back.
- tests/test_shape_doctor.py: #267's ROOT_PIN_FAULTS gains `files: 5`,
  `true` and `1.5`, each `update-pin-row`, so each is INVALID, exit 1,
  with `checkout HEAD` as its NEXT, which run as printed leaves the root
  COMPLIANT.

Main's other changes to this PR's files merge as text away from these
lines: #446 (245d775) in validate-pins.py, #406 (ec8e09a) in
validate-family.py, #379 (87d2eb7) in update-shape.py, and #375 (cd1fc79)
and #357 (5678f68) in shape-doctor.py. Neither module has changed on main
since decff87.

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 13, 2026
…every pinned copy with HEAD before its first write and refuses a difference as `update-branch-uncommitted`, so an in-place `apply` left uncommitted no longer ends in a commit its own validate-pins.py refuses, in git's "nothing to commit", or in "nothing to do" with no branch made (#436) (#474)

Every verdict `apply` reaches is read from the working tree (`read_pin`,
`classify`, `_nothing_to_do`), and the commit `--branch` makes is HEAD plus
only the paths the run writes. #379 made the `--pr` re-run compare the two;
every other `--branch` run still mixed them. Measured on d6df36b with local
bare remotes and a logging `gh` stub, upstream B, C and D each changing one
copy, after an in-place `apply --at C` left uncommitted on main:
`apply --at D --branch shape/d` exited 0 and committed Makefile, the pin and
the manifest, whose own validate-pins.py exits 1 on two copies the commit
left out; `apply --at C --branch shape/c [--push --pr]` said "nothing to
do", exit 0, and made no branch. On a branch whose HEAD re-pins B, the same
in-place C then `apply --at B --branch <it>` wrote, rolled back and exited 2
on git's "nothing to commit". An uncommitted edit taken with
`--accept-local`, and an uncommitted manifest edit, rode the same gap: the
first commit was red, the second carried the edit under the re-pin's
message.

- Before the first write, and before "nothing to do" is answered, every
  `--branch` run asks `_refuse_what_head_does_not_commit`: the pin, the
  kind's manifest and every pinned copy, compared with HEAD by bytes
  (#379's `_not_what_head_commits`: `git hash-object` against
  `HEAD:<path>`, the clean filter run, no index flag consulted).
- A difference on the re-run `_awaits_its_pull_request` recognises is still
  `update-rerun-uncommitted`, word for word. On any other `--branch` run it
  is the new `update-branch-uncommitted`, naming HEAD, each path, and both
  pins where they differ; its remediation is to commit those changes (on a
  branch, never the tracking branch) or put them back to HEAD's bytes, then
  run the same command again. Nothing is written, no branch is made, `gh`
  is not asked and nothing is pushed. Asked first, it also comes before
  `update-local-drift` and `update-conflict` for an uncommitted edit; once
  the edit is committed, those refusals follow as before.
- The commit's pathspec is not widened: what a person left uncommitted is
  theirs to commit or put back. Paths stay explicit, nothing is taken from
  the index, and README and untracked files are still not consulted.
- `_not_what_head_commits` now treats a path neither the disk nor HEAD
  holds (a pinned copy deleted and committed) as no difference, so that
  copy is still `update-conflict`'s `copy-missing`; and where a symlink
  stands on the path's way it compares the link's own text with HEAD's
  entry for the link, which is what git commits, so a pinned copy kept as
  a committed link is not refused as a change HEAD lacks. A pinned copy
  deleted and not committed is now refused as `update-branch-uncommitted`
  rather than `update-conflict`.
- A directory where HEAD commits a directory (a tree, or a gitlink, which
  git checks out as one) is no difference either: a pinned copy replaced by
  a directory, that type change committed, has nothing for `git
  hash-object` to read, and was refused `update-branch-uncommitted` for
  good by a remedy that cannot change it. It reaches `classify`'s
  `copy-missing` and `update-conflict`, as on main, before anything is
  written. Any other pair of kinds is still `update-branch-uncommitted`: a
  directory over a committed file (staged or not), a file over a committed
  directory, a directory where HEAD commits nothing.
- What stands at a path is `lstat`ed before git is asked to read it. A
  FIFO, a socket or a device is never read and is no difference: git
  commits none of them, and `classify` already calls such a copy
  `copy-missing`, so `update-conflict` names it, with main's exit code and
  output. Handed to `git hash-object`, a FIFO blocked the open for good
  and every `--branch` run hung where main refused; a socket was refused
  as `update-branch-uncommitted`, which no remedy changes.
- Under `--branch`, after `update-conflict` and `update-local-drift`, the
  pin, the manifest and every pinned copy the run writes or re-pins are
  walked for a symlink at or above them (`symlink_on_the_way`), and each
  one found is refused as `update-rewrite-symlink` before the first
  write. The comparison holds a committed link to be HEAD's, but the run
  reads, writes and re-pins the bytes at its far end, so a copy upstream
  rewrote through a link, an edit at a link's end taken with
  `--accept-local`, and the pin or the manifest kept as a link each exited
  0, on main too, over a commit holding the links as HEAD had them, whose
  own validate-pins.py is red in a clean clone. The remedy is to replace
  the link with what it points at, commit that on a branch, and run the
  same command again. A linked copy the run leaves unchanged is not
  refused: the pin keeps HEAD's row for it. The name is the one #286's
  fix gives the same refusal in every mode.
- Without `--branch` (the issue's a'), the run writes as before. Its NEXT
  line, which pasted made the same red commit, now names the pinned copies
  it did not write whose bytes HEAD does not commit, says the pin records
  them, and prints no commit to paste: commit them with the run's paths,
  or put both back and run apply again. A branchless run is not refused:
  the suite runs it over uncommitted edits on purpose (`--accept-local`
  over a local edit, a hand-edited pin), and nothing is committed there.

Unchanged: a clean tree, byte for byte where diffed (the probe's
clean-tree `--branch`, in-place and current-branch runs, and the
README-and-untracked run, give identical output on d6df36b and on this
change; the clean `--branch` run again on 03dc729 and on the final head);
#379's re-run refusal and its tests; #202's rollback; `check`; every
`--help` text; AGENTS.md, which does not name the new code. #285's two
refusals keep their places: `update-add-not-a-directory` is asked first,
in `resolve_additions`, and a write that fails after this comparison is
still `update-write-failed`, rolled back to the bytes it found to be
HEAD's, so the same command run again lands (probed on d6df36b and on this
change, identical output).

Left out: the branchless run that moves the pin back to what HEAD
already commits still prints a commit line git answers with "nothing to
commit", which no commit can make red. A branchless run still writes
through a symlink, as on main (#286). An accepted edit at a link's end
that is already committed is refused under `--branch` too, where main's
commit was green; the remedy lands it. A manifest replaced by a FIFO
hangs `apply` in every mode on main, in `_repin` (reading the manifest),
after the copies and the pin are written; under `--branch` the run now
reaches that point as main does.

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