Created
June 13, 2025 11:04
-
-
Save a-c-m/e65a4ab3ced328a7b136538147c4fec0 to your computer and use it in GitHub Desktop.
Git stats per dev (LOC) with delta and comparison
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 | |
# --- 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