一、依赖链究竟是如何建立的?
要自动化处理依赖,首先要理解 Gerrit 中“父子关系”的两种表现形式:
1. 基于 Git 提交图的隐式依赖
当你本地基于某个分支工作,依次生成多个 commit:
A --- B --- C (feature-branch)push 到 Gerrit 后,Gerrit 会为每个 commit 创建一个独立的 Change。此时:
- Change C 的 Git 父提交是 B
- Change B 的父提交是 A
Gerrit内部通过提交对象的 parent 字段建立依赖关系。无需任何额外标记。
2. 基于Depends-On页脚的显式依赖(跨分支/跨项目)
如果依赖关系无法通过提交父子关系表达(例如基于不同分支的 Change 需要顺序合入),可以在 commit message 中增加:
Depends-On: I0123456789abcdef...Gerrit 解析后会强制建立依赖。
绝大多数批量场景都是同一分支上的连续提交链,也就是第1种情况。下面的讨论均以此为主。
二、解读 Related Changes API 的返回细节
上一篇我们用了/related端点,这里看一下它的完整响应以及如何处理各种边界情况。
请求:
GET /changes/{change-id}/revisions/{revision-id}/related响应核心字段(去除了)]}'前缀后的 JSON):
{"changes":[{"project":"my-project","change_id":"my-project~master~Ie5f45c...","commit":{"commit":"a1b2c3d4...","parents":[{"commit":"base_sha"}],"subject":"first commit"},"_change_number":111,"_revision_number":1,"_current_revision_number":1,"status":"NEW"},{"project":"my-project","change_id":"my-project~master~I3f2a19...","commit":{"commit":"b5c6d7e8...","parents":[{"commit":"a1b2c3d4..."}],"subject":"second commit"},"_change_number":112,"_revision_number":1,"_current_revision_number":1,"status":"NEW"},{"project":"my-project","change_id":"...","_change_number":113,...}]}关键点:
changes数组的顺序已经是拓扑排序的(祖先在前,后代在后),可以直接遍历。- 数组中只包含“开放状态”或“尚未合入目标分支”的 Change。如果一个祖先已经被合入(MERGED),则不会被包含在内(但它的提交信息可能出现在
parents中)。这恰好是我们想要的:我们只需要处理那些还未合入的 Change。 - 每个元素中
_change_number可用于构造/changes/{project}~{number}的 ID,_revision_number是该 Change 最新的 Patch Set 号。通常我们操作当前 patch set,可以直接用current关键字。 - 如果依赖链很长(如超过 100 个),这个接口会一次性返回全部吗?官方实现会遍历整个相关图但有限制,通常 100 以内没问题,极端情况需考虑分页,但在日常批量合入中极少遇到。
由此 API 我们可以实现“从一个链接获取全部相关提交”,不需要再手动回溯父提交。
三、如果没有/related端点怎么办?(老版本 Gerrit <2.15)
某些老版本(或受限环境)可能没有这个端点。此时需要手动回溯,流程如下:
获取某个 Change 的当前 commit SHA
GET /changes/{change-id}/revisions/current/commit返回中有
commit和parents。用父 SHA 搜索对应的 Change
GET /changes/?q=commit:{parent_sha}+AND+status:open+AND+project:{project}注意:一个 commit 可能同时出现在多个 Change 中(如果是 cherry-pick 等),需要限定项目和状态。
递归向前追溯,直到找不到父 Change(说明已合入或不存在)。
该方式需处理分页和可能的多结果,复杂度远高于/related,此处仅作备选方案。
四、SSH 命令与 REST API 的深度对比
| 功能 | SSH 命令示例 | REST API 示例 | 备注 |
|---|---|---|---|
| 获取相关变更 | 无直接命令,需结合gerrit query | GET /changes/{id}/revisions/current/related | REST 完胜 |
| 评审打分 | ssh ... gerrit review 123,1 --code-review +2 | POST /changes/{id}/revisions/current/review | 都可 |
| 提交合入 | ssh ... gerrit review 123,1 --submit | POST /changes/{id}/submit或 review 中"submit": true | 都可 |
| 获取 change 详情 | ssh ... gerrit query change:123 --format JSON | GET /changes/{id}/detail | SSH query 功能强大但输出难解析 |
建议:
- 获取依赖关系必须用 REST。
- 批量操作如果脚本环境已配置好 HTTP 密码,全部用 REST 更统一。
- 如果只在命令行临时操作,SSH 更便捷(无需记 URL)。
五、顺序的绝对性:为什么必须先祖先后后代
Gerrit 在执行 Submit 时会进行“依赖检查”:
- 对于 Change B,如果它的父提交(即 Change A 的当前 patch set)尚未合入到目标分支,Submit 操作将被拒绝,返回:
depends on change ... which is not merged - 即使所有需要的 label 都已齐备,也必须等待父 Change 合入。
因此必须严格按拓扑排序执行 Submit。
而 Review(打分)本身不检查依赖关系,可以任意顺序打分,但为了避免混淆,我们也按照相同的祖先优先顺序操作。
六、处理非标准标签与 Submit 策略
1. 自定义标签
你的项目可能除了Code-Review还有Verified、Gatekeeper、Product-Review等。你需要知道成功合入所需的最低标签集合。
可以在 Change 页面中查看Permitted labels,或通过 API:
GET /changes/{id}/detail?o=LABELS返回的labels字段中,每个 label 有all列表,包含每个用户允许打分的范围。自动化脚本需要根据项目配置传入相应的 labels 字典。
示例:需要 Code-Review +2 且 Verified +1
{"labels":{"Code-Review":2,"Verified":1}}2. Submit 策略的影响
Gerrit 项目可以设置不同的 Submit 策略(Submit Type):
Merge If Necessary:默认,会创建一个 merge commit。Fast Forward Only:要求 Change 的 base 恰好是目标分支的当前 HEAD,不能有分叉。如果依赖链中两个 Change 之间有其他人合入了无关变更,FF 策略可能会失败。Rebase If Necessary:提交前会先 rebase。Cherry Pick:直接拣选提交,不创建合并提交。
如果是Fast Forward Only,你可能需要在 Submit 前确保没有冲突或分叉,否则 Submit 会失败。批量处理时,如果中间某个 Change 因为冲突合入失败,整个链条会中断,需要人工介入。
七、增强版自动化脚本设计要点
基于上一篇的脚本,我们可以增加以下鲁棒性设计:
7.1 预检查:确保所有 Change 可合入
在执行 Submit 前,可以先逐个检查每个 Change 的mergeable状态。
GET /changes/{id}/revisions/current/mergeable如果mergeable为 false,说明存在冲突,应提前报错并停止流程,避免部分合入后卡住。
7.2 处理 CI 投票延迟
如果你的项目依赖 CI 给出Verified+1,那么在 +2 之后,可能需要等待 CI 完成。脚本可以加入轮询:
importtime max_wait=300# 5分钟whileTrue:detail=requests.get(...,params={"o":"LABELS"}).json()verified=detail['labels']['Verified'].get('approved',{})ifverified.get('value',0)>=1:breaktime.sleep(5)7.3 处理部分 Change 已合入的情况
/related接口已经不返回已合入的 Change,但如果我们在操作过程中手动合入了某个祖先,脚本中再对该 Change 执行 Submit 会得到 409 Conflict。此时应捕获状态码并跳过(或确认已合入)。
7.4 完整的 Python 脚本升级框架
defwait_for_label(change_full_id,label_name,required_value,timeout=300):# 轮询指定 label 达到 required_value...defprocess_chain(change_url):change_id=get_change_id(change_url)chain=get_related_changes(change_id)# 检查可合并性forchinchain:ifnotis_mergeable(ch):raiseException(f"{ch['project']}~{ch['_change_number']}存在冲突")# 按序打分forchinchain:review_change(ch['project'],ch['_change_number'])# 按序合入,合入前等待必要标签forchinchain:full_id=f"{ch['project']}~{ch['_change_number']}"wait_for_label(full_id,'Verified',1)# 如需要submit_change(ch['project'],ch['_change_number'])八、认证与权限的实操细节
REST API 认证
- 在 Gerrit 网页端进入Settings → HTTP Password,生成一个密码(或获取)。
- 使用 HTTP Basic Auth 时,Authorization 头为
Basic base64(user:pass)。curl 的-u选项自动处理。 - 注意 Gerrit 的 REST API 前缀是
/a/,所有请求都需包含。例如/a/changes/...。如果遗漏,请求可能被重定向或拒绝。
SSH 认证
- 确保本地
~/.ssh/id_rsa.pub已上传到 Gerrit 的SSH Keys中。 - SSH 端口通常为 29418,可通过
ssh -p 29418 user@host gerrit version测试连通性。
权限
批量评审合入的账号至少需要:
- 对应 refs/heads/* 的
Label: Code-Review范围允许 -2…+2,且具备 +2 权限。 Submit权限(一般是Submit访问控制)。- 若需跳过某些标签的等待,可能需要
Forge Committer或管理员权限等,一般不必要。
九、常见陷阱排查
Change-Id 与 Change Number 混淆
- URL 中的数字(如
12345)是 Change Number(简短易变,不同项目可重复)。 change_id形如project~branch~I...是全局唯一字符串。
REST API 中查询单个 Change 时,两者都可用,但推荐使用project~number三元组形式(仅在同一 Gerrit 实例内有效)。
- URL 中的数字(如
跨项目依赖
/related端点可以跨越项目边界(如果依赖的确跨项目)。此时changes数组中project字段可能变化,需要正确处理。上面的脚本已经根据每个元素的project来构建 ID,可以自然支持。Change 有多个 Patch Set
如果某 Change 被多次 push 过,_revision_number会大于 1。通常我们想操作最新的 patch set,也就是current。我们一直用current是安全的。Submit 操作幂等性
对已经合入的 Change 再次调用 Submit,会返回 200 且不进行任何操作(或返回change already closed的 409)。可在脚本中捕获并继续。
十、总结
通过深入解析依赖链的本质、Related API 的细节、顺序合入的原因以及各种异常处理,我们可以构建一个非常稳健的“一键合入链”工具。核心要点:
- 使用
/related端点获取按序排列的所有相关未合入 Change。 - 严格按照祖先优先的顺序执行 Submit。
- 考虑项目特定标签、CI 等待和合并冲突等现实问题,增强脚本的容错和等待逻辑。
把这一套流程封装为企业内部的 ChatOps 命令或 CI 流水线中的一个步骤,能够极大地提升多提交依赖场景下的研发效率,减少因手动操作顺序错误导致的反复回滚或等待。