Created
April 19, 2026 15:46
-
-
Save ifthenelse/dc55421c0323230568013322cd59f441 to your computer and use it in GitHub Desktop.
Zsh script to prune locale branches already merged into the default branch
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
| #!/usr/bin/env zsh | |
| # Prune local Git branches that are already merged into the default base branch. | |
| # The script skips the base branch, the current branch, and branches with related stashes, | |
| # then asks for confirmation before deleting each eligible branch. | |
| prune_local_branches() { | |
| local version="1.0.0" | |
| case "${1:-}" in | |
| -h|--help) | |
| cat <<EOF | |
| Usage: git-prune-branches [OPTIONS] | |
| Prune local Git branches that are already merged into the default base branch. | |
| The script skips the base branch, the current branch, and branches with related | |
| stashes, then asks for confirmation before deleting each eligible branch. | |
| Options: | |
| -h, --help Show this help message | |
| -v, --version Show version number | |
| EOF | |
| return 0 | |
| ;; | |
| -v|--version) | |
| echo "git-prune-branches version $version" | |
| return 0 | |
| ;; | |
| "") | |
| ;; | |
| *) | |
| echo "Error: unknown option '$1'" >&2 | |
| return 1 | |
| ;; | |
| esac | |
| command -v git >/dev/null 2>&1 || { echo "Error: git is not installed."; return 1; } | |
| git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "Error: not a git repository."; return 1; } | |
| local base current branch ans stash_count s | |
| local -a removed skipped | |
| base=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null) | |
| base=${base#origin/} | |
| if [[ -z "$base" ]]; then | |
| if git show-ref --verify --quiet refs/heads/main; then | |
| base=main | |
| elif git show-ref --verify --quiet refs/heads/master; then | |
| base=master | |
| else | |
| echo "Error: could not determine main branch." | |
| return 1 | |
| fi | |
| fi | |
| current=$(git branch --show-current) | |
| while IFS= read -r branch; do | |
| [[ -z "$branch" ]] && continue | |
| if [[ "$branch" == "$base" ]]; then | |
| skipped+=("$branch: protected base branch") | |
| continue | |
| fi | |
| if [[ "$branch" == "$current" ]]; then | |
| skipped+=("$branch: current branch") | |
| continue | |
| fi | |
| stash_count=0 | |
| while IFS= read -r s; do | |
| [[ "$s" == "WIP on $branch:"* || "$s" == "On $branch:"* ]] && ((stash_count++)) | |
| done < <(git stash list --format='%gs') | |
| if (( stash_count > 0 )); then | |
| skipped+=("$branch: has $stash_count related stash entries") | |
| continue | |
| fi | |
| if ! git merge-base --is-ancestor "$branch" "$base" >/dev/null 2>&1; then | |
| skipped+=("$branch: not merged into $base") | |
| continue | |
| fi | |
| printf "Delete branch '%s'? [y/N] " "$branch" | |
| read -r ans | |
| if [[ "$ans" == [yY] ]]; then | |
| if git branch -d "$branch" >/dev/null 2>&1; then | |
| removed+=("$branch") | |
| else | |
| skipped+=("$branch: deletion failed") | |
| fi | |
| else | |
| skipped+=("$branch: user declined") | |
| fi | |
| done < <(git for-each-ref --format='%(refname:short)' refs/heads) | |
| if (( ${#removed[@]} == 0 )); then | |
| echo "no branch pruned" | |
| fi | |
| if (( ${#removed[@]} )); then | |
| echo "Removed branches:" | |
| printf ' - %s\n' "${removed[@]}" | |
| fi | |
| if (( ${#skipped[@]} )); then | |
| echo "Not removed:" | |
| printf ' - %s\n' "${skipped[@]}" | |
| fi | |
| } | |
| prune_local_branches |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment