写这篇 Gerrit 使用简介之前,我先说句真心话:如果你所在的团队还在用微信群收代码、口头 review、或者靠 GitHub 的 Pull Request 勉强撑着,那么 Gerrit 可能是你下一个值得花时间研究的工具。Gerrit 最早出自 Android 开源项目的代码审查流程,本质是一套基于 Git 的代码评审系统,核心玩法是:所有提交不直接进分支,而是先推到临时引用区,经过人工审查和自动验证后才能合入。很多人第一次用 Gerrit 会觉得“反人类”,因为 push 的语法、Change-Id、Patch Set 这些概念和 GitHub 那套完全不同。但一旦团队习惯了这种“评审驱动”的节奏,代码质量和管理清晰度会明显上一个台阶。
这篇内容不打算写成官方文档的翻译,而是把我实际用过的搭建、配置、提交流程、权限设置、问题排查,以及那些文档里不会写的坑,按顺序整理出来。适合刚接触 Gerrit 的开发、测试、运维,也适合正在评估自建代码审查平台的团队参考。
1. 内容整体设计与思路拆解
Gerrit 看上去是一个 Git 仓库管理工具,很多人第一反应是“这和 GitHub 有什么不一样?” 但真正理解 Gerrit 后你会发现,它把 Git 和代码审查强行绑定在一起,甚至可以说,仓库管理只是它的副产品,审查流程才是核心。
1.1 核心需求:把“审查”变成必经环节
传统 Git 工作流里,开发者 push 到远程分支后,其他人能不能看到、要不要看、什么时候看,靠的是自觉和协作约定。GitHub 的 PR 虽然把审查变成了一个显式流程,但 PR 本身仍然是一个可选的合并路径,而且很多团队对自己仓库的合并权限管得比较松。Gerrit 的设计理念更硬核:在它的默认配置下,任何提交都无法直接写入受保护的分支,必须先生成一个评审 Change,拿到足够的分数并执行 Submit 操作后,提交才会真正落到目标分支上。
这带来的好处不只是“有人看过了”,而是整个项目的提交历史变成了经过过滤的、可追溯的序列。每一条合入记录都有自己的 Change ID、评审人、得分、验证结果,出了问题可以精确回看是哪个环节放行的。
1.2 方案选型:为什么用 Gerrit 而不是本地 Git 或 GitHub
如果在内部想搭一套自己的代码审查平台,可选项其实不少。GitLab 有 Merge Request,Gitea 有 Pull Request,GitHub 也可以私有化部署。那为什么还要费劲上 Gerrit?
我个人的结论是:如果你对代码审查的严格程度要求非常高,或者团队规模较大、分支同时提交非常频繁,那么 Gerrit 的优势能吃得很透。它把每个提交都孤立成一个可独立审查和验证的单元,多个开发者同时在同一个分支上工作不会互相阻塞;每一次 push 新版本都形成一个补丁集,审查者可以清楚看到新旧版本的差异,而不是整个变更重新看一遍。这种设计在大型项目里非常省时间。
Gerrit 另一个常被人忽略的地方是它与 CI 的耦合方式。它提供了 Verified 标签,专门留给自动验证工具使用。Jenkins 等工具可以在后台轮询或通过事件触发构建,然后把结果写回到对应的 Change 上。很多团队用 Gerrit 之后,连“要不要等 CI 通过再合并”这类容易扯皮的问题,都直接靠标签规则落地了,少了很多口水战。
1.3 整体架构:前端、后端与存储
Gerrit 本质上是一个 Java Web 应用,用 Jetty 或者内嵌的 HTTP 服务来跑。代码存储在文件系统的 Git 仓库目录中;元数据(用户、群组、权限、Change 记录)一部分存在仓库的refs/meta/config分支里,一部分存在底层数据库里(默认 H2 或 MariaDB/PostgreSQL)。它支持 SSH 和 HTTP/HTTPS 两种 Git 访问方式,也可以配反向代理对外提供服务。
这套架构的好处是部署相对简单:一台中小型的服务器就可以支撑一个几十人的团队;坏处是高可用和集群方案不如现代托管平台那么自动化。如果团队超过百人,或者对可靠性要求比较苛刻,你可能需要把仓库文件放共享存储,并在前面挂负载均衡。
2. 核心细节解析与实操要点
Gerrit 的安装配置本身并不难,难的是理解它的对象模型。很多新手卡住,不是因为命令敲错,而是因为脑子里的模型还停留在 GitHub 的 PR 上。所以这一节先讲透核心概念,再给实际初始化和配置命令。
2.1 核心对象:Change 和 Patch Set
当开发者执行git push origin HEAD:refs/for/master时,Gerrit 会收到一个提交,但不会把它直接放到refs/heads/master上,而是把提交存到一个内部空间里,并生成一个整数编号的 Change。打个比方,Change 相当于一次“评审会话”,而每次你修改后又重新 push 的版本叫 Patch Set,相当于这次会话的第 1 版、第 2 版。
这里有三个细节容易搞混:
第一,同一个 Change 之所以能识别出是“同一件事”,靠的是提交信息里的Change-Id行。如果你改动提交信息时把这个行弄丢了,Gerrit 就会认为你提交的是一个全新的 Change,那你就得从头再走一遍评审流程,前面攒下的评论和分数全部作废。
第二,每个 Patch Set 代表一次完整的提交快照,审查者可以在 Gerrit Web 界面里对比任意两个 Patch Set 之间的差异。老的 Patch Set 不会消失,方便回溯。
第三,Change 的状态包括 New、Merged、Abandoned 等。如果中途你重新拉新分支推了一版,原 Change 可以选择 Abandon 掉,避免历史里堆一堆死编号。
2.2 refs/for 与 refs/heads 的区别
refs/for/master是 Gerrit 工作流里最重要的魔法引用。它表示“我不是要把提交推送到 master 这个分支,而是要提交给 master 分支的评审队列”。Gerrit 收到这个 ref 后,会先校验提交信息,然后走权限校验,最后生成或更新一个 Change。
对应地,refs/heads/*是真实分支的命名空间。只有拥有 Submit 权限的人,才会通过 Gerrit 界面把 Change 合入refs/heads/master。平时开发者是不应该直接 pushrefs/heads/master的,除非权限里允许直接推送。
实际使用中,我见过有人图省事,直接 push 到 refs/heads 绕过审查,结果全局的 Push 权限又没锁死。这种属于权限配置事故,后面我会在权限部分专门讲怎么看规则。
2.3 认证方式选择与初始配置
Gerrit 的认证方式在$GERRIT_SITE/etc/gerrit.config里配置。常见的有 OpenID、LDAP、OAuth 和 HTTP。对内部团队来说,LDAP 和 HTTP Header 认证最常用。如果只是本地试验,可以直接用默认的 OpenID 模式,并在etc/secure.config里临时打开auth.gitBasicAuth,方便用账号密码做 Git 操作。
初始化一个 Gerrit 站点的命令通常长这样:
java -jar gerrit.war init --batch -d review_site然后把 Git 仓库目录、监听端口、数据库连接都按实际环境调整好。这里我提醒一句:init过程中它会问很多问题,包括是否使用 H2 默认库、是否启用 SSH 端口、是否要配置 SMTP。不要全按默认一路回车,SMTP 对 Gerrit 很重要,因为有些功能(比如注册、密码找回、评审邮件通知)需要邮件服务。如果一开始没配,后面可以改etc/gerrit.config再重启,但不如一次性配清楚。
初始化完成后,默认是没有任何项目和用户的。此时你可以用管理员权限创建一个名为All-Projects的父项目,它会作为所有其他项目的权限继承根节点。实际上init会自动建出All-Projects和All-Users两个项目,但业务项目的创建还是需要手动做。
3. 实操过程与核心环节实现
进入真正的实操环节。从安装完成到项目正式跑起来,我按“管理员视角”和“开发者视角”两条线来讲。管理员负责建项目、配权限、接 CI;开发者负责日常推代码、更新补丁、处理冲突。
3.1 创建项目与初始权限配置
在 Gerrit Web 界面里,进入 Browse -> Projects -> Create New Project。项目名建议用组名/模块名的格式,例如backend/user-service,这样权限继承和分组管理都清晰。
创建项目时必须注意:
- 选择继承项目时,如果没有特别需求,继承
All-Projects即可。 - 如果你的项目需要把稳定分支保护起来,可以在创建时就选
Create initial empty commit。这个空提交的存在很有用,可以避免某些 Git 工具处理无 HEAD 仓库时出问题。
项目创建后,如果开发者以 SSH 方式访问,形式是这样的:
git clone ssh://用户名@服务器:端口/项目名比如我本地常用的是:
git clone ssh://admin@192.168.1.100:29418/backend/user-service注意这里端口不是常用的 22,而是 Gerrit 专属 SSH 端口,默认 29418。如果你用 HTTP,则直接是http://服务器:8080/项目名。
权限配置的核心入口在项目设置页的 Access 选项卡里。Gerrit 的权限规则是基于 ref 名字来限定的。比如我们要让backend/user-service项目下的所有普通开发者都能推送评审,但不能直接合入,可以这样建:
- 在
refs/heads/*上给Registered Users添加Push权限,值为refs/for/*。 - 给
Project Owners添加Code-Review +2和Submit权限。 Verified标签权限默认只给 CI 机器人。
实际配置界面里选权限名、组名、引用名,确实有点像发牌照。你可以把规则理解成白名单,所有操作默认都是拒绝的,加上一条规则才放通一类操作。
这里有一个容易被坑的地方:Gerrit 的权限是从父项目继承的。如果你发现某个人明明没在子项目里配置权限,却可以执行某项操作,大概率是All-Projects里放宽了。反过来,如果你想给某个项目单独收紧权限,也要小心继承来的规则可能反而更宽。
3.2 开发者提交代码:从 clone 到 push review
一个标准的 Gerrit 提交流程可以拆成下面几步:
- clone 仓库。
- 安装 commit-msg 钩子,这个钩子会自动为提交信息加 Change-Id。
- 新建特性分支并在分支上提交。
- push 到
refs/for/<目标分支>。 - 在 Gerrit 网页上查看 Change。
- 根据评审意见修改后 amend 提交,再次 push。
- 评审通过后合入。
第一步和第二部通常可以合并。克隆后执行一次:
gitdir=$(git rev-parse --git-dir) curl -o ${gitdir}/hooks/commit-msg http://服务器:8080/tools/hooks/commit-msg chmod +x ${gitdir}/hooks/commit-msg跳过钩子会导致提交信息里没有Change-Id,Gerrit 会直接拒绝这次推送。错误信息是:
! [remote rejected] HEAD -> refs/for/master (missing Change-Id in commit message)这是最常见的第一个坑,我建议第一次用 Gerrit 的团队,每个人都认一眼这个报错,因为后面还会遇到很多类似“格式合法但语义不满足”的拒绝。
接下来创建特性分支并提交:
git checkout -b feature/payment-timeout git add . git commit -m "fix(payment): adjust timeout logic"这里的习惯是只要本地提交一次就够了,不要养成频繁git commit --amend之外再叠一堆小提交的毛病,因为 Gerrit 的 Change 是围绕一个提交设计的。你如果连续做多个本地提交再推,Gerrit 会分别生成多个 Change,反而不利于一次评审一个完整功能。
推送命令:
git push origin HEAD:refs/for/master如果你想让评审人去推而不是用自身的信息,也可以在 push 时带一个 topic 组:
git push origin HEAD:refs/for/master%topic=payment-timeout这个%后面还可以跟很多参数,比如wip表示工作进展,notify=NONE表示不发邮件通知。但一般情况下只推荐用 topic,其他参数新手期少用。
如果你本地有多个提交要合到同一个 Change 里,通常需要先git rebase -i把它们揉成一个,再 amend。Gerrit 界面里也有一个 “Squash” 功能,但不同版本的实现程度不一样,我习惯在本地处理干净再推上来。
评审意见回来后,修改代码后重新推送时,不要新增一个fix review提交,而是用:
git add . git commit --amend git push origin HEAD:refs/for/master这样推送后,原 Change 会新增一个 Patch Set,并且 Web 界面上的评论历史还在。千万千万不要在新提交基础上直接再 push,那样 Gerrit 会为一个小意见开出一个新 Change,评审人会抓狂。
3.3 解决合并冲突的正确姿势
多人在同一个分支上工作时,冲突几乎避不开。Gerrit 检测到 Change 与目标分支的最新提交冲突时,Change 页面上会提示“Cannot Merge”。这时候有两条路:
第一条是点界面上的 Rebase 按钮,让 Gerrit 自动尝试 rebase 到目标分支。如果 Gerrit 自己能解决,它会生成一个新的 Patch Set,审查流程继续。
第二条是手动在本地处理:
git fetch origin git rebase origin/master # 手动解决冲突 git add . git commit --amend --no-edit git push origin HEAD:refs/for/master这里必须注意:如果当前 Change 里有多个 Patch Set,你手动 rebase 后的提交仍然是同一个Change-Id,所以 Gerrit 会把新版本追加为最新的 Patch Set,不会变成一个新的 Change。
另一种常见的处理方式,是去 Gerrit 网页上复制git fetch命令。比如 Change 编号是 12345,Patch Set 是 4,那么可以执行:
git fetch origin refs/changes/45/12345/4 && git cherry-pick FETCH_HEAD这种 fetch 出来的引用放在refs/changes命名空间里,专门用来手工拉取一个具体的补丁集。实际用中它还有一个好处:当你手里没有本地中间版本时,可以直接把网上的某个 Patch Set 拉到本地来做修改。
3.4 评审与合入:审查者的操作
管理员把权限配好之后,开发者的提交会出现在项目页面的 “Open Changes” 列表里。审查者点进 Change 后,可以按行评论、回复评论、给分数。分数标签常见的是:
Code-Review:人工审查。范围从 -2 到 +2,+2 表示明确认可。Verified:由 CI 或测试人员给,范围 -1 到 +1,+1 表示已验证通过。
要合入一个 Change,通常需要得到 +2 和 +1,然后有 Submit 权限的人点击 Submit。Submit 不是开一个计时器等通知,而是真正的 merge 操作。Gerrit 会根据项目的合并策略把提交合到目标分支。
这里我见过很多团队把 Submit 权限只给少数维护者,代码质量确实控制得住,但流程也可能变得很慢。建议至少给每个子团队两名负责人 Submit 权限,避免因为一个人出差导致整个项目合并停摆。
再提一个细节:如果 Change 被 Submit 后发现有问题,Gerrit 不会回滚。你需要直接通过一个新的 Change 来修复。所以团队里最好约定,那些“先合一个紧急修复”的请求,至少要让一个人快速 review 而不是直接绕过流程。
4. 常见问题与排查技巧实录
这一部分是我最想写的,因为 Gerrit 的报错信息虽然还算清楚,但它们背后的原因往往不是表面那么简单。整理了一个速查表,再挑几个高频问题展开讲。
| 现象 | 常见原因 | 处理建议 |
|---|---|---|
| push 被拒,提示 missing Change-Id | 没装 commit-msg 钩子 | 安装钩子后 amend 提交信息 |
| push 被拒,提示 no new changes | 提交内容和上次完全一样 | amend 后重新修改,或换新分支 |
| push 被拒,提示 branch not found | 目标分支不存在 | 确认项目分支名,权限里放开该 ref |
| change 一直处于 merge conflict | 与目标分支最新提交冲突 | 界面 Rebase 按钮或本地 rebase 后重推 |
| 找不到某个 Change 的编号 | 用的老 Patch Set 编号看错了 | 到网页的 Changes 列表里按摘要搜索 |
| 某个开发者能直接推 refs/heads | 权限里给了全量 Push | 给普通用户只开放 refs/for/* |
| 通过 HTTP 推送提示无法认证 | 没启用 gitBasicAuth | 在 secure.config 配置或改用 SSH |
4.1 为什么明明改了代码,push 还是被拒
很多人经常遇到一种情况:我git commit --amend改完了,重新 push,结果系统还是报 “no new changes”。原因通常是:你的 amend 实际上什么都没改,或者改的内容和上一个 Patch Set 完全一模一样。Gerrit 认为既然代码一模一样,那就不算新补丁,自然拒绝。
解决办法很朴素:改完代码后先本地检查一下git diff和提交内容,确认确有差异再推。这不是什么高深的坑,但团队里至少一半人踩过。
还有另一种情况,你改了代码但提交信息没更新,而 Gerrit 的提交内容只认提交哈希,如果哈希没变,它就觉得你没有提交新版本。注意,--amend之后哈希一定变化,但如果你是用git commit --fixup之类的操作没揉干净,也可能出现提交树上多了东西,实际上 Push 的还是旧状态。
4.2 权限配置后没有生效
Gerrit 权限的继承和优先级规则比较绕。默认情况下,子项目的精确 ref 规则会覆盖父项目的同名规则,但你如果一不小心把All-Projects里的某个组的权限加得太宽,子项目再怎么收紧也可能被父项目放开。
有一次我给某个项目设了普通用户只能通过refs/for/*推送,结果测试时发现开发者还是直接 push 到了refs/heads/master。查了一圈,发现All-Projects里有一条refs/heads/*的 Push 权限给了Registered Users,而且优先级是BLOCK还是ALLOW我又没注意。其实父子项目的规则不是简单的“谁更具体听谁的”,还要看规则类型。
排查时打开项目的 Access 页面,有个 “Effective Access” 预览功能,能看到某个用户/组在该项目下的最终权限集合。先把这条链路看明白,再动手改规则,能省很多时间。
4.3 用 SSH 操作时频繁要求输密码
Gerrit 最常见的 SSH 连接方式是密钥认证,但很多第一次用的人只把公钥粘到了用户设置页,却没有检查本机私钥是否加载到 ssh-agent,导致每次执行git push都要输一遍密码。尤其是像 Windows 的多用户环境,密钥权限和 agent 服务问题都会造成认证失败。
建议这样处理:
ssh-keygen -t ed25519 -C "你的邮箱" -f ~/.ssh/id_ed25519 cat ~/.ssh/id_ed25519.pub把公钥内容复制到 Gerrit 的 SSH Keys 列表里,然后本地测试:
ssh -p 29418 用户名@服务器如果连接成功回显有欢迎信息,那就说明 key 没问题。如果出现Permission denied (publickey),先去查 agent 是否加载了私钥:
ssh-add -l另外需要注意 Gerrit SSH 的端口不是默认 22,你需要在 clone 地址里显式写端口,或者在~/.ssh/config里给这个 Host 配置端口,否则会一直超时。
4.4 大量历史提交需要补充 Change-Id
团队迁移到 Gerrit 时,经常会碰到一批存量分支,它们本地的提交信息里都没有 Change-Id。如果你直接把整批分支推到refs/for上,就会整批被拒。
Gerrit 提供两种办法。一种是每个提交手动 amend 一行Change-Id,几十个的话很痛苦。另一种是用消息重写的方式:
git filter-branch --msg-filter 'cat; echo; echo "Change-Id: Ixxxx"' -- --all不过更推荐的是用 Gerrit 自带的gerrit-cherry-pick脚本,或者把旧分支先推成一个临时分支,再逐条 cherry-pick。反正不管哪种方式,重点都是让每个提交都带一个稳定的 Change-Id。
实际操作里,我建议在仓库初始化阶段就直接给所有开发者统一推 commit-msg 钩子的脚本,否则每次新人进来都要手工装一遍,漏装一次就是一条报错。
4.5 与 Jenkins 集成时 Verified 标签不刷新
很多团队把 Gerrit 和 Jenkins 接起来做自动构建。Jenkins 通过事件触发构建后,会把结果写回 Change,但如果 Jenkins 的账户在 Gerrit 里分配的权限不对,或者 Job 配置的 ref 监听有问题,那么 Change 页面上的 Verified 标签一直是空的。
排查思路分三步:
第一,确认 Jenkins 对应的 Gerrit 账户有Verified标签的+1权限。
第二,确认 Jenkins 的 webhook 或事件监听配置正确。Gerrit 插件常用的是com.googlesource.gerrit.plugins.events或者直接通过 REST API 查询,不同版本方案不同。
第三,如果发现 Jenkins 显示构建成功,但 Gerrit 的 Verified 一直是 0,很可能是 Jenkins 写回时用的 Change 编号不对。最稳定的做法是把 Change 号、Patch Set 号都从 Gerrit 的 Trigger 触发参数里读,不要自己拼。
5. 深度使用建议与体验总结
写完上面的流程,我觉得有必要再聊聊这个工具的适用边界和长期使用体会。
在团队规模小、节奏特别快、且大家都能自觉遵守规范的时候,Gerrit 确实显得“重”,因为它强制每个 commit 都走评审。但在稍微正式一点的项目里,哪怕只有两三个协作方,严格评审带来的收益都会迅速超过流程摩擦成本。尤其是当你要为外部贡献者开放提交通道时,Gerrit 可以做到别人只能推送refs/for/*,连分支名都不能乱建,这种可控性对项目维护者来说是很踏实的。
我个人的实操心得还有几条:
第一,不要把 Gerrit 的 Change 当 GitHub PR 来用。Gerrit 的粒度是“一个提交”,不是“一个分支”。你准备把它推成一次评审时,提交信息里就承载着完整改动说明,所以写清楚 commit message,比在网页上补充长篇描述更重要。
第二,善用 Dashboard。Gerrit 的 Web 界面里为每个用户提供了动态 Dashboard,可以看到别人指派给你的待办评审、你自己参与的 Open Changes 等。我平时上班打开第一件事就是刷一遍我的 Dashboard,比自己记住哪些 Change 待处理要高效得多。
第三,分支和权限规则尽量保持简单。曾经有一段时间我为了精细管控,给每个模块单独建一个权限规则,结果后来连管理员看着都头大。Gerrit 的权限系统灵活度很高,但很多场景下,一个“主干全保护 + 功能分支随意 + refs/for 推流”的粗粒度配置就足够用了。
最后再分享一个小技巧:如果要批量拉取并检查多个 Change,可以用git review这个第三方命令行工具。它是对 Gerrit 工作流的封装,通过简单的git review -d 12345就能把远程某个 Change 拉下来并切换到对应提交,省去手拼 refs/changes 路径的麻烦。当然,前提是你已经理解了底层机制,否则遇到报错还是得绕回原生 Git 命令来排查。
Gerrit 的学习曲线确实比 GitHub 陡,但这个陡峭的过程恰恰会逼着团队把代码审查本身变成一项正式活动。只要团队愿意投入几天时间去适应这种“先评审后合入”的节奏,你会发现它带来的收益远超付出的成本。