面对一个改动 300 个文件、横跨重构+新功能+配置迁移的巨型 PR,审查者最真实的感受不是"代码写得不好",而是"我不知道自己看完了没有"。哪些文件已经审过、哪些意见对方改了、哪些还没回复——这些全靠脑子记。传统 Git 给你一把 git diff,然后就把你丢进了信息的海洋。
Ben Gesoff 最近分享了一套用 Jujutsu(jj)做大型变更审查的工作流,核心思路很简单:不要一次性审完整个 PR,而是把大变更拆成可逐步推进的小单元,让审查进度本身变成可追踪的状态。
巨型 PR 审查的认知陷阱
传统审查流程有几个隐性成本:
- 边界模糊:300 个文件的 diff 是一整块输出,没有天然的"段落"分隔。你看到第 150 个文件时,注意力已经开始衰减。
- 状态不可见:哪些文件审过了、哪些意见已解决,全靠评论区的 thread 状态和你的记忆。GitHub 的"viewed"标记是唯一的辅助,但它只标记文件,不标记逻辑分组。
- 反馈交织:重构意见和新功能建议混在一起,作者改了一处可能影响另一处,审查者很难追踪因果关系。
这些问题的根源不是工具不够"智能",而是 Git 把变更当作一个扁平的、不可分割的整体。
jj 的关键差异:Change 是第一公民
Jujutsu 是一个 Git-compatible 的版本控制工具,但它的核心抽象不是 commit,而是 change。两者的区别很关键:
- Git 的 commit 是不可变的快照,一旦提交就定型了。你要修改只能追加新 commit(amend 也只是替换,不能拆分)。
- jj 的 change 是可塑的工作单元——你可以随时拆分、移动、合并、重排,而且这些操作是原生的,不需要
git rebase -i的交互式编辑器。
这意味着审查者可以要求作者把一个大 change 拆成几个逻辑独立的 change,每个单独审查,而不破坏整体的提交历史。
实际工作流:从一整块到可审查的序列
下面用一个具体场景走一遍流程。假设你收到一个 PR,包含以下三类改动混在一起:
- 数据库 schema 迁移(10 个文件)
- API 层重构(40 个文件)
- 新功能:批量导出接口(20 个文件)
第一步:作者用 jj split 拆分变更
作者在本地用 jj split 把一个大的 change 拆成三个逻辑独立的 change:
# 查看当前工作区的 change
jj log
# 把当前 change 拆分:先选中 schema 迁移相关的文件
jj split -d @ -- 'db/migrations/*' 'db/models/*' 'db/config/*'
# jj 会打开一个交互式界面,让你确认选中的文件构成第一个 change
# 剩余文件自动留在原 change 中
# 继续拆分:从剩余 change 中抽出 API 重构
jj split -d @ -- 'api/v2/*' 'api/middleware/*' 'api/tests/*'
# 现在日志里应该有三个 change,按逻辑顺序排列
jj log
拆分后的 jj log 输出大致如下:
@ zzzkkk you 5 minutes ago feat: batch export API
○ zzzmno you 10 minutes ago refactor: API v2 layer
○ zzzpqr you 15 minutes ago migrate: DB schema to v3
○ zzzabc main 2 hours ago main branch
每个 change 都有独立的描述和文件范围,审查者可以逐个看。
第二步:审查者逐 change 审查
审查者拉取作者的分支后,不再面对一个巨型 diff,而是按顺序审查:
# 拉取作者的分支
jj git fetch origin
jj new zzzkkk # 切到最新的 change
# 只看 schema 迁移的 diff
jj diff -r zzzpqr
# 审查完毕,给意见,然后看下一个
jj diff -r zzzmno
# 最后看新功能
jj diff -r zzzkkk
关键好处:每个 jj diff -r <change_id> 的输出量是可控的。你不需要在 300 个文件里翻找,而是看 10 个 → 40 个 → 20 个,每轮都有明确的完成感。
第三步:作者按 change 逐个回应
作者收到审查意见后,可以针对特定 change 修改,而不影响其他 change:
# 修改 schema 迁移 change 中的某个文件
jj edit zzzpqr
# 修改 db/migrations/003_add_export_table.sql
# ...
# 修改自动进入 zzzpqr,不会污染其他 change
jj diff -r zzzpqr # 确认改动范围
# 回到工作区继续
jj edit zzzkkk
审查者下次来看时,只需要重新 jj diff -r zzzpqr,就能精确看到作者改了什么——不需要在巨型 diff 里找"他到底改了哪几行"。
进阶技巧:用 jj move 调整审查顺序
有时候审查者发现依赖关系不对——比如新功能 change 里引用了重构后的 API,但重构 change 还有问题。这时可以要求作者调整顺序:
# 把新功能 change 移到重构 change 之后(如果顺序本来不对)
jj move --from zzzkkk --to zzzmno # 把部分改动合并到前一个 change
# 或者直接重排 change 的顺序
jj rebase -s zzzkkk -d zzzmno
这比 git rebase -i 更安全,因为 jj 的操作是确定性命令,不需要手动编辑一个容易出错的 todo 列表。
一个完整的审查追踪脚本
下面是一个可以直接用的 bash 脚本,帮你追踪多 change PR 的审查进度:
#!/usr/bin/env bash
# jj-review-tracker.sh — 追踪多 change PR 的审查进度
# 用法: ./jj-review-tracker.sh <branch_name>
set -euo pipefail
BRANCH="${1:?请指定分支名}"
REVIEW_DIR=".jj-reviews/$BRANCH"
mkdir -p "$REVIEW_DIR"
# 获取该分支上 main 之后的所有 change
CHANGES=$(jj log -r "ancestors($BRANCH) & ~ancestors(main)" --no-pager \
-T 'change_id.short() ++ "\t" ++ description.first_line() ++ "\n"')
echo "=== 分支 $BRANCH 的 change 列表 ==="
echo "$CHANGES"
echo ""
while IFS=$'\t' read -r cid desc; do
STATUS_FILE="$REVIEW_DIR/$cid.status"
if [ -f "$STATUS_FILE" ]; then
status=$(cat "$STATUS_FILE")
else
status="未审查"
fi
echo "[$status] $cid — $desc"
# 如果未审查,显示 diff 统计
if [ "$status" = "未审查" ]; then
jj diff -r "$cid" --stat
echo ""
fi
done <<< "$CHANGES"
echo ""
echo "=== 操作 ==="
echo "标记审查完成: echo '已审查' > $REVIEW_DIR/<change_id>.status"
echo "标记需修改: echo '需修改' > $REVIEW_DIR/<change_id>.status"
echo "重新审查: rm $REVIEW_DIR/<change_id>.status"
echo ""
echo "审查进度目录: $REVIEW_DIR"
运行效果:
$ ./jj-review-tracker.sh feat/batch-export
=== 分支 feat/batch-export 的 change 列表 ===
zzzkkk feat: batch export API
zzzmno refactor: API v2 layer
zzzpqr migrate: DB schema to v3
[未审查] zzzkkk — feat: batch export API
20 files changed, 890 insertions, 0 deletions
[已审查] zzzmno — refactor: API v2 layer
[需修改] zzzpqr — migrate: DB schema to v3
=== 操作 ===
标记审查完成: echo '已审查' > .jj-reviews/feat/batch-export/zzzpqr.status
...
这个脚本把审查状态从"脑子里的清单"变成了文件系统里的标记——你可以随时查看进度,不会遗漏。
什么时候该用,什么时候不该用
这套工作流适合的场景:
- 变更超过 50 个文件,或者横跨多个逻辑领域(重构+迁移+新功能)
- 审查周期长(跨几天),中间容易忘记之前看了什么
- 多人审查,每个人负责不同模块,需要明确分工
不适合的场景:
- 小 PR(10 个文件以内),拆分反而增加沟通成本
- 团队全员不熟悉 jj,引入新工具的学习曲线可能抵消效率收益
- 紧急修复,需要快速合并,没时间做拆分
一个务实的折中方案:作者继续用 Git 提交,审查者用 jj 拉取后本地拆分审查。jj 是 Git-compatible 的,不需要作者也切换工具。审查者只需要在自己的机器上安装 jj,把 Git 仓库当作 jj 仓库来操作就行:
# 在已有 Git 仓库中初始化 jj(不改变仓库结构)
jj init --git
# 之后所有 jj 命令都可以直接用
jj log
jj diff -r <任意commit的hash>
采纳建议
- 先在审查侧试用:不需要整个团队迁移,审查者一个人装 jj 就能开始拆分审查。
- 从最大的 PR 开始:找一个你平时最头疼的巨型 PR,用
jj diff -r逐 change 看,对比一下体验差异。 - 建立拆分约定:如果效果好,和团队约定一个规则——超过 N 个文件的 PR,作者需要用
jj split拆成逻辑独立的 change 再提交。 - 保留 Git 兼容性:最终推到远端的还是 Git commit,jj 的 change 在推送时会自动转化为 commit。CI/CD 不需要任何改动。
巨型 PR 的审查问题,本质上不是"代码太多",而是"信息没有结构"。jj 提供的不是更快的 diff 工具,而是给变更本身加上结构的能力——把一整块混沌变成一串可逐步消化的小单元。审查者的注意力是稀缺资源,结构化的变更序列是对这种资源最直接的尊重。