1. 先搞清楚:Gerrit 到底解决了什么问题
先抛个问题:团队用 Git 做代码管理,代码提交权限直接给到所有人,会出现什么情况?
最典型的一个场景:早上九点半,后端小哥往 master 分支推了一版“还差一点就写完”的代码,中午前端拉代码开始联调,结果编译直接挂了。再比如,有人在本地把历史提交改了个底朝天,强推上去之后,全组人的本地分支乱成一锅粥。这些破事我经历过太多次,后来团队引入 Gerrit,才算把节奏稳住。
Gerrit 本质上是一套基于 Git 的代码评审系统。它不替代 Git,而是架在 Git 之上,给代码提交加了一道“人工审核+自动校验”的关卡。开发者的代码不直接进仓库,而是先推送到 Gerrit 的评审分支上,生成一个“待评审变更(Change)”,经过编译检查、静态扫描、同事评审、打分确认之后,才允许合入真正的目标分支。
Git 本身没有一个内置的“代码评审流程”概念。Git 只知道你要把代码从 A 分支合并到 B 分支,至于这个合并是经过了三个人评审还是一键强推,它完全不管。Gerrit 做的事情,就是在 Git 的 push 和 merge 之间插进一个流程化的审核环节。
这套机制非常适合多人协作的中型团队。一个人写代码,另一个人直接改你的提交?在 Gerrit 里不行,你只能提评论、提建议,然后由原作者修改后重新推一个新版本,也就是 PatchSet。这样做的好处是提交历史干净可控,每一行合并进主干代码都有人把关。
如果你是刚接触 Gerrit 的开发者,或者团队准备从纯 Git 工作流切换过来,这篇小结能帮你少踩不少坑。我会从核心概念、日常操作、权限配置、问题排查这几个维度,把实际用下来的经验梳理一遍。
2. Gerrit 核心概念与代码评审流程拆解
2.1 Change、PatchSet、Reviewer:这三个词先搞明白
Gerrit 里的“Change”不等同于 Git 里的“Commit”。Change 是 Gerrit 自己定义的一个评审单元,通常代表一次完整的功能改动。你本地可能提交了 3 个 commit,推到 Gerrit 后,如果三者属于同一条改动线,会被合并成一个 Change。
PatchSet 则代表 Change 的一个版本。你第一次提交,是 PatchSet 1;评审人提了意见,你修改代码后再推一次,是 PatchSet 2。每一次推送,Gerrit 都会给这个 Change 生成新的 PatchSet,历史版本不会消失,随时可以对比,也可以回退到任何一版。
Reviewer 就是评审人。被指定为 Reviewer 的人会收到通知,需要对 Change 给出 Code-Review 打分。Gerrit 的打分体系不是简单的“同意/不同意”,而是有明确的分数层级。最常用的是 +2 和 -2,这两个分数可以阻塞合入。+1、-1 属于建议性质,不直接阻止合并,但会留下评审记录。
注意:在 Gerrit 的默认配置里,只有拥有 Verified +1 权限的账号(通常是 CI 机器人或者有构建权限的人)才能给验证分,普通开发者的 +1 不能保证代码合入。
我见过不少刚接触 Gerrit 的同事,把 GitHub 的 Pull Request 思维方式带进来,以为给个 +1 就完事了。实际上在 Gerrit 里,合入 Change 需要同时满足两个条件:
- 至少一个 Code-Review +2
- Verified +1(CI 编译构建通过)
这两个条件缺一不可。如果团队的 CI 系统还没接入 Gerrit,那 Verified 这一关就需要管理员手动处理,非常麻烦。所以很多团队搭建 Gerrit 的第一件事,就是先把 Jenkins 或者 GitLab CI 的集成做起来。
2.2 Gerrit 的工作流程:从本地提交到代码合入
用 Gerrit 之后,日常开发的节奏和用 GitHub 不完全一样。在 GitHub 上,你 fork 仓库、改代码、提交、发起 PR,维护者合并。在 Gerrit 上,流程更紧凑,整个操作都围绕同一个远程仓库进行。
下面这个流程是我在实际项目中跑了很多遍的:
- 本地新建分支,基于最新的远程主干分支开发
- 本地 commit,写好 commit message
- 用
git push origin HEAD:refs/for/master推送评审 - 在 Gerrit 页面上确认 Change 生成,指定 Reviewer
- 根据评审意见修改代码,本地追加一个 commit 或者 amend
- 再次推送同一条 refs/for/master,系统自动生成新的 PatchSet
- 评审通过、CI 通过后,Change 合入 master 分支
第 6 步值得多说两句。很多第一次用 Gerrit 的人会在这里犯一个经典错误:修改完代码之后,不在原来的分支上继续提交,而是新建一个分支重新推送,结果 Gerrit 页面上冒出了一堆重复的 Change,评审人也懵了。
正确的做法是:在同一个本地分支上,用git commit --amend修改上一次提交,或者再追加一个fixup提交,然后重新推送到同一个 refs/for 目标上。Gerrit 是靠 Change-Id 来识别是不是同一个 Change 的,只要你本地的 commit message 里带上了相同的 Change-Id,推送再多次,系统都只会更新已有的 Change,而不是新建。
那么这个 Change-Id 是从哪来的?答案在 commit-msg hook 里。Gerrit 服务器会提供一个 commit-msg 脚本,开发者需要把它放到本地仓库的 .git/hooks/ 目录下。这样每次 commit 的时候,Git 会自动执行脚本,给 commit message 追加上一行 Change-Id,保证一条改动线始终对应同一个 Change。
2.3 为什么选择 Gerrit 而不是继续用 Merge Request
我听到过很多类似的质疑:“我们已经用了 GitLab 的 Merge Request,有必要再搞一套 Gerrit 吗?”
GitLab MR 和 Gerrit 的评审理念有一个重要差别:MR 把分支作为评审单元,而 Gerrit 把 Change 作为评审单元。MR 模式下,一个 MR 可能包含几十个 commit,评审人看的时候只能看整体 diff,很难做到逐行把关。Gerrit 把每一次推送的版本差异都独立展示,一个 Change 对应一个明确的改动目标,commit 粒度更细,diff 更聚焦。
再有一个差异是对提交历史的控制力。GitLab MR 合并之后,合入的 commit 是开发者在本地已经生成好的。Gerrit 合入 Change 时,默认会做一次“重新提交”,把当前 PatchSet 的 diff 直接应用到目标分支的最新提交上,生成一个干净的落库提交。开发者本地的 commit 历史并不原样进主干,这从机制上保证了主干提交历史是一条平滑的直线,不会被乱七八糟的中间提交弄乱。
当然,Gerrit 的上手门槛比 GitLab MR 高,这一点我不否认。refs/for 这种推送方式、Change-Id 的概念、+2/+1 的评分体系,都需要一段适应期。但一旦团队跑顺了,代码质量的可控性确实比 MR 模式更严格。尤其是对于需要做代码走查、需要满足审计要求的项目,Gerrit 在流程完整性上更有优势。
3. Gerrit 安装部署与基础配置实操
3.1 部署方式和版本选择
Gerrit 的部署方式灵活,可以单机跑,也可以容器化。对于小型团队(50 人以内),一台 4 核 8G 的云主机就够用了,不必一开始就上高配。
版本选择上,建议直接上最新的稳定版。Gerrit 的老版本之间有比较大的配置项差异,特别是数据库、认证方式、权限模型这三大块,老版本升级到新版本往往要处理数据迁移,比较麻烦。官网的 releases 页面会标明每个版本的兼容性,按需选一个看着顺眼的稳定版即可。
单机部署的核心步骤是:
- 准备 Java 环境(Gerrit 是基于 Java 的,要求 JDK 11 或 17)
- 下载 Gerrit 的 WAR 包
- 执行初始化向导
- 配置反向代理(Nginx 或者 Apache)
- 创建管理员账号
初始化向导是交互式的,会依次问你数据库类型(默认 H2 就可以)、认证方式(推荐 HTTP/LDAP)、邮箱发送配置等。全部回答完,Gerrit 的基础实例就能跑起来。
3.2 认证方式的选择:从 HTTP 到 LDAP
认证方式这一块,小团队可以直接用 HTTP 认证,也就是在 Gerrit 前面挂一层 Nginx,由 Nginx 做 Basic Auth 账号密码验证。这种方式简单直接,不需要额外引入用户管理系统,适合 10 人以内的小规模场景。
团队规模上来之后,建议切换到 LDAP 认证。Gerrit 可以直接对接 OpenLDAP 或者 Active Directory,用户在部门系统里建好,Gerrit 这边自动识别。好处很明显:不用每个系统维护一套账号密码,员工离职时只用在 LDAP 里禁用账号,Gerrit 权限立刻失效。
LDAP 配置的关键参数在 etc/gerrit.config 文件里:
[auth] type = LDAP gitBasicAuth = true [ldap] server = ldap://your-ldap-server:389 accountBase = ou=People,dc=company,dc=com accountPattern = (uid=${username}) groupBase = ou=Group,dc=company,dc=com groupPattern = (cn=${groupname})配置完成后,重启 Gerrit 服务生效。如果 LDAP 服务器地址变了或者账号结构调整了,记得同步更新这些参数。我见过有人改完配置忘了重启,排查了半天一直连不上。
3.3 反向代理配置:以 Nginx 为例
Gerrit 建议始终通过反向代理对外提供服务,不直接暴露 8080 端口。Nginx 配置里需要注意两个关键参数:
server { listen 80; server_name gerrit.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } }X-Forwarded-Proto这个请求头很关键,没配的话,Gerrit 拿到请求后以为自己还在 HTTP 裸奔,生成的页面链接可能会是 http 而不是 https,导致浏览器直接报不安全。配了 HTTPS 终结在 Nginx 这一层之后,Gerrit 本身不需要开启 SSL,所有加解密都交给 Nginx 处理,Gerrit 专注自己的业务就行。
3.4 仓库初始化与权限配置基线
Gerrit 安装好后,需要在 All-Projects 项目下创建新的仓库。创建仓库有两种方式:
- 通过 Web 界面操作
- 通过 SSH 命令创建
SSH 命令创建的方式更干净:
ssh -p 29418 admin@gerrit.example.com gerrit create-project your-project-name --branch master创建好仓库之后,要给仓库配置访问权限。Gerrit 的权限模型比较细,按 ref 分组,可以精确控制哪些人能推代码、哪些人能打标签、哪些人能合入。
一个实用的基线权限配置包括三组:
| 权限组 | 权限范围 | 用途 |
|---|---|---|
| 普通开发者 | refs/for/* 推送权限 | 提交代码评审 |
| 评审人(Reviewer) | refs/* 的 Code-Review +2 权限 | 代码评审打分 |
| CI 账号 | refs/* 的 Verified +1 权限 | 自动构建验证 |
这里有一个新手容易踩的坑:如果你给某个用户直接配了 refs/heads/* 的 Push 权限,那么他可以绕过评审流程直接往分支里推代码。Gerrit 设计上,评审流走的推送目标是 refs/for/,直接推送 refs/heads/是管理员级别的操作。权限分配的时候切记不要混淆这两者。
4. 日常使用全流程实操记录
4.1 克隆仓库并配置 commit-msg hook
新成员入职之后,第一步是配置 Git 环境。克隆仓库的操作和普通 Git 一样:
git clone ssh://username@gerrit.example.com:29418/your-project克隆之后,需要把 Gerrit 的 commit-msg hook 安装到本地,否则提交的时候不会自动生成 Change-Id。安装 hook 的最新方式是使用 scp 命令:
scp -p -P 29418 username@gerrit.example.com:hooks/commit-msg .git/hooks/执行完毕后检查一下ls .git/hooks/,确认 commit-msg 文件存在即可。如果大家用的 Gerrit 版本比较新,也支持通过 HTTP 直接下载 hook 文件,原理一样。
如果你跳过了这一步,本地 commit 的 message 里就没有 Change-Id,推送到 refs/for/master 之后有两种可能:要么被 Gerrit 拒绝并提示“missing Change-Id”,要么直接报错。这个坑几乎每个新人都踩过,所以我来来回回强调了很多遍。
4.2 推代码评审:refs/for 的正确姿势
日常开发中最常见的推送命令是下面这条:
git push origin HEAD:refs/for/master这里的HEAD表示当前分支的最新提交。refs/for/master是固定的命名规范,意思是“为本地的 HEAD 提交创建一条目标为 master 分支的评审请求”。Gerrit 收到之后,会计算当前提交和 master 之间的差异,生成一个 Change。
有几个变体命令也在实际工作中非常常用。比如你要推送时给评审人留一条说明,可以用:
git push origin HEAD:refs/for/master%topic=batch-import%topic给 Change 加了一个标签,多个相关的 Change 会按 topic 分组,方便管理。这个功能在处理多 Change 依赖关系时特别有用。
还有一个小技巧:如果你想让某个用户直接成为 Reviewer,可以在推送时指定:
git push origin HEAD:refs/for/master%r=reviewer@email.com不过这个功能需要一定的权限配置,普通用户可能没有指定 Reviewer 的资格,具体的权限在 Access 策略里控制。
4.3 修改代码后如何生成新的 PatchSet
评审人提了意见之后,你需要在本地做修改。这里分两种常见情况。
情况一:修改量小,追加到现有提交上。用 amend 操作:
git add modified-file.c git commit --amend # 修改 commit message 或者保持原样 git push origin HEAD:refs/for/masterGerrit 看到 Change-Id 和已存在的 Change 一样,就会自动更新为 PatchSet 2,不会产生新的 Change。
情况二:评审过程中目标分支有了新提交,需要先 rebase 再推送。标准操作是:
git rebase origin/master # 处理冲突 git push origin HEAD:refs/for/masterrebase 之后如果产生冲突,Git 会停下来让你手动解决。解决完记得git add再git rebase --continue,直到 rebase 结束再推送。
有同事问过我,为什么不直接 merge origin/master 再推送。在 Gerrit 流程里,rebase 会保持提交历史线性,merge 会产生合并提交节点。评审人看 diff 的时候,rebase 之后的 diff 更清晰,因为引入的只是你的修改,而不是合并的一大堆中间提交。所以我的建议是默认用 rebase。
4.4 在 Gerrit Web 页面做评审
评审人登录 Gerrit 后,打开 Change,页面分几个区域:Commit message、Diff 面板、评论区域、打分按钮、操作按钮。
Diff 面板默认展示当前 PatchSet 和前一个 PatchSet 的差异。如果你想看这个 Change 相比目标分支的全部改动,可以切换 diff 模式。左侧是旧代码,右侧是新代码,改动行会有高亮。点开某一行,可以在行内评论,这是代码走查最核心的交互。
评论之后,点击“Reply”按钮,可以填写总评意见,同时打分。打分区间是 -2 到 +2:
- +2:认可,允许合入
- +1:基本认可,有小问题可以改
- -1:有问题需要修改
- -2:有严重问题,禁止合入
打 -2 的时候系统会要求你说明理由,这个操作会阻塞 Change,任何管理员需要额外权限才能强行合入。
4.5 Change 合入方式:Submit 的几种策略
评审通过之后,需要点击页面上的“Submit”按钮把 Change 合入分支。Gerrit 默认的合入方式是“Merge Always”,但在项目设置里可以通过配置修改。
我重点说一下两种常见的 Submit 策略:
- Fast Forward Only:只有当前 PatchSet 的父提交恰好在目标分支顶部时才能合入。意思就是必须严格线性,不允许创建合并提交。如果目标分支上已有新的提交,你需要先 rebase。
- Merge Always:总是创建一个合并提交,即使当前 PatchSet 已经是最新的,也会额外生成一个 merge commit。这种方式适合希望保留完整合并痕迹的团队。
对于大多数追求主干清爽的团队,Fast Forward Only 是比较好的选择。配合“Change 合入前必须 rebase”的规范,主干历史会是一条干净的直线。
重要提醒:Submit 操作一旦执行,Change 就合入了目标分支。不可以通过 Gerrit 直接把已合入的 Change 撤销。如果你发现合入的代码有问题,正确的做法是创建一个新的 Change 去回滚,而不是把已合入的 Change 打回重来。
5. 权限体系配置详解
5.1 看懂 Gerrit 的权限模型
Gerrit 的权限模型核心是“项目-引用-动作”三层结构。每个项目的 Access 页面下,可以针对不同的 ref(比如 refs/heads/master、refs/heads/* 等)配置不同的权限组。
常用权限动作包括:
- Code-Review:评审打分权限
- Verified:验证打分权限
- Push:直接推送权限
- Submit:合入 Change 的权限
- Read:读取代码的权限
- Create Reference:创建分支或标签的权限
这些权限可以分配给具体的用户组,而不是单个用户。管理上更建议按角色建组,比如 Developers、QA、Maintainers,然后项目里按组分配权限。
5.2 三个必备权限组的配置建议
我维护过多个 Gerrit 项目,经过反复调整,总结出下面这套基线配置:
| 用户组 | refs/for/* | refs/heads/* | refs/tags/* |
|---|---|---|---|
| Developers | PUSH,创建 Change | 只读 | 只读 |
| Reviewers | PUSH + Code-Review +1/+2 | 只读 | 只读 |
| Maintainers | PUSH + Submit | PUSH + Submit | PUSH + Create Tag |
Developers 组只拥有 refs/for/* 的推送权限,不能直接推 head or tag,这样从机制上保证了所有代码改动都必须经过评审流程。
Reviewers 组在 Developers 基础上,增加了 Code-Review +2 的权限。+2 是一个很重要的门槛,应该只给那些对项目足够熟悉、评审经验足够丰富的人。
Maintainers 组保留了直接推分支和打标签的权限,一般用于紧急热修复等特殊情况。生产环境建议把 Maintainers 的人数控制在两三个人以内。
5.3 给 CI 账号配置 Verified 权限
Verified 分数是合入 Change 的必要条件之一。大部分团队会把这个分数交给 CI 系统,让编译构建通过后自动打 +1。
在配置权限时,需要为 CI 账号单独建一个组,然后在项目的 Access 页面里,给 refs/heads/* 设置 Verified +1 权限。注意不要给 CI 账号分配 Code-Review 权限,否则 CI 机器人也可以给代码评分了,这不符合人机分离的原则。
如果 CI 暂时没接入,也可以由管理员手动给 Change 打 Verified +1,但这样会增加管理员的工作量,而且容易形成瓶颈。我见过一些早期团队,代码评审通过后卡在 Verified 上,等管理员手动验证。所以如果你要在团队里正式推行 Gerrit,把 CI 集成放在部署清单的第二优先级,仅次于用户认证。
5.4 权限变更不生效怎么办
权限配置改了,但用户行为没有变化,这种情况大多是缓存问题。Gerrit 有权限缓存机制,修改权限后需要等待一段时间才能生效,不是立刻刷新。
想立即生效的话,重启 Gerrit 服务是最粗暴但有效的办法。如果不想重启,可以用命令刷新缓存:
ssh -p 29418 admin@gerrit.example.com gerrit flush-caches正常情况几秒钟就完成,然后权限就按照新配置走了。
6. 常见问题与排查技巧实录
6.1 推送被拒绝:missing Change-Id
这是 Gerrit 新手最常见的报错之一。推送的时候 Gerrit 直接返回“missing Change-Id”,原因就是本地 .git/hooks/ 目录下没有 commit-msg 脚本,或者脚本存在但没有执行权限。
排查步骤:
- 检查
.git/hooks/commit-msg是否存在 - 检查文件是否有执行权限(
ls -l看 x 位) - 检查现有 commit 信息里是否已有 Change-Id 字段
如果改动已经 commit 但没有 Change-Id,不需要担心。用下面的命令可以给最近的提交补上:
git commit --amend --no-edit如果 commit-msg hook 已经正确安装,amend 时就会自动追加 Change-Id。然后重新推送即可。
6.2 推送被拒绝:not permitted by any of the project label
这个报错常见于权限配置不当。意思是当前用户没有权限把代码推到某个 ref 上。
排查方向有两个:
- 检查用户是否被加入正确的权限组
- 检查项目里 refs/for/* 是否配了 Push 权限
如果确认权限都在,但依然报错,再看一下权限是否被某个更高级别的策略覆盖了。Gerrit 的权限是从多个层级继承的,项目的父级项目也会影响权限结果。
6.3 Change 页面显示“Merge Conflict”
原因很简单:当前 PatchSet 和目标分支的最新代码产生了冲突,无法自动合并。解决办法是本地 rebase 到目标分支最新,解决冲突后重新推送。
举个例子:
git fetch origin master git rebase origin/master # 修冲突 git add . git rebase --continue git push origin HEAD:refs/for/master推送之后,当前 Change 会自动生成一个新的 PatchSet,Gerrit 会重新计算差异。
这里有个细节,rebae 过程中可能不止产生一次冲突,要耐心把所有冲突都解完再推送。千万别急着推送半成品,否则评审人会看到一个明显编译不过的版本。
6.4 SSH 密钥配置和常用 SSH 命令
Gerrit 的 SSH 端口通常是 29418,不是 22。很多老 Git 用户第一次用会在这里卡住,以为连不上 Git 服务器。
本地添加 SSH 公钥之后,常见操作命令:
# 创建项目 ssh -p 29418 admin@gerrit.example.com gerrit create-project project-name # 列出所有项目 ssh -p 29418 admin@gerrit.example.com gerrit ls-projects # 刷新缓存 ssh -p 29418 admin@gerrit.example.com gerrit flush-caches如果遇到 SSH 连接超时,先检查本地防火墙是否放行 29418 端口,再看 Gerrit 的 sshd 配置是否启用了该端口。
6.5 常见问题速查表
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 推送报 missing Change-Id | hook 未安装或没执行权限 | 重新安装 commit-msg hook,amend 补提交 |
| 推送报 not permitted | refs/for/* 没配 Push 权限 | 检查项目权限配置 |
| Change 无法 Submit | 缺少 +2 或者 Verified | 联系评审人打分,检查 CI 状态 |
| Change 显示 Merge Conflict | 目标分支有新提交冲突 | rebase 目标分支,解决冲突后重新推送 |
| 登录页面一直跳转 | Nginx 的代理配置问题 | 检查 X-Forwarded-Proto 是否配置正确 |
| 权限修改不生效 | 权限缓存未刷新 | 执行 gerrit flush-caches 或重启 |
6.6 两个不太容易想到的细节
第一个是关于空目录和文件权限的。Gerrit 的评审页面只显示文件内容的 diff,不显示文件权限的变更。如果一个提交只是修改了文件的执行权限,Gerrit 页面上看起来是“无变化”,但合入时仍然会带上权限变更。这种情况比较隐蔽,建议用本地命令确认:
git show --summary HEAD第二个是关于删除分支的。Gerrit 默认的权限配置下,普通用户不能删除远程分支。这个设计是有意的,防止误操作。如果需要删除已合并的特性分支,需要找 Maintainers 处理,或者在项目里给特定用户组配置 Delete Reference 权限。
7. 实际用过之后的一些体会
这套 Gerrit 流程在我们团队跑了一年多,整体的稳定性和流程严密性确实经受住了考验。相比以前直接用 Git 裸奔,最明显的变化是主干分支的提交质量上来了,代码评审常态化之后,很多潜在问题在合入前就被拦下来了,线上故障要少很多。
如果让我给刚开始用 Gerrit 的团队一个建议,那就是先别急着把权限体系做得太复杂。把基础权限配好,保证所有代码都走评审流程,再加一个 CI 自动验证,这三件事做好,就已经覆盖了 80% 的价值。权限这个东西,后续可以随着团队规模和管理需求慢慢加,一开始整太复杂反而会让团队产生抵触情绪。
最后分享一个小技巧:作为管理员,可以定期用gerrit ls-projects命令导出项目列表,配合数据库里的 Change 数据,统计一下各项目的评审周期和合入率,这些数据对后续优化团队开发流程非常有帮助。我每次做团队效能改进的时候,都会拿这些数据说话,比单纯凭感觉管理靠谱得多。