新一代高效Git协同模型AGit-Flow详解

Git工作流概述及AGit-Flow的优势

目前,Git已成为源代码管理的标准和基础设施。“为什么Git能这么成功”?Git的创建者Linux在Git十周年的一次采访中,道出了其中的奥秘:

The big thing about distributed source control is that it makes one of the main issues with SCM’s go away – the politics around “who can make changes.”

他认为Git能成功最关键的不是因为它更快、更安全,也不是因为Git是分布式的,而是解决了“到底谁能够贡献代码”这个问题。传统的集中式版本控制系统只能针对核心用户开放写授权,长期来看这对项目做大、做强是不利的。而Git改变了传统版本控制系统不能够让跟多开发者贡献代码这个顽疾,让“只读用户”也可以通过“代码评审”的方式参与到项目开发中。

当前业界有两种最常用Git 工作流:GitHub和Gerrit。他们都具备仓库的授权模型简单,只读用户可参与代码贡献的特点。



如上图所示,我对这两种GIT工作流做了优劣势分析:
**
代码评审模式不同:**
GitHub 的代码评审称为 “pull request”,每个特性改动生成一次代码评审。
Gerrit 的代码评审称为 “Change”,每个提交生都会生成一个“变更单”,这个变更单就是一次代码评审。

工作流类型不同:
GitHub 的工作流属于分布式,当开发者需要参与项目的时候,虽然没有“写”的权限,但是可以通过“Fork”的方式创建一个个人仓库(派生仓库),他就可以在这个派生仓库中去创建代码分支,创建pull request。GitHub 底层采用的是原生的 Git(即 CGit)。

Gerrit 的工作流是集中式,所有用户工作在统一管控的集中式仓库中。Gerrit 要求用户在本地克隆仓库中安装一个 “commit-msg” 钩子,以便在生成的提交中插入唯一的“Change-Id”,向服务器推送要使用特殊的 git push 命令。Gerrit 采用的是 JGit(Java 的 Git 实现)。

**各自优势:
**
GitHub简单易用,使用标准 Git 命令即可完成代码贡献;对派生仓拥有完全控制力,不受上游项目影响;可以创建跨项目的开源社区,全球开发者大协同,这也是GIT可以形成全球最大的开源社区的原因之一。

Gerrit 因为采用集中式的工作流,管理员可以对项目进行严格管控,可以严格控制谁可以访问仓库,谁可以对我的仓库做贡献。Gerrit另外一个优势是可以实现多仓库项目管理。我们知道“Android”项目具有1000多个仓库,就是使用Gerrit进行管理的,很难想象如何使用GitHub来管理Android的1000多个仓库。

各自劣势:
正如前面所说GitHub很难管理类似 Android 的多仓库项目。另外因为GitHub 使用派生仓库的工作模式,会产生服务端数据冗余的问题。

Gerrit 需要集中管控,由管理员负责创建项目,而普通用户不能创建项目,这就使得一个 Gerrit 实例通常只管理一个项目或一个组织内的项目,难以在项目之间形成代码复用,也很难汇集跨项目的开发者组成开发者社区。

通过对GitHub和Gerrit等Git工作流的调研和学习,我们“取长补短”创建了阿里巴巴的Git工作流,即AGit-Flow。


在阿里巴巴,我们喜欢 pull request、CGit,喜欢在命令行直接创建代码评审的集中式工作流,喜欢开放的开发者社区。我们不喜欢 “commit-msg” 钩子方式关联提交的代码评审,我们不喜欢一个一个分散的代码平台。

我们还开发了配套的客户端工具 “git-repo”,既能在单仓库下工作,又支持类似 Android 的多仓库项目协同。

在阿里巴巴,我们如何使用AGit-Flow

下面给大家演示一下,在阿里巴巴内部,我们是如何使用AGit-Flow工作的。


我们首先使用Git标准命令将仓库克隆到本地,然后在本地仓库内开发,创建提交。在工作区中执行 git pr 命令,推送本地提交到服务器,服务器端会自动创建一个新的代码评审,例如:pull request #123。团队的代码评审者就可以打开编号“123”的代码评审提交评审意见。开发者根据评审意见,在本地工作区继续开发、新增或修改提交。工作区中再次执行 git pr 命令,推送本地提交到服务器。服务器发现目标分支上已经存在来自同一用户、同一本地分支的 pull request,因此用户此次推送没有创建新的 pull request,而是更新已经存在的 pull request。

如果经过多次修改,代码依然不OK。代码评审者也可以直接发起对评审代码的修改,帮助原开发者更新 pull request。代码评审者可以使用 git download 123 下载编号为 123 的 pull request 到本地仓库,代码修改完毕后,执行 git pr --change 123 命令,将本地修改推送到服务端。服务端接收到代码评审者的特殊 git push 命令,更新之前由开发者创建的 pull request。项目管理者通过点击 pull request 评审界面的合并按钮,将 pull request 合入 master 分支。master 分支被更新,同时关闭 pull request。

使用AGit-Flow工作流无需在服务器上创建新的分支,不需要给新加入的同学创建写入权限,可以对所有开发者只分配读取权限,然后通过创建代码评审再合并到主干的方式更新主干代码。这也是目前比较流行的主干研发模式。

目前大家可以通过云效代码管理平台(Codeup)来实现AGit-Flow工作流。

AGit-Flow实现原理



AGit-Flow是如何实现的呢? 首先客户端使用特殊的 git push 命令向服务端发起代码推送请求,触发 AGit-Flow 工作流。为什么说这个git push 命令特殊呢?因为它的目标分支是一个包含特殊的前缀 “refs/for/"的代码分支,分支后面又跟了一个 “” 。这个“”用于区分本地分支名,不同开发者提交的代码评审包含不同的“”,所以不会相互覆盖。

我们还可以通过“-o”来传递不同的参数,比如可以指定由谁来对我的代码进行评审,我的代码评审会关联哪个“issue”。这些操作都可以通过“git push”命令完成,后来我们发现这个git push 命令比较复杂,于是我们封装了一个命令行工具git-repo。目前git-repo已经对外开源,大家可以免费使用。

接下来这个“Push”命令就会打入到服务端,服务器端会启动一个进程“git-receive-pack”。(我们对服务器端的前端授权模块做了一些修改,使其能够识别这个特殊的git push 命令,允许只读用户也能“Push”)如上图所示,“git-receive-pack”我做了星号标记,因为它是一个特殊的“git-receive-pack”。当它发现 push命令的目标是一个特殊的引用后,它不会走Git原来内部的工作流,而是走“外部钩子”。通过“外部钩子”完成一些好玩的操作,比如创建代码评审。

在今年(2020年)3月份,我们已经把这个修改过的git-core贡献给了Git社区,目前正在评审中,后续Git新版本会包含这个新特性:proc-receive。此特性已经历经15次迭代,从最初的服务端扩展到集合了服务端扩展和客户端协议升级的完整解决方案。我们将这个技术开源,一方面繁荣了Git 生态,让更多人能从阿里巴巴的技术中获益;另外一方面,阿里巴巴也得到了收益,我们的代码贡献得到了Git客户端的支持, Git适配了我们新的玩法,让包含阿里巴巴在内的Git用户得到了更好的体验。

AGit-Flow实现的技术细节

为了解释AGit-Flow实现的技术细节,我们先来了解一下git push命令原有的实现方式。
![05.jpg](ucc.alicdn.com/pic/
developer-ecology/8552743fa0b24934bd9a93146ffedef4.jpg)

如上图左侧所示, git push命令包含两部分信息,一个是被打成包的数据,叫“packfile”;另一个是推送的命令,叫“commands”。 “packfile”和“commands”被推送到服务端后,“packfile”会走左侧的路径,首先进入“quarantine”中,进行“隔离”。 当“commands”经过“pre-receive” 钩子检查,认为用户的权限OK、提交说明OK、提交修改的文件ok,“packfile”才会从隔离区释放出来,进入对象库(objects)。(如果 “pre-receive” 钩子脚本失败,则删除隔离区,并返回错误信息,终止推送命令的执行。)

接下来“commands”会传递给内置的 “execute_commands()” 函数,实现分支的创建、更新、删除等操作。然后通过“repor()”函数报告给客户端,最后执行 “post-receive”钩子脚本,完成事件通知。

新钩子,新生态。AGit-Flow 对 “git-receive-pack” 的源码做了改动,新的流程如下图所示:



当客户端执行新的git push命令后,“packfile”的传播路径没有改变,但是我们更改了"git-receive-pack"命令,增加了一个“过滤器”(图中漏斗部分)。过滤器将 “commands” 分成两组,一组是标准的Git命令(group1),一组是AGit-Flow特殊的命令(group2)。这两组命令经过“pre-receive” 钩子检查后,左侧普通的命令(group1)会执行Git内置的 execute_commands 函数,生成新的引用,进行分支的创建、更新等。右侧这个特殊的命令会调用一个新的外部钩子 “proc-receive” ,然后创建一个特殊的代码评审引用,如“refs/pull/123/head”,并且可以用过特殊的Git命令将它下载到本地。



我们为Git贡献的这个新特性,由三部分组成。第一个部分就是“过滤器”,通过在服务端新增新配置变量 "receive.procReceiveRefs"来实现。只要定义了这个特殊的配置变量,当客户端使用git push命令推送的时候,Git就会根据配置变量去匹配,当匹配到相应的命令时,这个命令就会走特殊流程。这个配置变量属于多值变量,例如阿里巴巴代码平台的设置是:

  • git config --add receive.procReceiveRefs refs/for
  • git config --add receive.procReceiveRefs refs/drafts
  • git config --add receive.procReceiveRefs refs/for-review

这三条配置变量对应git pr的三种推送模式,会产生标准的pull request、草稿模式的pull request,或者一个代码评审者想推送指定的pull request。





第二部分是proc-receive 钩子。我们这个钩子应该说是Git中有史以来最复杂的一个钩子,它可以和服务端(git-receive-pack)进行双向通讯。首先服务端和钩子会做“版本协商”,因为我们认为这个协议后续会升级,为了保证向后兼容,所以我们首先要协商一个版本。服务端告诉钩子我是哪个版本,客户端告诉钩子我支持哪个版本,后面Git就可以用相应的版本协议与钩子进行通讯。服务端会用“pkt-line”编码命令,这个命令是三段式的,包含老的ID,新的ID和需要更新的引用名称。服务端会把命令和参数发送给钩子。钩子会对这些命令和参数进行处理,这些处理由开发者以API调用的方式实现,然后将处理结果报告给钩子。钩子再把这个执行结果以特殊的方式报告给服务端。



展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java