Created
December 15, 2025 13:35
-
-
Save myleshk/87f959f102aa93cc439136274f14d442 to your computer and use it in GitHub Desktop.
Handy script to cleanup stale (merged/old) branches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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