用 Jujutsu 拆解巨型 PR:把审查从"心理清单"变成可操作的工作流

2026-06-03 16 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:12 分钟

面对一个改动 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,包含以下三类改动混在一起:

  1. 数据库 schema 迁移(10 个文件)
  2. API 层重构(40 个文件)
  3. 新功能:批量导出接口(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>

采纳建议

  1. 先在审查侧试用:不需要整个团队迁移,审查者一个人装 jj 就能开始拆分审查。
  2. 从最大的 PR 开始:找一个你平时最头疼的巨型 PR,用 jj diff -r 逐 change 看,对比一下体验差异。
  3. 建立拆分约定:如果效果好,和团队约定一个规则——超过 N 个文件的 PR,作者需要用 jj split 拆成逻辑独立的 change 再提交。
  4. 保留 Git 兼容性:最终推到远端的还是 Git commit,jj 的 change 在推送时会自动转化为 commit。CI/CD 不需要任何改动。

巨型 PR 的审查问题,本质上不是"代码太多",而是"信息没有结构"。jj 提供的不是更快的 diff 工具,而是给变更本身加上结构的能力——把一整块混沌变成一串可逐步消化的小单元。审查者的注意力是稀缺资源,结构化的变更序列是对这种资源最直接的尊重。


相关推荐