👓Git 统计多项目代码变更量

git log + awk + bash 三件套,跨仓库统计任意作者在任意时间范围内的代码贡献(提交数、新增/删除/净增行数),适合写周报、季度绩效自查、团队活跃度盘点。


🚀 一、快速开始

30 秒试用:把第五节的脚本存为 gitstat.sh,然后运行:

chmod +x gitstat.sh
./gitstat.sh 2025-04-01 2025-06-30 zk@xxx.com /path/to/repo1 /path/to/repo2

输出样例:

📊【最终汇总】zk@xxx.com (2025-04-01 至 2025-06-30)
📌 提交总次数:   312
📌 新增总行数:   50033
📌 删除总行数:   16497
📌 净增总行数:   33536

🎯 二、适用场景

场景 说明
周报 / 月报 自动统计时间段内的贡献,省去手工数提交
季度绩效自查 提前看一下自己在各个仓库的代码量
团队活跃度盘点 循环不同 $AUTHOR 得到人均贡献
离职交接评估 快速看某人在关键仓库的历史投入
合规审计 配合 .mailmap 统计某个身份在某时间段的所有改动

⚠️ 代码行数只是的指标,不直接等于价值。重构、删废代码、review 质量都不会体现在行数里——用它辅助判断,不要用它做唯一考核。


💡 三、核心原理(一行看懂)

整个脚本的核心浓缩成这一行:

read add subs loc <<< "$(git log ... | awk ...)"

它串起了三步:

  1. git log --numstat — 输出每次提交的每个文件的增删行数
  2. awk — 汇总为「总新增 总删除 净增」三个数字
  3. read + <<< — 按空格拆分,分别赋值给 addsubsloc

为什么用 <<< 而不是管道?

# ❌ 错误:Bash 中管道右侧在子 shell 执行,read 的变量赋值出了子 shell 就失效
git log ... | awk ... | read add subs loc

# ✅ 正确:Here String 让 read 运行在当前 shell,变量保留
read add subs loc <<< "$(git log ... | awk ...)"

🔍 四、分步拆解

4.1 Git 命令

git log \
  --all \
  --since="$SINCE" \
  --until="$UNTIL" \
  --author="$AUTHOR" \
  --no-merges \
  --pretty=tformat: \
  --numstat
参数 说明
--all 统计所有分支,避免遗漏未合入主干的提交
--since / --until 时间窗口(支持 2025-04-01"2 weeks ago" 等)
--author 作者(正则匹配,支持邮箱/姓名/模糊匹配)
--no-merges 排除 merge commit,避免合并提交的整块 diff 算到个人头上
--pretty=tformat: 不输出 commit hash/message,只留 --numstat 的行
--numstat 每个文件输出 新增 删除 路径

典型输出

10  2  README.md
5   1  src/main.c
3   0  src/helper.c
-   -  logo.png     ← 二进制文件,不参与统计

4.2 AWK 汇总

awk '/^[0-9]/ {a+=$1; b+=$2; c+=$1-$2} END {print a+0, b+0, c+0}'
片段 作用
/^[0-9]/ 只处理数字开头的行,跳过二进制文件的 - -
a+=$1 累加新增行数
b+=$2 累加删除行数
c+=$1-$2 累加净增行数
a+0 空值强制为 0,避免输出空串

4.3 变量赋值与默认值

read add subs loc <<< "$(...)"

add=${add:-0}
subs=${subs:-0}
loc=${loc:-0}

${var:-0} 是 Bash 参数扩展:变量为空则使用默认值 0,保证下游 $((...)) 算术不会报语法错误。


📜 五、完整脚本

#!/bin/bash
set -u  # 使用未定义变量时报错,防止打错参数名

# ---------- 参数校验 ----------
if [ "$#" -lt 4 ]; then
  cat <<EOF
Usage: $0 <since_date> <until_date> <author> <repo1> [repo2] ...
Example:
  $0 2025-04-01 2025-06-30 zk@xxx.com /path/to/repo1 /path/to/repo2
EOF
  exit 1
fi

SINCE="$1"
UNTIL="$2"
AUTHOR="$3"
shift 3
REPOS=("$@")

# ---------- 打印入参 ----------
echo "📊 参数设置:"
echo "📅 时间范围: $SINCE 至 $UNTIL"
echo "👤 作者:     $AUTHOR"
echo "📁 仓库数量: ${#REPOS[@]}"
echo "----------------------------"

# ---------- 抽取成函数,便于测试/复用 ----------
stat_repo() {
  local repo="$1"
  (
    cd "$repo" || exit 1
    git log --all --no-merges \
      --since="$SINCE" --until="$UNTIL" \
      --author="$AUTHOR" \
      --pretty=tformat: --numstat \
    | awk '/^[0-9]/ {a+=$1; b+=$2; c+=$1-$2} END {print a+0, b+0, c+0}'
  )
}

count_commits() {
  local repo="$1"
  (
    cd "$repo" || exit 1
    git log --all --no-merges \
      --since="$SINCE" --until="$UNTIL" \
      --author="$AUTHOR" --oneline | wc -l
  )
}

# ---------- 循环统计 ----------
total_add=0
total_subs=0
total_loc=0
total_commits=0

for repo in "${REPOS[@]}"; do
  echo "📂 正在处理: $repo"

  # 校验是否为合法 git 仓库(子 shell 隔离,不污染主 shell 的 cwd)
  if ! (cd "$repo" 2>/dev/null && git rev-parse --is-inside-work-tree >/dev/null 2>&1); then
    echo "❌ 非法或非 git 仓库,跳过"
    echo "----------------------------"
    continue
  fi

  repo_name=$(basename "$repo")

  read add subs loc <<< "$(stat_repo "$repo")"
  commits=$(count_commits "$repo")

  add=${add:-0}
  subs=${subs:-0}
  loc=${loc:-0}
  commits=${commits:-0}

  printf "✅ %s\n" "$repo_name"
  printf "   🔢 提交次数:   %6d\n" "$commits"
  printf "   ➕ 新增行数:   %6d\n" "$add"
  printf "   ➖ 删除行数:   %6d\n" "$subs"
  printf "   📊 净增行数:   %6d\n" "$loc"
  echo "----------------------------"

  total_add=$((total_add + add))
  total_subs=$((total_subs + subs))
  total_loc=$((total_loc + loc))
  total_commits=$((total_commits + commits))
done

# ---------- 最终汇总 ----------
echo ""
echo "📊【最终汇总】$AUTHOR ($SINCE 至 $UNTIL)"
printf "📌 提交总次数:   %d\n" "$total_commits"
printf "📌 新增总行数:   %d\n" "$total_add"
printf "📌 删除总行数:   %d\n" "$total_subs"
printf "📌 净增总行数:   %d\n" "$total_loc"

脚本的几个设计要点

要点 原因
封装 stat_repo / count_commits 函数 职责单一、便于单元测试或换成其他统计口径
子 shell (cd ... && ...) 不污染主 shell 的工作目录;也避免相对路径下次循环错乱
git rev-parse --is-inside-work-tree 先校验再统计,防止对普通目录跑 git log 产生误报
--no-merges 排除合并提交的整块 diff
awk '/^[0-9]/' 兼容二进制文件的 - -
set -u 打错变量名立刻暴露,减少静默 bug

🛠️ 六、使用示例

命令

./gitstat.sh 2025-04-01 2025-06-30 zk@xxx.com \
  D:/GIT/project1 D:/GIT/project2 D:/GIT/project3 \
  D:/GIT/project4 D:/GIT/project5 D:/GIT/project6 \
  D:/GIT/project7 D:/GIT/project8

输出

📊 参数设置:
📅 时间范围: 2025-04-01 至 2025-06-30
👤 作者:     zk@xxx.com
📁 仓库数量: 8
----------------------------
📂 正在处理: D:/GIT/project1
✅ project1
   🔢 提交次数:       45
   ➕ 新增行数:     1629
   ➖ 删除行数:      202
   📊 净增行数:     1427
----------------------------
...
📂 正在处理: D:/GIT/project8
✅ project8
   🔢 提交次数:       28
   ➕ 新增行数:     1432
   ➖ 删除行数:      847
   📊 净增行数:      585
----------------------------

📊【最终汇总】zk@xxx.com (2025-04-01 至 2025-06-30)
📌 提交总次数:   312
📌 新增总行数:   50033
📌 删除总行数:   16497
📌 净增总行数:   33536

🚀 七、进阶玩法

7.1 按月拆分,看贡献曲线

把时间轴按月切片循环调用,导出到 CSV:

for m in 01 02 03 04 05 06; do
  read add subs loc <<< "$(stat_repo /path/to/repo 2025-${m}-01 2025-${m}-31)"
  echo "2025-${m},${add:-0},${subs:-0},${loc:-0}" >> monthly.csv
done

导入 Excel / 飞书多维表格就能画出月度代码量曲线。

7.2 过滤自动生成代码、第三方目录

--numstat 后面可以加 pathspec,只统计「真正写的代码」:

git log ... --numstat -- \
  ':(exclude)**/node_modules/**' \
  ':(exclude)**/dist/**' \
  ':(exclude)**/*.lock' \
  ':(exclude)**/*.min.js'

7.3 多作者对比

AUTHOR 做成数组,加一层循环:

AUTHORS=("zk@xxx.com" "alice@xxx.com" "bob@xxx.com")
for a in "${AUTHORS[@]}"; do
  AUTHOR="$a"
  # ...跑一遍统计
done

7.4 用 .mailmap 合并身份

同一个人可能在不同仓库用了 工作邮箱 / Gmail / 名字缩写。在仓库根目录建 .mailmap

Z K <zk@xxx.com> <zk@gmail.com>
Z K <zk@xxx.com> <zk@old-company.com>

git log 会自动把这些身份归一到规范邮箱,统计不再漏人。

7.5 导出 CSV 给 BI

把脚本尾部改成 CSV 输出,便于二次加工:

echo "repo,commits,added,deleted,net"
for repo in "${REPOS[@]}"; do
  read add subs loc <<< "$(stat_repo "$repo")"
  commits=$(count_commits "$repo")
  echo "$(basename "$repo"),${commits:-0},${add:-0},${subs:-0},${loc:-0}"
done

⚠️ 八、常见坑与 FAQ

问题 说明
--all 重复计数 同一 commit 出现在多个分支(cherry-pick、rebase 保留旧分支)可能被重复统计。必要时改用 --branches=main--first-parent 限定主干。
作者匹配不上 --author 是正则,跨邮箱同一人用 --author="zk|kk";或用 .mailmap 统一身份。
代码行数 ≠ 贡献量 自动生成代码、格式化、批量重命名都会膨胀行数,配合 pathspec 过滤更准。
Windows 路径 Git Bash 下用 D:/GIT/xxx 正斜杠格式;反斜杠会被 Bash 当转义字符。
大仓库性能慢 --shallow-since=2025-01-01 缩小扫描范围,或 git gc --aggressive 压缩仓库。
输出是空的 / 全 0 检查:① 邮箱大小写;② 时间范围有没有错;③ 该分支是否被 fetch 下来;④ 是否是 shallow clone(没有完整历史)。
read: command not found 脚本用了 #!/bin/sh 而非 #!/bin/bash<<< 是 Bash 专属语法。
结果比 GitHub Insights 大很多 GitHub 默认排除二进制和生成代码,本脚本默认不排除;用 7.2 节的 pathspec 对齐。

⚖️ 九、和 git shortlog / cloc 的对比

工具 能做什么 不够的地方
git shortlog -sn 按作者统计提交次数 没有行数维度
git log --author --shortstat 单仓库的 add/del 不方便跨仓库汇总
cloc 按语言统计当前代码快照行数 不感知历史,不按作者
本脚本 跨仓库、跨时间、按作者,同时给出提交数 + 行数 需要自己维护作者身份映射

简单说:看现状快照cloc;看谁做了多少用本脚本;只要排名用 git shortlog


🎉 十、小结

这个脚本用最小的依赖(git + awk + bash)解决跨仓库贡献统计,核心技巧有四:

  1. Here String (<<<) 保留 read 的变量赋值,避开管道子 shell 陷阱
  2. --numstat + awk 单次遍历同时算出三列指标
  3. 子 shell (cd ... && ...) 隔离目录切换,函数化后更清晰
  4. --no-merges + /^[0-9]/ 规避合并提交 & 二进制文件两个典型坑

进一步可以按月切片、pathspec 过滤、.mailmap 归一身份,把它从「数行数」升级到「可分析的贡献数据源」。

代码行数是入门指标,别把它当成终点——它最好和 review 质量、缺陷率、用户价值一起看。

posted @ 2026-05-07 19:11  丿似锦  阅读(74)  评论(0)    收藏  举报