·4 min read·Mounaji Studio

Is that old PR already in main? Checking after a squash merge

After a squash or rebase merge, git branch --merged, commit counts and git cherry all say a branch has pending work when it is already in. How to ask the content instead with git merge-tree, spot the branch that would roll the base back, and archive before you delete.

engineeringgitgithub

Also in: Español

You have eighty open PRs and half of them are months old. Many are already in: someone squash-merged them, or copied the changes into another PR, and the branch was left hanging. Closing them one by one by reading diffs does not scale. Asking git looks easy, and that is the trap: the usual three questions look at commits, and with a squash merge the branch's commits never reach the base.

What fails, in a toy repo

A feature branch with two commits, merged with git merge --squash. Its content is fully in main. Here is what git says:

git branch --merged main        # * main        → feature is not listed
git rev-list --count main..feature   # 2         → "2 pending commits"
git cherry main feature         # + a48b573 / + 89b678e → "2 new patches"

A squash-merged branch faces four questions. branch --merged, the commit count and git cherry say work is missing; only merge-tree, which compares content, answers that it is already in.

git cherry compares patch-ids, a fingerprint of each patch. A squash folds several patches into one and the fingerprint changes, so the originals never show up as applied. On a real sample of three squashed branches it got one right.

The right question: would merging it change anything?

What commits the branch has does not matter; what matters is which files you would end up with if you merged it. git merge-tree --write-tree (git 2.38+) performs the merge in memory, without touching your checkout, and returns the resulting tree. If it equals the base tree, the branch adds nothing:

git merge-tree --write-tree main feature   # be43f2a…
git rev-parse 'main^{tree}'                # be43f2a…  → identical: already in

It works the same for squash, rebase or a hand-made cherry-pick, because it ignores history and looks at the result.

Decision tree: if the merge-tree result equals the base tree, archive and close. If not, and every file already existed in the base, the base moved past it: do not merge. If any file is new, review it by hand.

The tricky case: the base moved past it

Some branches do change the tree and still have nothing new. The branch changed b to B, was squash-merged, and later someone on main edited it to B2. Now merge-tree returns a different tree (or a conflict) and the diff looks like pending work:

-B2
+B

Timeline: the branch changes b to B and lands with a squash; later main changes B to B2. The diff between main and the branch shows B2 → B, a rollback, not pending work.

It is not work: it is an old version of something that already landed. Merging it would roll main back. To tell them apart, ask for every file the branch touched whether that exact version ever existed in the base after the point where they split:

mb=$(git merge-base main branch)
for f in $(git diff --name-only "$mb" branch); do
  blob=$(git rev-parse "branch:$f")
  if git log --format=%H "$mb..main" -- "$f" | while read c; do
       [ "$(git rev-parse "$c:$f" 2>/dev/null)" = "$blob" ] && echo hit; done | grep -q hit
  then echo "already was in main: $f"
  else echo "NEW: $f"; fi
done

If every file says "already was", the branch adds nothing and does not get merged. If any says "NEW", that one needs a human look. Comparing only against the tip of main is not enough: you have to search the history.

Before deleting, a tag

A squashed branch has commits that exist nowhere else. Delete it and the content is still in main but the history is gone, and if you were wrong, so is the content. A tag costs nothing:

Four steps in order: create the archive tag at the branch tip, push it, check with ls-remote that it exists on the remote, and only then delete the branch. If the tag did not land, the branch is left alone.

git tag "archive/2026-10-17/feature" origin/feature
git push origin "archive/2026-10-17/feature"
git ls-remote --tags origin "archive/2026-10-17/feature"   # check it landed
git push origin --delete feature                          # only now

And one rule for automating it: if you could not measure (a network error, a truncated diff), the verdict is undetermined and nothing gets deleted. "I don't know" can never fall on the irreversible side.

To start today: pick your oldest open PR, run git merge-tree --write-tree against its base and compare the tree. If it matches, you already know what to do with the other seventy-nine.

Comments and corrections

Chat with us