Gerrit:把代码审查当成可追踪的工程对象
/research/gerrit-code-review-workflow-research.md
Gerrit 表面上是一个 Web 代码审查系统,但只把它理解成“GitHub PR 的替代品”会低估它。它真正特殊的地方在于:Gerrit 把一次代码修改抽象成一个稳定的 change,围绕这个对象保存补丁版本、审查意见、投票、CI 结果、权限规则和最终提交状态。代码可以反复 amend、rebase、上传新 patch set,但审查对象不会丢。
这和普通 pull request 工具的气质不一样。GitHub PR 和 GitLab MR 更像围绕一个分支展开协作;Gerrit 更像围绕一个可演化的补丁展开审查。前者更适合产品团队常见的 feature branch 协作,后者更适合大代码库、强权限、强 CI、强审查纪律的工程组织。
它放在什么位置
Gerrit 官网对自己的定位很明确:open source、patchset based、面向 Git-based development 的 code review platform。它覆盖的是 Git server、访问控制、代码审查、提交门禁和自动化集成这一整层能力,单纯说成代码托管页面或 diff 评论 UI 都不准确。
它的历史也解释了这种设计气质。Gerrit 的历史页面说,它来自 Google 内部 Mondrian/Rietveld 这一脉,最早服务 Android Open Source Project。这个背景很重要:Gerrit 从一开始就面向大型代码库里“提交前必须经过可追踪审查”这个问题,而不是围绕“小团队随手开 PR”的轻量体验展开。
官方用户指南里有一句很能说明模型的话:Gerrit 的 code review workflow要求每个 commit 在进入代码库前先作为 change 上传到 Gerrit 的 staging area;只有通过审查并 submit 后,才会进入目标分支。换句话说,Gerrit 的默认心智不是“先推分支,再合并 PR”,而是“先把补丁送审,再让系统决定何时进入主线”。
一次修改在 Gerrit 里怎么流动
典型流程是这样的:开发者本地改代码,提交一个 commit,然后把它 push 到 refs/for/<branch>。Gerrit 会把这个 commit 变成一个 change,而不是直接写入目标分支。reviewer 在 Web UI 里给代码、整体 change 和标签投票;CI 或 bot 给 Verified、Lint、Code-Style 等标签投票;满足项目配置的 submit requirements 后,change 才能 submit。
这个流程里有两个关键对象。
第一个是 Change-Id。官方 Change-Id 文档解释得很清楚:Gerrit 需要识别哪些 commit 属于同一次 review。开发者根据反馈 amend 出第二个 commit 后,commit SHA 变了,但底部的 Change-Id 不变,Gerrit 就能把它挂回同一个 change。这样 review 讨论不会因为一次 amend 变成另一个孤立对象。
第二个是 patch set。Gerrit 的 Changes 概念文档说明,一个 change 代表一个正在审查的 commit;多个带同一 Change-Id 的 commit 会成为同一个 change 的多个 patch sets;最终 submit 时只把最新版本提交进去。这就是 Gerrit 最有价值的地方:它保留了“这个修改如何被审查、如何被修订”的演化轨迹。
我用 Gerrit 自己的公开实例查了一条已合并 change:597701。REST API 返回的信息显示,它有 2 个 revisions、5 条评论、Code-Review、Verified、Lint、Commit-Queue、Frontend-Verified、Code-Style、SLSA-Policy-Verified 等多个标签,最后状态是 MERGED。这个页面更接近一条小型工程决策记录:谁上传了什么版本、哪些自动化检查参与了门禁、什么时候合并、哪些评论已解决,都挂在同一个 change 上。
为什么它会让重视工程记忆的人上头
Gerrit 的吸引力不是 UI 漂亮,也不是上手轻。它吸引人的地方正好相反:它愿意牺牲一点顺手程度,换来更强的工程可追溯性。
GitHub PR 很适合开放协作。一个分支可以有多个 commit,Conversation、Commits、Checks、Files changed 都集中在同一个 PR 页面;GitHub 官方文档也把 pull request 定义成 propose、review、merge code changes 的基础协作功能。GitLab MR 也是类似定位,GitLab 文档强调 merge request 是团队 review、讨论、跟踪代码变化和 CI/CD 状态的中心位置。
这些模型的问题是:PR/MR 往往以分支或功能为单位,粒度由人控制。团队纪律好时可以拆得很细;纪律不好时,一个 PR 就会变成几十个文件、多个意图、半个项目重构的混合包。审查意见挂在 diff 行上,代码被 force push、重写、squash 后,很多讨论会变成 outdated context。它们当然能用,只是默认把“协作体验”放在前面,把“每个 commit 的审查身份”放在后面。
Gerrit 反过来。它默认鼓励 per-commit review,一个 change 通常对应一个 commit;修改通过 amend 后上传新 patch set,而不是在 PR 里继续堆 commit。Coreboot 的面向 GitHub 用户的 Gerrit walkthrough把差异说得很直白:Gerrit 里 review 是单个 commit,创建评审靠 git push origin HEAD:refs/for/master;reviewer 用 Code-Review -1/0/+1/+2 之类的标签表达意见;项目还可能要求额外的 Verified 或其他正向投票。
这套模型对个人小项目可能显得笨重;但对大工程,它能逼迫 change 变小、让审查状态可查询、让 CI 门禁变成项目规则,而不是靠口头约定。
权限、标签和门禁才是 Gerrit 的企业味
Gerrit 不是只有 patch set。它真正适合大组织的原因,是它把权限和提交条件做得很细。
Project Owner Guide强调,project owner 负责项目权限、access control list 和项目设置。Gerrit 的权限可以按 group、reference、branch、label 细分,这对普通开源小项目是负担,对 Android、Chromium 这类超大代码库则是刚需。
Review Labels 文档说明,reviewer 可以按项目配置给每个 label 打分,常见的 Code-Review 只是其中一个。是否需要 CI 的 Verified+1、是否允许某些票在新 patch set 后继续 sticky、谁有权打 +2,都可以配置。Submit Requirements 文档进一步把“什么时候可以提交”做成规则,例如要求非上传者的 Code-Review approval、要求特定 footer、移除继承的 Verified requirement 等。
这意味着 Gerrit 不是“大家看一眼然后点 merge”。它更像一个工程闸门:审查、自动化测试、安全合规、所有权规则一起决定 change 能不能进入主线。它的 REST API 和插件体系也服务于这一点:REST API提供 /changes/、/projects/、/accounts/ 等端点,插件开发文档覆盖事件监听、验证器、SSH 命令、UI 扩展、metrics、reviewer suggestion 等扩展点。大组织可以围绕 Gerrit 做很重的流程集成。
和 Jujutsu 的契合点
Gerrit 最近值得重新看的原因之一,是它和 Jujutsu 这种新 VCS 心智很接近。Jujutsu 也把 change 当成一等对象,而不是只盯着 commit SHA。Gerrit 传统上用 commit message footer 里的 Change-Id 跟踪同一次 review;Jujutsu 则有自己的 change id。
Gerrit 的 Jujutsu 支持设计文档已经把方向说清楚了。use cases 文档的目标是让贡献者可以用 Jujutsu 本地创建、修改、rebase、cherry-pick changes,并上传到 Gerrit;验收标准包括:Jujutsu 用户不需要安装 commit-msg hook,不需要在 commit message footer 里写 Change-Id,Gerrit 可以从 Git commit header 的 change-id 读取 Jujutsu change ID。solution 文档进一步说明,如果 commit message footer 里没有 Gerrit Change-Id,Gerrit 会从 change-id commit header 读取 Jujutsu change ID;如果两者都有,则 Gerrit footer 的 Change-Id 优先。
这个方向不是小功能。它说明 Gerrit 的模型和 jj 的模型在抽象层上是对齐的:核心对象都是“可演化的 change”,而不是一次性 commit 或一个临时分支。对未来的 Agent coding 工作流,这个模型也很有启发。Agent 产出的 patch 不应该只是一次性 diff,而应该成为有身份、有审查状态、有修订历史、有验证结果的 change。
什么时候值得用,什么时候不要碰
Gerrit 适合几类场景。
第一类是多人协作的大代码库。尤其是每次提交都需要审查、CI、owner approval、合规检查的项目。Chromium 的贡献文档就围绕 CL、Gerrit review、reviewer、approval、automated tests 展开,这类项目需要的不只是一个 PR 页面,而是一套围绕 change 的提交秩序。
第二类是想强制小粒度 review 的团队。Gerrit 会让“一个 change 一个意图”更自然,因为 amend 和 patch set 是主路径。对 AI 写代码尤其重要:Agent 很容易一次生成一大坨,Gerrit 这类模型可以迫使产物拆成可审查的 change。
第三类是需要把 review 结果长期沉淀成工程记忆的组织。Gerrit 让 review comment、label、CI 和 patch set 演化都挂在 change 上,后续可以通过 search/API 查回来。这对追溯“当时为什么这么改”很有价值。
不适合的场景也很明显。
如果只是个人项目、小团队原型、外部贡献者体验优先的开源项目,Gerrit 的门槛可能会伤害贡献率。commit-msg hook、refs/for/*、label 投票、权限模型、submit requirements、管理员配置,这些概念对没接触过 Gerrit 的人都不友好。Coreboot walkthrough 专门为 GitHub 用户解释 Gerrit,恰恰说明这个迁移需要教育成本。
如果团队的代码审查文化本来就不强,上 Gerrit 也不会自动变好。它会把流程显性化,但不会替团队决定 change 应该怎么拆、review 应该看什么、CI 应该覆盖哪些风险。工具能提供约束,不能替代工程判断。
对 vibe coding 的启发
Gerrit 对老大的最大启发,不是“马上部署一个 Gerrit 替换 GitHub”。更现实的启发是:AI 时代的软件协作,需要把 patch 变成可追踪对象。
现在的 vibe coding 很容易产生三类失忆:Agent 为什么这么改,reviewer 为什么接受或拒绝,后续修订为什么改变方向。普通聊天记录和 git diff 都不够稳定;聊天记录离代码太远,git diff 又只记录结果。Gerrit 的 change 模型给了一个中间层:每次修改有身份,修改过程有 patch set,判断过程有 comment 和 label,最终合并有 submit record。
这和之前关于 vibe coding 时代软件工程(内部笔记,未公开) 的判断可以接上:AI 不只需要更强的写代码能力,还需要更强的工程反馈环。一个可借鉴的个人工作流是:让 Codex worker 产出小 change;每个 change 绑定目标、影响范围、测试证据和 review 结论;如果修订,就形成 patch set,而不是在聊天里反复覆盖。即使不部署 Gerrit,也可以借它的对象模型改造自己的工作流。
最小可试路径可以先避开迁移成本:挑一个非关键 repo 做实验,部署本地 Gerrit 或使用公开可测试实例,跑一次从 git push HEAD:refs/for/main 到 patch set、label、submit 的完整流程;再让一个 Agent 生成小改动,另一个 Agent 只针对该 change 做 review,把 review 结论记录下来。一个月后回头查,如果能回答“这个改动当时为什么这样合并”,实验就有价值。
资料来源与验证
- Gerrit Code Review 官网
- Gerrit 的历史
- Gerrit User Guide
- Uploading Changes
- Change-Ids
- Changes 概念文档
- Review Labels
- Submit Requirements
- REST API
- Plugin Development
- Support posting changes from Jujutsu - Use Cases
- Support posting changes from Jujutsu - Solution
- Basic Gerrit Walkthrough — For GitHub Users
- GitHub Docs: About pull requests
- GitLab Docs: Merge requests
- Chromium Docs: Contributing to Chromium
- Gerrit 公开 change 示例:597701