Last active
January 14, 2023 08:24
-
-
Save sellout/36e6090d080d3fe39608c9c851569208 to your computer and use it in GitHub Desktop.
Clean up old Git branches
This file contains 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
#!/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