本文标题:浅谈 Pull Request 与 Change Request 研发协作模式
本文链接:https://zoker.io/blog/talk-about-pullrequest-and-changerequest
关于 CodeReview 是什么,Gerrit 官方文档给了一个非常形象的解释,一个软件或者 Feature 的生产过程,如同一首歌的问世前一样,需要歌手反复录制不同的片段达到最佳的效果,要保证每一个片段都令人满意,并且要把所有的片段融合起来,最终目的就是把最好的版本提供给大家品鉴。Queen 乐队有一首《波西米亚狂想曲》,用了3周进行录制,其中有一些片段甚至重复录制了180多次,最终才有了这首经典。
同样的情况也在软件工程领域每天上演着,开发者们根据同事的反馈,一遍又一遍的修改、优化自己的代码,最终产出一个高质量版本的产品交付到用户手中,这就是 CodeReview(代码评审)所做的事情。
一件理论或者实践的推动,总是要有相应的价值产生。那么 CodeReview 能够给我们带来什么呢,来看看 Gerrit 的总结:
摘自: https://gerrit-documentation.storage.googleapis.com/Documentation/3.2.3/intro-rockstar.html#review
简单翻译一下:
PullRequest 最初是由 Github 推出的一种开源协作模式,目前 Gitee、Gitlab等平台都支持这种协作模式,而且这种模式目前更多的用在了团队或者企业的内部代码评审层面,这种协作模式一般是由开发者 Fork 一个仓库,或者在仓库内新起一个分支如zoker/feature-1
,无论是哪种方式,所有的大前提是:开发者没有目标分支的写权限,写权限是被严格控制的,必须经过 PullRequest 方式合入代码。在相应的分支研发并自测完成后,通过 Web 界面 提交一个 PullRequest 请求将这些代码合入到目标分支,这个过程会有自动化测试、自动编译构建、代码评审、代码改进、代码合并等一系列操作,最终完成代码向目标分支的合入。
ChangeRequest 是由 Gerrit 推出的一个概念,Gerrit 是为 AOSP(Android Open Source Project)写的,结合 Repo 工具用来管理庞大的安卓项目,多仓管理也是他的优势之一,但是更多的人把它视为代码评审神器,能够使每一个 Commit 都是可靠。这种模式同样需要严格管控目标分支的写权限,开发者可以在本地起一个分支进行开发,与 PullRequest 不同的是,ChangeRequest 不需要开发者到 Web 上进行评审请求的提交,推送一个 Commit 到对应的分支之后,会自动创建一个评审单。而且另外一个比较大的区别就是,ChangeRequest 的评审是针对单 Commit 评审,而 PullRequest 针对的是一个或者多个 Commit 的集中评审。
下面就以 Gitee 和 Gerrit 为例,从几个方面简单介绍下两者在日常研发协作上的区别
从使用上来讲,PullRequest 以及 ChangeRequest 的最大的差别在以下几点:
PullRequest 需要我们的研发人员在做完一项研发工作后,在推送相应的特性分支到 Gitee 后,需要在 Gitee 上进行评审单的创建;而 ChangeRequest 则不需要,因为在推送后,Gerrit 会自动创建一个评审,也就是一个 ChangeRequest。
PullRequest 由于是开发者直接提交的,所以可以在提交 PullRequest 的时候就写好各种描述信息;而 ChangeRequest 仍需要前往 Gerrit 的 Web 界面进行重新完善
Gitee 创建 PullRequest 界面
PullRequest 一般都是包含一个以上的 Commit 的评审,是一个 Commit 集合。当开发者接受到修改意见后,一般都是直接基于当前的提交继续进行提交,来处理代码的改进,Gitee 会自动更新 PullRequest 来更新这些改动,在一个 PullRequest 的生命周期中,可能会产生多个提交。因为在最终可交付的时候,整个 PullRequest 可能存在很多提交,这些提交可能是代码改进,也可能是冲突的合并,不过在合并的时候可以有多种形式的 Merge 方式,可以选择是将历史合入还是重新整合为一个 Commit 进行提交。
Gitee 多种合并方式的选择:
而对于 ChangeRequest 来说,则是针对于单 Commit 的评审,每个 Change 都只有一个提交。对代码的改进一般都是修改当前 Commit,也就是git commit --amend
,Gerrit 接受到新的提交后,会自动更新这个 Change,并且会保留之前的提交,作为本 Change 的一个 Patchset。同样,在一个 ChangeRequest 生命周期中,可能会有多个 Patchset 产生,不过在最终合并的时候合入的是经过多次修改后最优质的那个 Patchset。
Gerrit 的一个 Change 包含多个 Patchset:
那肯定会有人有疑问,我每次推送到同一个 ChangeRequest 的 Commit,由于修改了内容或者描述,CommitID 早就变了,Gerrit 如何辨别区分是同一个修改?这就是 Gerrit 设计上优秀的地方,虽然 CommitID 变了,但是可以通过 Git 的钩子插入一个 ChangeID,这个 ChangeID 就是识别同一个 Commit 的重要武器,详细可以参阅官方文档,这里不再赘述。
以上两个是在使用上最主要的差异,Gitee 提供的 PullRequest 功能对功能需求拆分度要求不高,因为我们可以在同一个 PullRequest 里面做很多事(这是不推荐的),但是对于 Gerrit 的 ChangeRequest 来说,需要对需求拆分的相对独立,需求粒度一定要小,相互之间不可以相互影响,否则对于按照 Commit 进行评审的模式来说,各种冲突依赖就乱套了。
从配置管理员的角度来看,Gitee 和 Gerrit 的配置管理差别还是比较大的,尤其是权限管理。
Gitee 对于权限的管理主要是从开发人员角色、分支权限等角度进行配置,相对来说比较简答,配置项清晰明了,可以通过简单的配置即可实现 PullRequest 协作流程的基础功能。从使用成本上来讲是非常高效的,各个权限隔离也比较清晰,就算是一个入门的配置管理员也能够快速理解并完成配置。
Gerrit 的配置相对来说就比较复杂了,但是更加灵活,各个配置项均可灵活配置,但是灵活带来的就是使用成本的提升,使用成本相对 Gitee 比较高。不过好在提供了全局的继承功能,能够在已经有配置模版的情况下快速覆盖,Gerrit 中可以自建用户组,并且可以对不同项目的不同用户组进行灵活的权限配置。坏处就是如果一不小心动到一个并不了解的配置项,可能就会造成代码权限的泄漏,所以对配置管理员的要求更高。
在一些大型的工程如安卓项目、鸿蒙OS等,将所有的代码放到一个代码库显然是不可能的事情,一方面是体积过大,无法高效管理,严重降低开发者的效率;另外一个重要的方面是权限管理,每个开发者都拥有一整个系统的代码显然是不合适的。
那么,该如何解决这种问题呢,Git 给出了他自己的两种解决方案:
Git Submodule 是目前用的比较多的一种解决方案,主要的功能就是将其他仓库的某个版本作为当前仓库的一个目录,挂在当前仓库下,所有的配置信息都存放在当前仓库的.gitmodules
文件下,包含了子仓库的信息以及映射的目录
[submodule "lib"]
path = lib
url = https://gitee.com/xxxxx/lib.git
这样做的好处是lib
目录可以单独进行权限的管理,不至于权限过于发散。Git Subtree 的实现类似,它是 Git 1.5.2 之后官方新增的一个功能,旨在替代 Git Submodule 管理共用仓库,两种方式都可以达到多仓管理的功能,只不过 Submodule 是引用,而 Subtree 是拷贝,具体的使用可以参见 Git - - subtree与submodule。Submodule 以及 Subtree 在主流的代码托管平台都支持,Gitee 也一样,毕竟这是 Git 的 Native Feature。
那么,为什么已经有了 Submodule 了,谷歌团队还要搞一个 Gerrit 出来呢?主要还是管理上的不方便,开发上的效率低下,所有才有了 Gerrit + Repo 这套工具。
如上所述,Repo 工具是谷歌开发的用于管理 Android 版本库的一个工具。Repo 并不是用来取代 Git的,它是使用 Python 对 Git 的一层封装,简化了对多个 Git 版本库的管理方式。Repo 主要是结合着 Gerrit 来使用,它以一个manifest.yml
为中心,通过对项目结构化的描述,达到与 Submodule 同样的效果,但是比 Submodule 更加灵活和方便。
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<remote name="gitee"
fetch="git@gitee.com:{namespace}"
autodotgit="true" /> <!--fetch=".." 代表使用 repo init -u 指定的相对路径 也可用完整路径,example:https://gitee.com/MarineJ/manifest_example/blob/master/default.xml-->
<default revision="master"
remote="gitee" /><!--revision为默认的拉取分支,后续提pr也以revision为默认目标分支-->
<project path="lib" name="lib" /> <!--git@gitee.com:{namespace}/{name}.git name项与clone的url相关-->
<project path="src" name="src" />
</manifest>
为什么说 Repo 主要是结合着 Gerrit 来使用呢?因为 Gerrit 的特性就是推送新的提交可以自动创建评审,那么在一个大型工程的研发流程下,一个功能经常性的需要跨仓库进行开发,开发完成后需要提交评审单进行评审。如果一个功能涉及到了三个仓库,那么在进行了对应的开发后,只需要执行 Repo 提供的相关命令,即可在 Gerrit 上产生映射在三个仓库的三个 ChangeRequest,并且还可以在 Repo 命令追加评审人、话题等一系列属性。
当然,Repo 工具如果作为批量仓库提交工具,也是可以在 Gitee 上使用的,但是推送完成之后呢?需要开发者手动创建三个 PullRequest!这无疑带来了非常大的开发成本,开发者也会因此而抱怨。所以 Gerrit + Repo 这套工具其实就是为了解决大型工程的项目管理及代码评审而设计的。
不过,在今年年中的时候,我们 Gitee 团队对 Repo 工具进行了 Fork Flow 的支持,让 Repo 工具可以在推送之后,自动在相关的仓库里面创建 PR,免去了复杂的创建流程,相关使用和实现可以参见 oschina/repo
前面我们介绍了可以使用 Submodule 或者 Submodule 来统一管理多仓的场景,在这种模式下,持续构建变得也容易了,毕竟所有的代码都是主仓库拥有的。这里以 Jenkins 为例,我们只需要配置 Gitee-Jenkins-Plugin 插件即可, Gitee-Jenkins-Plugin 是 Gitee 团队基于 GitLab Plugin 二开的一个 Jenkins 插件,包含但不限于以下功能:
可是,如果使用 Repo 工具在 Gitee 进行多仓管理呢?所有的仓库都是独立的,我们该如何知道哪些仓库变更了呢?在 oschina/repo 的实现中,我们也遇到了这个问题,当时与客户沟通提了这么个解决方案:用户在提交代码后,Repo 工具会自动创建 PullRequest,与此同时,开发者将仓库以及对应的 PullRequest 信息提交到manifest
仓库的一个 Issue,通过 Issue 的 Webhooks 触发流水线,流水线获取信息后进行对应的编译构建操作。这么做虽然可以解决问题,但是太不优雅了,对开发者非常不友好。
那么,Gerrit 如何处理这种情况呢?
在 Gerrit 的一个 Change 中,可以创建 Topic 将多个 Change 关联起来,这样就可以使用 Jenkins 上面的两个插件: Jenkins Repo 和 Gerrit Trigger
可以利用 Topic 这个特性,结合这两个插件,通过配置,在有新的 Patchset 提交的时候,通过 Topic 的关联,来针对性的拉取有更新的 Change 进行编译构建。这里需要注意的是,Repo 工具每次的提交可能是多个仓库同时进行推送的,因为开发者在本地的行为是改动多个依赖仓,本地验证通过后一并提交的。
使用 Repo 作为仓库源的配置:
使用 Gerrit Trigger 进行灵活的配置:
话说天下大势,分久必合,合久必分,道理放到哪里都一样。使用 Gerrit 就必须重视配置管理,对于一些使用体验上就没有那么便捷,前期的使用培训成本较高,Review 过程相对隔离,对于一直使用 PullRequest 模式的开发者并不是特别友好。而 Gitee 能够让我们快速构建起一个标准的研发协作流程,但在使用 Gitee 提供的各种方便功能,减少我们配置的复杂度的同时,对于多仓管理及评审的场景,Gitee 并不是强项,但是 Gitee 也在进步:
Gitee(其它平台也一样)可以做的事情也还有很多:
Gerrit 也有可改进的点:
最后,附一张来自 AOSP 的关于 Gerrit 的《Life of a patch》供大家参考学习
(END)
本文标题:浅谈 Pull Request 与 Change Request 研发协作模式
本文链接:https://zoker.io/blog/talk-about-pullrequest-and-changerequest
评论