Skip to content

Instantly share code, notes, and snippets.

@sellout
Last active January 14, 2023 08:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sellout/36e6090d080d3fe39608c9c851569208 to your computer and use it in GitHub Desktop.
Save sellout/36e6090d080d3fe39608c9c851569208 to your computer and use it in GitHub Desktop.
Clean up old Git branches
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n'
## Deletes branches (both local and remote) that have been merged upstream.
function usage () {
echo "Usage:"
echo
echo " $(basename "${BASH_ARGV0}") [-h] [-b DEFAULT_BRANCH] [-u UPSTREAM] [-o ORIGIN]"
echo
echo " DEFAULT_BRANCH serves multiple purposes. On UPSTREAM, it’s the branch we’re"
echo " checking to see if we’re merged. On ORIGIN and locally, it’s"
echo " a branch we want to keep even if it’s merged. It defaults to"
echo " `git config init.defaultBranch`."
echo
echo " UPSTREAM is the name of the remote that we expect to be merged into. It"
echo " defaults to “upstream”."
echo
echo " ORIGIN is the name of a fork that we have push access to, so we can delete"
echo " merged branches from it. If it is not provided, we delete local"
echo " branches instead."
}
## TODO:
## • report branches that could have been deleted but weren’t because a worktree
## is tracking them.
## • ensure we don’t delete an `origin_remote` branch that still has a local
## branch with unmerged changes tracking it (and vice-versa).
init_default_branch="$(git config init.defaultBranch)"
upstream_default_branch="${init_default_branch}"
origin_default_branch="${init_default_branch}"
upstream_remote="upstream"
while getopts "hb:u:o:" option; do
case "${option}" in
h)
usage
exit 0
;;
b)
upstream_default_branch="${OPTARG}"
origin_default_branch="${OPTARG}"
;;
u)
upstream_remote="${OPTARG}"
;;
o)
origin_remote="${OPTARG}"
;;
?)
usage
exit 1
;;
esac
done
if [[ -v "${OPTIND}" ]]; then
usage
exit 1
fi
merged_against="${upstream_remote}/${upstream_default_branch}"
## This function sets the variable `proceed`.
function get_permission () {
if [[ -v origin_remote ]]; then
where="on ${origin_remote}"
else
where="locally"
fi
echo "The following branches are about to be deleted (${where}):"
for i in "${branches[@]}"; do
echo "• ${i}"
done
read -p "Do you want to proceed? (y/N) " proceed
}
git fetch --quiet "${upstream_remote}" "${upstream_default_branch}"
if [[ -v origin_remote ]]; then
## remote branches
git fetch --prune "${origin_remote}"
set +e # `read` returns `1` here for some reason
read -rd '' -a branches <<<"$( \
git branch --remotes --list "${origin_remote}/*" --merged "${merged_against}" \
| grep -v "${origin_remote}/${origin_default_branch}" \
| sed "s#^[[:space:]]\+${origin_remote}/##")"
set -e
else
## local branches
set +e # `read` returns `1` here for some reason
read -rd '' -a branches <<<"$( \
git branch --merged "${merged_against}" \
| grep -v '[*+] ' \
| grep -v " ${origin_default_branch}" \
| sed "s#^[[:space:]]\+##")"
set -e
fi
if [[ "${#branches[@]}" -eq 0 ]]; then
echo "There are no branches to delete."
else
get_permission
if [[ "${proceed}" =~ [Yy] ]]; then
if [[ -v origin_remote ]]; then
refspecs=()
for i in "${branches[@]}"; do
refspecs+=(":${i}")
done
echo "Deleting ${#refspecs[@]} branch(es) on ${origin_remote}."
git push "${origin_remote}" "${refspecs[@]}"
else
echo "Deleting ${#branches[@]} local branch(es)."
git branch --delete "${branches[@]}"
fi
else
echo "Canceling deletions."
fi
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment