Gerrit审查流程
AOSP 的代码审查不是把本地 branch 直接推送到远端 branch。开发者先在某个 Git project 中创建 topic branch,提交带 Change-Id 的 commit,再由 repo upload 推送到 Gerrit 的 refs/for/<target-branch>。Gerrit 将提交转换为 change;后续 amend 并重新上传,会为同一个 change 生成新的 patch set。
这里有四个容易混淆的对象:
| 对象 | 所在位置 | 作用 |
|---|---|---|
| Topic branch | 本地 Git project | 组织待上传 commit |
| Commit | Git 对象 | 保存一次代码快照和提交消息 |
| Change | Gerrit | 聚合针对同一目标分支的审查 |
| Patch set | Gerrit change 内部 | 同一 change 的某次修订 |
repo初始化与同步 已说明 manifest revision 与 worktree, repo日常命令实战 已说明 repo start、status、diff 和 branches。本文从一个干净 topic branch 开始,追踪提交、上传、修订和下载的完整流程。
本文面向已经能在固定 AOSP checkout 中创建 topic branch、但还没有把 Git commit、Gerrit change 和 patch set 区分开的读者。问题边界是客户端如何形成、上传和取回一次审查变更;不覆盖 Gerrit 服务端权限配置、submit rule 或 CI 实现。读完后,读者应能从 repo upload 入口追到 Project.UploadForReview、refs/for/<branch> 和同一 Change-Id 的 patch set,并能判断失败发生 在本地 branch、客户端上传还是服务端审查边界。
若要继续回到构建输入与产物,可阅读 lunch目标与编译变体;它解释 审查中的源码如何在固定 product/release/variant 下进入可比较的构建环境。
1. Project
1.1 project确认
cd path/to/source
repo list .
git status --shortAOSP 的一个功能修改可能跨多个 project。每个 project 都需要独立 commit,也会在 Gerrit 中形成 独立 change。跨 project 修改不能压成一个 Git commit。
1.2 Project入口
repo start fix-display-race frameworks/base frameworks/native这会在两个 project 中分别创建同名 branch。branch 名相同只是便于组织,它们没有共享 Git 历史。 如果修改只涉及一个 project,应只对该 project 创建 branch。
提交前先检查:
repo status frameworks/base frameworks/native
repo diff frameworks/base frameworks/native
repo branches frameworks/base frameworks/native2. Change 身份
2.1 Change-Id创建
Git commit hash 会在 git commit --amend 后变化。Gerrit 使用提交消息中的 Change-Id,结合 repository 和 target branch,把修订后的 commit 归入同一个 change:
Fix display transaction ordering
Explain why the previous ordering could publish stale state and how the
new barrier preserves visibility.
Change-Id: I0123456789abcdef0123456789abcdef01234567同一个 Change-Id 不应复制到无关修改中。审查中的修改应在原 branch 上 amend,保留原 Change-Id。
2.2 commit hook
git-repo 自带的 commit-msg hook 会计算标识,并在提交消息缺少合法 Change-Id/Link trailer 时插入。
源码文件:git-repo/hooks/commit-msg
相关函数/类型:Change-Id generation
random=$({
git var GIT_COMMITTER_IDENT
echo "$refhash"
cat "$1"
} | git hash-object --stdin)
# 说明:这里省略可选的 Gerrit Link trailer 分支。
token="Change-Id"
value="I$random"
pattern=".*"
# 说明:amend 时已有合法 trailer,hook 不会重新生成 Change-Id。
if git interpret-trailers --parse < "$1" |
grep -q "^$token: $pattern$" ; then
exit 0
fi
# ...commit 后没有 Change-Id 时,检查 project 的 .git/hooks/commit-msg 是否存在且可执行。正确修复是 安装 hook 后 amend 提交消息,而不是从另一条 change 复制 Change-Id。
源码文件:git-repo/hooks/commit-msg
相关函数/类型:trailer 检查
refhash="$(git rev-parse HEAD 2>/dev/null || git hash-object -t tree /dev/null)"
random=$({ git var GIT_COMMITTER_IDENT; echo "$refhash"; cat "$1"; } |
git hash-object --stdin)
reviewurl="$(git config --get gerrit.reviewUrl)"
if test -n "$reviewurl"; then
token="Link"
value="${reviewurl%/}/id/I$random"
pattern=".*/id/I[0-9a-f]\{40\}"
else
token="Change-Id"
value="I$random"
pattern=".*"
fi
if git interpret-trailers --parse < "$1" |
grep -q "^$token: $pattern$"; then
exit 0
fi
# ... 使用 interpret-trailers 写入 trailer,并原子替换提交消息hook 的 owner 是本地 Git commit,而不是 Gerrit 服务端;它只在提交消息缺少合法身份时写入 trailer,不能 替代服务端权限和目标分支校验。可执行验证是准备一条无 Change-Id 的测试提交,断言 amend 后新增一条 40 位十六进制 Change-Id;再 amend 一次,断言已有值保持不变。
2.3 Patch set
git add <files>
git commit --amend
repo upload --current-branch <project>amend 产生新 commit hash,但保留原 Change-Id。Gerrit 将新上传识别为同一 change 的新 patch set。
3. 上传路径
3.1 upload分支
repo upload 在指定 project 中寻找尚未发布的 topic branch,并计算相对 tracking base 新增的 commits。
# 当前project中的候选branch
repo upload .
# 只上传当前branch
repo upload --current-branch frameworks/base
# 明确本地branch
repo upload --branch fix-display-race frameworks/base
# 只执行本地检查和Git dry-run push
repo upload --dry-run --current-branch frameworks/base不传 project 时,repo 会搜索 manifest 选中的全部 project。大型工作区应明确 project 和 branch, 避免选择无关 change。
3.2 上传前检查
upload 会检查:
- 当前 branch 是否存在并跟踪 remote;
- remote 是否配置 Gerrit review URL;
- branch 是否包含未发布 commit;
- 是否存在需要确认的未跟踪文件;
- commit 数量是否异常;
- label、reviewer 和目标分支是否合法。
默认异常 commit 阈值是 5。超过阈值通常意味着 rebase 或 branch 起点不符合预期。
3.3 refs/for
Project.UploadForReview 最终构造 Gerrit refspec:
源码文件:git-repo/project.py
相关函数/类型:Project.UploadForReview
if dest_branch is None:
dest_branch = self.dest_branch
if dest_branch is None:
dest_branch = branch.merge
# 说明:目标分支归一化为refs/heads。
if not dest_branch.startswith(R_HEADS):
dest_branch = R_HEADS + dest_branch
url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
if url is None:
raise UploadError("review not configured", project=self.name)
cmd = ["push", "--progress", "--no-follow-tags"]
if dryrun:
cmd.append("-n")
# ...
dest_branch = dest_branch[len(R_HEADS):]
# 说明:topic branch推送到refs/for,而不是直接覆盖目标branch。
ref_spec = f"{R_HEADS + branch.name}:refs/for/{dest_branch}"
opts = []
if topic is not None:
opts += [f"topic={topic}"]
opts += ["r=%s" % person for person in people[0]]
opts += ["cc=%s" % person for person in people[1]]
if wip:
opts += ["wip"]
if opts:
ref_spec += "%" + ",".join(opts)
cmd.append(ref_spec)
GitCommand(self, cmd, bare=True, verify_command=True).Wait()refs/for/<dest> 表示“为目标分支创建或更新 review”,不是直接更新 refs/heads/<dest>。
4. 目标分支
4.1 dest-branch
默认目标来自 project 的 dest-branch,未配置时回退到 topic branch 的 merge ref。需要显式覆盖:
repo upload --current-branch \
--destination android-17.0.0_r1 \
frameworks/base目标错误可能生成错误 change 或被 Gerrit 拒绝。上传前应检查 manifest 和 branch 配置。
4.2 目标分支入口
repo upload --current-branch \
--reviewers reviewer@example.com \
--cc team@example.com \
--topic display-race \
--hashtag graphics \
--wip \
frameworks/base| 选项 | 作用 |
|---|---|
--reviewers | 请求已注册用户审查 |
--cc | 添加关注者 |
--topic | 组织多个相关 change |
--hashtag | 增加可搜索标签 |
--label | 上传时附加合法 label |
--wip / --ready | 设置或清除 Work in Progress |
topic 只负责组织,不会把跨 project commits 合并成原子提交。--no-cert-checks 会关闭 TLS 证书 验证,不应作为普通网络错误解决方案。
5. 上传边界
上传完成不表示 change 已合入。权限检查、review、CI、submit rule 和最终 submit 属于 Gerrit 服务端 流程,不能只从 git-repo 客户端源码推导。
6. 修改审查中change
6.1 Change-Id保留
git status --short
git add <files>
git commit --amend
repo upload --current-branch <project>删除 Change-Id 后重新生成,通常会创建新 change,而不是更新现有审查。
6.2 commit/change
topic branch 上的每个 commit 通常对应独立 Gerrit change。多个 commit 可以按依赖顺序形成 change 链。修改较早 commit 并 rebase 后,后续 commit hash 也会变化,需要重新上传对应 patch sets。
上传前查看相对 upstream 的 commit 栈:
git log --oneline --decorate <upstream>..HEAD
git log -1 --format=full
repo status <project>7. 下载 patch set
7.1 参数和模式
repo download <project> <change>/<patchset>| 模式 | 命令 | 结果 |
|---|---|---|
| Checkout | repo download project change/ps | 检出 patch set commit |
| 新 branch | repo download -b review-x project change/ps | 从 patch set 创建 branch |
| Cherry-pick | repo download -c project change/ps | 应用到当前 branch |
| Revert | repo download -r project change/ps | 生成反向修改 |
| Fast-forward | repo download -f project change/ps | 仅允许快进 |
-x 只与 cherry-pick 搭配,用于记录 origin。
7.2 实现入口
源码文件:git-repo/subcmds/download.py
相关函数/类型:Download._ExecuteHelper
for project, change_id, ps_id in self._ParseChangeIds(opt, args):
dl = project.DownloadPatchSet(change_id, ps_id)
# ...
if opt.cherrypick:
# 说明:cherry-pick应用到当前branch。
project._CherryPick(
dl.commit,
ffonly=opt.ffonly,
record_origin=opt.record_origin,
)
elif opt.revert:
project._Revert(dl.commit)
elif opt.ffonly:
project._FastForward(dl.commit, ffonly=True)
else:
if opt.branch:
# 说明:-b从patch set创建明确branch。
project.StartBranch(opt.branch, revision=dl.commit)
else:
project._Checkout(dl.commit)默认 checkout 适合快速查看,但可能处于 detached 状态。需要继续修改时,-b 或 cherry-pick 到 明确 branch 更清晰。
8. 失败边界
8.1 审查失败
| 现象 | 入口 |
|---|---|
| not currently on a branch | 当前是 detached HEAD,创建或切换 topic branch |
| branch does not track a remote | 检查 branch merge/remote,优先由 repo start 创建 |
| remote has no review URL | 检查 manifest remote 的 review |
| missing/wrong Change-Id | 检查 commit-msg hook,并 amend 提交 |
| reviewer/label rejected | 检查注册状态、权限和 label 语法 |
| too many commits warning | 检查 branch 起点和 rebase 历史 |
8.2 审查测试
tests/test_subcmds_upload.py 验证:
UploadError转换为命令级UploadExitError;GitError转换为命令级UploadExitError;- 未预期异常不会被错误吞掉。
这 3 个测试证明客户端错误会被提升到命令级失败;Gerrit 服务端是否接受 change,还取决于权限、 目标 branch、review label 和 submit rule。
测试输入分别是 mock upload 返回 UploadError、底层 Git 返回 GitError 和抛出未预期异常;断言前两者 变成 UploadExitError,第三者继续向上抛出。这个安排/动作/断言只覆盖 repo upload 的错误分类,不证明 真实 Gerrit 已创建 change,也不证明网络重试策略。
9. change 收束
一项修改从本地进入 Gerrit 后,会经历三次身份保持:
| 阶段 | 必须保持的关系 | 关系破坏后的结果 |
|---|---|---|
| 本地开发 | topic branch 基于正确 manifest revision | diff 混入旧提交或错误基线 |
| 首次上传 | 一个可审查 commit 对应稳定 Change-Id | 创建错误 change 或拆分关系混乱 |
| patch set 更新 | amend 原 commit 并上传到同一目标 branch | 新建 change,或更新到错误审查目标 |
上传前,repo status、repo diff 和 commit log 应共同证明修改范围、提交粒度与基线;服务端 reviewer、 topic、WIP 和 label 则描述审查状态。跨 project 修改不能依赖一个“全局 commit”,而要用多个 changes 及其依赖顺序表达。
收到审查意见后,在原 topic branch 上 amend 并重新上传。获取他人 patch set 时,根据“只查看、继续 修改、应用到当前 branch、撤销或只允许快进”选择 download 模式,再重新检查 status、diff 和测试。 这样出现失败时,能够明确问题属于本地 branch、commit/Change-Id,还是 Gerrit 目标与权限。
10. 源码入口
在不连接 Gerrit 的条件下,仍可以完成一条可执行的闭环:在临时 project 中创建 topic branch, 提交一次带 Change-Id 的 commit,修改同一 commit 后执行 amend,并比较两次 commit hash 与 Change-Id。断言应是 hash 改变而 Change-Id 保持不变;这只证明客户端身份保持逻辑,不证明远端 Gerrit 一定接受 change。再用 git show --format=fuller 和 git diff HEAD^ 保存输入与结果, 读者可以把每个对象重新映射到本文的入口、所有者和消费者。
