Skip to content

Instantly share code, notes, and snippets.

@MrZoidberg
Created August 19, 2025 14:01
Show Gist options
  • Select an option

  • Save MrZoidberg/69a03d89e281b17dd33886bdc600b3d2 to your computer and use it in GitHub Desktop.

Select an option

Save MrZoidberg/69a03d89e281b17dd33886bdc600b3d2 to your computer and use it in GitHub Desktop.
Git - Prune local branches that are merged or deleted at remote
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