其他

Git |为什么叫pull request 而不是push request?

274 人赞同了该文章

在团队中我承担了Committer 的责任,也就是帮同事们检视代码(Code Review)和合入代码,经常听到有同事在群里喊:“大佬,帮我合个 PR”,“大佬,我刚提交了一个 MR,帮忙合一下,急着出补丁”。我有点懵了,PR 和 MR 到底哪个才是正确的,这两个到底有什么区别,我决定先搞清楚这两个概念再合入他们的代码。

什么是Pull Request?


PR 的全称是Pull Request,经常用 Github的同学对这个肯定很熟悉了。Github 聚集了4000万开发者,过亿的开源项目,如果想给别人的开源仓库贡献代码,通常是先 fork 别人的项目,然后本地修改完成提交到自己的个人 fork 仓库,最后提交 PR 等待别人合入你的代码。

Github 的工作流:

我们重点看一下第6步,小明写完代码了想合入到原作者的仓库,新建了一个“pull request”,拉请求?这明明是推啊,小明将自己的修改推到原作者的仓,感觉叫“push request”比较合适吧。

既然 Github 坚持叫“pull request”,我们试着理解一下它的思路,小明写完代码了心里肯定是在想:原作者大神,我改了点东西,你快把我的修改拉回去吧。站在原作者的角度思考,叫pull request好像也说得过去,每天有大量的人从我这里 fork 代码走,我只会拉取我感兴趣的代码回来。

我一直觉得与其叫pull request, push request 听起来更加直观,后来发现它的命名有其背后的原因和逻辑。

Pull Request 的命名由来


  • 从接收者的角度命名:pull request 是从代码库维护者或项目所有者的角度命名的。我们正在请求他们“pull”(拉取)我们的更改并将其合并到他们的代码库中。因此,它强调的是请求接收者采取行动来拉取你的更改。
  • Git 工作流程:在 Git 的工作流程中,pull 操作指的是从远程仓库拉取代码到本地。pull request 这一名称反映了这一点,即我们请求项目维护者从你的分支拉取代码并合并到他们的分支。
  • 请求而非强制:pull request 表明你是请求合并更改,而不是强制性地推送更改。项目维护者有权审查、讨论并最终决定是否接受你的更改。这种方式强调协作和代码审查,而不是单方面的更改。

为什么不叫 Push Request?


  • Push 是单方面的操作:在 Git 中,push 是将本地更改推送到远程仓库的一种操作。它是单向的,不需要远程仓库的即时交互或同意。使用 push request 可能会误导人们认为更改会被直接推送并应用到远程仓库,而不需要审查。
  • 强调协作和审查:pull request 强调的是一个协作的过程,包括代码审查、讨论和最终的合并决策。它不仅仅是提交更改,更重要的是在团队中协作和确保代码质量。

Git中的”pull request”真正比较的是什么?


利用git版本控制工具时,我们通常会从主分支拉出新分支进行开发,开发完成后创建pr(也就是pull request),让其他小伙伴帮忙review,确定代码没有问题后再将新分支合并到主分支上。但是,你真的理解pull request中比较的两个分支到底是谁吗?

下面以一个虚拟案例进行说明:假设主分支名为“Master”,拉出来的新分支名为“developBrance1”。

注:图中的箭头指代工作推进方向,而不是提交的指向(提交指向总是由当前提交指向父提交,和这里的箭头是反着的)

上图中,我们从主分支Master的m1提交点拉出新分支developBranch1,然后在developBranch1分支上开发(开发过程中产生了d1、d2、d3共3个提交),开发完成后创建pr,然后经过Review后将其合并到主分支上形成新的提交点N。自然而然地,我们创建pr时选择的源和目标为:

src[developBranch1] -> dest[Master]

我们期望pr比较的是developBranch1和Master这两个分支的最新提交点,pr实际比较的也是developBranch1的d3提交点和Master分支的m1提交点之间的差异。

d3 并不包含 d1 和 d2 的实际内容 ,但在比较差异时,d1 和 d2 的影响是存在的 。以下是具体说明:

在版本控制系统(如 Git )中,每个提交(commit )都是一个独立的快照,记录了当时代码仓库的状态 。d1、d2、d3 都是在 developBranch1 分支上不同时间点的独立提交。d3 是该分支上最新的状态,它基于 d2 提交而来,d2 又基于 d1 提交而来,但它们在存储上是各自独立的记录 。

当创建拉取请求(pr )比较 developBranch1 分支(d3 提交点 )和 Master 分支(m1 提交点 )差异时:

  • 系统会从 d3 开始,沿着提交历史回溯,分析从 m1 之后在 developBranch1 分支上所有提交带来的变化。虽然直接比较的是 d3 和 m1 ,但实际上会考虑从 m1 diverge(分叉 )之后在 developBranch1 分支上的所有提交(即 d1、d2、d3 带来的累计变更 )。
  • 例如,d1 可能修改了某个函数的参数,d2 增加了函数的功能逻辑,d3 修复了 d2 引入的一个小 bug 。在比较 d3 和 m1 时,这些从 d1 开始逐步引入的变更都会被纳入差异分析范围,最终呈现出从 m1 状态到 d3 状态整个分支上的代码变化情况,以便审查者了解该分支相对主分支做了哪些修改。

要是 cherry pick d3,是不是就不看d1和d2了?

是的,这和 cherry pick 不同 。Git 的 cherry pick 是将指定的提交应用到当前分支的操作 。如果 cherry pick d3 ,确实就只关注 d3 这个提交本身的内容,不看 d1 和 d2 。

  • cherry pick 原理:它会把指定提交(这里是 d3 )所做的文件修改、代码变动等,在目标分支上重新应用一遍,创建一个新的提交(新提交内容和 d3 相同,但提交哈希值不同 ) 。比如 d3 提交是修改了 file3 文件中的某个函数逻辑,cherry pick d3 到其他分支时,就会在目标分支上对 file3 文件做相同的函数逻辑修改 。
  • 与拉取请求差异对比:拉取请求(pr )比较 d3 和 m1 时,会考虑 developBranch1 分支从 m1 分叉后所有提交(d1、d2、d3 )带来的累计变化;而 cherry pick d3 只关注 d3 这一次提交的内容,将其单独复制到目标分支,不涉及 d1 和 d2 的相关逻辑 。

什么是 Merge Request


MR 的全称是 Merge Request,相信玩过 Gitlab的同学都知道这个。

插播一下,Github这么好用了为什么还有人玩 Gitlab,这就要几年前说起了。在微软没有收购 Github 之前,Github 上面所有的项目必须是公开的,也就是说自己很渣的代码也必须要公开,不能藏着噎着。但是在一些小的公司或者创业团队,代码这种核心资产是不希望被公开,他们迫切需要私密仓这种需求,所以很多人都选择了 Gitlab。当然后面 Github 也放开了私有仓库,这是后话了。

团队中每个人都从远程仓库 develop 分支拉取代码,本地基于 develop 分支新建特性分支,修改完代码将特性分支推到远程仓,紧接着新建 Merge Request 期望将自己的特性分支合入 develop 分支。

从上面这个流程来看Merge Request 就是将自己的特性分支合入到主干分支。

Pull Request VS Merge Request


总结一下上面两个例子。

Github 是玩 fork 模式的,开发者提交自己的代码新建 Pull Request,请求原作者:“把我的代码拉回去吧”。

Gitlab 是玩分支模式的,开发者提交自己的代码新建 Merge Request,想将自己的特性分支合并到主干。

上面总结的好像很有道理,但是不要忘了,Github 也可以玩分支模式,Gitlab 也可以玩 fork 模式,更令人无语的是:

Github 上合并分支还是叫 Pull Request;Gitlab 上fork 模式也是叫 Merge Request;

不行,这种答案我没法接受,去 stackoverflow上搜一些大家是怎么理解的。果然有一个帖子很火:

有一个回答摘取了 Gitlab 的官方解释:

    Merge or pull requests are created in a git management application and ask an assigned person to merge two branches. Tools such as GitHub and Bitbucket choose the name pull request since the first manual action would be to pull the feature branch. Tools such as GitLab and Gitorious choose the name merge request since that is the final action that is requested of the assignee. In this article we'll refer to them as merge requests.

翻译过来简单理解就是:这两个没有本质区别,站在不同立场说法不一样而已。

好了,官方已经盖棺定论了,这两个就是一个东西,不要纠结啦~

疯狂吐槽:对于初学者来说,Github 的 pull request 确实让人难以理解,我们去各大网站看看用户的声音。

从国外到国内都有大量的用户对这个名字不理解,明明是提交提交代码,为什么是 pull request,有些人甚至怀疑是名字打错了。

如果让我来给 Github 取名字,我可能会取:

  • push request 推请求
  • merge request 合并请求

想多了,不会有如果。[嘿哈] Pull Request 和Merge Request 本质上都是合入代码,只是站在不同角度有不同的说法而已,因此在学习和工作中无论用哪一个都没有问题。

git merge 进行合并


在使用 git merge 进行合并时,Git 会自动完成比较和合并的操作,下面为你详细介绍具体的流程和可能出现的情况。

自动比较和合并的流程

  1. 确定合并的提交:你需要指定一个目标分支和一个源分支,Git 会将源分支上的提交合并到目标分支上。例如,你当前处于 master 分支,想要合并 feature 分支的更改,你可以使用以下命令:
git checkout master
git merge feature
  1. 寻找共同祖先:Git 会在提交历史中寻找两个分支的共同祖先(即两个分支开始分叉的提交点)。假设 master 分支的提交历史为 A -> B -> C,feature 分支的提交历史为 A -> D -> E,那么共同祖先就是提交 A。
  2. 比较差异:Git 会分别比较共同祖先与目标分支(如 master)以及共同祖先与源分支(如 feature)的差异。例如,它会找出从 A 到 C 以及从 A 到 E 之间文件的修改、新增和删除情况。
  3. 自动合并:如果两个分支的更改没有冲突,Git 会尝试自动合并这些更改。它会将源分支上的更改应用到目标分支上,生成一个新的合并提交。例如,将 feature 分支上从 A 到 E 的更改应用到 master 分支的 C 提交之后。

冲突处理

如果在比较过程中发现两个分支对同一文件的同一部分进行了不同的修改,就会发生冲突。此时,Git 无法自动合并,需要你手动解决冲突。

  1. 查看冲突文件:Git 会标记出发生冲突的文件,你可以使用 git status 命令查看这些文件。
  2. 编辑冲突文件:打开冲突文件,你会看到类似下面的标记:
<<<<<<< HEAD
这是目标分支(如 master)的内容
=======
这是源分支(如 feature)的内容
>>>>>>> feature

你需要手动决定保留哪一部分内容,或者将两部分内容进行整合。编辑完成后,删除冲突标记。

git merge时比较差异,并非简单的全文件比较,而是会采用智能算法对文件进行细致比对,即使差异的行号不同也能精准识别:

比较机制

  • 行级比较:Git 以行为单位对文件进行比较,逐行检查两个版本文件的内容。它会尝试找出文件中发生变化的最小单元,也就是具体的行。对于文本文件,即使行号不同,只要某一行的内容发生了改变,Git 就能识别出来。例如,在一个代码文件中,原本位于第 10 行的代码被移动到了第 20 行,同时内容也有修改,Git 会检测到这一行的变化,并准确记录。
  • 内容感知:Git 会考虑行的内容而不仅仅是行号。它使用了一种叫做 “最长公共子序列”(LCS)的算法,该算法能够找出两个文件中相同内容的最大连续行序列。通过这种方式,Git 可以确定哪些行是新增的、哪些行是删除的以及哪些行只是位置发生了变化。比如,一个函数在代码文件中的位置被移动了,但函数内部的代码基本保持不变,Git 会识别出这是一个位置变动,而不是全新的代码。
  • 文件状态判断:除了行级的比较,Git 还会判断文件的整体状态,如文件是否被新增、删除、重命名或权限发生了改变。如果一个文件在一个提交中被删除,而在另一个提交中被修改,Git 会将这种情况标记为冲突,需要用户手动解决。