Last active
August 6, 2025 13:14
-
-
Save JarJak/19426e9d0fcc4d007a53dd9267074fd3 to your computer and use it in GitHub Desktop.
Bash script to delete all remote git branches older than provided days (runs in dry mode by default)
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
#!/bin/bash | |
# This script identifies and suggests deletion of remote Git branches | |
# that haven't been updated in a specified number of days. | |
usage() { | |
echo "Usage: $0 <stale_days> [--force] [--path=<repo_path>]" | |
echo " <stale_days>: REQUIRED. The number of days after which a branch is considered stale." | |
echo " --force: Optional. Execute the deletion commands. By default, a dry run is performed." | |
echo " --path=<repo_path>: Optional. Specify the path to the local Git repository (defaults to current directory)." | |
exit 1 | |
} | |
STALE_DAYS="" | |
DRY_RUN=true | |
FORCE_DELETE=false | |
REPO_PATH="." | |
# Check if at least one argument (stale_days) is provided | |
if [ -z "$1" ]; then | |
usage | |
fi | |
# The first argument is always STALE_DAYS | |
STALE_DAYS="$1" | |
# Validate STALE_DAYS is a positive integer | |
if ! [[ "$STALE_DAYS" =~ ^[0-9]+$ ]] || [ "$STALE_DAYS" -le 0 ]; then | |
echo "Error: <stale_days> must be a positive integer." | |
usage | |
fi | |
# Shift the first argument (STALE_DAYS) out, then parse remaining optional arguments | |
shift | |
for arg in "$@"; do | |
case "$arg" in | |
--force) | |
FORCE_DELETE=true | |
DRY_RUN=false | |
;; | |
--path=*) | |
REPO_PATH="${arg#*=}" | |
;; | |
*) | |
echo "Error: Unknown argument '$arg'" | |
usage | |
;; | |
esac | |
done | |
echo "Analyzing repository: $REPO_PATH" | |
if [ "$DRY_RUN" = true ]; then | |
echo "Running in DRY RUN mode. No branches will be deleted." | |
else | |
echo "Running in FORCE mode. Branches will be deleted." | |
fi | |
echo "---------------------------------------------------------------------" | |
echo "Fetching the latest remote branch information and syncing with local branches..." | |
# Fetch all remote branches to ensure we have the most up-to-date information | |
# Use -C to specify the repository path | |
git -C "$REPO_PATH" fetch --prune | |
echo "" | |
echo "Calculating age for all remote branches (excluding main/master/1.x)..." | |
echo "---------------------------------------------------------------------" | |
branches_to_delete=() | |
OLDEST_STALE_BRANCH_NAME="" | |
NEWEST_STALE_BRANCH_NAME="" | |
OLDEST_STALE_TIMESTAMP=0 # Initialize with a very old timestamp | |
NEWEST_STALE_TIMESTAMP=$(date +%s) # Initialize with current timestamp | |
# Array to store all branch info for sorting: "age_days|commit_date|branch_name" | |
declare -a ALL_BRANCH_AGES=() | |
current_timestamp=$(date +%s) | |
# Create a temporary file to store branch data to avoid subshell issues | |
TEMP_BRANCH_FILE=$(mktemp) | |
if [ -z "$TEMP_BRANCH_FILE" ]; then | |
echo "Error: Could not create temporary file." | |
exit 1 | |
fi | |
# Populate the temporary file with branch data | |
git -C "$REPO_PATH" for-each-ref --format='%(committerdate:unix) %(committerdate:format:%Y-%m-%d) %(refname:short)' refs/remotes/origin/ > "$TEMP_BRANCH_FILE" | |
# Loop through the temporary file to process branch data | |
while read commit_timestamp_raw commit_date branch; do | |
# Convert commit_timestamp_raw to integer | |
commit_timestamp=$((commit_timestamp_raw)) | |
# Calculate difference in seconds, then convert to days | |
age_seconds=$((current_timestamp - commit_timestamp)) | |
age_days=$((age_seconds / (60 * 60 * 24))) | |
# Exclude HEAD and master/main/1.x branches from being considered stale or listed here | |
if [[ "$branch" != "origin/HEAD" && "$branch" != "origin/master" && "$branch" != "origin/main" && "$branch" != "origin/1.x" ]]; then | |
# Store info for sorting: "age_days|commit_date|branch_name" | |
ALL_BRANCH_AGES+=("$age_days|$commit_date|${branch#origin/}") | |
if (( age_days > STALE_DAYS )); then | |
# Add the branch name (without 'origin/') to the array | |
branches_to_delete+=("${branch#origin/}") | |
if [ -z "$OLDEST_STALE_BRANCH_NAME" ] || (( commit_timestamp < OLDEST_STALE_TIMESTAMP )); then | |
OLDEST_STALE_TIMESTAMP=$commit_timestamp | |
OLDEST_STALE_BRANCH_NAME="${branch#origin/}" | |
fi | |
if [ -z "$NEWEST_STALE_BRANCH_NAME" ] || (( commit_timestamp > NEWEST_STALE_TIMESTAMP )); then | |
NEWEST_STALE_TIMESTAMP=$commit_timestamp | |
NEWEST_STALE_BRANCH_NAME="${branch#origin/}" | |
fi | |
fi | |
fi | |
done < "$TEMP_BRANCH_FILE" | |
# Clean up the temporary file | |
rm "$TEMP_BRANCH_FILE" | |
# Sort the ALL_BRANCH_AGES array by age (numeric sort on the first field) | |
# Using printf "%s\n" "${ALL_BRANCH_AGES[@]}" to handle potential newlines in branch names, | |
# then piping to sort. | |
IFS=$'\n' sorted_branches=($(printf "%s\n" "${ALL_BRANCH_AGES[@]}" | sort -n -t'|' -k1)) | |
unset IFS | |
# Print the sorted list of all branches | |
for branch_info in "${sorted_branches[@]}"; do | |
# Split the string back into components using IFS | |
IFS='|' read -r age_days commit_date branch_name <<< "$branch_info" | |
echo " Branch: $branch_name (Last commit: $commit_date, Age: $age_days days)" | |
done | |
echo "---------------------------------------------------------------------" | |
if [ ${#branches_to_delete[@]} -eq 0 ]; then | |
echo "No stale remote branches found older than $STALE_DAYS days in $REPO_PATH." | |
else | |
echo "" | |
echo "Summary of stale branches in $REPO_PATH (older than $STALE_DAYS days):" | |
echo " Number of branches to delete: ${#branches_to_delete[@]}" | |
if [ -n "$NEWEST_STALE_BRANCH_NAME" ]; then | |
# Use date -r for BSD date compatibility | |
echo " Newest stale branch: $NEWEST_STALE_BRANCH_NAME (Last updated: $(date -r "$NEWEST_STALE_TIMESTAMP" +%Y-%m-%d))" | |
fi | |
if [ -n "$OLDEST_STALE_BRANCH_NAME" ]; then | |
# Use date -r for BSD date compatibility | |
echo " Oldest stale branch: $OLDEST_STALE_BRANCH_NAME (Last updated: $(date -r "$OLDEST_STALE_TIMESTAMP" +%Y-%m-%d))" | |
fi | |
echo "" | |
if [ "$FORCE_DELETE" = true ]; then | |
echo "Deleting the identified stale branches..." | |
# Execute the deletion commands | |
for branch in "${branches_to_delete[@]}"; do | |
echo "Executing: git push origin --delete $branch" | |
git -C "$REPO_PATH" push origin --delete "$branch" | |
done | |
echo "" | |
echo "Deletion process complete." | |
else | |
echo "To delete these branches, run the script with the --force option:" | |
echo "" | |
echo "Example: $0 $STALE_DAYS --force --path=\"$REPO_PATH\"" | |
echo "" | |
echo "Please review the summary carefully before executing the commands." | |
fi | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment