Created
August 19, 2025 14:01
-
-
Save MrZoidberg/69a03d89e281b17dd33886bdc600b3d2 to your computer and use it in GitHub Desktop.
Git - Prune local branches that are merged or deleted at remote
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
| function git-prune-merged --description 'Delete local branches merged to a base (default: main), plus branches whose upstream is gone on remote' | |
| # Options: -b <base>, -y, -h/--help | |
| set -l base_branch main | |
| set -l assume_yes 0 | |
| set -l expect_b 0 | |
| function __gpm_usage | |
| echo "Usage: git-prune-merged [-b base_branch] [-y]" | |
| echo "" | |
| echo "Deletes:" | |
| echo " 1) Local branches fully merged into the base (safe delete: git branch -d)" | |
| echo " 2) Local branches whose upstream is gone on remote (force delete: git branch -D)" | |
| echo "" | |
| echo "Options:" | |
| echo " -b <branch> Base branch to compare against (default: main)" | |
| echo " -y Do not prompt for confirmation (assume yes)" | |
| echo " -h, --help Show this help" | |
| end | |
| for arg in $argv | |
| switch $arg | |
| case -h --help | |
| __gpm_usage | |
| return 0 | |
| case -y | |
| set assume_yes 1 | |
| case -b | |
| set expect_b 1 | |
| case '-b=*' | |
| set -l parts (string split -m1 '=' -- $arg) | |
| set base_branch $parts[2] | |
| case '*' | |
| if test $expect_b -eq 1 | |
| set base_branch $arg | |
| set expect_b 0 | |
| else | |
| echo "Unknown argument: $arg" | |
| __gpm_usage | |
| return 2 | |
| end | |
| end | |
| end | |
| if test $expect_b -eq 1 | |
| echo "Option -b requires a value." | |
| return 2 | |
| end | |
| # Ensure we're in a git repo | |
| if not git rev-parse --is-inside-work-tree >/dev/null 2>&1 | |
| echo "Error: Not inside a Git repository." | |
| return 1 | |
| end | |
| # Remember original ref (may be DETACHED) | |
| set -l orig_ref (git symbolic-ref --quiet --short HEAD 2>/dev/null; or echo DETACHED) | |
| # Update refs (also prunes deleted remote tracking refs) | |
| echo "Fetching from origin (with prune)..." | |
| git fetch --prune origin | |
| # Ensure base branch exists locally or on origin, then switch & fast-forward | |
| if git show-ref --verify --quiet "refs/heads/$base_branch" | |
| echo "Checking out '$base_branch' and fast-forwarding..." | |
| git switch "$base_branch" >/dev/null 2>&1; or git checkout "$base_branch" | |
| if git show-ref --verify --quiet "refs/remotes/origin/$base_branch" | |
| git pull --ff-only --no-rebase origin "$base_branch" | |
| end | |
| else if git show-ref --verify --quiet "refs/remotes/origin/$base_branch" | |
| echo "Creating local '$base_branch' from origin/$base_branch..." | |
| git switch -c "$base_branch" "origin/$base_branch" >/dev/null 2>&1; or git checkout -b "$base_branch" "origin/$base_branch" | |
| else | |
| echo "Error: Base branch '$base_branch' not found locally or on origin." | |
| return 1 | |
| end | |
| # 1) Local branches merged into base (safe delete) | |
| echo "Finding local branches merged into '$base_branch'..." | |
| set -l merged_raw (git for-each-ref refs/heads --merged "$base_branch" --format='%(refname:short)') | |
| set -l delete_merged | |
| for b in $merged_raw | |
| if test -n "$b"; and test "$b" != "$base_branch" | |
| set -a delete_merged $b | |
| end | |
| end | |
| # 2) Local branches whose upstream is gone on remote (force delete) | |
| # Use for-each-ref to read upstream and tracking status; [gone] appears when upstream is missing | |
| echo "Finding local branches whose upstream is gone on remote..." | |
| set -l delete_gone | |
| set -l lines (git for-each-ref refs/heads --format='%(refname:short):::%(upstream:short):::%(upstream:track)') | |
| for line in $lines | |
| set -l parts (string split ':::' -- $line) | |
| set -l name $parts[1] | |
| set -l upstream $parts[2] | |
| set -l track $parts[3] | |
| # Skip base, and require an upstream to have been set | |
| if test -z "$name"; or test "$name" = "$base_branch"; or test -z "$upstream" | |
| continue | |
| end | |
| if string match -rq '\[gone\]' -- "$track" | |
| # Avoid duplicates if already in merged list | |
| if not contains -- $name $delete_merged | |
| set -a delete_gone $name | |
| end | |
| end | |
| end | |
| # Nothing to do? | |
| if test (count $delete_merged) -eq 0; and test (count $delete_gone) -eq 0 | |
| echo "Nothing to delete." | |
| # Restore original ref if appropriate | |
| if test "$orig_ref" != "DETACHED"; and test "$orig_ref" != "$base_branch" | |
| git switch "$orig_ref" >/dev/null 2>&1; or git checkout "$orig_ref" | |
| end | |
| return 0 | |
| end | |
| # Show plan | |
| if test (count $delete_merged) -gt 0 | |
| echo "" | |
| echo "Local branches fully merged into '$base_branch' (will delete with 'git branch -d'):" | |
| for b in $delete_merged | |
| echo " - $b" | |
| end | |
| end | |
| if test (count $delete_gone) -gt 0 | |
| echo "" | |
| echo "Local branches whose upstream is gone on remote (will FORCE delete with 'git branch -D'):" | |
| for b in $delete_gone | |
| echo " - $b" | |
| end | |
| end | |
| if test $assume_yes -ne 1 | |
| set -l merged_n (count $delete_merged) | |
| set -l gone_n (count $delete_gone) | |
| read -P "Proceed to delete ($merged_n merged, $gone_n remote-gone)? [y/N]: " ans | |
| switch (string lower -- $ans) | |
| case y yes | |
| # continue | |
| case '*' | |
| echo "Aborted." | |
| return 0 | |
| end | |
| end | |
| # Delete merged branches safely | |
| for b in $delete_merged | |
| echo "Deleting local merged branch: $b" | |
| git branch -d "$b"; or echo "Could not delete '$b' (not fully merged?). Skipping." | |
| end | |
| # Force-delete remote-gone branches | |
| for b in $delete_gone | |
| echo "Force-deleting local branch with gone upstream: $b" | |
| git branch -D "$b"; or echo "Could not force-delete '$b'." | |
| end | |
| # Restore original branch if it still exists and isn't the base | |
| if test "$orig_ref" != "DETACHED"; and test "$orig_ref" != "$base_branch" | |
| if git show-ref --verify --quiet "refs/heads/$orig_ref" | |
| git switch "$orig_ref" >/dev/null 2>&1; or git checkout "$orig_ref" | |
| end | |
| end | |
| echo "Done." | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment