Skip to content

Instantly share code, notes, and snippets.

@myleshk
Created December 15, 2025 13:35
Show Gist options
  • Select an option

  • Save myleshk/87f959f102aa93cc439136274f14d442 to your computer and use it in GitHub Desktop.

Select an option

Save myleshk/87f959f102aa93cc439136274f14d442 to your computer and use it in GitHub Desktop.
Handy script to cleanup stale (merged/old) branches.
#!/bin/bash
#
# git-cleanup
# A comprehensive script to clean up local Git branches.
# Designed to be executed as: git cleanup [DAYS]
# Input validation: DAYS_OLD must be provided as a positive integer argument.
if [[ "$1" =~ ^[0-9]+$ && "$1" -gt 0 ]]; then
DAYS_OLD=$1
else
# Exit with an error if the argument is missing or invalid
echo "---------------------------------------------------------" >&2
echo "ERROR: Invalid or missing required argument for days old." >&2
echo "USAGE: git cleanup <DAYS>" >&2
echo "Example: git cleanup 90 (to delete branches older than 90 days)" >&2
echo "---------------------------------------------------------" >&2
exit 1
fi
# =========================================================
# GLOBAL CONSTANTS AND ARGUMENT CHECK
# =========================================================
# Define the critical branches to exclude from deletion
readonly EXCLUDE_BRANCHES="master|main|develop"
echo "========================================================="
echo "🚀 Starting Comprehensive Git Cleanup..."
echo "========================================================="
# Get a list of branches currently active in worktrees.
# We strip the 'refs/heads/' prefix for accurate matching against 'git branch --merged' output.
ACTIVE_WORKTREE_BRANCHES=$(git worktree list --porcelain | \
grep branch | \
awk '{print $2}' | \
sed 's|^refs/heads/||' | \
tr '\n' '|' | \
sed 's/|$//')
# Combine the critical exclusion list with active worktree branches
EXCLUDE_ALL_BRANCHES="(${EXCLUDE_BRANCHES}|${ACTIVE_WORKTREE_BRANCHES})"
# Remove the trailing '|' if present
EXCLUDE_ALL_BRANCHES="${EXCLUDE_ALL_BRANCHES%|}"
# echo "⚠️ EXCLUDING: ${EXCLUDE_ALL_BRANCHES//\|/, }."
echo "⚠️ EXCLUDING: ${EXCLUDE_ALL_BRANCHES}."
# =========================================================
# STEP 1: PRUNING REMOTE TRACKING
# =========================================================
echo "⚙️ STEP 1/3: Pruning stale remote-tracking branches (git fetch --prune origin)..."
git fetch --prune origin --quiet || { echo "Error: Failed to fetch/prune remote 'origin'. Aborting cleanup." >&2; exit 1; }
# =========================================================
# STEP 2: SAFE CLEANUP (LOCALLY MERGED)
# =========================================================
echo "---"
echo "⚙️ STEP 2/3: Running safe cleanup for locally merged branches..."
echo "🧹 Deleting branches merged into the current branch (using -d)."
# Execute the safe delete command
git branch --merged | grep -Ev "$EXCLUDE_ALL_BRANCHES" | xargs -r git branch -d
# =========================================================
# STEP 3: ADVANCED/AGGRESSIVE CLEANUP (REMOTE GONE + OLD)
# =========================================================
echo "---"
echo "⚙️ STEP 3/3: Running aggressive cleanup for old, remote-gone branches..."
# Determine days ago in epoch time (seconds since 1970)
# Check for GNU date (Linux) first, then BSD date (macOS)
THRESHOLD_EPOCH=0
if command -v gdate >/dev/null 2>&1; then
# Use gdate if available (for environments that install it alongside BSD date)
THRESHOLD_EPOCH=$(gdate -d "$DAYS_OLD days ago" +%s)
elif date --version >/dev/null 2>&1; then
# Assume GNU date (Linux)
THRESHOLD_EPOCH=$(date -d "$DAYS_OLD days ago" +%s)
elif date -v -1d >/dev/null 2>&1; then
# Assume BSD date (macOS)
THRESHOLD_EPOCH=$(date -v "-${DAYS_OLD}d" +%s)
else
echo "Error: Cannot determine epoch time. Skipping age check. Please install coreutils (GNU date)."
# Exit gracefully here, as the previous steps were successful.
exit 0
fi
echo "🔍 Finding branches (remote gone AND last commit before $(date -r $THRESHOLD_EPOCH +%Y-%m-%d))..."
# 1. List branches, 2. Match '[gone]', 3. Exclude critical branches, 4. Extract branch name
git branch -vv | \
grep -E "[\S+: gone]" | \
grep -Ev "$EXCLUDE_ALL_BRANCHES" | \
awk '{print $1}' | \
while read branch; do
# Get the last commit date in epoch time
last_commit_epoch=$(git show --format="%at" "$branch" | head -n 1)
# Compare the dates
if [[ "$last_commit_epoch" -lt "$THRESHOLD_EPOCH" ]]; then
echo "✅ Deleting branch: $branch (Last commit: $(date -d @$last_commit_epoch +%Y-%m-%d 2>/dev/null || date -r $last_commit_epoch +%Y-%m-%d))"
git branch -D "$branch"
else
echo "⏭️ Keeping branch: $branch (Last commit: $(date -d @$last_commit_epoch +%Y-%m-%d 2>/dev/null || date -r $last_commit_epoch +%Y-%m-%d))"
fi
done
echo "========================================================="
echo "✨ COMPLETE: Local repository is clean!"
echo "========================================================="
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment