Skip to content

Gerrit审查流程

串联 AOSP topic branch、Change-Id、repo upload、Gerrit patch set 和 repo download,说明代码审查的完整边界与常见失败。

基于android-17.0.0_r1
AndroidAOSPGerritrepo

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
CommitGit 对象保存一次代码快照和提交消息
ChangeGerrit聚合针对同一目标分支的审查
Patch setGerrit 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确认 ​

bash
cd path/to/source
repo list .
git status --short

AOSP 的一个功能修改可能跨多个 project。每个 project 都需要独立 commit,也会在 Gerrit 中形成 独立 change。跨 project 修改不能压成一个 Git commit。

1.2 Project入口 ​

bash
repo start fix-display-race frameworks/base frameworks/native

这会在两个 project 中分别创建同名 branch。branch 名相同只是便于组织,它们没有共享 Git 历史。 如果修改只涉及一个 project,应只对该 project 创建 branch。

提交前先检查:

bash
repo status frameworks/base frameworks/native
repo diff frameworks/base frameworks/native
repo branches frameworks/base frameworks/native

2. Change 身份 ​

2.1 Change-Id创建 ​

Git commit hash 会在 git commit --amend 后变化。Gerrit 使用提交消息中的 Change-Id,结合 repository 和 target branch,把修订后的 commit 归入同一个 change:

text
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

sh
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 检查

sh
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 ​

bash
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。

bash
# 当前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

python
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。需要显式覆盖:

bash
repo upload --current-branch \
  --destination android-17.0.0_r1 \
  frameworks/base

目标错误可能生成错误 change 或被 Gerrit 拒绝。上传前应检查 manifest 和 branch 配置。

4.2 目标分支入口 ​

bash
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保留 ​

bash
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 栈:

bash
git log --oneline --decorate <upstream>..HEAD
git log -1 --format=full
repo status <project>

7. 下载 patch set ​

7.1 参数和模式 ​

bash
repo download <project> <change>/<patchset>
模式命令结果
Checkoutrepo download project change/ps检出 patch set commit
新 branchrepo download -b review-x project change/ps从 patch set 创建 branch
Cherry-pickrepo download -c project change/ps应用到当前 branch
Revertrepo download -r project change/ps生成反向修改
Fast-forwardrepo download -f project change/ps仅允许快进

-x 只与 cherry-pick 搭配,用于记录 origin。

7.2 实现入口 ​

源码文件:git-repo/subcmds/download.py

相关函数/类型:Download._ExecuteHelper

python
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 revisiondiff 混入旧提交或错误基线
首次上传一个可审查 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^ 保存输入与结果, 读者可以把每个对象重新映射到本文的入口、所有者和消费者。