fix(prune): reclaim merged branches and size the volume for the clone #21

Merged
claude merged 4 commits from fix/prune-merged-live-and-seed-headroom into main 2026-09-07 05:41:12 +00:00
3 changed files with 34 additions and 16 deletions
Showing only changes of commit 38a6387936 - Show all commits
+21 -7
View File
@@ -278,15 +278,29 @@ the obvious order, because a snapshot is hardlinked to everything cloned from
it, so removing one frees almost no real bytes while costing every future PR it, so removing one frees almost no real bytes while costing every future PR
its warm start. its warm start.
**A branch is dead in two ways, and the second is the one that reclaims **A branch is dead in two ways, and neither signal makes the other
anything here.** Gitea keeps a PR's branch after the merge unless the repo redundant.** The first is that the branch is gone from origin. The second is
opts into delete-on-merge, so `git ls-remote` reports merged branches forever
and "gone from origin" fires for almost nothing. The second signal is
ancestry: a branch still on origin whose tip is an ancestor of a protected ancestry: a branch still on origin whose tip is an ancestor of a protected
branch's tip holds no commit that branch does not, so its cache will never be branch's tip holds no commit that branch does not, so its cache will never be
read again and goes in the same pass. On zemyna's volume that is the read again and goes in the same pass.
difference between reclaiming nothing and reclaiming a 40 GB directory per
merged PR (gitdan-actions#20). **Turn delete-on-merge on** (`default_delete_branch_after_merge`, per repo) —
it is the setting this scheme is cheapest under, because a deleted branch is
decidable from `ls-remote` alone, with no checkout, no objects and no walk.
The ancestry signal is what covers the rest, and the rest is not a corner:
- **Every branch merged before the setting was turned on.** They stay on
origin forever; nothing retroactively deletes them. zemyna carried 48 of
them at the time the setting was enabled, and ancestry is the only thing
that reclaims a cache dir belonging to any of them.
- **Every merge the deletion declines or fails.** Gitea's delete is
best-effort and silent: it declines for a protected branch and for one
another open PR still uses, and an API merge that omits the flag — which
`tea pulls merge` does — simply never asks.
- **Repos that have not enabled it**, which is the default.
That is the difference between reclaiming nothing and reclaiming a 40 GB
directory per merged PR on a full volume (issue 20).
Ancestry is answered from the commits in the job's own checkout, so it is only Ancestry is answered from the commits in the job's own checkout, so it is only
answered where they are there to answer it — and "cannot tell" is never folded answered where they are there to answer it — and "cannot tell" is never folded
+5 -5
View File
@@ -46,10 +46,10 @@
# empty a tree its owner may still restore under a live cache name. Its # empty a tree its owner may still restore under a live cache name. Its
# fixture is an OLD directory renamed a moment ago — production's shape, # fixture is an OLD directory renamed a moment ago — production's shape,
# and what lets it tell the two timestamps apart. # and what lets it tell the two timestamps apart.
# 15. A MERGED-BUT-UNDELETED BRANCH IS DEAD TOO. This forge keeps branches # 15. A MERGED-BUT-UNDELETED BRANCH IS DEAD TOO. A branch the forge did not
# after merge, so `ls-remote` reports them forever and scenario 1's # delete at merge stays on `ls-remote` forever, so scenario 1's signal
# signal never fires for them — which is how three 40 GB caches sat on a # never fires for it — which is how three 40 GB caches sat on a full
# full volume until somebody removed them by hand (gitdan-actions#20). A # volume until somebody removed them by hand (gitdan-actions#20). A
# branch whose tip is an ancestor of a protected branch's tip is pruned # branch whose tip is an ancestor of a protected branch's tip is pruned
# like a deleted one; an unmerged branch beside it is not. # like a deleted one; an unmerged branch beside it is not.
# 16. AND "CANNOT TELL" IS STILL NOT DEATH, at both granularities: a branch # 16. AND "CANNOT TELL" IS STILL NOT DEATH, at both granularities: a branch
@@ -314,7 +314,7 @@ assert_log "may still be evicting it" "the deferral gives its actual reason"
echo echo
echo "=== 15: a merged-but-undeleted branch is dead too ===" echo "=== 15: a merged-but-undeleted branch is dead too ==="
# This forge keeps a PR's branch after the merge, so `ls-remote` reports it # A branch the forge did not delete at merge, which `ls-remote` then reports
# forever. Built the way that happens: a branch merged into dev with a merge # forever. Built the way that happens: a branch merged into dev with a merge
# commit, still pushed, beside one branched at the same point and NOT merged. # commit, still pushed, beside one branched at the same point and NOT merged.
git="git -c user.email=t@t -c user.name=t -c commit.gpgsign=false" git="git -c user.email=t@t -c user.name=t -c commit.gpgsign=false"
+8 -4
View File
@@ -28,8 +28,9 @@
# removed UNCONDITIONALLY, not gated on free space. A directory for a # removed UNCONDITIONALLY, not gated on free space. A directory for a
# branch nothing will build again is pure loss; waiting for disk pressure # branch nothing will build again is pure loss; waiting for disk pressure
# to notice means paying for it until then. Two signals make a branch # to notice means paying for it until then. Two signals make a branch
# dead, and the second exists because the first alone is inert on a forge # dead, and the second exists because the first alone is inert wherever
# that keeps branches after merge (gitdan-actions#20): # a merged branch stays on origin — the default, and still the outcome
# whenever delete-on-merge declines or is not asked (gitdan-actions#20):
# #
# DELETED — the branch is no longer on origin at all. # DELETED — the branch is no longer on origin at all.
# MERGED — the branch is still on origin, but its tip is an ancestor # MERGED — the branch is still on origin, but its tip is an ancestor
@@ -434,8 +435,11 @@ fi
# #
# True when the branch this directory belongs to is still on origin but every # True when the branch this directory belongs to is still on origin but every
# commit it holds is already on a protected branch — a merged PR whose branch # commit it holds is already on a protected branch — a merged PR whose branch
# the forge did not delete, which is the case zemyna hits on every merge and # the forge did not delete, which the deleted-branch signal above can never
# which the deleted-branch signal above can never see. # see. Enabling delete-on-merge narrows this to the branches merged before it
# was enabled, the ones its deletion declines (protected, or used by another
# open PR), and the merges that never ask (an API merge without the flag);
# see README's eviction section.
# #
# Memoised per tip because target-<key> and snapshot-<key> share one branch, # Memoised per tip because target-<key> and snapshot-<key> share one branch,
# and because the "cannot tell" warning belongs to the branch rather than to # and because the "cannot tell" warning belongs to the branch rather than to