接手一个模块多、仓库也多的大型项目时,最早能把你逼疯的事情之一,就是“管理几十个Git仓库”。每个仓库有自己的历史、自己的分支、自己的提交,靠纯Git命令一个个去 init、fetch、pull、push,效率极低,还容易漏。后来我接触了 Google 那套经典的 Repo 多仓库管理工具,再配合 Gerrit 做代码审查,整个流程才真正顺起来。
Git-Repo-Gerrit 这三个词经常一起出现,但很多刚接触的人其实分不清它们各自管什么。简单说,Git 是底层的版本控制引擎,Repo 是操作 Git 的批处理调度器,Gerrit 是卡在提交和合入之间的代码审查闸门。三者串起来,就是一条"本地改代码 -> 多仓库统一同步 -> 提交走审查 -> 质量没问题再合入主干"的完整流水线。
这篇文章我会从概念拆解、工作流设计、实操步骤、常见问题排查、团队落地经验五个层面展开。无论你是刚接手大型项目的开发者,还是准备在公司内部搭建代码审查体系的负责人,这篇文章都能直接拿来参考。
1. 先把概念拆开:Git、Repo、Gerrit各自是什么
这三样东西分开来看都不复杂,麻烦的是它们经常被混在一起说。先一个个理清楚,后面看工作流就不会懵。
1.1 Git:底层版本管理引擎
Git 是分布式版本控制系统,靠的是每个仓库独立保存完整的提交历史和对象数据库。它解决的问题是"文件改动了什么、谁改的、怎么回溯"。
在服务端可以搭一个裸仓库(bare repository)作为中心,开发者本地各自 clone 一份,提交通过 push 推到远端。Git 的分支模型很轻,创建、切换、合并的成本都极低,这让多人并行开发成为可能。
但如果项目有 30 个 Git 仓库,问题就来了:版本号该怎么对齐?一个功能涉及 5 个仓库的改动,怎么保证一起提交、一起回滚?光靠 Git 本身,答案是"没办法"。这就要 Repo 出场。
1.2 Repo:解决"仓库多到管不过来"的问题
Repo 是一个基于 Python 写出来的脚本工具,本身不替代 Git,而是在 Git 外面包了一层批量操作的能力。它通过一个特殊的 manifest 仓库,以 XML 文件描述整个项目里应该有哪些子仓库、分别在哪个路径、用哪个分支、哪个 commit。
日常只要几条命令,比如repo sync、repo start、repo upload,就能让所有子仓库进入一致的状态。对一个移动端系统这种动辄几百个 Git 仓库的项目,没有 Repo,基本没法手工维护。
Repo 的价值简单概括就是:把"管理一批 Git 仓库"从手工操作变成配置驱动。
1.3 Gerrit:把"改代码"变成"走流程"
Gerrit 是一个代码审查服务器,也是一个 Git 服务端。它的核心机制很特别:开发者提交代码时不能直接 push 到目标分支,而是要 push 到一个特殊的引用上,比如refs/for/master。
Gerrit 收到这个 push 后,会把它包装成一个待审查的 Change(一组通过修改集产生的变更单元),生成对应的评审页面。团队成员在页面上逐行点评、打分,CI 系统可以自动跑测试并给 Verified 标签,全部通过后,有合入权限的维护者再点 Submit,这个 Change 才会真正合入目标分支。
本质上,Gerrit 把 Git 的提交动作拆成了"提交 -> 审查 -> 合入"三步,让每一行进入主干的代码都被至少一个人看过、确认过。
1.4 三者怎么串成一条流水线
用一句话说明白:Git 是你的代码保存机制,Repo 是批量管理这些代码仓库的调度员,Gerrit 是门口检查质量的保安。
在典型的工作流里,开发者先执行repo sync把本地所有仓库同步到最新状态,然后在某一个子仓库里改代码,用 Git 提交,提交时自动带上 Gerrit 需要的 Change-Id。接下来执行git push origin HEAD:refs/for/分支名,代码就进了 Gerrit 的审查队列。审查通过后合入主干。这时其他同事再执行repo sync,就能拿到包含这次改动的完整代码了。
这套流程最关键的地方在于:提交和合入被彻底分离。所以它天生适合对代码质量要求高、迭代节奏稳定、多人协作频繁的团队。
2. 从几个核心痛点出发,理解这套设计思路
光知道概念还不够,你得明白为什么是这套组合,而不是别的方案。我从处理过的实际项目里,挑了三个典型痛点来说明。
2.1 为什么采用"先审查再合入"而不是直接 push
很多团队一开始使用 Git 的方式很简单:给每个成员开一个开发分支,开发完自己 merge 到 master。这种方式在三人小团队里问题不大,人一多就会出事,比如代码风格混乱、功能被意外覆盖、甚至有人把调试代码直接带到主干。
Gerrit 的审查模型就避免了这个问题。因为在它这里,除了管理员,普通开发者根本没有"直接 push 到主干"的权限。你所有改动都走 Change,每条 Change 必须有评审人给 Code-Review +2,CI 给 Verified +1,最终由指定维护者合入。这等于用机制而不是自觉来保证质量。
这里还有一个容易被忽略的设计细节:push 到refs/for/master时,Gerrit 并没有真正改变 master 分支的内容,只是创建了一个 pending 状态的 Change。只有当合入动作发生,Gerrit 才从 Change 里取出补丁,重新 rebase 或 merge 到目标分支。所以即使某条 Change 里改的东西非常激进,在合入前它都是个"影子",不会影响正在使用主干开发的其他人。
2.2 为什么用 Repo 包一层 Git,而不是写一堆 shell 脚本
在多仓库场景里,常见的一个偷懒做法是写一个 for 循环,把git pull逐个跑一遍。这种做法短期够用,但有几个问题:仓库清单变化后脚本要同步改;某个仓库 checkout 到别的分支或又冲突时,处理逻辑会越来越复杂;多个功能分支之间切换时,很难保证所有子仓库都切到正确状态。
Repo 用 manifest 把仓库清单、路径映射、分支选择固化成了配置文件,这一下就从"命令编排"变成了"声明式管理"。manifest 本身也是一个 Git 仓库,可以被 review、打 tag、回滚。你想切到某个发布版本,只需要把 manifest 切到对应 tag,然后执行repo sync,全部子仓库就会精确地跑到那个版本。
而且 Repo 内置了依赖关系处理。当一个仓库需要另一个仓库在本地先存在时,manifest 里的linkfile和copyfile可以自动处理文件链接和拷贝。这些细节,手写脚本处理起来很容易出错。
2.3 这套组合真的适合所有项目吗?
不是。判断标准很实在:你的项目是否拆成了多个仓库、是否有严格的质量门禁需求、参与人数是否足够多。
如果是一个单独的应用仓库,两三个后端开发者协作,直接用 Git 加某代码托管平台的 Merge Request 或 Pull Request 流程就足够,没有必要引入 Gerrit。Gerrit 的部署、配置、权限管理都有学习成本,小团队用它反而是负担。Repo 同理,单仓库项目给它再包一层 manifest,纯属多余。
所以更准确的说法是:Git-Repo-Gerrit 是面向中等规模以上、多仓库、强协作项目的重武器。项目越大、发布节奏越稳定,这套组合的收益越明显。
3. 核心细节解析与实操:从搭 Gerrit 到完成一次代码审查
这一节是全文的实操担当。我会按照真实操作顺序,拆解从零搭服务到一次提交审查落地的全过程。
3.1 服务端准备工作:部署 Gerrit 实例
Gerrit 的部署方式很多,最简单可控的是用 Docker 方式。照着下面这些思路操作即可。
先用 Docker 启动一个基础实例:
docker run -d -p 8080:8080 -p 29418:29418 \ --name gerrit \ -v /data/gerrit/etc:/var/gerrit/etc \ -v /data/gerrit/git:/var/gerrit/git \ -v /data/gerrit/db:/var/gerrit/db \ gerritcodereview/gerrit:3.x这里解释两个端口:8080 是 Web 页面和管理接口;29418 是 Gerrit 内置的 SSH 端口,用来做 Git 推送和内部管理。生产环境如果有人把 8080 暴露到公网,必须配合反向代理和 HTTPS,否则审查内容会明文传输,这在合规层面会出问题。
提示:Gerrit 初始化后,默认管理员账号是通过
review-site/etc/gerrit.config里的管理员邮箱决定的。第一次访问网页版时,用该邮箱注册的账号会自动成为管理员。这个细节很多人会漏,导致注册后发现自己没权限建项目,以为装失败了。
3.2 建立工程仓库并创建访问权限
Gerrit 里每个仓库叫 Project,可以用命令行创建,也可以用网页创建。命令行方式是这样的:
ssh -p 29418 admin@gerrit-host gerrit create-project --name my-project --branch master项目创建后,默认只有管理员有权限。需要到 Settings -> Access 里配置 refs/for/* 的 Push 权限,以及 refs/heads/master 的 Submit 权限。
一个比较稳妥的初始权限模型是看这张表:
| 角色 | 权限项 | 说明 |
|---|---|---|
| Developer | Push refs/for/* | 可以提交 Change 进审查 |
| Developer | Code-Review +1 | 可以给别人打分,但不能合入 |
| Maintainer | Code-Review +2 | 可以通过审查 |
| Maintainer | Submit | 可以真正合入 Change |
| CI 账号 | Verified +1 | 由自动化测试结果决定 |
注意:在实际操作里,"给开发者开 Code-Review +2"这种操作要非常谨慎。+2 意味着这个人在事实上拥有合入审查的能力,一旦给错人,审查流程形同虚设。
3.3 客户端配置 Repo 与 manifest 仓库
服务端准备好后,客户端要拉代码,第一步是创建或获取 manifest 仓库。
manifest XML 大概长这样:
<?xml version="1.0" encoding="UTF-8"?> <manifest> <remote name="origin" fetch="ssh://gerrit-host:29418/" /> <default revision="master" remote="origin" /> <project name="manifest" path="manifest" /> <project name="platform/core" path="core" /> <project name="platform/apps" path="apps" /> <project name="platform/libs/util" path="libs/util" /> </manifest>然后初始化并同步:
mkdir workspace && cd workspace repo init -u ssh://gerrit-host:29418/manifest -b master repo sync -j8repo sync执行时,会逐个 clone 或 fetch 所有列出的仓库。首次同步通常很慢,建议加-j8甚至更高的并行数。
实践心得:如果团队网络环境不稳定,
repo sync到一半失败的概率不低,我会习惯性地在命令后面加--no-tags,减少不必要的 tag 拉取;同时在.repo/local_manifests/下维护自己的附加仓库清单,用来放团队私有但不需要进公共 manifest 的仓库。
3.4 一次完整的提交与审查过程
现在模拟一个真实的改动流程。
进入某个子仓库:
cd platform/apps repo start feature-login appsrepo start会在指定仓库上创建本地分支,并自动设置与远端分支的 tracking 关系。注意这里不写cd进 manifest 整体切换分支,因为 Repo 会在各仓库里各自建分支。
修改代码后,提交:
git add . git commit提交时,Gerrit 官方提供的 commit-msg hook 会自动执行,给 commit message 追加一行 Change-Id。有一套特定的 hash 格式,长这样:
Change-Id: I1234567890abcdef1234567890abcdef12345678这个 Change-Id 是 Gerrit 识别 Change 的核心标识。同一行代码反复修改、多次 push,只要 Change-Id 不变,Gerrit 就会认为它们是同一组变更,把新的 revision 挂到同一个 Change 下,而不是生成一堆新 Change。
push 到审查队列:
git push origin HEAD:refs/for/masterpush 成功之后,Gerrit 会在终端直接打出一个下载链接,比如https://gerrit-host/#/c/my-project/+/12345。点进去,就能在页面上看到这次改动的文件级 diff,可以针对某一行留下 comment。
评审流程到了这一步,是这样的节奏:
- 同事浏览代码,有问题就逐行加 comment。
- 开发者根据意见修改后,用同样的 Change-Id
git commit --amend提交并重新 push。 - 达成一致后,评审人给 Code-Review +2。
- CI 如果接了,跑完测试后给 Verified +1。
- 维护者点 Submit,Change 合入 master。
注意:提交信息末尾的 Change-Id 千万不能删掉。只要删掉,再 push 就会生成一条全新 Change,之前的讨论记录就全部遗失了,这对团队协作是灾难级别的体验。
3.5 针对多仓库项目的批量操作技巧
如果一次改动跨了多个子仓库,有两种常规做法。
第一种是逐个仓库 push 生成多条 Change,然后到 Gerrit 网页上把相关 Change 用同一组 Topic 关联起来。Topic 相同的 Change 会出现在同一个审查页面里,维护者可以按依赖顺序提交。
第二种方式是在 Gerrit 支持"提交链"的模式下,让多个 Change 之间按先后依赖排列。这么做的好处是变更集能保持编译一致性,坏处是如果最底层那个 Change 被 reject,后面的全都要重新 rebase。新手阶段我更推荐先拆开提交、用 Topic 关联,等熟悉了再玩提交链。
# 给 Change 设置 Topic ssh -p 29418 admin@gerrit-host gerrit set-topic --topic login-function 123454. 常见问题与排查技巧实录
这套工具链配置好之后,日常运行中会遇到的问题其实很集中。我把这些年踩过的坑整理成了一份排查实录,按照出现频率排序。
4.1 问题一:repo sync 永远卡住或者反复断线
现象很直白:执行repo sync -j8,进度走到某个仓库就停住,等半天没反应,最后报 connection timeout。
原因通常不是 Repo 本身,而是网络通道不稳定,或者某个仓库历史包太大。处理办法有几个,可以组合用:
# 调低并行度,减小网络压力 repo sync -j4 # 如果只是个别仓库卡住,单独同步该仓库 repo sync platform/apps # 网络很差时,先完整拉取镜像,再本地镜像走文件协议 repo sync --no-clone-bundle--no-clone-bundle的意思是跳过服务器预打包的 clone bundle 下载。这个 bundle 有时候比整仓库还大,网络差的时候下载它纯属煎熬。
4.2 问题二:push 报错 missing Change-Id in commit message
Gerrit 拒绝接收推送,提示missing Change-Id in commit message。出现这种问题,通常是三种情况:
- 新 clone 的仓库里没有安装 commit-msg hook;
- 安装 hook 前已经写好的提交没有补上 Change-Id;
- 有人删掉了 Change-Id。
修复方式有两种。
方式一,重新安装 hook 并 amend:
gitdir=$(git rev-parse --git-dir) scp -p -P 29418 admin@gerrit-host:hooks/commit-msg "$gitdir/hooks/" git commit --amendhook 会在 amend 时自动识别旧 message,并在合适位置补上 Change-Id。
方式二,如果不想改历史,也可以手动把 Change-Id 复制到前一个提交上,再匿名推送。但这种方式很容易造成 Change 分裂,我建议只把它当成兜底手段,不要作为常规操作。
4.3 问题三:Code-Review 已经 +2,但 Submit 按钮变灰
这是最让人困惑的问题之一。代码被人给了 +2,Verified 也是 +1,但在网页上却找不到 Submit 按钮,或者按钮是灰的。
排查顺序要从权限和状态两方面入手。
先确认你在该项目里是否被授予 Submit 权限。很多时候维护者在建项目时只给了一两个人 Submit 权限,你虽然是 Owner 的同事,但没有实际提交权。
再检查目标分支是否设置过策略,比如不允许多人确认、不允许空提交、禁止某些后缀文件。还有一个最常见的坑,就是 Change 不是基于目标分支最新代码,存在冲突。Gerrit 页面顶部会有醒目的 "Cannot merge due to path conflict" 提示。这种情况下 Submit 灰掉是必然的,需要先手动 rebase。
4.4 问题四:push 时提示非快进更新,被拒
这不是 Gerrit 独有的问题,而是 Git 的普通规则。你本地基于旧 commit 改了代码,直接往refs/for/masterpush,Gerrit 会拒绝,因为它依赖的 parent commit 已经不在目标分支的最新路径上。
解决办法是在本地 rebase 目标分支,或者直接让 Gerrit 在网页端重新基于最新分支自动 rebase。建议后端接 CI 的情况下,走页面上的 "Rebase" 按钮,这样历史保留在服务器端,减少本地来回切换上下文的麻烦。
实操心得:我先会让本地开发者尽量保持小步提交、频繁 sync。一次分支落后 100 个 commit 和落后 5 个 commit,rebase 时的心态完全不同。代码评审拖得越久,合并成本就越高,这是节点型团队的常识。
4.5 常见问题速查表
| 问题现象 | 根因 | 快速解法 |
|---|---|---|
| repo sync 超时 | 网络不稳定 / 仓库过大 | 降低并行度、单独同步、加代理或镜像 |
| 推送被拒 missing Change-Id | 未装 commit-msg hook | scp 拉取 hook 后 amend |
| Change 无法合并 | parent commit 落后目标分支 | 网页端 rebase,或本地 rebase 后重新 push |
| Submit 按钮不可用 | 无 Submit 权限 / 有冲突 | 检查权限矩阵,解决冲突 |
| 提交信息变更丢失 Change-Id | 用户擅自编辑 message | 保留 Change-Id,尽量用 amend 修改 |
| 多条 Change 同步合入需求 | 跨仓改动 | 统一设 Topic |
5. 进阶玩法与团队落地经验
这套工具链能不能在团队里真正跑起来,其实比搭建本身更难。我总结了几条容易被忽略但很管用的进阶经验。
5.1 用 submit-rule 实现保护分支之外的门禁
Gerrit 的合入门禁不只有 Code-Review 和 Verified。它提供一种叫 submit rule 的机制,本质上是一段 Prolog 代码,可以自定义合入条件。
比如强制要求"必须有两位评审人 +1 以上才能合入"、"禁止周六合入"、"关键模块必须有某小组的 +2"、甚至"前端仓库合入时必须带有 CI 的静态检查通过标签"。这些规则一旦配好,就变成了服务器端的统一约束,不是靠成员自觉执行。
很多团队会把 CI 的 Verified 和静态分析、代码扫描绑定,只要扫描出安全级别为严重的问题,CI 就自动给 Verified -1,无论代码审得再好,都没办法合入。这种做法我就是因为吃过"审查通过但上线崩了"的亏,后来坚决补上的。
5.2 给 Repo 写简易的本地封装
Repo 命令本身虽然强大,但有些组合操作每次都拼字符串很烦。我一般在~/.bashrc里加几个别名或函数:
function repo-up() { repo sync -j8 --current-branch repo start "$1" } function repo-push-all() { for project in $(repo list --path-only); do ( cd "$project" git push origin HEAD:refs/for/master ) done }repo push-all这个函数我特别常用。跨仓修改时,只要每一条提交都有 Change-Id,就能一条命令把多个子仓库的 Change 全部推到 Gerrit。注意函数里加了一层 subshell,避免 cd 影响主 shell 的工作目录,这个小细节能避免很多低级错误。
5.3 培养团队的代码审查习惯
工具再强,如果团队成员不习惯认真 review,一切白搭。我自己会规定几条硬要求,如果你要落地这套方案,可以参考。
一是自查优先。push 之前,先自己用git diff过一遍改动。很多低级问题自己看一遍就能发现,不要浪费别人的 review 时间。
二是评论要有归属感。在 Gerrit 上写 comment 时,尽量指定具体行号和具体建议,而不是只丢一句"感觉这里有问题"。被 review 的同事通常更容易接受可执行的意见。
三是限时审查。Change 在队列里停留超过一个工作日的,我会主动在群里公示一次。审查拖着不决,改动越多,后面 rebase 冲突概率越高,最终代价是整个团队承担。
5.4 兼容老的单仓库开发习惯
团队里总会有人习惯旧的单仓 workflow:clone 下来,天天 pull,搞一个自己的大分支,干到天荒地老再合并。这类同事接入 Repo+Gerrit 时,一般会经历一个适应期,经常听到的说法是"太麻烦,我直接给你一个 patch 吧"。
我的建议是,前期不要硬推开满大流程。可以先让他用 Repo 拉代码,但允许他继续用 Git 直接改、直接提交;等 Gerrit 审查模型带来的安全感被感知到之后,再逐步要求所有合入都走 Change。温水煮青蛙式落地,比风暴式推行顺利得多。
另外就是文档。把常用命令整理成一份速查卡,贴在团队内部知识库里。不要只写"用 repo sync 更新代码",要写"什么场景用什么命令、最常见的两个报错以及解法"。文档足够具体,工单量能少一半。
写在最后,一点个人体会
使用这套工作流许多年后,我最大的感受是:它真正改变的其实是团队的协作习惯。Git 只是工具,Repo 只是管道,Gerrit 只是门禁,但当一个团队习惯于"每一行进入主干的代码都要被另一个人看过、都被 CI 验证过"之后,代码质量的下限被兜住了。我见过太多新成员第一次提交时手忙脚乱,被 Gerrit 报错挡回去几次,然后就养成了自查和阅读 review 意见的习惯。
最后再分享一个小技巧:如果你和我一样经常需要跨多个子仓库开发,把常用操作封装成函数脚本,同时在提交信息里保留好 Change-Id,你的 Git-Repo-Gerrit 这条流水线就会越用越顺手。能做到这一步,这套工具链才算是真正落进项目里了。