Git+Repo+Gerrit:多仓库代码评审与合入门禁实战

发布时间:2026/10/17 7:23:00
Git+Repo+Gerrit:多仓库代码评审与合入门禁实战 第一次把Git、Repo、Gerrit三样工具组合到同一条工作流里是因为一个很现实的问题一个移动系统源码工程几十个独立模块仓库铺开十几个团队并行开发单靠Git仓库加中心服务器已经完全管不过来了。分支冲突每天发生权限又粗想给某个模块设置“需要评审才能合入”的规则在纯Git上做起来非常别扭。最后我们引入了Repo来统一管理多仓库清单用Gerrit作为评审门禁形成了一条“本地Commit→推送评审→评审通过→自动合入”的链路。这篇文章不是官方文档的复述而是我在这条链路里从部署到日常使用踩过的坑和沉淀下来的经验适合正在评估是否要上这套组合、或者已经在用但经常被Change-Id和refs/for搞晕的团队参考。1. 为什么单Git仓库撑不住非要Repo和Gerrit一起上经常有人问我“我们Git用得挺好的干嘛还要加Repo和Gerrit”我的回答一般都很简单——规模和协作方式变了。1.1 单仓库在大规模并行下为什么先崩溃单Git仓库在小团队里玩得很顺三五个人各自拉分支改完合并冲突量小处理起来也快。可一旦仓库数量上到几十个、团队扩到十几个三个问题会立刻浮出水面。第一分支与合入的冲突无法收敛。几十个模块并行时每次merge都要处理一堆非功能性冲突比如自动生成文件的变动、目录结构调整、格式化差异。这些冲突跟业务逻辑毫无关系却要消耗大量工程师时间而且A团队改了公共接口B团队半夜合入才发现编译不过的情况特别多。第二权限粒度太粗。Git原生对仓库的权限控制主要集中在“能否推送”它很难表达“谁可以创建分支、谁可以推送到主干分支、谁必须经过评审才能合入”。一旦开发人员变多很容易出现有人绕过评审直接push到受保护分支的事故事后你想查是谁合入的、谁批准的都无从下手。第三审计和追溯能力弱。代码评审中产生的讨论、谁给了2、谁触发了合入——这些信息在纯Git日志里基本是缺失的。项目复盘时想还原一个改动的完整决策过程只能靠聊天记录这显然不合理。1.2 Repo补上的“多仓库清单”短板Repo解决的问题很粗暴但很有效它给多仓库工程加了一个“清单层”。这个清单通常是一个独立的manifest仓库里面用XML文件记录着所有子仓库的远程地址、默认分支、固定版本。每个开发者只要拉到同一份manifest再执行一次repo sync本地的几十个仓库就能整整齐齐地恢复到同一状态。没有Repo的时候你新加入一个项目得手动一个一个git clone还很容易从不同的远程分支拉到不一致的代码版本有了Repo之后“可复现”从口号变成了默认能力。Repo还提供了跨仓库操作的口子。比如repo start可以一次性为所有仓库创建同名分支repo status可以一眼看到几十个仓库的整体改动情况repo grep可以在跨仓库代码里搜索关键字。这些能力本身不复杂但在多仓库场景下能把日常操作的效率提升一个量级。1.3 Gerrit填上的“评审与门禁”缺口Gerrit本质上是一个代码托管加评审服务器但它最关键的贡献是把“合入”这件事变成了可以被评审、被记录、被阻止的受控事件。开发者默认不能直接把commit推到refs/heads/xxx只能推到refs/for/xxx。Gerrit收到后不会立刻更新目标分支而是生成一个待评审的Change。评审人在界面上打分、留评论全部通过后合入动作才由Gerrit服务端真正执行。这个设计把“修改代码”和“进入主干”彻底分离权限控制、流程审计、质量门禁全都有了落点。1.4 什么项目适合什么项目别生搬硬套项目类型是否适合这套组合原因几十个仓库、多团队并行的工程非常适合清单统一管理有收益评审门禁能挡事故3到5人的单仓库小团队不适合引入Repo和Gerrit的运维成本超过收益需要大量外部临时贡献者的项目视情况而定评审有价值但Gerrit门槛可能吓跑新手纯个人项目完全不适合一个Git远端备份足够我见过有团队只有六个仓库也要硬上Gerrit每天被Change-Id和权限配置折腾得苦不堪言最后一怒之下退回GitLab。工具组合没有绝对的好坏只有合不合适的场景。2. 把Repo同步链跑通init、sync与manifest的配合确定要上这套组合之后第一步就是把这根同步链跑通。这一步看着简单但版本兼容、manifest写法、本地分支策略都是容易出问题的地方。2.1 基础工具Git版本、Repo脚本与Python兼容性建议把Git升级到较新的稳定版老版本Git在处理浅克隆、安全策略和部分子命令时行为差异很大出问题后很难排查。Repo本身是一个Python脚本早期版本依赖Python 2现在主流版本都要求Python 3安装时最好直接用发行版仓库里带的repo或者从官方渠道拉取并校验版本。一个常见的坑直接用apt install repo装上的版本可能偏旧和某些Gerrit服务端的协议不兼容表现就是同步时报奇怪的SSL错误或者解析XML失败。遇到这种情况优先升级repo脚本版本而不是去折腾服务端。2.2 manifest仓库设计remote、default与project的意义manifest仓库是整个多仓库工程的“总蓝图”理解它比背命令重要得多。下面是一个典型的manifest文件结构manifest remote nameorigin fetchssh://git.example.com/ reviewhttps://gerrit.example.com// default revisionmain remoteorigin/ project pathapp/core namemodules/core / project pathapp/network namemodules/network revisiondev-1.2 / /manifestremote标签里的fetch是所有子仓库URL的前缀review字段是Gerrit地址供repo upload生成评审目标。default标签里的revision是默认分支所有没单独写revision的project都会用这个分支。某个模块需要紧跟特定分支时就在project标签里单独覆盖像上面的modules/network一样锁定在dev-1.2。我个人的建议是manifest里尽量锁定固定revision至少对关键模块使用tag或SHA而不是裸的分支名。分支名跟着远端走今天同步的是这个状态明天可能就是另一个状态出了问题很难复现。2.3 首次走通repo init与sync初始化一组仓库命令很简洁但每一步都有事情在发生repo init -u ssh://git.example.com:29418/manifest.git -b main repo sync -j8 -crepo init会在当前目录生成一个.repo目录里面存放manifest仓库本体、各项目的引用信息和钩子配置。repo sync的任务是把manifest里记录的每个项目同步到本地-j8是8个任务并行拉取-c是只同步当前分支而不是所有远端分支能省不少时间。首次同步的体验跟网络环境关系很大如果之前有人同步过同样的工程可以配置本地镜像或者缓存目录repo init -u manifest地址 --reference/path/to/mirror这能把后续同步的耗时压缩到分钟级。没有镜像的情况下几十个仓库逐一clone确实会等很久这是多仓库工作的常态耐心点就好。2.4 用repo start建立特性分支同步完成后不要直接在默认分支上改代码而是用repo start为所有仓库创建本地主题分支repo start stable-1.0 --all这条命令的含义是在每一个仓库里基于当前manifest指定的版本创建一个名为stable-1.0的本地分支并让它追踪对应的远端分支。这样做的好处是后续你在任何一个仓库里的提交都发生在自己的命名空间里不会跟某个版本的快照纠缠在一起。改完代码后Git分支名、Gerrit上显示的目标分支关系一目了然。3. 从commit到评审合入的完整闭环同步链跑通之后日常工作的核心就是“本地提交→推送评审→通过合入”这个闭环。很多新人在这个阶段被Change-Id和refs/for绕得晕头转向我拆细一点说。3.1 Change-IdGerrit识别同一笔改动的身份证Git里每次commit有一个唯一的哈希值但这个哈希在--amend、rebase之后会变化。如果Gerrit靠commit哈希来识别评审任务那么同一笔逻辑改动连续修改两次就会在系统里变成两个互不相关的Change评审历史和讨论全部断裂。所以Gerrit定义了Change-Id以字母I开头后面跟40位十六进制字符的一行字符串放在commit message末尾。无论commit哈希怎么变只要Change-Id还在Gerrit就把多次推送归入同一个Change并生成不同的Patch Set。评审人在新Patch Set上继续追评上下文不断。3.2 commit-msg hook的安装与验证Change-Id通常不是人工手写的而是由Git的commit-msg钩子在每次提交时自动追加。Gerrit服务端提供了这个hook开发者需要把它安装到仓库的.git/hooks/目录里。安装命令一般是这样的gitdir$(git rev-parse --git-dir) curl -o $gitdir/hooks/commit-msg https://gerrit.example.com/tools/hooks/commit-msg chmod x $gitdir/hooks/commit-msg也可以用scp的方式拉取scp -p -P 29418 usernamegerrit.example.com:hooks/commit-msg .git/hooks/安装后验证一下ls -l .git/hooks/commit-msg只要这个文件存在且有执行权限之后每次git commithook都会在提交信息自动追加一行Change-Id。3.3 推送到refs/for而不是refs/heads评审流的推送命令是git push origin HEAD:refs/for/main这里的refs/for/main是Gerrit的特殊命名空间推到这里的commit不会直接更新main分支而是生成一个待评审的Change。真正更新main分支的动作由Gerrit在评审通过后执行。新人最容易犯的错是直接执行git push origin HEAD:refs/heads/main这在多数团队里会被权限系统直接拒绝因为refs/heads/*通常只开放给维护者。如果权限配置不当没有拦住那就是绕过评审直接改主干属于流程事故。所以团队里一定要做保护性配置**普通开发者只有refs/for/的推送权限refs/heads/只留给机器用户或特定维护组。3.4 评审、打分、合入生命周期一个Change的完整旅程大致如下开发者推送refs/for/mainGerrit生成新Change。在Gerrit页面上添加Reviewer评审人逐行看代码、留评论。代码质量通过后有权限的人给Code-Review 2。CI或验证人员跑完测试后给Verified 1。两个条件都满足后点Submit合入。如果合入时发现目标分支已经领先Gerrit会提示Need Rebase。这时需要在本地把目标分支最新代码拉下来rebase自己的提交再重新推送。Gerrit会在同一个Change下生成新的Patch Set评审记录保持连续。4. 部署Gerrit侧的准备账号、权限与合入策略很多团队在客户端这边折腾了半天服务端配置却是随手搞的导致后面流程各种别扭。Gerrit服务端的几个关键点值得好好设计。4.1 SSH别名与Gerrit账号打通开发者连Gerrit一般走SSH端口默认是29418。为了方便我会在每个人的~/.ssh/config里加一段Host gerrit HostName gerrit.example.com Port 29418 User username这样后面所有带gerrit主机名的操作都会自动用配置的端口和用户名。第一次连接时Gerrit会提示你把SSH公钥上传到账号里验证一下ssh -p 29418 usernamegerrit.example.com能返回类似**** ****的欢迎信息账号就通了。4.2 权限组与Label的设计Gerrit的权限模型有一个核心概念权限继承。根项目All-Projects定义了全局权限所有项目可以从它继承子项目可以覆盖或追加。这样团队只需要维护一套基线规则不用每个仓库单独配置。我常用的配置基线是refs/for/*允许所有注册开发者Push。refs/heads/*默认拒绝Push仅通过评审Submit合入。Label Code-Review2权限只授予主程和维护者组成的组。Label Verified1权限通常授予CI机器人。这一套配下来就能保证“谁都能提代码但只有被授权的评审人能放行”。4.3 三种合入策略怎么选Gerrit支持好几种Submit策略影响的是合入后目标分支的历史形态。我把它们放在一起对比策略历史形态适用场景Merge If Necessary保留真实合并记录存在merge节点并行分支多、不希望频繁rebaseFast Forward Only线性历史没有merge节点追求简洁历史允许本地rebaseCherry Pick提交被重放到最新主干严格单线开发版本管理要非常干净我自己更偏好Fast Forward Only历史干净回滚定位容易代价是每个开发者在推送前需要把目标分支最新代码rebase到本地。如果团队实在不愿意频繁处理rebase冲突再退回Merge If Necessary。4.4 让CI去当Verified很多团队的Verified是负责人手动点的这很不科学。正确做法是接入CI流水线每次Change更新时Gerrit触发一次构建CI跑完把结果打回Verified 1或Verified -1。没有Verified通过的Change不允许合入这样合入质量从人肉保证变成了自动化保证。5. 高频故障的完整排查链路工具链用得越久踩坑越多但这些坑大多是同一类问题反复出现。我把实战中最高频的四个故障和处理过程完整写出来照着排查能省很多时间。5.1 缺Change-Id被拒从现象到根因现象推送时出现类似下面的报错! [remote rejected] HEAD - refs/for/main (missing Change-Id in commit message)这是Gerrit收到commit后发现没有Change-Id直接拒绝接收。完整的排查链路如下先确认本机是否安装了commit-msg hookls -l .git/hooks/commit-msg如果文件不存在安装它。如果文件存在但没执行权限chmod x .git/hooks/commit-msg。查看本地提交信息是否已经有Change-Idgit log -1 --format%B确认缺失时对最近一条提交执行git commit --amend让hook重新生成Change-Id并追加到message里。再次推送。这里有一个细节如果commit message里出现了两行Change-IdGerrit会懵因为它不知道该以哪个为准。所以amend后要检查message保留一行就好。5.2 repo sync撞上本地未提交改动现象执行repo sync时报错error: path/to/repo/: uncommitted changes are presentRepo为了不破坏本地工作遇到未提交改动会直接中断同步。排查和处理的正确顺序是先进入报错仓库查看具体改动cd path/to/repo git status如果这些改动是你要保留的先暂存起来git stash回到工程根目录重新同步repo sync同步完成后再恢复暂存内容git stash pop有一个需要提醒的点不要一上来就加-f强制同步。-f只是跳过出问题的仓库继续同步其他仓库那个仓库的本地改动并不会被处理最终容易形成“你以为同步了其实某个仓库还在旧状态”的假象。5.3 提交进错了目标分支现象本想在release-1.2分支上开发推送时手滑点成了refs/for/mainGerrit上生成一个目标分支错误的Change。排查步骤看当前本地HEAD在哪个分支git log --oneline -3 git branch -vv确认最近的commit是不是指向了错误方向。处理方式分两种情况如果commit还没推送到Gerrit直接软回退git reset --soft HEAD~1然后切到正确分支重新提交。如果已经推到了Gerrit成了Change在Gerrit页面上先给这个错误Change点Abandon本地把commit迁移到正确的目标分支再重新推送到对应的refs/for/xxx。预防方法是每次推送前用dry-run看一眼目标git push --dry-run origin HEAD:refs/for/release-1.2这条命令会打印出将要推送的内容和目标不会真的提交花一秒钟能挡住大部分手滑。5.4 rebase后Change-Id丢失的补救现象本地rebase或者处理冲突时commit message被重写Change-Id行丢了再次推送时又报missing Change-Id。处理思路是让Change-Id回到message里并保持和Gerrit上原Change一致。git commit --amend在打开的编辑器里把原来的Change-Id行重新粘贴回去保存退出再推送。如果连原来的Change-Id都不记得了可以到Gerrit页面上找到原Change里面每一行都能看到Change-Id。我习惯在重新rebase之后批量检查所有提交里是否都还有Change-Idgit log --format%H %s %b | grep -c Change-Id:跟预期提交数量对得上才push对不上就先处理。6. 长期协作下来真正有价值的心法工具层面的坑都聊完了最后说几个跟流程设计和团队习惯有关的心法。这些是让整套组合真正发挥价值的软性因素。6.1 一个Change只干一件事评审人最反感的是一个大Change里既改了接口又修了bug还顺手改了格式。Gerrit的评审界面是按文件逐行展示的多件事情混在一起评审人很难判断每一处改动是不是合理。坚持一个Change只解决一个问题评审速度会快很多出问题回溯时也清晰。6.2 Topic把跨仓库改动绑在一起多仓库场景下一个功能经常要同时改好几个仓库。Gerrit支持给Change设置Topic同名Topic的多个Change可以一起展示、一起评审、一起合入还能在页面上看到它们之间的依赖关系。这比在备注里写“配合xxx分支的xxx提交”靠谱得多。6.3 让验证变成自动化的声音人工点Verified这件事又慢又不可靠人在疲劳时根本不记得点。把CI接入Verified之后合入门禁有了客观标准开发者也更愿意在推送前自己先跑一遍静态检查和单测因为CI没过推上去也是白推。6.4 定期修剪僵尸Change与本地分支Gerrit上有大量长期不更新的Change会慢慢变成“僵尸”。我每个迭代结束会花半小时清理确认已经合入的Change不必处理但长期没动静且功能已经放弃的果断Abandon。本地仓库里那些repo start开出来的旧特性分支合入后尽快删掉否则几十个仓库的本地分支列表会膨胀到没法看。6.5 代码评审里最有用的沟通习惯最后一点经验跟技术无关但非常影响效率评审意见里多写“为什么”少写“怎么改”。比如“这里应该用事务包裹因为两步操作需要原子性”比“这里加个Transactional”有用得多。前者教的是设计原则后者只是给了个补丁。评论区一旦变成“按我说的改”的战场代码质量反而会下降。回到“Git-Repo-Gerrit”这套组合本身。有人抱怨它门槛高但一旦团队过了磨合期它会变成一种“默认有人兜底”的安全感。合入门禁防的是手滑清单统一防的是沉默的版本漂移Change-Id防的是补丁在评审中丢失脉络。我们从一开始觉得多了一道工序到后来完全离不开中间靠的就是反复踩坑和把这些约定变成肌肉记忆。如果你们团队也在考虑这套组合希望这篇文章能帮你把第一天到第一百天的路都铺平一点。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询