authors.sh 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. #!/bin/sh
  2. set -eu
  3. print_help() {
  4. cat <<EOF
  5. Usage: $0 [-a alias_file] [-s start_ref -e end_ref]
  6. Options:
  7. -h Show this help message and exit
  8. -a alias_file Path to a file listing aliases (format: email,canonical_name).
  9. Lines starting with '#' or empty lines are ignored.
  10. -s start_ref Git ref (tag or commit) to start range (inclusive).
  11. -e end_ref Git ref (tag or commit) to end range (inclusive).
  12. EOF
  13. }
  14. ALIAS_FILE=""
  15. START_REF=""
  16. END_REF=""
  17. # Parse options.
  18. while getopts "a:s:e:h" opt; do
  19. case "$opt" in
  20. a)
  21. ALIAS_FILE="$OPTARG"
  22. ;;
  23. s)
  24. START_REF="$OPTARG"
  25. ;;
  26. e)
  27. END_REF="$OPTARG"
  28. ;;
  29. h)
  30. print_help
  31. exit 0
  32. ;;
  33. *)
  34. echo "Unknown option: -$OPTARG" >&2
  35. print_help
  36. exit 1
  37. ;;
  38. esac
  39. done
  40. RANGE_SPEC=""
  41. if [ -n "$START_REF" ] && [ -n "$END_REF" ]; then
  42. RANGE_SPEC="$START_REF..$END_REF"
  43. elif [ -n "$START_REF" ] || [ -n "$END_REF" ]; then
  44. echo "Error: both -s start_ref and -e end_ref must be provided together" >&2
  45. exit 1
  46. fi
  47. git log ${RANGE_SPEC} --format='%an|%ae|%ad' --date=format:'%Y' |
  48. awk -F'|' -v aliasfile="$ALIAS_FILE" '
  49. BEGIN {
  50. # Load aliases if provided.
  51. if (aliasfile != "") {
  52. while ((getline line < aliasfile) > 0) {
  53. if (line ~ /^#/ || line == "")
  54. continue;
  55. split(line, arr, ",");
  56. alias_map[arr[1]] = arr[2];
  57. }
  58. close(aliasfile);
  59. }
  60. }
  61. {
  62. author = $1;
  63. email = $2;
  64. year = $3;
  65. # Skip invalid entries.
  66. if (author == "" || email == "" || year == "")
  67. next;
  68. # Determine canonical name by email alias or first-seen name.
  69. if (email in alias_map) {
  70. canonical = alias_map[email];
  71. } else if (!(email in name_map)) {
  72. name_map[email] = author; canonical = author;
  73. } else {
  74. canonical = name_map[email];
  75. }
  76. # Aggregate.
  77. counts[canonical]++;
  78. if (!(canonical in min_year) || year < min_year[canonical])
  79. min_year[canonical] = year;
  80. if (!(canonical in max_year) || year > max_year[canonical])
  81. max_year[canonical] = year;
  82. }
  83. END {
  84. # Emit count|canonical|years
  85. for (c in counts) {
  86. if (c == "")
  87. continue;
  88. start = min_year[c];
  89. end = max_year[c];
  90. years = (start == end ? start : start "–" end);
  91. print counts[c] "|" c "|" years;
  92. }
  93. }' |
  94. grep '|' |
  95. awk -F'|' 'BEGIN { print "author,commits,years" } { print $2 "," $1 "," $3 }'