Skip to content

Instantly share code, notes, and snippets.

@seantunwin
Created January 30, 2026 02:20
Show Gist options
  • Select an option

  • Save seantunwin/33ee8b60740b1113ab344cd6cab58c12 to your computer and use it in GitHub Desktop.

Select an option

Save seantunwin/33ee8b60740b1113ab344cd6cab58c12 to your computer and use it in GitHub Desktop.
Cururent Branch Summary Script
#!/bin/bash
# Branch Effort Summary Script
#
# Generates a concise report of commits, files changed, and line statistics
# for commits matching a pattern or within a commit range.
#
# Usage:
# ./branch-summary.sh # All commits vs 'main'
# ./branch-summary.sh <start-hash>^..HEAD # From commit to HEAD (recommended)
# ./branch-summary.sh <start-hash>..<end-hash> # Specific commit range
# ./branch-summary.sh "monitoring" # Commits matching pattern (regex)
# ./branch-summary.sh main "monitoring" # Base branch + filter pattern
# ./branch-summary.sh --by-author # Same as default + per-author breakdown
# ./branch-summary.sh main --by-author # Base branch + per-author breakdown
# ./branch-summary.sh main --author "Name" # Only commits by author (regex)
# ./branch-summary.sh main --author "Name" --by-author # Filter by author + breakdown
# ./branch-summary.sh 7b3ed1504^..HEAD --author "Name" # Range + confine to author (option)
#
# Options (can be combined with any range or base):
# --by-author Show the same stats broken down by git author (after main summary).
# --author PATTERN Optional. Restrict to commits whose author matches PATTERN (regex).
# Works with commit range, base branch, or base+filter.
#
# Examples:
# ./branch-summary.sh 7b3ed1504^..HEAD # All commits from first working branch commit to HEAD
# ./branch-summary.sh 7b3ed1504^..HEAD --author "Sean" # Same range, only that author's commits
# ./branch-summary.sh main "monitoring" --author "Name" # Base + message filter + author
# ./branch-summary.sh "monitoring|composables" # Filter by pattern
#
# Understanding commit ranges:
# - <hash>^ means "the commit before <hash>" (parent commit)
# - <hash>^..HEAD means "from the commit before <hash> to HEAD" (includes <hash>)
# - If <hash> is your FIRST working branch commit, then <hash>^ is the LAST source branch commit
# - Example: 7b3ed1504^..HEAD includes 7b3ed1504 and all commits after it
#
# Finding your first working branch commit:
# git log --oneline main..HEAD | tail -1 # Shows first commit (oldest)
# git log --oneline --reverse main..HEAD | head -1 # Alternative method
#
# Note: This script is useful for IDE agents via slash commands to generate branch summaries.
set -e
BRANCH=$(git branch --show-current)
BY_AUTHOR=""
AUTHOR_FILTER=""
# Parse optional flags (--by-author, --author PATTERN) and collect positional args
POSITIONALS=()
i=1
while [ $i -le $# ]; do
eval "arg=\${$i}"
case "$arg" in
--by-author)
BY_AUTHOR=1
;;
--author)
i=$((i + 1))
eval "AUTHOR_FILTER=\${$i}"
;;
*)
POSITIONALS+=("$arg")
;;
esac
i=$((i + 1))
done
# Parse positional arguments (same logic as before, using POSITIONALS)
if [ ${#POSITIONALS[@]} -eq 0 ]; then
BASE="main"
FILTER=""
elif [ ${#POSITIONALS[@]} -eq 1 ]; then
if git rev-parse --verify "${POSITIONALS[0]}" >/dev/null 2>&1 || git rev-parse --verify "origin/${POSITIONALS[0]}" >/dev/null 2>&1; then
BASE="${POSITIONALS[0]}"
FILTER=""
elif echo "${POSITIONALS[0]}" | grep -q "\.\."; then
BASE=""
FILTER=""
COMMIT_RANGE="${POSITIONALS[0]}"
else
BASE="main"
FILTER="${POSITIONALS[0]}"
fi
else
BASE="${POSITIONALS[0]}"
FILTER="${POSITIONALS[1]}"
fi
# Git log author option when filtering by author
GIT_AUTHOR_OPT=""
if [ -n "$AUTHOR_FILTER" ]; then
GIT_AUTHOR_OPT="--author=$AUTHOR_FILTER"
fi
# Determine commit range
if [ -n "$COMMIT_RANGE" ]; then
# Explicit commit range provided
COMMITS=$(git log --oneline $GIT_AUTHOR_OPT "$COMMIT_RANGE" 2>/dev/null)
if [ -n "$AUTHOR_FILTER" ]; then
# When confined by author, list files only from those commits
FILES_LIST=""
for hash in $(echo "$COMMITS" | awk '{print $1}'); do
FILES_LIST="$FILES_LIST$(git diff-tree --no-commit-id --name-only -r "$hash" 2>/dev/null)"
done
FILES_LIST=$(echo "$FILES_LIST" | sort -u | grep -v "^$")
else
FILES_LIST=$(git diff --name-only "$COMMIT_RANGE" 2>/dev/null | sort -u)
fi
elif [ -n "$BASE" ]; then
# Compare against base branch
BASE_REMOTE="origin/$BASE"
BASE_COMMIT=$(git merge-base HEAD "$BASE_REMOTE" 2>/dev/null || git merge-base HEAD "$BASE" 2>/dev/null || echo "")
if [ -z "$BASE_COMMIT" ]; then
echo "Error: Could not find base branch '$BASE' or '$BASE_REMOTE'"
exit 1
fi
if [ -n "$FILTER" ]; then
# Filter commits by pattern (use grep -E for regex)
COMMITS=$(git log --oneline $GIT_AUTHOR_OPT "$BASE_COMMIT"..HEAD 2>/dev/null | grep -iE "$FILTER")
# Get files from filtered commits only
COMMIT_HASHES=$(echo "$COMMITS" | awk '{print $1}')
FILES_LIST=""
for hash in $COMMIT_HASHES; do
FILES_LIST="$FILES_LIST$(git diff-tree --no-commit-id --name-only -r "$hash" 2>/dev/null)"
done
FILES_LIST=$(echo "$FILES_LIST" | sort -u | grep -v "^$")
else
# All commits
COMMITS=$(git log --oneline $GIT_AUTHOR_OPT "$BASE_COMMIT"..HEAD 2>/dev/null)
FILES_LIST=$(git diff --name-only "$BASE_COMMIT"..HEAD 2>/dev/null | sort -u)
fi
else
echo "Error: Invalid arguments"
exit 1
fi
# Count commits
COMMIT_COUNT=$(echo "$COMMITS" | grep -v "^$" | wc -l)
# Count unique files
FILES_COUNT=$(echo "$FILES_LIST" | grep -v "^$" | wc -l)
# Calculate totals from individual commits
TOTAL_FILES=0
TOTAL_INS=0
TOTAL_DEL=0
while read -r commit; do
if [ -n "$commit" ]; then
COMMIT_HASH=$(echo "$commit" | awk '{print $1}')
STAT=$(git show --stat --oneline "$COMMIT_HASH" 2>/dev/null | tail -1)
if echo "$STAT" | grep -q "files changed"; then
FILES=$(echo "$STAT" | awk '{print $1}')
# Extract insertions (handle both "210 insertions(+)" and "1761 insertions(+)" formats)
INS=$(echo "$STAT" | grep -oE '[0-9]+ insertions' | grep -oE '[0-9]+' || echo "0")
# Extract deletions (may not be present)
DEL=$(echo "$STAT" | grep -oE '[0-9]+ deletions' | grep -oE '[0-9]+' || echo "0")
TOTAL_FILES=$((TOTAL_FILES + ${FILES:-0}))
TOTAL_INS=$((TOTAL_INS + ${INS:-0}))
TOTAL_DEL=$((TOTAL_DEL + ${DEL:-0}))
fi
fi
done <<< "$COMMITS"
NET=$((TOTAL_INS - TOTAL_DEL))
# Output
echo "=========================================="
echo "Branch Effort Summary: $BRANCH"
if [ -n "$BASE" ]; then
echo "Base: $BASE"
if [ -n "$BASE_COMMIT" ]; then
echo "Base commit: ${BASE_COMMIT:0:8}"
fi
fi
if [ -n "$FILTER" ]; then
echo "Filter: $FILTER"
fi
if [ -n "$COMMIT_RANGE" ]; then
echo "Range: $COMMIT_RANGE"
fi
if [ -n "$AUTHOR_FILTER" ]; then
echo "Author filter: $AUTHOR_FILTER"
fi
echo "=========================================="
echo ""
echo "Commits: $COMMIT_COUNT"
echo "Tracked files touched: $FILES_COUNT"
echo ""
echo "Line changes:"
echo " Insertions: $TOTAL_INS"
echo " Deletions: $TOTAL_DEL"
echo " Net change: +$NET"
echo ""
if [ $COMMIT_COUNT -gt 0 ]; then
echo "Commits:"
echo "$COMMITS" | sed 's/^/ /'
echo ""
fi
# Per-author breakdown (--by-author)
if [ -n "$BY_AUTHOR" ] && [ $COMMIT_COUNT -gt 0 ]; then
AUTHOR_TMP=$(mktemp)
trap 'rm -f "$AUTHOR_TMP"' EXIT
while read -r commit; do
if [ -n "$commit" ]; then
COMMIT_HASH=$(echo "$commit" | awk '{print $1}')
AUTHOR=$(git log -1 --format='%an' "$COMMIT_HASH" 2>/dev/null || echo "?")
echo "$AUTHOR|$COMMIT_HASH"
fi
done <<< "$COMMITS" > "$AUTHOR_TMP"
echo "------------------------------------------"
echo "By author"
echo "------------------------------------------"
for author in $(cut -d'|' -f1 "$AUTHOR_TMP" | sort -u); do
AUTHOR_HASHES=$(grep -F "$author|" "$AUTHOR_TMP" | cut -d'|' -f2)
A_COMMITS=0
A_INS=0
A_DEL=0
while read -r hash; do
[ -z "$hash" ] && continue
A_COMMITS=$((A_COMMITS + 1))
STAT=$(git show --stat --oneline "$hash" 2>/dev/null | tail -1)
if echo "$STAT" | grep -q "files changed"; then
INS=$(echo "$STAT" | grep -oE '[0-9]+ insertions' | grep -oE '[0-9]+' || echo "0")
DEL=$(echo "$STAT" | grep -oE '[0-9]+ deletions' | grep -oE '[0-9]+' || echo "0")
A_INS=$((A_INS + ${INS:-0}))
A_DEL=$((A_DEL + ${DEL:-0}))
fi
done <<< "$AUTHOR_HASHES"
A_NET=$((A_INS - A_DEL))
echo ""
echo " $author:"
echo " Commits: $A_COMMITS"
echo " Insertions: $A_INS Deletions: $A_DEL Net: +$A_NET"
done
echo ""
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment