Skip to main content
Glama
deleonio
by deleonio
cleanup-local-branches.sh7.12 kB
#!/usr/bin/env zsh # cleanup-local-branches.sh # Safescript to clean up old local git branches. # Usage: run from a git repo root. Default is a dry-run; use --do-it to actually delete. set -euo pipefail IFS=$'\n\t' PROGNAME=$(basename $0) print_usage() { cat <<EOF Usage: $PROGNAME [options] Options: --prune Run 'git fetch --prune' before any listing (recommended) --delete-gone Delete local branches whose upstream REMOTE branch is gone --delete-merged Delete local branches already merged into the main branch --delete-all-except Delete all local branches except keep-list and current (force) --keep <list> Comma-separated list of branches to keep (default: develop,release/*) --main <name> Name of your main branch (default: develop) --backup <file> Path to write a backup file with branch names and commit hashes --do-it Actually delete branches. Without this flag script runs in dry-run mode. --force Use force deletion (-D) instead of safe deletion (-d) when removing branches --yes Skip confirmation prompt (useful for automation) -h, --help Show this help and exit Examples: # Dry-run: show orphaned remote branches $PROGNAME --prune --delete-gone # Actually delete orphaned remote branches (safe delete) $PROGNAME --prune --delete-gone --do-it # Delete merged branches (except main/master/develop) after dry-run $PROGNAME --delete-merged # Backup branch list then force-delete everything except main and current $PROGNAME --backup ~/branches.txt --delete-all-except --do-it --force EOF } # Default configuration PRUNE=false DELETE_GONE=false DELETE_MERGED=false DELETE_ALL_EXCEPT=false KEEP_LIST="develop,release/*" MAIN_BRANCH="" DO_IT=false FORCE=false BACKUP_FILE="" YES=false # Parse args (simple loop) while [[ $# -gt 0 ]]; do case $1 in --prune) PRUNE=true; shift ;; --delete-gone) DELETE_GONE=true; shift ;; --delete-merged) DELETE_MERGED=true; shift ;; --delete-all-except) DELETE_ALL_EXCEPT=true; shift ;; --keep) KEEP_LIST=$2; shift 2 ;; --main) MAIN_BRANCH=$2; shift 2 ;; --backup) BACKUP_FILE=$2; shift 2 ;; --do-it) DO_IT=true; shift ;; --force) FORCE=true; shift ;; --yes) YES=true; shift ;; -h|--help) print_usage; exit 0 ;; *) echo "Unknown option: $1"; print_usage; exit 2 ;; esac done # Ensure we are in a git repository if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then echo "Error: This script must be run inside a git repository root." >&2 exit 1 fi # Auto-detect main branch if not provided if [[ -z "$MAIN_BRANCH" ]]; then if git show-ref --verify --quiet refs/heads/develop; then MAIN_BRANCH=develop else # fallback to current MAIN_BRANCH=$(git rev-parse --abbrev-ref HEAD) fi fi # Compute current branch CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) # Helper: backup current local branches and their tip commit hashes backup_branches() { echo "Backing up local branches -> $BACKUP_FILE" echo "# Branch backup created: $(date)" >| "$BACKUP_FILE" git for-each-ref --format='%(refname:short) %(objectname:short) %(authorname) %(authordate:iso8601)' refs/heads/ >> "$BACKUP_FILE" echo "Backup written. You can recreate a branch with: git branch <name> <commit>" >> "$BACKUP_FILE" } # Run prune if requested if $PRUNE; then echo "Running git fetch --prune ..." git fetch --prune fi # Prepare keep-regex from comma-separated list (supports glob patterns like release/*) KEEP_REGEX=$(echo "$KEEP_LIST" | awk -F',' '{for(i=1;i<=NF;i++){gsub(/^[ \t]+|[ \t]+$/,"",$i);if($i!=""){gsub(/\*/,".*",$i);a[a_count++]=$i}}} END{for(j=0;j<a_count;j++){printf "%s%s",(j?"|":""),"^"a[j]"$"}}') # Collect lists branches_to_delete=() # 1) orphaned Remote branches (upstream gone) if $DELETE_GONE; then echo "Scanning for local branches whose upstream is gone..." gone_list=() while IFS= read -r line; do gone_list+=("$line") done < <(git branch -vv | awk '/: gone]/{print $1}') if [[ ${#gone_list[@]} -eq 0 ]]; then echo "No branches with gone upstream found." else echo "Found ${#gone_list[@]} gone branches:"; printf '%s\n' "${gone_list[@]}" branches_to_delete+=("${gone_list[@]}") fi fi # 2) branches merged into main if $DELETE_MERGED; then echo "Scanning for branches merged into '$MAIN_BRANCH'..." merged_list=() while IFS= read -r line; do [[ -n "$line" ]] && merged_list+=("$line") done < <(git branch --merged "$MAIN_BRANCH" | sed 's/^[ *]*//' | grep -Ev "^($MAIN_BRANCH|$CURRENT_BRANCH)$" | grep -Ev "$KEEP_REGEX" || true) if [[ ${#merged_list[@]} -eq 0 ]]; then echo "No merged branches (that match criteria) found." else echo "Found ${#merged_list[@]} merged branches:"; printf '%s\n' "${merged_list[@]}" branches_to_delete+=("${merged_list[@]}") fi fi # 3) delete all except keep list and current if $DELETE_ALL_EXCEPT; then echo "Preparing to delete ALL local branches except keep-list and current branch '$CURRENT_BRANCH'..." all_list=() while IFS= read -r line; do [[ -n "$line" ]] && all_list+=("$line") done < <(git branch --format='%(refname:short)' | grep -Ev "^($CURRENT_BRANCH)$" | grep -Ev "$KEEP_REGEX" || true) if [[ ${#all_list[@]} -eq 0 ]]; then echo "No branches to delete by --delete-all-except." else echo "Found ${#all_list[@]} branches to delete:"; printf '%s\n' "${all_list[@]}" branches_to_delete+=("${all_list[@]}") fi fi # Unique and sort branches_to_delete if [[ ${#branches_to_delete[@]} -gt 0 ]]; then # Deduplicate using associative array (more robust than awk) declare -A seen uniq_list=() for b in "${branches_to_delete[@]}"; do if [[ -z "${seen[$b]:-}" ]]; then seen[$b]=1 uniq_list+=("$b") fi done else uniq_list=() fi if [[ ${#uniq_list[@]} -eq 0 ]]; then echo "Nothing to delete based on the selected options."; exit 0 fi # Show summary echo "\nSummary: ${#uniq_list[@]} branches selected for deletion:"; printf '%s\n' "${uniq_list[@]}" # Dry-run or perform if ! $DO_IT; then echo "\nDRY-RUN: No branches will be deleted. Rerun with --do-it to actually remove them." exit 0 fi # Backup before deletion if [[ -z "$BACKUP_FILE" ]]; then BACKUP_FILE="$HOME/branches-backup-$(date +%Y%m%d-%H%M%S).txt" fi backup_branches # Ask for final confirmation unless --yes provided if ! $YES; then read "confirm?Proceed to delete the listed branches? (y/N): " if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then echo "Aborted by user. No branches deleted."; exit 0 fi fi # Choose deletion flag DELFLAG='-d' if $FORCE; then DELFLAG='-D' fi # Perform deletion for b in "${uniq_list[@]}"; do echo "Deleting branch: $b" if git show-ref --verify --quiet "refs/heads/$b"; then git branch $DELFLAG "$b" || echo "Failed to delete $b (you can try git branch -D $b)" else echo "Branch $b does not exist locally anymore. Skipping." fi done echo "Done. If you need to restore a branch, inspect the backup file: $BACKUP_FILE" exit 0

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/deleonio/public-ui-kolibri'

If you have feedback or need assistance with the MCP directory API, please join our Discord server