Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions scripts/delete-gone-branches.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/bash

set -euo pipefail

usage() {
cat <<'EOF'
Usage: bash scripts/delete-gone-branches.sh [--apply]

Find local branches whose upstream is marked "[gone]" and delete them.

Options:
--apply Actually delete the branches with `git branch -D`
--help Show this help text

Without --apply, the script prints what it would delete.
EOF
}

apply=false

case "${1:-}" in
"")
;;
--apply)
apply=true
;;
--help|-h)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac

git fetch --prune --quiet

mapfile -t gone_branches < <(
git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads |
while IFS= read -r line; do
branch=${line% *}
tracking=${line#"$branch "}
Comment on lines +41 to +44
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The for-each-ref output is parsed by splitting on the last space (branch=${line% *}), which is brittle and can mis-parse if the formatted fields ever contain spaces (or if the format is adjusted later). Prefer using an explicit delimiter in the --format (e.g., a tab) and read the two fields separately to make the selection of [gone] branches robust.

Suggested change
git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads |
while IFS= read -r line; do
branch=${line% *}
tracking=${line#"$branch "}
git for-each-ref --format=$'%(refname:short)\t%(upstream:track)' refs/heads |
while IFS=$'\t' read -r branch tracking; do

Copilot uses AI. Check for mistakes.
if [[ "$tracking" == "[gone]" ]]; then
printf '%s\n' "$branch"
fi
done
)

if [[ ${#gone_branches[@]} -eq 0 ]]; then
echo "No local branches with gone upstreams found."
exit 0
fi

current_branch="$(git branch --show-current)"

echo "Found ${#gone_branches[@]} branch(es) with gone upstreams:"
printf ' %s\n' "${gone_branches[@]}"

if [[ "$apply" != true ]]; then
echo
echo "Dry run only. Re-run with --apply to delete them."
exit 0
fi

deleted_count=0

for branch in "${gone_branches[@]}"; do
if [[ "$branch" == "$current_branch" ]]; then
echo "Skipping current branch: $branch"
continue
fi

git branch -D "$branch"
deleted_count=$((deleted_count + 1))
Comment on lines +75 to +76
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the script runs with set -e, any failure from git branch -D (e.g., branch checked out in another worktree, permissions, etc.) will terminate the whole script and skip deleting the remaining branches. Consider handling deletion per-branch (capturing failures and continuing) and only incrementing deleted_count on successful deletes, so one problematic branch doesn’t stop the cleanup run.

Suggested change
git branch -D "$branch"
deleted_count=$((deleted_count + 1))
if git branch -D "$branch"; then
deleted_count=$((deleted_count + 1))
else
echo "Failed to delete branch: $branch" >&2
fi

Copilot uses AI. Check for mistakes.
done

echo
echo "Deleted $deleted_count branch(es)."
Loading