Skip to content

Instantly share code, notes, and snippets.

@a-c-m
Created June 13, 2025 11:04
Show Gist options
  • Save a-c-m/e65a4ab3ced328a7b136538147c4fec0 to your computer and use it in GitHub Desktop.
Save a-c-m/e65a4ab3ced328a7b136538147c4fec0 to your computer and use it in GitHub Desktop.
Git stats per dev (LOC) with delta and comparison
#!/bin/bash
# --- Config ---
EXCLUDE_EXTENSIONS="sql|log|csv|tsv|json|png|jpg|jpeg|gif|bmp|svg|mp4|mp3|zip|gz|tar|pdf|xlsx?|docx?"
WEEKS=4
COMPARE=false
# --- Args ---
for arg in "$@"; do
if [[ "$arg" == "--compare" ]]; then
COMPARE=true
elif [[ "$arg" =~ ^[0-9]+$ ]]; then
WEEKS="$arg"
fi
done
# --- BSD-Compatible Dates ---
NOW=$(date +"%Y-%m-%d")
START=$(date -v -"${WEEKS}"w +"%Y-%m-%d")
PREV_START=$(date -v -"$((${WEEKS} * 2))"w +"%Y-%m-%d")
echo "Analyzing commits from $START to $NOW"
if $COMPARE; then
echo "Comparing with previous $WEEKS weeks: $PREV_START to $START"
fi
echo
# --- Get authors as Name <email> using newline separation ---
authors=$(git log --no-merges --since="$START" --until="$NOW" --format='%aN <%aE>' | sort | uniq)
# Temp files for storing stats
tmp_now=$(mktemp)
tmp_prev=$(mktemp)
trap 'rm -f "$tmp_now" "$tmp_prev"' EXIT
# --- Helpers ---
human_format_value() {
local num=$1
local val_str=""
if (( num < 1000 )); then
val_str=$(printf "%d" "$num")
elif (( num < 10000 )); then # 1000 to 9999 -> 1.0k to 9.9k
val_str=$(awk -v n="$num" 'BEGIN{printf "%.1fk", n/1000}')
elif (( num < 1000000 )); then # 10000 to 999999 -> 10k to 999k
val_str=$(awk -v n="$num" 'BEGIN{printf "%.0fk", n/1000}')
elif (( num < 10000000 )); then # 1,000,000 to 9,999,999 -> 1.0M to 9.9M
val_str=$(awk -v n="$num" 'BEGIN{printf "%.1fM", n/1000000}')
else # 10,000,000+ -> 10M, 123M etc.
val_str=$(awk -v n="$num" 'BEGIN{printf "%.0fM", n/1000000}')
fi
echo "$val_str" | sed 's/\.0k/k/g; s/\.0M/M/g' # Clean up .0k and .0M
}
format_delta() {
local num=$1
local sign_char
local abs_val_to_format
if [[ "$num" -lt 0 ]]; then
sign_char="-"
abs_val_to_format=$((num * -1))
else
sign_char="+"
abs_val_to_format=$num
fi
printf "%s%s" "$sign_char" "$(human_format_value "$abs_val_to_format")"
}
sign_commit_delta() {
local num=$1
if [[ "$num" -ge 0 ]]; then
echo "+$num"
else
echo "$num"
fi
}
get_stats() {
local since="$1"
local until="$2"
local author="$3"
# Extract email from "Name <email>" format for more reliable matching
local email=$(echo "$author" | sed -n 's/.*<\([^>]*\)>.*/\1/p')
if [[ -n "$email" ]]; then
git log --no-merges --since="$since" --until="$until" --author="$email" --pretty=tformat: --numstat \
| grep -vE "\.($EXCLUDE_EXTENSIONS)$" \
| awk '{a+=$1; r+=$2} END {print (a ? a : 0), (r ? r : 0)}'
else
echo "0 0"
fi
}
get_commits() {
local since="$1"
local until="$2"
local author="$3"
if [[ -n "$author" ]]; then
git log --no-merges --since="$since" --until="$until" --format='%aN <%aE>' | grep -Fxc "$author"
else
echo "0"
fi
}
# --- Collect stats ---
while IFS= read -r author; do
[[ -z "$author" ]] && continue
stats_now=($(get_stats "$START" "$NOW" "$author"))
commits_now=$(get_commits "$START" "$NOW" "$author")
echo "$author|$commits_now|${stats_now[0]:-0}|${stats_now[1]:-0}" >> "$tmp_now"
if $COMPARE; then
stats_prev=($(get_stats "$PREV_START" "$START" "$author"))
commits_prev=$(get_commits "$PREV_START" "$START" "$author")
echo "$author|$commits_prev|${stats_prev[0]:-0}|${stats_prev[1]:-0}" >> "$tmp_prev"
fi
done <<< "$authors"
# --- Output ---
if $COMPARE; then
printf "%-30s %-25s %-25s %-25s %-25s\n" "Author" "Commits (Now, Prev, Δ)" "Added (Now, Prev, Δ)" "Removed (Now, Prev, Δ)" "Changed (Now, Prev, Δ)"
else
printf "%-30s %10s %9s %9s %9s\n" "Author" "Commits" "Added" "Removed" "Changed"
fi
while IFS= read -r author; do
[[ -z "$author" ]] && continue
display=$(echo "$author" | sed -E 's/ <.*>//')
now=$(grep "^$(printf '%s' "$author" | sed 's/[[\.*^$()+?{|]/\\&/g')|" "$tmp_now" || echo "$author|0|0|0")
IFS='|' read -r _ c_now a_now r_now <<< "$now"
if $COMPARE; then
prev=$(grep "^$(printf '%s' "$author" | sed 's/[[\.*^$()+?{|]/\\&/g')|" "$tmp_prev" || echo "$author|0|0|0")
IFS='|' read -r _ c_prev a_prev r_prev <<< "$prev"
c_diff=$((c_now - c_prev))
a_diff=$((a_now - a_prev))
r_diff=$((r_now - r_prev))
ch_now=$((a_now + r_now))
ch_prev=$((a_prev + r_prev))
ch_diff=$((ch_now - ch_prev))
# Format values for display
c_diff_s=$(sign_commit_delta "$c_diff")
a_now_h=$(human_format_value "$a_now")
a_prev_h=$(human_format_value "$a_prev")
a_diff_h=$(format_delta "$a_diff")
r_now_h=$(human_format_value "$r_now")
r_prev_h=$(human_format_value "$r_prev")
r_diff_h=$(format_delta "$r_diff")
ch_now_h=$(human_format_value "$ch_now")
ch_prev_h=$(human_format_value "$ch_prev")
ch_diff_h=$(format_delta "$ch_diff")
printf "%-30s %7d %7d %7s %7s %7s %7s %7s %7s %7s %7s %7s %7s\n" \
"$display" \
"$c_now" "$c_prev" "$c_diff_s" \
"$a_now_h" "$a_prev_h" "$a_diff_h" \
"$r_now_h" "$r_prev_h" "$r_diff_h" \
"$ch_now_h" "$ch_prev_h" "$ch_diff_h"
else
ch_now=$((a_now + r_now))
a_now_h=$(human_format_value "$a_now")
r_now_h=$(human_format_value "$r_now")
ch_now_h=$(human_format_value "$ch_now")
printf "%-30s %10s %9s %9s %9s\n" "$display" "$c_now" "$a_now_h" "$r_now_h" "$ch_now_h"
fi
done <<< "$authors"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment