Skip to content

Instantly share code, notes, and snippets.

@danielrose7
Last active March 26, 2026 17:00
Show Gist options
  • Select an option

  • Save danielrose7/700415aedd7e78e200ec1bff21354073 to your computer and use it in GitHub Desktop.

Select an option

Save danielrose7/700415aedd7e78e200ec1bff21354073 to your computer and use it in GitHub Desktop.
trim — zsh function to delete local git branches (+ worktrees) that have been merged. Catches both true merges and squash merges via GitHub PR status. Auto-detects default branch from origin/HEAD.
# Delete local branches merged to the default branch + their worktrees
# Catches both true merges (git) and squash merges (GitHub PR status)
# Reads default branch from origin/HEAD — works with main, staging, etc.
# Usage: trim [--force] (--force skips confirmation)
trim() {
local repo_root
repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "Not in a git repo"; return 1; }
local base_branch
base_branch="$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null)"
base_branch="${base_branch##refs/remotes/origin/}"
if [[ -z "$base_branch" ]]; then
echo "Cannot determine default branch. Run: git remote set-head origin --auto"
return 1
fi
git fetch origin "$base_branch" --quiet
# 1) Branches merged via git (fast-forward / true merge)
local -A to_delete=()
local branch
while IFS= read -r branch; do
branch="${branch//[[:space:]]/}"
branch="${branch#+}"
[[ -z "$branch" || "$branch" == "$base_branch" || "$branch" == "main" || "$branch" == \** ]] && continue
to_delete[$branch]=git
done < <(git branch --merged "origin/$base_branch")
# 2) Branches whose GitHub PR was squash-merged
local -a local_branches=()
while IFS= read -r branch; do
branch="${branch//[[:space:]]/}"
branch="${branch#+}"
[[ -z "$branch" || "$branch" == "$base_branch" || "$branch" == "main" || "$branch" == \** ]] && continue
local_branches+=("$branch")
done < <(git branch)
if [[ ${#local_branches[@]} -gt 0 ]]; then
local -A merged_prs=()
while IFS= read -r branch; do
[[ -n "$branch" ]] && merged_prs[$branch]=1
done < <(gh pr list --state merged --limit 200 --json headRefName --jq '.[].headRefName' 2>/dev/null)
for branch in "${local_branches[@]}"; do
if [[ -n "${merged_prs[$branch]+x}" && -z "${to_delete[$branch]+x}" ]]; then
to_delete[$branch]=squash
fi
done
fi
if [[ ${#to_delete} -eq 0 ]]; then
echo "No merged branches to clean up."
git worktree prune
return 0
fi
# Build worktree map
local wt_info wt_path answer
wt_info="$(git worktree list --porcelain)"
echo "Branches to delete (${#to_delete} total):"
for branch in "${(@k)to_delete}"; do
wt_path=$(echo "$wt_info" | awk -v b="refs/heads/${branch}" '/^worktree /{wt=$2} /^branch /{if($2==b)print wt}')
local label="[${to_delete[$branch]}]"
if [[ -n "$wt_path" ]]; then
echo " $label $branch → $wt_path"
else
echo " $label $branch"
fi
done
if [[ "$1" != "--force" ]]; then
echo ""
read "answer?Proceed? [y/N] "
[[ "$answer" =~ ^[Yy]$ ]] || { echo "Aborted."; return 0; }
fi
local count=0
for branch in "${(@k)to_delete}"; do
wt_path=$(echo "$wt_info" | awk -v b="refs/heads/${branch}" '/^worktree /{wt=$2} /^branch /{if($2==b)print wt}')
if [[ -n "$wt_path" && -d "$wt_path" ]]; then
echo "Removing worktree: $wt_path"
rm -rf "$wt_path"
fi
git branch -D "$branch" 2>/dev/null
(( count++ ))
done
git worktree prune
echo "Done. Cleaned $count branch(es)."
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment