docs(cache): recommend delete-on-merge, and say what ancestry still covers
CI / shellcheck + selftests (pull_request) Successful in 1m27s

`daniel/zemyna` enabled `default_delete_branch_after_merge` after this
branch was written, so the deleted-branch signal will fire there on
future merges. That makes the setting worth recommending — it is the
cheapest case for this scheme, decidable from `ls-remote` with no
checkout, no objects and no walk — and it does not make the ancestry
signal redundant.

Three things it leaves behind, now enumerated in the README's eviction
section rather than implied: every branch merged before the setting was
turned on, of which zemyna carried 48 and which nothing retroactively
deletes; every merge whose deletion the forge declines or is never asked
to make, since it is best-effort and silent and an API merge without the
flag never asks; and every repo that has not enabled it, which is the
default.

Two present-tense claims about one repo's configuration are reworded
into the conditions they were standing in for, in prune-cache.sh's
header and beside is_merged_dead, plus the two in the selftest that
asserted the forge keeps branches rather than describing the fixture.
No behaviour change; the suite is green and unchanged at 61 assertions.

Refs #20.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXMQCJ5Eg5f9G9cfYzyh4Z
This commit is contained in:
2026-09-07 00:27:11 -05:00
co-authored by Claude Fable 5.1
parent 24f87a6b98
commit 38a6387936
3 changed files with 34 additions and 16 deletions
+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