merge/rebase 的方向或顺序有区别吗?

Does the direction or order of a merge/rebase make a difference?

假设您在处理功能分支时从远程存储库中提取内容,并且新提交已添加到主分支。

您完成了功能分支的工作,提交您的更改,然后 merge/rebase 将其提交到 master。在拉取提交之前或之后,在 master 分支中的哪个位置插入了功能提交?如果您从未进行过拉动,它们会插入哪里?会得到相同的结果吗?

此外,如果您 merge/rebase 掌握功能分支而不是相反,会发生什么情况?如果你在 pull 之前或之后 merged/rebase master 插入功能分支的哪里?会有同样的结果吗?它的历史看起来与前面提到的 merge/rebase 的功能有什么不同吗?

更简洁地说,合并的方向 and/or 合并的顺序对最终的合并有不同的结果吗?我觉得没关系。

旁白:这个问题对于 Git 的人来说可能很容易回答,但是有成百上千的文档和教程资源在那里,我从未见过直接解决它。在不知道答案的情况下,Git 可能会非常难以理解。

因为操作顺序在某些 Git 概念和一般编程中很重要,所以它可能会导致某些人得出结论它有所作为。此外,我们通常倾向于认为修订是按时间顺序发生的,并期望 Git 以相同的方式工作。用户界面和命令行也通过暗示操作顺序来发挥这种作用。

其他版本跟踪工具在抽象这种认知腐蚀操作方面做得更好。

合并时,远程和本地提交都完整保留,合并提交指的是两者。

当你变基时,远程提交被保留(或者至少它们应该被保留!),并且你的本地提交被应用在它之上。永远不要试图强迫 git 向其他方向变基,这会导致灾难。

有关合并与变基差异的更多信息,请阅读此答案What's the difference between 'git merge' and 'git rebase'?

You finish working on the feature branch, commit your changes and then merge/rebase it onto master.

Where in the master branch are the feature commits inserted, before or after the pulled commits?

After: merge/rebase 将考虑 master 分支的当前状态。
如果该分支已经发展(通过拉取),那么 merge/rebase 将使用当前的主 HEAD(现在引用拉取的提交)

Where are they inserted if you never performed a pull?

相同:它将使用当前的主 HEAD(不反映任何新的提交,因为没有完成拉取)

Will it have the same result?

不,考虑到两种情况下 master HEAD 不同。

Also, what happens if you merge/rebase master onto the feature branch instead of the other way around?

首先,不要这样做:这不是最佳做法。
但如果这样做,merge/rebase 将使用 HEAD 功能。

Where is master inserted into the feature branch if you merged/rebase before or after the pull? Will it have the same result?

相同的结果,因为在这两种情况下,它都使用了 HEAD 功能,当您执行(或不执行)拉取 master 时,该功能没有改变。

TL;DR

Moreover, we normally tend to think of revisions as occurring in chronological sequence and expect Git to work the same way.

这是一个可怕的错误。 Git 按 图顺序 ,而不是时间顺序。当图形顺序是按时间顺序排列时——有时但并非总是如此——然后它工作正常。

(我什至不打算在这里触及 rebase。有关 rebase 的适当讨论,请参阅之前的帖子。我将专注于合并中的对称性,以及接缝可以显示的地方.)

More succinctly, do the direction of the merge and or order of the merge have different outcomes on the resulting merge? I suspect it doesn't matter.

答案既不是也不是。

要理解为什么这是答案,您需要了解关于Git的几件事。

首先,让我们考虑一下 Git 如何处理提交,与其他人做对比(大多数其他人?所有其他人?必须至少有一个其他 VCS 也这样做......)。在版本控制系统中,在 b运行ch 中提交的过程将新提交 添加到 b运行 ch;但是在 Git 中,包含提交 一旦提交 的 b运行 集合可以是,并且是动态变化的东西。也就是说,一旦提交,独立于任何 b运行ch,并且如果您愿意,可以在 no[=191] 上提交=] b运行ch.

例如,在 Mercurial 中,这是完全不可能的。虽然 Mercurial 在许多方面与 Git 非常相似,但提交 的存在需要 其 b运行ch 的存在。提交是在 on that b运行ch 上进行的,并且永远是 b运行ch 的一部分。这是保存提交的 only b运行ch:提交永久附加到其 b运行ch.

在Git和Mercurial中,每个提交都有一个唯一的ID,每个提交存储其parent(单数)提交的唯一ID ,如果是普通提交。通过遵循这个 backwards-looking 链,从 last 提交开始并向后工作,我们可以找到 b运行ch 上的提交历史:

...  <-grandparent  <-parent  <-child

在 Mercurial 中,这些提交永远在它们的 b运行ch 上,所以我们可以在左边写上 b运行ch 名称:

branch-A:  ...--I--J

在 Mercurial 中查找最新的提交很容易,因为提交在本地存储库中有一个简单的序列号(以及用于在存储库之间共享的唯一哈希 ID)。

在 Git 中,b运行ch names 是可移动的。在提交 J 存在之前,名称 branch-A 存储提交 I:

的原始哈希 ID
...--H--I   <-- branch-A

进行新提交时,Git 只需将新提交的哈希 ID 写入 b运行ch 名称,以便 b运行ch 指向新提交:

...--H--I--J   <-- branch-A

合并既是名词又是动词

在版本控制中,合并动词,或合并一些提交,是一个过程。结果——至少在 Git 和 Mercurial 中——是一个 merge commit,我们使用 merge 这个词作为形容词修改commit,或简称,a merge(名词)。

合并提交 成为 合并提交的原因是它们有 两个 parent,将两条开发线放在一起:

...--I--J
         \
          M
         /
...--K--L

在这里,Git 和 Mercurial 基本相同,有一个非常重要的区别:在 Mercurial 中,合并提交专门针对 one b运行ch,当你 运行 hg merge 时,无论你在哪个 b运行ch。提交——包括合并提交——永久地附在它们的 b运行ches 上。一旦合并过程完成并且存在合并提交,合并提交就在那个 b运行ch 上。在 Git 中,合并提交进入当前 b运行ch,但是因为我们可以移动 name,所以在某种意义上这并不重要。

但我们必须同时查看两个部分:动词部分 to merge,然后仔细查看名词部分 a merge. (旁白:Git 还允许合并提交有 超过两个 parent,而 Mercurial 将每个合并限制为两个 parent。没有什么这些时髦的合并,Git 调用 octopus 合并 ,可以做到你不能用一系列成对合并做的事情,但章鱼合并在某些情况下可以更清楚地显示意图.)

要合并,动词

例如,假设 branch-Abranch-B 在合并之前是这样的:

...--I--J   <-- branch-A

...--K--L   <-- branch-B

在我们合并这些之前,我们必须追溯两个历史足够远才能找到合并基础,共同提交来自这两条发展路线发生了分歧。 (在 Mercurial 中也是如此。)所以让我们再补充一点:

       I--J   <-- branch-A
      /
...--H
      \
       K--L   <-- branch-B

在这里,commit H 是共同的起点。在 Git 中,提交 H 同时在 两个 b运行 上 (但不是在 Mercurial 中)。小号生病了,两个版本控制系统都以几乎相同的方式执行 合并 过程:您首先选择一个提交来检出,例如使用 [=30 提交 J =] 或 hg update branch-A。然后使用命令的 merge 动词选择另一个提交:git merge branch-Bhg merge branch-B。 VCS 找到合适的合并基础,即提交 H,并将 H 的内容与 J 的内容进行比较,以查看 you更改:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed

并重复相同的比较以找出 他们 改变了什么:

git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed

Git 将这些更改合并为一个大的 "all changes",将这些更改应用到 base(提交 H),并使新提交。

(Mercurial 做的大致相同,尽管在 Mercurial 如何知道重命名方面存在一些非常重要的差异。这些仅在您自合并基础后重命名了一些文件时才重要。如果您 重命名文件,并且有 rename/rename 冲突,合并顺序变得非常重要。但我们假设没有重命名问题。)

这里最有趣的事情是您可以控制如何更改组合。在 Git 中,您可以通过 合并策略 扩展策略参数 执行此操作;在 Mercurial 中,您可以通过选择的 合并工具 来完成此操作。如果您使用 "ours" strategy/tool,VCS 会完全忽略它们的更改:合并后的更改只是 您的 更改,因此 VCS 使用与当前提交中的代码相同。这里的合并顺序显然很重要:如果我们忽略 他们的 更改,我们最好确定谁是 "us"!

即使没有 "ours" 策略,您的更改和他们的更改之间也可能存在冲突。如果是这样,Git 和 Mercurial 都可以被告知:更喜欢我的更喜欢他们的。这些会给出不同的结果,所以在这里,合并顺序再次很重要。当然,此时有一个对称选项:选择我的选择他们的。如果你交换角色,你可以交换选项——所以虽然顺序很重要,但它并不那么重要。

合并,名词

让我们假设没有冲突,也没有特殊的 ours/theirs 事件,并且 运行 git checkout branch-A; git merge branch-B。如果一切顺利,VCS 将合并提交 M 及其两个 parents:

       I--J
      /    \
...--H      M   <-- branch-A
      \    /
       K--L   <-- branch-B

在 Mercurial 中,合并顺序在这里很重要,因为一旦提交 M,它就会永远停留在它的 b运行ch 上。但是 Git 允许我们 在事后移动 b运行ch names。我们可以把M做成这样,放在原处,把branch-A往后推一步,再次指向J,然后把branch-B向前移动,指向M,给出:

       I--J   <-- branch-A
      /    \
...--H      M   <-- branch-B
      \    /
       K--L

这与我们 git checkout branch-B; git merge branch-A 得到的结果几乎相同。所以看起来在这里,合并顺序是无关紧要的(前提是 verb 部分没有任何障碍)。但是这里的图表中缺少一些东西,那就是 firstnot-first parents![= 的概念55=]

合并提交M第一个parent,在Git和Mercurial中,是"same branch"parent。也就是说,由于我们在 运行 合并时提交 J,因此 Mfirst parent 是提交 J。这使得 L 成为 第二个 parent。如果我们将 b运行ch 标签推到 Git 周围,我们可以知道我们已经这样做了,因为第一个和第二个 parents 仍然是另一个顺序。

所以我们在这里看到对于 Git,即使 to merge 动词没有顺序问题,结果 merge commit(形容词或名词)确实有一个命令。由您(用户)决定该订单对您是否重要。如果不是,则对称性可用。如果提交 parent 顺序很重要,那么合并的 direction/order 在 Git 中和在 Mercurial 中一样重要。

图表是关键

在任何 graph-based 版本控制系统中——因此在 Git 和 Mercurial 中——图表 历史。特别是在 Git 中,没有 文件历史这样的东西; 该图不仅是 主要 历史,而且是 只有历史。 (Mercurial 以更常规的方式存储文件数据,因此既有提交历史 也有 文件历史,但无论如何如果没有提交历史,文件历史也没有用。)

为了能够理解 Git 的作用和原因,您需要了解足够的图论来驾车或骑车 public t运行 坐在城市周围。这并不是那么多——但如果你没有意识到 Git 有这些 one-way 铁轨/街道/桥梁,从 last 提交回来迈向,很多事情就没有意义了。 Git 开始一个结束,然后倒退到开始。 b运行ch 名称让您进入图表,在 branch-tip 结束。其他一切都是提交图的函数。

TLDR: Merge, merges argument branches into checkout branch, rebase, rebase puts checkout branch on top of argument branch (ie, ahead of, in the future, nearest to now)

合并会影响结帐分支标签的推进,顺序无关紧要

例如,提供给 git merge A B 的分支将合并到结帐分支中

(没什么大不了的,如果你写错了) 提示git branch -f <oops-branch> <go-back-to-commit> # don't be on oops-branch

这样做

99% 的时间你想要做的是检查一些分支 git checkout A 然后将另一个分支合并到它 git merge B

$ git merge B #I am on branch A, megre B into A

分支 A 标签被推进到合并提交,而 B 不是。

Rebase 顺序影响哪些提交被重写

(有点大不了,但你可以从错误中恢复) 提示git branch -f <oops-branch> <go-back-to-commit> # don't be on oops-branch

这样做

99% 的时间你只想使用 rebase 将更改从一个分支拉到你的开发分支。此 rebase 语法与上面显示的惯用合并方式相反(即,检查分支“A”然后 merge 分支 'B' 进入 'A')

rebase 想象一下在一根棍子上烤棉花糖,最后一个棉花糖堆在前一个上面。这就是 rebase 语法的工作原理。

$ git rebase main dev #I am on branch main but it doesn't matter what branch I am on bc I've explicitly told git the rebase order, rebase dev on top of main

不要这样做

你能看出这里的问题吗?

$ git rebase dev #I am on branch main, rebases main on top of dev