Prune Stale Git Branches with a Date Cutoff
List local and remote branches older than a cutoff, confirm none are linked to open PRs, and delete them in one scripted pass with a dry-run first.
Task
Remove every branch that has not been touched in 60 days and is not linked to an open pull request.
When to use this
The repo has 140 branches and nobody knows which are alive. Engineers cannot find their own branches in the list. The team wants a monthly prune that is safe and auditable.
Prerequisites
- git 2.40 or later
- A way to list open pull requests by branch (gh CLI)
- A dry-run habit before destructive commands
Steps
1. List every branch with its last commit date in a machine-readable format.
git for-each-ref --format="%(refname:short) %(committerdate:short)" refs/heads refs/remotesExpected output: A list of branch names with dates, one per line, covering both local and remote refs.
2. Filter to branches older than 60 days and not linked to an open PR.
const open = await gh.prsByBranch();
const stale = branches.filter(b => daysSince(b.date) > 60 && !open.has(b.name));
Expected output: A list of branch names that are both stale and unlinked, safe to delete.
3. Dry-run the deletion and print every command, then run for real once the list is confirmed.
node tools/prune-branches.mjs --dry-run && node tools/prune-branches.mjsExpected output: The dry run prints git branch -D and git push origin --delete lines; the real run executes them and reports the count.
Troubleshooting
Problem
A branch is the default branch of an open PR the script missed
Solution
Query the PR API by head ref, not by name, and abort the prune if any match; a missed PR is a sign the filter is wrong.
Problem
A branch is referenced by a tag or a release
Solution
Check git for-each-ref refs/tags before deleting, and skip any branch that is the target of a tag.
Limitations
- The prune is destructive; a deleted remote branch needs a force push to restore, so keep the dry run as the default.
- A branch older than the cutoff may still be in active review; always cross-check with the PR API.
List, filter, delete
List every branch with its last commit date, filter to those older than 60 days, cross-check against open pull requests, and delete the rest. Always dry-run first.
git for-each-ref --format="%(refname:short) %(committerdate:short)" refs/heads refs/remotes
Cross-check with PRs
A stale branch may still be the head of an open PR. Query the PR API by head ref and skip any branch that matches. A branch referenced by a tag or a release is also off limits.
node tools/prune-branches.mjs --dry-run
Expected outcome
The dry run prints every deletion command, the real run executes them and reports the count, and no branch linked to an open PR or a tag is removed. Run the prune monthly so the branch list stays navigable.