Skip to content

fix: prune stale tracking refs before pushing so re-split branches are not rejected as stale - #157

Closed
vitali87 wants to merge 25 commits into
fix/clean-tolerates-deleted-branchesfrom
fix/prune-stale-remote-refs
Closed

vitali87 wants to merge 25 commits into
fix/clean-tolerates-deleted-branchesfrom
fix/prune-stale-remote-refs

Conversation

@vitali87

Copy link
Copy Markdown
Owner

Summary

Split branch names are reused across runs (pr-split/<ns>/<group>). After pr-split merge (gh deletes the head branch), clean, or GitHub auto-deleting head branches, the remote branch is gone but refs/remotes/origin/pr-split/… still points at the old head — so every git push --force-with-lease of the next split was rejected with "stale info", PRCreationError fired, and the plan was saved with branches but no PRs. Re-splitting the same dev branch was impossible without a manual git fetch --prune.

  • _push_and_create_prs now runs git remote prune origin once before the push pool (warning-only if origin is unreachable). It drops only refs whose remote branch is gone — deliberately not fetch --prune, which would refresh live refs and silently defeat the lease against a reviewer's fixup push to a reused branch (the review caught that in the first version).
  • delete_branch(remote=True) drops refs/remotes/origin/<branch> after the remote delete, covering the "already gone on origin" case.

Stacked on #156 (stack #152 = #55→#151→#156→this).

Test plan

  • real-git TestStaleRemoteTrackingRefs: remote branch deleted behind our back → push rejected as stale → prune → push succeeds; a third-party fixup push is still rejected after prune (lease intact); remote delete drops the tracking ref; already-deleted remote still drops it
  • TestPushPrunesFirst (prune before any push); mocked delete_branch tests stub the helper
  • Review re-ran its probe (third-party push, deleted branch, single-branch clone, unreachable origin)
  • 464 tests pass, ruff clean
  • Local review: 5/5 (after one fix→re-review round)

@coderabbitai

coderabbitai Bot commented Aug 30, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 56 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: fa2ce2c0-d795-4bcf-b2b0-47a09f098c71

📥 Commits

Reviewing files that changed from the base of the PR and between 769ba85 and 2c0fd0e.

📒 Files selected for processing (12)
  • pr_split/cli.py
  • pr_split/exceptions.py
  • pr_split/git_ops/__init__.py
  • pr_split/git_ops/branches.py
  • pr_split/logs.py
  • pr_split/planner/client.py
  • tests/network_guards.py
  • tests/test_cli_coverage.py
  • tests/test_cli_helpers.py
  • tests/test_cli_new_features.py
  • tests/test_client.py
  • tests/test_git_branches.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@vitali87
vitali87 force-pushed the fix/prune-stale-remote-refs branch from e2495a9 to 72fb0aa Compare August 30, 2026 19:24
vitali87 and others added 24 commits August 30, 2026 20:47
A push/PR-creation failure leaves the plan with branches recorded and no
PRs. execute then refused to run ('use clean first') and clean deletes the
plan file entirely, so recovering from a transient network failure meant
regenerating the whole plan - for the LLM backend, paying for planning
again and possibly getting a different split. Branch creation is
idempotent (add_worktree recreates an existing branch), so execute now
retries when the saved state has branches but no PRs, and only refuses
when PRs already exist.
git diff and git show were run with text=True, whose universal-newline
translation strips the CR from CRLF files, and the worktree write used
the platform newline. A one-line change to a CRLF file therefore became
a whole-file line-ending rewrite in every sub-PR. Capture git output as
bytes and decode it, and write reconstructed files with newline="".
git quotes and octal-escapes non-ASCII paths by default
(core.quotePath). unidiff then fails on new/deleted files ('Target
without source') and, for modified files, records the literal quoted
string as the path so materialization asks git for a path that does
not exist. Run git diff with core.quotePath=false.
…ansient push errors

A push that failed with a transient server error left some groups with PRs
and the rest without, and execute then refused to run at all, so the only way
on was clean (closing good PRs) and a full re-run. push_branch now retries
known transient server errors, and execute keeps the groups that already have
PRs (their branches untouched) and creates only the rest.
Conflicts resolved:
- cli.py: the rollback on a failed branch keeps groups whose PRs already
  exist (this branch) under main's note that failed groups clean up their
  own branch; a PR-creation failure still prints a clean message and exits
  (this branch), and stacks are linked once, after the plan is saved (main).
- git_ops/branches.py: push retry on transient errors kept; the unused
  checkout/commit helpers main removed stay removed.
- logs.py: PUSH_RETRY kept alongside main's CP_SAT_NOT_OPTIMAL.
- tests: both sides' imports and test classes kept. Tests that feed execute
  a mocked diff now patch validate_coverage, which main's execute runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rnEEjXWDBdePRhKMmZr1w
… diff

main's execute validates the plan against the parsed diff; these tests mock
parse_diff, so the check would reject the plan before the retry logic runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rnEEjXWDBdePRhKMmZr1w
…sh that landed

- execute: when some layers already have PRs, a stacked resume now also
  keeps the recorded branches of those layers' ancestors. Rebuilding an
  ancestor gave it a new commit and force-pushed it, so the kept child's
  open PR (which targets it) showed the ancestor's changes again. The kept
  ancestors still get their PRs opened.
- push_branch: after a transient error (hung-up remote, timeout) the push
  may have landed; the retry is then rejected as a stale lease. If the
  remote branch already holds the local head, the push counts as done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rnEEjXWDBdePRhKMmZr1w
…x/execute-resume-partial-prs

The base branch's history was rewritten when #174 landed, so its commits
reappear here. Every conflict is this branch's version of execute's resume
logic and its tests, which already build on the base's change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rnEEjXWDBdePRhKMmZr1w
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rnEEjXWDBdePRhKMmZr1w
#146 (the base of this branch) is already in main, so the merge keeps
main's code and adds only #168's change: an Anthropic token-count
failure is an LLMError, and a rejected key names ANTHROPIC_API_KEY.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rnEEjXWDBdePRhKMmZr1w
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rnEEjXWDBdePRhKMmZr1w
Since execute lays out any plan with dependency edges along its DAG,
stacked or not, an unstacked child also sits on its parent's branch;
rebuilding the parent on resume would rewrite the base under the child's
open PR. Keep recorded ancestors whenever the plan has dependencies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rnEEjXWDBdePRhKMmZr1w
fix: resume execute past groups whose PRs already exist, and retry transient push errors
fix: report an Anthropic token-count failure as an LLM error, naming a rejected key
Also stub prune_remote_tracking_refs in the CLI network guards so tests
that push through _push_and_create_prs never run `git remote prune origin`
against the real checkout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rnEEjXWDBdePRhKMmZr1w
@github-actions

Copy link
Copy Markdown

pr-split analysis

Metric Value
Total LOC 637
Files changed 12
Suggested groups 6
Largest group 268 LOC
LOC overflow 0
File scatter 0

This PR has 637 LOC and could be split into 6 smaller PRs:

Group Title Diff Depends On Files
pr-1 chore(split): review test-git-branches-1 +262/-6 — tests/test_git_branches.py
pr-2 chore(split): review test-cli-coverage-2 +128/-1 — tests/test_cli_coverage.py
pr-3 chore(split): review-slice-3 +175/-15 — pr_split/cli.py, pr_split/exceptions.py, pr_split/git_ops/__init__.py, pr_split/git_ops/branches.py, pr_split/logs.py, pr_split/planner/client.py, tests/test_client.py
pr-4 chore(split): review test-cli-helpers-4 +40/-0 — tests/test_cli_helpers.py
pr-5 chore(split): review network-guards-5 +8/-0 — tests/network_guards.py
pr-6 chore(split): review test-cli-new-features-6 +2/-0 — tests/test_cli_new_features.py

Run pr-split split locally to create these sub-PRs, or adjust --max-loc to change the target size.

Copy link
Copy Markdown
Owner Author

This change is already in main: it landed via the merge train #243, which includes this PR's head commit. GitHub can't mark a stacked PR merged when it lands that way, so I'm closing it as done.


Generated by Claude Code

@vitali87 vitali87 closed this Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants