Skip to content

Instantly share code, notes, and snippets.

@boneskull
Last active June 5, 2025 23:20
Show Gist options
  • Save boneskull/0d2eccd12e1c144aeeca47ffb494e8ad to your computer and use it in GitHub Desktop.
Save boneskull/0d2eccd12e1c144aeeca47ffb494e8ad to your computer and use it in GitHub Desktop.
git: cleanup remote & local rebased branches (zsh)

git-cleanup

A Zsh script to clean up local and remote Git branches that have already been rebased (fully merged) onto a target branch.

Depends on branch name prefixes; for example, feature/, bugfix/, etc. I tend to use my username boneskull.

Features

  • Deletes local branches matching a prefix that have been rebased onto a target branch.
  • Deletes remote branches matching a prefix that have been rebased onto a target branch.
  • Prompts for confirmation before deleting any branch.
  • Deletes local branches that track remote branches that have been deleted (via git fetch --prune); not limited to branches matching the prefix!
  • Automatically detects the default branch (probably)

Usage

git-cleanup [remote] [target] [prefix]
  • remote: The remote repository (default: origin)
  • target: The target branch to check against (default: remote's default branch)
  • prefix: The prefix for branches to consider (default: user.branchPrefix config or boneskull)

Example

git cleanup origin main feature

This will clean up all local and remote branches starting with feature/ that have been rebased onto origin/main.

Requirements

  • Zsh (tested w/ v5.9)
  • Git (tested w/ v2.49.0)

Installation

You probably want to set the default branch prefix in your Git config:

git config --global user.branchPrefix 'some-prefix'

Usage via git (optional)

Put git-cleanup.zsh in your PATH, but rename it to git-cleanup; it will now be available as a git command (git cleanup). Ensure it is executable (chmod +x git-cleanup).

Notes

  • The methods used here were adapted from StackOverflow posts.
  • I stuffed the config in user.branchPrefix but it may belong somewhere else.
  • The script is not compatible with Bash.
  • Copilot wrote most of this README.
  • Always review the branches to be deleted when prompted!
  • I don't know how this handles merge commits. It probably doesn't.
  • Please comment if something is broken!

License

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to https://unlicense.org/

#!/usr/bin/env zsh
# git-cleanup: A script to clean up local and remote branches in a Git
# repository.
#
# Usage: git cleanup [remote] [target] [prefix]
#
# - remote: The remote repository (default: 'origin').
# - target: The target branch to check against (default: the default branch of
# the remote).
# - prefix: The prefix for remote branches to consider (default:
# user.branchPrefix config value or 'boneskull'); e.g., prefix 'boneskull'
# will match branches like 'boneskull/feature-xyz'.
#
# To set the branch prefix globally, use `git config --global user.branchPrefix
# <prefix>` (without trailing slash)
#
# WARNING: not compatible with bash.
set -euo pipefail
default_prefix='boneskull'
log() {
print -P "$@" >&2
}
ok() {
log "%F{green}✓%f $@"
}
warn() {
log "%F{yellow}⚠%f $@"
}
info() {
log "%F{blue}ℹ%f $@"
}
error() {
log "%F{red}✗%f $@"
exit 1
}
em() {
echo "%F{yellow}$@%f"
}
bold() {
echo "%F{red}$@%f"
}
# Gets the branch prefix from git config or uses the default.
get_prefix() {
local prefix=$(git config get user.branchPrefix)
if [[ -z $prefix ]]; then
echo $default_prefix
else
echo $prefix
fi
}
# Checks that the target branch exists on the remote.
check_target() {
if ! git show-branch "${remote}/${target}" > /dev/null 2>&1; then
error "No such branch: $(em "${remote}/${target}")"
fi
}
# Determines the default branch of the remote repository
default_branch() {
echo $(git rev-parse --abbrev-ref "${remote}/HEAD" | cut -c8-)
}
# Prompts the user for confirmation with a default response.
confirm() {
local message="${1}"
local default="${2:-N/y}"
print -Pn "${message} "
read -q "confirm?[${default}]? "
echo
if [[ ${confirm} =~ ^[Yy]$ ]]; then
return 0
else
return 1
fi
}
# Removes local branches that have been rebased onto the target branch
cleanup_local_rebased() {
local deleted=false
local branches=($(git branch --list "${prefix}/*" --format='%(refname:short)'))
if [[ -z $branches ]]; then
info "No local branches found matching $(em "${prefix}/*")."
return
fi
for branch in $branches; do
local cherry=$(git cherry "${remote}/${target}" "${branch}")
if [[ -n $cherry && ! $cherry =~ '^\+' ]]; then
info "Local branch $(bold "${branch}") appears in $(em "${remote}/${target}")."
confirm "Delete local branch $(bold "${branch}")" && {
git branch -D "${branch}"
deleted=true
}
fi
done
if [[ $deleted == true ]]; then
ok "Deleted local branch(es) that have been rebased onto $(em "${remote}/${target}")."
else
info "Did not remove any local branches."
fi
}
# Removes remote branches that have been rebased onto the target branch
cleanup_remote_rebased() {
local deleted=false
local branches=($(git branch -r --list "${remote}/${prefix}/*" --format='%(refname:lstrip=3)'))
if [[ -z $branches ]]; then
info "No remote branches found matching ${remote}/${prefix}*."
return
fi
for branch in $branches; do
if [[ -z $(git cherry "${remote}/${target}" "${remote}/${branch}" 2> /dev/null | grep '^+') ]]; then
info "Remote branch $(bold "${remote}/${branch}") appears in $(em "${remote}/${target}")."
confirm "Delete remote branch $(bold "${remote}/${branch}")" && {
git push "${remote}" --delete "${branch}"
deleted=true
}
fi
done
if [[ $deleted == true ]]; then
ok "Deleted remote branch(es) that have been rebased onto $(em "${remote}/${target}")."
else
info "Did not remove any remote branches."
fi
}
# Remote repository
remote=${1:-origin}
# "Trunk" branch on remote repository
target=${2:-$(default_branch)}
# Branch prefix
prefix=${3:-$(get_prefix)}
info "Using remote: $(em "${remote}"), trunk $(em "${target}"), and prefix $(em "${prefix}")."
# this will remove local branches that track deleted remote branches
git fetch --prune "${remote}"
check_target
cleanup_local_rebased
cleanup_remote_rebased
ok "Done."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment