GitHub 和 Gerrit 都是诞生于 2008 年的代码平台,两个平台各自形成了相互独立的生态。GitHub 及其模仿者们成为行业主流,托管着大多数开源项目和商业项目的源代码,而 Gerrit 也有一众“粉丝”,像大名鼎鼎的安卓(Android)、OpenStack、Golang等。虽然 GitHub 和 Gerrit 都是 Git 仓库的托管和研发协同平台,但是二者背后的技术大相径庭。采用 GitHub 模式的代码平台的后端使用原生 Git(cgit)实现,而 Gerrit 则采用 JGit(用 java 重新实现 Git 接口)实现。再有两者理念不同,一种采用分布式协同,一种是集中式协同。二者的详细对比参考下面的表格:
我们可以看出 GitHub 模式和 Gerrit 模式各有优劣。那么能否有两全其美的代码平台呢?Git 2.29 让这成为可能。
1.Git2.29的新功能,让Git“牵手”Gerrit
Git 2.29.0 于 2020年10月发布,其中包含了两个阿里巴巴贡献的新特性。阿里巴巴贡献的新特性让 Git 牵手 Gerrit,让 GitHub 模式的代码平台可以像 Gerrit 一样工作。
1.1服务端新钩子proc-receive
Git 2.29 在服务端增加了 proc-receive 钩子。对Git原理熟悉的用户可能知道 Git 服务端经常使用的两个钩子:pre-receive 和 post-receive,在 git 推送时服务端运行的这两个钩子会进行前置检查(授权检查等)和后处理(发送通知、触发构建等)。而新引入的 proc-receive 钩子的执行顺序,是介于这两个钩子之间,用于替代 Git 内置功能完成分支(引用)的更新操作。这个新钩子提供给 Git 代码平台更为强大的服务端定制能力,存在丰富的想象空间。
例如:一个用户使用如下命令向服务端推送:
git push origin HEAD:refs/for/master
服务端如果是 Git 2.29 之前的版本,会直接在服务端仓库中创建名为 refs/for/master 的引用。而 Git 2.29 版本引入的 proc-receive 钩子,会接管 Git 更新引用的操作。proc-receive 钩子能做什么,完全取决于开发者的想象:
创建一个代码评审,并在仓库中产生名为 refs/pull/123/head 的引用,便于用户下载相关代码。
或者,推送包含的每一个提交都产生一个独立的代码评审,就像 Gerrit 那样。每个评审都产生类似 refs/changes/ 的引用。
或者,不在服务端产生任何引用,而是将用户新增提交以邮件方式发到邮件列表,类似 GitGitGadget [1] 那样。
那么如何能开发一个 proc-receive 钩子呢?相比 pre-receive 和 post-receive 钩子,proc-receive 钩子实现难度稍微大一些,因为它和 Git 服务端程序 git-receive-pack 有着复杂的双向通讯:服务端程序调用钩子,将 git push 的命令以及 push-options(如果有的话)发送给钩子,然后钩子调用 API 替代 Git 完成引用的更新。如下图所示:
https://ucc.alicdn.com/images/lark/0/2020/png/158866/1602674624880-e7c314a7-b3c3-4d34-8e58-a9c0211356ea.png
1.2客户端新能力report-status-v2
阿里巴巴在给 Git 社区贡献的第一个版本中,只在服务端引入新的钩子,并未修改客户端相关代码。为了能让社区接受修改,我以实现 Gerrit 的类似功能作为卖点向社区进行“推销”。Junio(Git 维护者,Google)第一时间承认这个贡献的价值:
And I think it is reasonable to add a new hook that takes over the whole flow in "git receive-pack" to do so.
同时指出疑问:向 Gerrit 推送一个引用A (refs/for/master),Gerrit 创建了另外的引用B (refs/changes/1/123),那么 Gerrit 是如何告诉客户端正确地更新本地跟踪分支的?
How do Gerrit folks deal with the "we pushed to the server, so let's pretend to have turned around and fetched from the same server immediately after doing so" client-side hack, by the way?
只有屈指可数的人才能像 Junio 这样发出灵魂的拷问!
于是在后续的代码评审中,与 Junio 以及 GitHub 的 Jeff King (Peff) 之间进行了多次交流,代码迭代了19个版本[2],为 Git 服务端和客户端新增了一个能力:report-status-v2。
简单的说,在老版本的 report-status 能力下,如果客户端发送推送命令要求服务器更新 A 分支(如: refs/for/master),而服务端转而创建了分支 B(refs/changes/1/123)。这种情况下,服务端也只能通知客户端分支A被创建,而非分支B,否则客户端会报错:“服务端没有按照我的要求去执行”。
扩展后的 report-status-v2,服务端可以报告给客户端实际修改的分支,可以报告不同的分支初始指向和最新指向,甚至客户端的一条命令可以对应多条分支的更新。支持该能力的客户端也能正确地将服务端实际更新的分支显示出来。
2.云效Codeup是业界第一个支持git 2.29新功能的代码平台
阿里云云效Codeup(https://codeup.aliyun.com)是业界首个支持 Git 2.29 新功能的代码平台。当用户执行 git push 命令时,特殊的目标分支会触发服务端 proc-receive 钩子,完成特定功能。
2.1命令行创建代码评审
在云效Codeup的“新建合并请求”按钮的下方,有一条低调的提示,如图:
https://ucc.alicdn.com/images/lark/0/2020/png/158866/1602687193823-9bfe6e3a-c302-4cf4-aebe-2f0c8e4c73ac.png
参照提示信息的说明,用户会看到用标准的 Git 命令行就可以直接在仓库中创建代码评审。例如用户执行下面命令将当前(HEAD)的更改推送到服务端,向服务端的 master 分支创建代码评审: git push origin HEAD:refs/for/master/local/branch
说明:
1.引用表达式的目标分支包含特殊的前缀“refs/for/",用于向远程仓库特定分支“master”发起代码评审。其中的“local/branch”通常写做客户端的本地分支名。多次git push请求,如果是相同用户、相同的目标分支、相同的“local/branch",则对应用同一个pull request。
2.此外Codeup还支持“refs/drafts/"、“refs/for-review/”等特殊前缀。前缀“refs/drafts/”的格式和“refs/for/”类似,也是针对目标分支创建或者更新pull request,区别在于创建的pull request处于草稿状态,只能发表评审意见,不能合入。前缀“refs/for-review/”后面跟指定的pull request ID,用于更新指定的pull request。
2.2AGit-Flow工作流
使用上面介绍的命令行创建代码评审,可以实现无需仓库派生、无需特性分支、无需特殊授权设置,完成代码评审的创建和合入。阿里巴巴代码平台上支持的这种代码协同模式,我们称之为AGit-Flow。
图中的两个角色,一个是开发者,另外一个是评审者。
开发者通过如下操作,创建和更新pullrequest:
1.开发者克隆仓库。
2.本地仓库内开发,创建提交。
3.工作区中执行命令,推送本地提交到服务器。
4.服务器自动创建新的代码评审(例如:pullrequest#123)。
5.开发者根据评审意见,在本地工作区继续开发,新增或修改提交。
6.工作区中再次执行gitpr命令,推送本地提交到服务器。
7.服务器发现目标分支上已经存在来自同一用户、同一本地分支的pullrequest,因此用户此次推送没有创建新的pullrequest,而是更新已经存在的pullrequest。
代码评审者,不但可以给出评审意见,也可以直接发起对评审代码的修改,更新pullrequest:
8.代码评审者执行gitdownload123下载编号为123的pullrequest到本地仓库。
9.代码评审者本地修改代码后,执行gitpr--change123命令,将本地修改推送到服务端。
10.服务端接收到代码评审者的特殊gitpush命令,更新之前由开发者创建的pullrequest。
11.项目管理者通过点击pullrequest评审界面的合并按钮,将pullrequest合入master分支。master分支被更新,同时关闭pullrequest。
2.3GitHub是否会引入Git2.29的新功能?
GitHub 引入 Git 2.29 新功能没有那么快,原因是 GitHub 的架构是分布式三副本架构,使用的是定制版本的 Git,不能通过升级到 2.29 来支持 proc-receive 钩子,需要另行开发。
当 proc-receive 特性在 Git 社区评审过程中,我邀请了 GitHub 的 Jeff King 参与代码评审。我在邮件中提到了如何在分布式多副本架构中引入 proc-receive 钩子的建议,因为我知道 GitHub 的分布式三副本和阿里巴巴的代码平台的分布式架构都面临 proc-receive 钩子可能被多次执行的问题。我们采用的路径是对 Git 协议进行扩展以实现 proc-receive 钩子执行的幂等性。Jeff King 在回复中介绍了 GitHub 的后端实现:
We do run receive-pack on each replica backend. We have a hacky patch for a config option that tells receive-pack to just skip the actual ref-transaction, leaving it up to the proxy layer to do. I've been pushing for us to actually abandon receive-pack entirely, since most of its heavy lifting can be done by sub-programs (for-each-ref for the advertisement, index-pack to receive the pack, and update-ref to update refs). But it's a non-trivial change, and the benefits are only moderate, so it hasn't quite been worth the effort yet.
就是说 GitHub 的分布式多副本服务器上的 git-receive-pack 是修改版本,并不执行引用更新的操作,而是由代理层执行,主要目的是为了避免 pre-receive、post-receive 等钩子的多次执行。阿里巴巴的多副本方案和 GitHub 多副本实现不同,我们的实现可以复用大部分 git-receive-pack 的功能。相关讨论如下:
Thanks to Peff for providing technical details of the architecture. I understand that "receive-pack" of GitHub backend is not involved in references update (executing the commands), so the "proc-receive" hook won't be turned on for GitHub's architecture. While in our architecture (inspired by "spokes" of GitHub), the proxy will deliver > not only packfile, but also commands to all three replicas. The proxy will execute "receive-pack" on the replica with a special argument, so the proxy can talk with "receive-pack" with an extended protocol. After running pre-receive hook and release the packfile from quarantine, the replica will stop and wait for the proxy to coordinate. After creating a distributed lock, the proxy will tell all the replicas continue to update the references. One problem we met is the proc-receive and the post-receive hook must be executed once. We > can make the execution of the hooks idempotent, or let only one of the > replica run the hook. We choose the latter. OK, that makes more sense. We solve that by not updating the refs at all via receive-pack (which gives us flexibility to run our own hooks separately on just one replica, etc).
OK, that makes more sense. We solve that by not updating the refs at all via receive-pack (which gives us flexibility to run our own hooks separately on just one replica, etc).
2.4对于Gerrit会有什么影响么?
Gerrit 拥有两个核心特性,一个是集中式的工作流,一个是逐提交评审。集中式工作流可以通过 Git 2.29 的新功能在 GitHub 生态中推广,而 Gerrit 独特的逐提交评审界面依旧具有强大的生命力。
Git 2.29 版本包含的 report-status-v2 特性,可以为 Gerrit 用户带来新的体验。可以预见 Gerrit 会在服务端增加 report-status-v2 相关实现以便更好地适配 Git 新客户端。
3.用git-repo扩展Git命令集
Gerrit 生态包含多款客户端工具,例如:Google 为安卓项目开发了名为 repo 的客户端工具实现多仓库管理;OpenStack 社区开发了名为 git-review 的工具,以便简化 Gerrit 工作流的命令行操作。
我们也为阿里巴巴的 AGit-Flow 工作流设计了一款名为 git-repo 的客户端工具,这款工具既能像 OpenStack 社区的相关工具那样对单仓库执行,也能像 Android 社区的 repo 那样实现多仓库项目的协同。
我们将git-repo开源,仓库地址:https://github.com/alibaba/git-repo-go
关于git-repo的安装和使用,访问网址:https://git-repo.info
git-repo 除了可以适配阿里巴巴的代码平台(如:云效Codeup)、Gerrit 之外,还可以通过扩展支持其他实现了 Git 2.29 新特性的代码平台,详见 git-repo 相关文档。
未来已来,全新的Git体验,访问云效Codeup。
本文作者:蒋鑫