Skip to content

prune never reclaims squash-merged worktrees: ancestry test cannot detect a squash merge #85

Description

@atqamz

prune decides a worktree is merged by testing commit ancestry against the default branch. A squash merge produces a commit with no parent link to the branch commits, so the branch tip is genuinely not an ancestor even though its content has landed. On a repository that squash merges, every branch that ships leaves behind a worktree that prune classifies as unmerged and refuses to remove, permanently.

Version: v2.1.1

Reproduce

git init --bare origin.git
git clone origin.git work && cd work
git commit --allow-empty -m init && git push -u origin main

git switch -c feature
echo hi > f.txt && git add f.txt && git commit -m "add f"
echo there >> f.txt && git commit -am "extend f"
git push -u origin feature

# squash merge, as GitHub "Squash and merge" does
git switch main
git merge --squash feature && git commit -m "add f (#1)"
git push

git merge-base --is-ancestor feature main; echo "ancestor: $?"   # 1, not an ancestor
git diff main feature                                            # empty, content is identical

The branch has shipped. Its content is byte-identical to main. prune sees exit 1 from the ancestry test and holds the worktree.

Observed on a real branch: PR merged, upstream issue auto-closed by the merge, the squash commit an ancestor of origin/main, and HEAD..origin/main containing only that squash commit plus one unrelated commit, so the branch was strictly behind and held nothing unique. prune still reported:

unmerged:
1     HEAD not merged into refs/remotes/origin/main

Why this compounds

Squash merge is the default or the house rule on a lot of projects, including every repository this pool serves. So the failure is not occasional, it is every shipped branch. Each one leaves a worktree that no prune run will ever reclaim, and because prune reports success while doing it, nothing surfaces the growth. The pool leaks in exactly the workflow treehouse is built for.

An unattended prune --all --yes on a timer makes it quieter still: the job succeeds every week and the held set only grows.

Possible tests that survive a squash

None of these is obviously best, so listing rather than prescribing:

  • Content comparison. Compute the branch's own contribution, git diff $(git merge-base HEAD <default>) HEAD, and check whether the default branch already contains it. Catches squashes of any number of commits, which is the common case.
  • git cherry. Patch-id equivalence catches a rebased or cherry-picked single commit, and a squash of exactly one commit. It does not catch several commits collapsed into one, so it is a partial answer on its own.
  • Ask the forge. Query whether a PR whose head is this branch is merged. Accurate, but adds a network dependency and forge-specific code to what is currently pure git.
  • Deleted upstream branch as a hint. A gone remote-tracking branch is a decent signal that the branch shipped, since most forges delete the head branch on merge. Cheap, and safe if treated as a hint rather than proof.

A conservative combination would be ancestry first, then content comparison, and only report unmerged when both say no.

Separate observation, happy to split into its own issue

prune also treats an available pool slot as stale when it is clean and merged. That is literally true, but an available pre-warmed slot is the thing the tool exists to provide, and deleting it means the next consumer pays full cold cost. On a large repository, a weekly sweep deleted tens of GiB of warm pool that then had to be rebuilt from scratch.

So the same predicate errs in both directions at once: it discards warm slots that are still useful, and keeps shipped-branch worktrees that are not. Say the word and I will open that half separately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions