这里有很多东西要学。有些人——好吧,at least one——将 Git 的学习曲线称为“学习墙”。在我看来,在 Git 上添加 GitHub 之类的 Web 服务实际上更难了:不再可能判断某些东西是在 Git 中,还是由 GitHub 提供。拉取请求实际上属于 GitHub 提供的类别。但它们是基于 Git 的 merge 动词(再加上 GitHub 存储 your 存储库和 其他人的 存储库这一事实)。
让我们暂时搁置所有这些,从 Git 本身开始。了解某个文件中是否以及何时会发生合并冲突的关键隐藏在 graph theory 后面。
提交中有什么
首先,让我们提一下提交保存文件。更准确地说,每个提交都包含您的文件所有的快照,截至您进行该提交时。但是每个提交也有关于提交的额外信息,例如谁提交、何时提交以及为什么提交(一条日志消息)。
在 Git 中,每个提交都由其哈希 ID 唯一标识。这是一个大的、丑陋的、几乎不可读的、对人类完全无用的字符串,例如5d826e972970a784bd7a7bdf587512510097b8c7(Git 存储库中的实际提交)。这些是 Git 查找提交的方式,尽管除了复制粘贴或链接目的之外,我不建议 您 过多地使用它们。 :-)
不过,重要的是要知道 Git 就是这样做的,因为几乎每个提交都列出了至少一个 parent 提交的原始哈希 ID。父级是在此提交之前 的提交。确定不会列出父级的一个提交是有史以来第一次提交,当然之前没有提交。
这意味着从任何分支上的 last 提交开始,Git 可以向后 工作,直到之前的提交。然后 Git 有一个表格,从人类可读的分支名称(如 master)到原始提交哈希 ID。向分支添加 new 提交:
- Git 从分支名称获取当前提交哈希 ID。
- Git 将其放入 new 提交中,作为新提交的父级。当然,Git 也会放入新的快照,还有你的名字等等。这会产生一个新的、唯一的、又大又丑的哈希 ID。
- Git 然后将 new 提交的哈希 ID 写入名称
master。
当某物拥有提交哈希 ID 时,我们说该物指向提交。这样,分支名称总是指向分支上的 last 提交。最后一个提交指向它的前任,它指向另一个步骤,依此类推,所以 Git 可以从那里向后工作。这种退一步,一次提交,是 Git 保存你和其他人所做的一切历史的方式:每次提交都会及时记录一个快照,并且每次提交(第一次除外)都有一个“以前,事情看起来像......”历史链接。
绘制图表
当你创建一个新分支时,Git 会创建一个新的表条目,以便两个分支都指向 same 提交。出于绘图目的,让我为每个提交使用单个大写字母,而不是一个大而丑陋的哈希 ID。那么我们可能有这个:
... <-F <-G <-H <-- master, develop (HEAD)
一旦提交,其中的任何内容都不会改变。所以H总是指向G,依此类推。 (分支名称可以并且确实一直在变化,通常是为了适应新的提交。)所以再次,为了在 StackOverflow 文本中绘图,我将省略内部箭头,只保留分支名称箭头:
...--F--G--H <-- master, develop (HEAD)
名称HEAD 附加到分支名称之一。这就是 Git 知道我们正在使用哪个分支的方式,因为当我们进行 new 提交时,Git 必须更新这个分支名称。现在让我们做一个新的提交,并调用它的哈希I:
...--F--G--H <-- master
\
I <-- develop (HEAD)
现在让我们切换回master 并在那里进行新的提交,我们可以调用J:
J <-- master (HEAD)
/
...--F--G--H
\
I <-- develop
注意H 和更早的提交都在两个 分支上。
合并为动词
如果我们现在让 Git 合并 develop(即,提交 I)回到 master(即,提交 J),Git 会找到 最佳共同祖先提交。粗略地说,这是 Git 通过从两个分支提示向后工作可以找到的第一个 shared 提交。在这种情况下,这显然是提交 H。
找到共同的祖先提交——Git 称之为 merge base——Git 现在必须弄清楚 我们 改变了什么,以及他们 改变了。也就是说,Git 必须针对两个分支提示 diff 提交 H,即合并基础:--ours,即提交 J,和 --theirs,即提交 I。这实际上需要两个单独的git diff 命令:
git diff --find-renames <hash-of-H> <hash-of-J> # what we changed
git diff --find-renames <hash-of-H> <hash-of-I> # what they changed
Git 现在可以组合这两组更改。我们接触了一些文件,他们也接触了一些文件。当我们和他们接触 same 文件时,我们将合并基础中的一些行更改为 our 提交,并且他们从合并基础中更改了一些行他们的提交。
如果 Git 决定我们接触了 same 行,但对这些行进行了 不同 更改,则会发生冲突。 Same 这里很明显——如果我更改了第 17 行,而他们更改了第 17 行,我们显然更改了相同的行。但是,same 可能有点奇怪:例如,如果我们都在文件的 end 处添加不同的文本,Git 不知道将它们放在哪个顺序,所以这也是一个冲突。
但只要我们接触到不同的行,或不同的文件,就不会有冲突:Git 将两组组的更改应用于每个合并基础文件,得到合并后的结果。然后 Git 可以从合并的更改中进行新的提交。
合并过程称为three-way merge。另见Why is a 3-way merge advantageous over a 2-way merge?
合并为形容词或名词
这个新的提交有一个特殊的属性;让我们通过绘制结果来展示它:
J
/ \
...--F--G--H K <-- master (HEAD)
\ /
I <-- develop
这里发生的事情是新提交K 记住两个 以前的提交:首先,它自己的以前的提交是J。那是 K 的 first 父母。但是K 也有第二个父母:它还记得它来自I。这允许 Git 在 future 合并中做更少的工作,因为它改变了哪个提交将是 later 的合并基础。 (这是图论中最复杂的部分,我们将略过。?)
合并更改并进行新提交的过程是单词merge的动词形式,即合并。 by 的提交在这个过程中使用了同一个词作为形容词:K 是一个合并提交。 Git 和 Git 的用户经常将其缩短为 K 是一个合并,它使用单词 merge 作为名词。
合并提交只是任何至少有两个父母的提交。 git merge 命令经常进行此类提交。 (但并非总是如此!这是 Git 陡峭学习曲线的另一部分。现在,让我们忽略这一点,因为 GitHub 尝试——半成功地——向你隐藏它。)最后,当你有合并(名词),它是由合并(动词)的过程制成的。该过程使用三个输入——合并基础和两个分支提示——产生组合的变化。然后git merge 命令生成名词形式的结果。
grok 这个合并过程非常重要,这主要需要时间和练习。原因是不仅仅是 git merge 执行了 merge as a verb 操作:许多其他 Git 命令也执行此操作,它们只是不执行 merge 提交当他们完成时。每次运行 git rebase、git cherry-pick 或 git revert 时,您都将使用 Git 的合并机制。即使是看似简单(我认为是欺骗性的)git stash 也使用 Git 的合并机制。当 Git 自己做对时——大多数情况下——你不必考虑它,但当它出错时,你需要知道该怎么做。
(对于 Git,我发现将配置选项 merge.conflictStyle 设置为 diff3 也很有帮助,这样当 Git 留下混乱的合并冲突的混乱供您修复时,它会显示 输入合并基础以及两个分支技巧。其他人喜欢使用花哨的基于窗口的合并工具,所以这是一个品味问题。)
在 GitHub 拉取请求上
现在我们有了上面的背景,让我们看看 GitHub 的“提出拉取请求”按钮是如何工作的。为了告诉您您的分支是否可以与其他人的分支合并,GitHub 所做的实际上是 do 合并,作为测试合并。1 此测试合并GitHub 所做的根本不在 any 分支上,但它仍然遵循相同的总体思路。如果没有合并冲突,则测试成功并且 GitHub 说能够合并。如果存在冲突,GitHub 会丢弃测试合并——它无法完成它,而 Git 本身也无法完成——并告诉你存在冲突。
如果存在 冲突,当然,由您自己决定如何处理。现在您需要了解上述三向合并过程,这需要了解图——或者,如果不是真正了解所有图论,至少对什么是合并有一个很好的了解基地群岛您可以找到基础,将其与两个技巧进行比较,看看冲突是如何产生的。 (或者,将merge.conflictStyle 设置为diff3,Git 会将冲突源留在工作树中,您可以直接在其中进行编辑。)
1我在这里跳过了 Git 如何将提交从一个存储库传输 的一些重要方面。正如我所提到的,GitHub 在同一个网站上同时拥有 your 和 their 存储库,因此他们可以并且确实在这里作弊——他们不必在任何地方转移任何东西,他们拥有一切。同样,它们实际上并不运行git merge,因为这需要工作树:GitHub 上的存储库都是所谓的 bare 存储库,没有工作树。但所有这些都是挑剔的细节,破坏了“GitHub 运行测试合并”的良好概述想法:它们确实实际上运行了测试合并。他们只是有很多侧面的棘手部分,他们必须使用它来实现它。
还值得一提的是,在这个脚注中,GitHub 的作用相当于这里的git merge --no-ff。如果没有任何快进控制旋钮,则无法执行 git merge --ff-only 或 git merge 的等效操作。
对于拉取请求N,拉取请求的提交本身在引用 refs/pull/<em>N</em>/head 下是可提取的。如果测试合并成功,则在引用refs/pull/<em>N</em>/merge下。