Created
May 29, 2026 08:16
-
-
Save kvalv/21fca5e4607cafd59a921ef5e632f04c to your computer and use it in GitHub Desktop.
git-contributors
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
| #!/usr/bin/env bash | |
| # | |
| # git-contributors [options] <path> | |
| # | |
| # Show the main contributors for a given path, ranked by commits, with | |
| # lines added/removed, the date range of their activity, and a per-author | |
| # sparkline of commit activity over time (one cell per month). | |
| # | |
| # Defaults to the last 12 months. Pass --since "" to include all history. | |
| set -euo pipefail | |
| usage() { | |
| cat <<'EOF' | |
| git-contributors — who are the active contributors for a path? | |
| Usage: | |
| git-contributors [options] <path> | |
| Options: | |
| -s, --since <when> Window to consider (default: "12 months ago"). | |
| Use --since "" for all history. | |
| -n, --top <N> Show only the top N contributors (default: all). | |
| -e, --email Group by email instead of name. | |
| -h, --help Show this help. | |
| Examples: | |
| git-contributors . # this directory, last 12 months | |
| git-contributors internal/activities # a subdirectory | |
| git-contributors --top 10 . # top 10 contributors | |
| git-contributors --since "3 months ago" . # narrower window | |
| git-contributors --since "" services/pipeline # all history | |
| git-contributors --email . # group by email | |
| The ACTIVITY column is a sparkline of commits per month (oldest -> newest) | |
| across the selected window, so you can see who is ramping up or trailing off. | |
| EOF | |
| } | |
| # No arguments at all -> show help. | |
| if [[ $# -eq 0 ]]; then | |
| usage | |
| exit 0 | |
| fi | |
| since="12 months ago" | |
| top=0 | |
| group_by="name" | |
| path="" | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -s|--since) since="$2"; shift 2 ;; | |
| -n|--top) top="$2"; shift 2 ;; | |
| -e|--email) group_by="email"; shift ;; | |
| -h|--help) usage; exit 0 ;; | |
| -*) echo "unknown option: $1" >&2; usage >&2; exit 1 ;; | |
| *) path="$1"; shift ;; | |
| esac | |
| done | |
| if [[ -z "$path" ]]; then | |
| echo "error: no path given" >&2 | |
| echo >&2 | |
| usage >&2 | |
| exit 1 | |
| fi | |
| if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then | |
| echo "error: not inside a git repository" >&2 | |
| exit 1 | |
| fi | |
| since_args=() | |
| [[ -n "$since" ]] && since_args=(--since="$since") | |
| if [[ "$group_by" == "email" ]]; then | |
| fmt="%ae" | |
| else | |
| fmt="%an" | |
| fi | |
| total_commits=$(git log "${since_args[@]}" --no-merges --oneline -- "$path" | wc -l | tr -d ' ') | |
| echo "Contributors for: $path" | |
| if [[ -n "$since" ]]; then | |
| echo "Window: since $since" | |
| else | |
| echo "Window: all history" | |
| fi | |
| echo "Total commits: $total_commits" | |
| echo | |
| if [[ "$total_commits" == "0" ]]; then | |
| echo "No commits found." | |
| exit 0 | |
| fi | |
| # Emit per-commit author + month, plus numstat lines. awk aggregates totals | |
| # and a per-author per-month commit count, then renders a sparkline spanning | |
| # the full month range present in the data. | |
| git log "${since_args[@]}" --no-merges \ | |
| --pretty=format:"C%x09${fmt}%x09%ad" --date=format:'%Y-%m' --numstat -- "$path" \ | |
| | awk -F'\t' ' | |
| function spark(count, levels, i) { | |
| if (count <= 0) return " " | |
| # 8 levels of block characters | |
| split("▁▂▃▄▅▆▇█", levels, "") | |
| i = count | |
| if (i > 8) i = 8 | |
| return levels[i] | |
| } | |
| /^C\t/ { | |
| author = $2 | |
| month = $3 | |
| commits[author]++ | |
| cell[author SUBSEP month]++ | |
| months[month] = 1 | |
| if (!(author in first) || month < first[author]) first[author] = month | |
| if (!(author in last) || month > last[author]) last[author] = month | |
| next | |
| } | |
| NF >= 3 && $1 ~ /^[0-9]+$/ { | |
| added[author] += $1 | |
| removed[author] += $2 | |
| } | |
| END { | |
| # Ordered list of all months seen. | |
| n = 0 | |
| for (m in months) order[++n] = m | |
| # insertion sort (small n) | |
| for (i = 2; i <= n; i++) { | |
| key = order[i]; j = i - 1 | |
| while (j >= 1 && order[j] > key) { order[j+1] = order[j]; j-- } | |
| order[j+1] = key | |
| } | |
| printf "%-26s %7s %8s %8s %-*s %s\n", "AUTHOR", "COMMITS", "+LINES", "-LINES", (n>9?n:9), "ACTIVITY", "RANGE" | |
| for (a in commits) { | |
| line = "" | |
| for (i = 1; i <= n; i++) line = line spark(cell[a SUBSEP order[i]]) | |
| printf "%d\t%-26s %7d %8d %8d %-*s %s -> %s\n", \ | |
| commits[a], a, commits[a], added[a], removed[a], (n>9?n:9), line, first[a], last[a] | |
| } | |
| } | |
| ' \ | |
| | { read -r header; echo "$header"; sort -rn -k1,1 | cut -f2-; } \ | |
| | { [[ "$top" -gt 0 ]] && { read -r header; echo "$header"; head -n "$top"; } || cat; } | |
| if [[ -n "$since" ]]; then | |
| echo | |
| echo "(ACTIVITY: one cell per month, oldest -> newest; taller = more commits)" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment