prune deletes the remote twin on a proof computed only from local refs #566

Open
opened 2026-09-21 21:08:26 +00:00 by hexajon · 1 comment
Owner

Part of #563.

The second gap in the prune machine-proof carve-out. Separate from #564, same file.

The defect

After a local branch deletes cleanly, prune deletes its remote twin and then drops the tracking ref: deleteLocalBranches (internal/service/prune/run.go:748-774) and deleteSquashMerged (run.go:620-632) call branchAPI.Delete, then deleteTrackingRef (run.go:43-46). No step reads origin/<name>.

The local delete is not a proof about the remote ref. git branch -d accepts a branch merged into HEAD or merged into its upstream, and merged-into-HEAD alone is enough. So a local branch can pass -d while its remote twin holds a commit that is in no other ref. Under --delete-branches --remote that commit then loses its forge ref and its local tracking ref in the same run. Nothing recoverable remains.

The orphan path in the same file is proven correctly: findRemoteOrphans checks git branch -r --merged origin/<default> (run.go:407-413). Only the twin path skips the check.

Fix

Check containment of the remote ref before the API delete, for example git merge-base --is-ancestor origin/<name> <defaultBranch>, and route a non-contained twin to the skipped list the way deleteSquashMerged already routes a non-contained local branch.

Acceptance criteria

  • prune never deletes a remote branch whose tip is not contained in the default branch.
  • A branch whose remote twin is ahead of its local counterpart is covered by a regression test, and that test fails against the current code.
  • The skipped-branch reporting names the remote twin case, so the user learns why it was left alone.

Note on severity

Whether this is as urgent as #564 is a judgment call. #564 fires on an ordinary local workflow. This one needs the remote twin to be ahead, which is a narrower state, but the loss is the same and there is no reflog on the forge side.

Part of #563. The second gap in the `prune` machine-proof carve-out. Separate from #564, same file. ## The defect After a local branch deletes cleanly, `prune` deletes its remote twin and then drops the tracking ref: `deleteLocalBranches` (`internal/service/prune/run.go:748-774`) and `deleteSquashMerged` (`run.go:620-632`) call `branchAPI.Delete`, then `deleteTrackingRef` (`run.go:43-46`). No step reads `origin/<name>`. The local delete is not a proof about the remote ref. `git branch -d` accepts a branch merged into HEAD or merged into its upstream, and merged-into-HEAD alone is enough. So a local branch can pass `-d` while its remote twin holds a commit that is in no other ref. Under `--delete-branches --remote` that commit then loses its forge ref and its local tracking ref in the same run. Nothing recoverable remains. The orphan path in the same file is proven correctly: `findRemoteOrphans` checks `git branch -r --merged origin/<default>` (`run.go:407-413`). Only the twin path skips the check. ## Fix Check containment of the remote ref before the API delete, for example `git merge-base --is-ancestor origin/<name> <defaultBranch>`, and route a non-contained twin to the skipped list the way `deleteSquashMerged` already routes a non-contained local branch. ## Acceptance criteria - `prune` never deletes a remote branch whose tip is not contained in the default branch. - A branch whose remote twin is ahead of its local counterpart is covered by a regression test, and that test fails against the current code. - The skipped-branch reporting names the remote twin case, so the user learns why it was left alone. ## Note on severity Whether this is as urgent as #564 is a judgment call. #564 fires on an ordinary local workflow. This one needs the remote twin to be ahead, which is a narrower state, but the loss is the same and there is no reflog on the forge side.
Author
Owner

Verification status, same caveat as #564.

The code path is confirmed by reading: deleteLocalBranches and deleteSquashMerged call branchAPI.Delete and then drop the tracking ref, and neither reads origin/<name> first.

The git behaviour the finding turns on, that git branch -d accepts merged-into-HEAD alone and so passes while the remote twin is ahead, is not independently confirmed. Re-run it before writing the regression test, and correct this issue if it does not hold.

Verification status, same caveat as #564. The code path is confirmed by reading: `deleteLocalBranches` and `deleteSquashMerged` call `branchAPI.Delete` and then drop the tracking ref, and neither reads `origin/<name>` first. The git behaviour the finding turns on, that `git branch -d` accepts merged-into-HEAD alone and so passes while the remote twin is ahead, is not independently confirmed. Re-run it before writing the regression test, and correct this issue if it does not hold.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
sixfold-space/madtea#566
No description provided.