Skip to content

Instantly share code, notes, and snippets.

@nelsoneldoro
Last active January 16, 2026 23:03
Show Gist options
  • Select an option

  • Save nelsoneldoro/9f6f54cce2631e6990b6ad8a7915a354 to your computer and use it in GitHub Desktop.

Select an option

Save nelsoneldoro/9f6f54cce2631e6990b6ad8a7915a354 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -e
# Script to list stale branches (branches that haven't been updated in a specified time period)
# Default: 2 months (60 days)
help() {
echo "List stale branches that haven't been updated in a specified time period."
echo ""
echo "Usage: stale-branches [OPTIONS]"
echo ""
echo "Options:"
echo " -d, --days DAYS Number of days to consider a branch stale (default: 60)"
echo " -l, --local Only show local branches"
echo " -r, --remote Only show remote branches (default)"
echo " -a, --all Show both local and remote branches"
echo " -m, --merged Only show branches that have been merged"
echo " -n, --no-merged Only show branches that have not been merged"
echo " -c, --csv Output in CSV format"
echo " -o, --output FILE Output CSV to file (requires --csv)"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " stale-branches # List remote branches stale for 60+ days"
echo " stale-branches -d 30 # List branches stale for 30+ days"
echo " stale-branches -a -d 90 # List all branches stale for 90+ days"
echo " stale-branches -l -m # List merged local branches stale for 60+ days"
echo " stale-branches --csv # Output as CSV to stdout"
echo " stale-branches --csv -o output.csv # Output as CSV to file"
echo ""
echo "Note: When checking remote branches, consider running 'git fetch --prune' first to ensure"
echo " you have the latest remote branch information."
exit 0
}
# Default values
DAYS=60
BRANCH_TYPE="remote"
SHOW_MERGED=""
CUTOFF_DATE=""
CSV_OUTPUT=false
OUTPUT_FILE=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-d|--days)
DAYS="$2"
shift 2
;;
-l|--local)
BRANCH_TYPE="local"
shift
;;
-r|--remote)
BRANCH_TYPE="remote"
shift
;;
-a|--all)
BRANCH_TYPE="all"
shift
;;
-m|--merged)
SHOW_MERGED="--merged"
shift
;;
-n|--no-merged)
SHOW_MERGED="--no-merged"
shift
;;
-c|--csv)
CSV_OUTPUT=true
shift
;;
-o|--output)
OUTPUT_FILE="$2"
CSV_OUTPUT=true
shift 2
;;
-h|--help)
help
;;
*)
echo "Unknown option: $1"
help
;;
esac
done
# Validate CSV output file option
if [ -n "$OUTPUT_FILE" ] && [ "$CSV_OUTPUT" = false ]; then
echo "Error: --output requires --csv option"
exit 1
fi
# Calculate cutoff date
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
CUTOFF_DATE=$(date -v-${DAYS}d +%s 2>/dev/null || date -j -v-${DAYS}d +%s)
else
# Linux
CUTOFF_DATE=$(date -d "${DAYS} days ago" +%s)
fi
if [ -z "$CUTOFF_DATE" ]; then
echo "Error: Unable to calculate cutoff date. Please ensure 'date' command is available."
exit 1
fi
# Function to check if a branch is stale
is_stale() {
local branch=$1
local last_commit_date=$(git log -1 --format=%ct "$branch" 2>/dev/null)
if [ -z "$last_commit_date" ]; then
return 1
fi
if [ "$last_commit_date" -lt "$CUTOFF_DATE" ]; then
return 0
else
return 1
fi
}
# Function to escape CSV fields
escape_csv() {
local field="$1"
# If field contains comma, quote, or newline, wrap in quotes and escape quotes
if [[ "$field" =~ [,\"$'\n'] ]]; then
field="${field//\"/\"\"}"
field="\"$field\""
fi
echo "$field"
}
# Function to get branch info as array
get_branch_info() {
local branch=$1
git log --no-merges -n 1 --format="%ci|%cr|%an|%ae|%s" "$branch" 2>/dev/null
}
# Function to format and display branch info
display_branch() {
local branch=$1
local info=$(get_branch_info "$branch")
if [ -n "$info" ]; then
IFS='|' read -r date relative author email subject <<< "$info"
if [ -n "$date" ]; then
if [ "$CSV_OUTPUT" = true ]; then
echo "$(escape_csv "$date"),$(escape_csv "$relative"),$(escape_csv "$author"),$(escape_csv "$email"),$(escape_csv "$subject"),$(escape_csv "$branch")"
else
printf "%-25s %-15s %-30s %s\n" "$date" "$relative" "$author" "$branch"
fi
fi
fi
}
# Main execution
STALE_COUNT=0
STALE_BRANCHES=()
# Collect stale branches
if [ "$BRANCH_TYPE" = "local" ] || [ "$BRANCH_TYPE" = "all" ]; then
for branch in $(git branch $SHOW_MERGED 2>/dev/null | sed 's/^[ *]*//' | grep -v HEAD); do
if is_stale "$branch"; then
STALE_BRANCHES+=("$branch")
((STALE_COUNT++))
fi
done
fi
if [ "$BRANCH_TYPE" = "remote" ] || [ "$BRANCH_TYPE" = "all" ]; then
for branch in $(git branch -r $SHOW_MERGED 2>/dev/null | sed 's/^[ *]*//' | grep -v HEAD); do
if is_stale "$branch"; then
STALE_BRANCHES+=("$branch")
((STALE_COUNT++))
fi
done
fi
# Output results
if [ "$CSV_OUTPUT" = true ]; then
# CSV output
{
# CSV header
echo "Last Commit Date,Relative Time,Author,Email,Subject,Branch"
# CSV rows
for branch in "${STALE_BRANCHES[@]}"; do
display_branch "$branch"
done
} > "${OUTPUT_FILE:-/dev/stdout}"
if [ -n "$OUTPUT_FILE" ]; then
echo "CSV output written to: $OUTPUT_FILE" >&2
echo "Total stale branches: $STALE_COUNT" >&2
fi
else
# Table output
echo "Stale branches (not updated in ${DAYS} days or more)"
echo "=================================================="
echo ""
printf "%-25s %-15s %-30s %s\n" "Last Commit Date" "Relative" "Author" "Branch"
echo "--------------------------------------------------------------------------------"
for branch in "${STALE_BRANCHES[@]}"; do
display_branch "$branch"
done
echo ""
if [ $STALE_COUNT -eq 0 ]; then
echo "No stale branches found."
else
echo "Total stale branches: $STALE_COUNT"
fi
fi
@nelsoneldoro
Copy link
Author

Consider running git fetch --prune to report branches that actually exist on the remote

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment