Require git proof before prune deletes a branch
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

A merged or closed PR no longer authorises a delete on its own: HEAD must
be contained in the PR's head commit or in origin/<branch>, otherwise the
worktree goes and the branch stays. Branch deletion runs `git branch -d`
first and falls back to -D only for a proven branch.

Reword the cherry check to say patches reached the default branch's
history, print the verdict --keep-branches will actually perform, and warn
when a PR listing hits the pagination cap instead of reading it as "no PR".
This commit is contained in:
2026-09-10 00:00:22 +10:00
parent 4bbeaae8f0
commit c1c02c01cf
7 changed files with 249 additions and 47 deletions
+23 -9
View File
@@ -124,17 +124,31 @@ agentws token
|---|---|
| uncommitted or untracked changes | keep |
| branch has an open PR | keep |
| tip contained in `origin/<default>`, or `git cherry` finds no unmerged patch, or its PR is merged | remove worktree + local branch |
| PR closed unmerged and the branch is still on origin | remove worktree + local branch |
| tip contained in `origin/<default>` | remove worktree + local branch |
| every commit patch-equivalent to one in `origin/<default>`'s history | remove worktree + local branch |
| PR merged **and** HEAD contained in the PR's head commit (or in `origin/<branch>`) | remove worktree + local branch |
| PR closed **and** HEAD contained in `origin/<branch>` | remove worktree + local branch |
| anything else | remove worktree, keep the branch |
The git checks are authoritative and work offline: these repos squash-merge, so
a merged branch's commits carry different SHAs upstream and a plain
`rev-list origin/<default>..HEAD` count proves nothing. Gitea PR state only adds
to the git answer — when it cannot be reached, prune says so and never deletes a
branch it could not prove. Matching a branch to its PR uses `head.label`, since
Gitea rewrites `head.ref` to `refs/pull/<n>/head` once the branch is deleted on
merge.
A branch is deleted only where git proves its commits survive elsewhere. PR
state alone never authorises that: a merged or closed PR whose branch picked up
commits since keeps its branch, because those commits exist nowhere but here.
The delete runs `git branch -d` first so git's own unmerged check is a backstop,
falling back to `-D` only for a proven branch — squash merges keep the guard
tripping even once the work has landed.
Patch equivalence comes from `git cherry`, which these squash-merging repos need
because a merged branch's commits carry different SHAs upstream. It proves the
patches reached the default branch's history at some point — a later revert
still counts — not that they stand at its tip.
Gitea PR state only adds to the git answer: when it cannot be reached, prune
says so and never deletes a branch it could not prove, and a PR listing that
hits the pagination cap is reported rather than read as "no PR". Matching a
branch to its PR uses `head.label`, since Gitea rewrites `head.ref` to
`refs/pull/<n>/head` once the branch is deleted on merge.
`--keep-branches` removes worktrees only, and its verdicts print as `remove`.
### Auth / credential-helper design