当进行git合并时,什么是主要的分支?

11

假设我的当前分支是分支A

分支A包含:

line 1 -- "i love you Foo"

分支B包含:

line 1 -- "i love you Bar"

如果我做a:

git merge Branch B 

我会得到什么?


4
可能会发生冲突,必须选择 Foo 或 Bar。 - Luke Hutton
2
在创建分支之前,那行代码是什么样子的? - Philipp
3个回答

28
“dominant”这个修饰词在Git中没有定义。你使用这个词的方式似乎存在一个不正确的假设,因此我认为这个问题无法回答。但只需稍作修改,答案就变得简单了:它既不是A分支也不是B分支的“主导分支”。这两个分支是平等的合作伙伴,在合并时结果相同,无论是将A合并到B还是将B合并到A——但你可以通过多种方式改变这一点。
这个问题暴露出来了很多有用的要点,让我们探讨一下。最后,我们将看到如何正确地提出问题以及可能的答案。
提醒:Git存储具有父链接的快照。
每个提交都存储了所有文件的完整副本,尽管是压缩的。一些版本控制系统从初始快照开始,然后为每个提交存储自上一个提交或自上一个快照以来的更改集。因此,这些其他VCS可以轻松显示更改(因为它们具有更改),但很难获取实际的文件(因为它们必须组装许多更改)。Git采取相反的方法,每次都存储文件,并且仅在您要求时计算更改。
就使用而言,这并没有太大区别,因为给定两个快照,我们可以找到变化,给定一个快照和一个变化,我们可以应用该变化以获得一个新的快照。但这确实有些重要,我在下面提到了这些快照。有关更多信息,请参见Git如何存储文件?Git的包文件是否是增量而不是快照?

同时,在Git中,每个提交还记录了一个提交。这些父链接形成了一条向后的提交链,我们需要这些链接,因为提交ID似乎非常随机:

4e93cf3 <- 2abedd2 <- 1f0c91a <- 3431a0f

第四个提交“指向”第三个提交,第三个提交“指向”第二个提交,第二个提交“指向”第一个提交。(当然,第一个提交没有父提交,因为它是第一个提交。)这就是Git如何在最新或“tip”提交中找到以前的提交。单词“tip”出现在Git术语表中,但仅在“分支”的定义下。
合并的目标
任何合并的目标都是将工作组合起来。在Git中,与任何其他现代版本控制系统一样,我们可以有不同的作者在不同的分支上工作,或者“我们”可以是一个人(一个作者,皇家的“我们”:-))在不同的分支上工作。在这些不同的分支中,我们-无论“我们”是一个人还是多个人-可以对文件进行不同的更改,具有不同的意图和不同的结果。
最终,我们决定以某种方式“合并”其中的一些。将这些不同的更改集合起来,以实现特定的结果,并且通常记录我们“确实”将它们合并在一起的事实,这就是合并。在Git中,这个动词形式的合并——合并几个分支——是使用git merge完成的,结果是一个合并,即合并的名词形式。1 名词可以变成形容词:任何具有两个或多个父节点的提交都是合并提交。所有这些都在Git词汇表中得到了正确的定义。

每个合并提交的父提交都是之前的一个head(见下文)。第一个这样的父提交是HEAD所在的head(也见下文)。这使得合并提交的第一个父提交很特殊,这就是为什么git loggit rev-list有一个--first-parent选项的原因:它允许你只查看“主”分支,其中所有“侧”分支都被合并到其中。为了使其按预期工作,至关重要的是需要仔细地执行所有合并操作,并具有适当的意图,这需要不使用git pull来执行任何合并操作。

这是新手应避免使用git pull命令的几个原因之一。关于--first-parent属性的重要性或缺乏重要性取决于您如何使用Git。但是如果您是Git的新手,您可能还不知道如何使用Git,因此您不知道这个属性是否对您很重要。随意使用git pull会出现问题, 因此您应该避免使用git pull


1令人困惑的是,git merge也可以实现动作动词,但使用--squash会产生一个普通的非合并提交。 --squash选项实际上抑制了提交本身,但--no-commit也有同样的效果。无论哪种情况,最终运行的git commit都会进行提交,除非你在运行git merge时使用了--squash。为什么--squash意味着--no-commit,即使你确实可以运行git merge --squash --no-commit跳过自动提交步骤,这还是一个谜。


Git合并策略

git merge文档指出有五种内置的策略,分别命名为resolverecursiveoctopusourssubtree。这里需要注意的是,subtree只是对recursive的微小调整,因此也许更好的做法是声称只有四种策略。此外,resolverecursive实际上非常相似,因为递归只是resolve的递归变体,这样就只剩下三种了。

所有三种策略都使用Git所谓的headsGit定义了head这个词:

一个分支的顶部指向的提交,可以通过名称引用。
但是Git在使用git merge时并不完全符合这个定义。特别是,您可以运行git merge 1234567来合并提交1234567,即使它没有命名引用。它被简单地视为一个分支的顶部,这是有效的,因为在Git中“分支”这个词本身的定义比较弱(参见我们所说的“分支”究竟是什么?):实际上,Git创建了一个匿名分支,因此您有一个未命名的引用,指向该未命名分支的顶部提交。
一个头总是HEAD
Git中保留了名称为HEAD的关键字,也可以拼写为@。它始终指向当前提交(总是有一个当前提交)。您的HEAD可能是分离的(指向特定提交)或附加的(包含分支名称,分支名称又指向特定提交,因此该提交是当前提交)。
对于所有合并策略,HEAD都是要合并的头之一。 octopus策略确实有些不同,但在解决可合并项方面,它的工作方式与resolve非常相似,只是它不能容忍冲突。这使得它避免在第一次合并冲突时停止,从而使其能够解决多个头。除了不能容忍冲突和能够解决三个或更多头之外,您可以将其视为常规解决合并,我们稍后会讲到。
"ours"策略完全不同: 它完全忽略了所有其他分支。因为没有其他输入,所以永远不会有合并冲突:合并的结果,新的HEAD中的快照与之前的HEAD中的内容相同。这也允许此策略解决多于两个分支 - 并且还为我们定义了“主导头”或“主导分支”的方法,尽管现在定义并不是特别有用。对于ours策略,“主导分支”是当前分支 - 但ours合并的目标是记录历史上的合并,而不实际获取来自其他分支的任何工作。也就是说,这种合并是微不足道的:动词形式的“合并”根本不起作用,然后名词形式的“合并”是一个新提交,其第一个父项具有相同的快照,其余父项记录其他分支。"

2有一个例外是当你处于Git称之为“未诞生的分支”或“孤立分支”的状态时。大多数人最常遇到的例子是新创建的仓库的状态:你在分支master上,但是还没有任何提交。名称HEAD仍然存在,但是分支名称master 尚不存在,因为它没有可以指向的提交。Git通过在创建第一个提交时立即创建分支名称来解决这种棘手的情况。

您随时可以使用git checkout --orphan再次进入此状态,以创建一个尚不存在的新分支。详细信息超出了本答案的范围。


如何解决/递归合并工作

除了ours类型的合并之外,我们通常在谈论合并时想到的都是其他类型的合并。在这里,我们确实是在合并更改。我们有我们自己分支上的更改,他们有他们自己分支上的更改。但由于Git存储快照,因此首先我们必须找到这些更改。那么,确切地说,这些更改是什么?

Git能够生成我们的更改列表和他们的更改列表的唯一方法是首先找到一个共同起点。它必须找到一个提交 - 一个我们都拥有并且都使用的快照。这需要查看历史记录,Git通过查看父级来重建历史记录。当Git回溯HEAD的历史记录 - 我们的工作 - 和另一个头部的历史记录时,最终会找到一个合并基础:我们都从中开始的提交。3 这些通常在视觉上很明显(取决于我们如何仔细地绘制提交图):

          o--o--o   <-- HEAD (ours)
         /
...--o--*
         \
          o--o      <-- theirs

这里的合并基础是提交*:我们都从那个提交开始,我们做了三次提交,而他们做了两次。
由于Git存储快照,它通过实质上运行git diff base HEADgit diff base theirs来查找更改,其中base是合并基础提交*的ID。然后Git结合这些更改。

3技术上,合并基础被定义为由提交之间的单向弧连接形成的有向无环图或DAG的最低公共祖先(LCA)。 提交是图中的顶点/节点。 LCA在树中很容易,但DAG比树更一般化,因此有时不存在单个LCA。 这就是递归合并与解决合并不同的地方:解决通过随意选择这些“最佳”祖先节点之一来工作,而递归选择所有这些节点,并将它们合并形成一种虚假提交:一个虚拟合并基础。细节超出了本答案的范围,但我在其他地方已经展示了它们。


对于resolve/recursive,没有默认的主分支

现在我们终于得到了你问题的答案:

Given my current branch is Branch A

Branch A contains [in file file.txt]:

line 1 -- "i love you Foo"

Branch B contains:

line 1 -- "i love you Bar"

if i do a:

git merge BranchB 

what would i get?

为了回答这个问题,我们需要再获得一些信息:合并基础中有什么? 你在BranchA上改变了什么,他们在BranchB上改变了什么?不是两个分支中的内容,而是自从基础以来每个人都改变了什么
假设我们找到了合并基础的ID,并且它(以某种方式)是ba5eba5。然后我们运行:
git diff ba5eba5 HEAD

找出我们改变了什么,并且:

git diff ba5eba5 BranchB

要找出他们改变了什么,我们可以使用git diff命令来比较两个文件的不同之处。类似地,我们可以使用git show ba5eba5:file.txt并查看第一行,尽管只做两个git diff更容易一些。显然,我们中至少有一个人改变了某些东西,否则两个文件的第一行将是相同的。

如果我们改变了第一行而他们没有,合并结果就是我们的版本。

如果我们没有改变第一行,而他们改变了,合并结果就是他们的版本。

如果我们都改变了第一行,合并失败,会产生合并冲突。Git会把两行都写入文件并停止合并,并给出错误提示,让我们清理这个混乱:

Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt

使用默认样式,我们看到:
$ cat file.txt
<<<<<<< HEAD
i love you Foo
=======
i love you Bar
>>>>>>> BranchB

如果我们将merge.conflictStyle设置为diff3git config merge.conflictStyle diff3git -c merge.conflictStyle=diff3 merge BranchB或类似命令),5 Git会写入不仅是我们的两行内容,还会包括原本存在的内容:
$ cat file.txt
<<<<<<< HEAD
i love you Foo
||||||| merged common ancestors
original line 1
=======
i love you Bar
>>>>>>> BranchB

注意,顺便提一下,Git不会查看任何中间提交。它只是使用两个git diff命令将合并基地与两个头进行比较。

4这意味着存在一个单一的合并基地。通常情况下是这样的,我不想在这里讨论虚拟合并基地,除非在其中一些脚注中提到。我们可以使用git merge-base --all HEAD BranchB找到所有的合并基地,并且通常只会打印一个提交ID;那就是(唯一的)合并基地。

5我使用这种diff3风格,在我的--global Git配置中进行设置,因为我发现在解决冲突时,看到合并基地中有什么很好。我不喜欢找到合并基地并检出它;对于真正的递归合并,当Git构建了一个虚拟合并基地时,没有什么可以检出的。不可否认的是,当出现虚拟合并基地时,这可能会变得非常复杂,因为虚拟合并基地中可能存在合并冲突!请参见Git - diff3 Conflict Style - Temporary merge branch中的示例。


如何设置优先版本

为了处理合并冲突或潜在的合并冲突,让我们定义主导头为那些自动优先使用其更改的版本。在递归和解决策略中有一种简单的方法来设置这个。

当然,Git给了我们-s ours合并策略,它甚至消除了潜在的合并冲突。但是,如果我们没有更改第1行,而他们更改了,这仍然使用我们的第1行,所以这不是我们想要的。我们只想在只有我们或只有他们更改它时,采用我们的他们的第1行;我们只想让Git在我们同时更改它的情况下,优先使用我们的头部或他们的头部。

这些是-X ours-X theirs strategy-option参数。6 我们仍然使用递归或解决策略,就像以前一样,但我们使用-X ours告诉Git,在发生冲突的情况下,它应该优先考虑我们的更改。或者,我们使用-X theirs来优先考虑他们的更改。在任何情况下,Git都不会因为冲突而停止:它会采取首选(或占主导地位)的更改并继续进行。
这些选项有一定危险性,因为它们取决于Git正确获取上下文。Git对我们的代码或文本文件一无所知:它只是逐行进行差异比较,试图找到最小的指令集:“在这里删除这一行,在那里添加那一行,这将使您从基本提交的版本到一个头版本。”诚然,“没有首选/主导头”合并也是如此,但在这些情况下,我们不会采用它们的更改,然后用我们的更改覆盖另一个附近的“他们”的更改,这可能会在我工作的文件类型中遇到麻烦。
无论如何,如果你正常地运行git merge而没有使用其中一个-X参数,你将像往常一样会遇到冲突。然后你可以在任何一个文件上运行git checkout --ours来选择我们的文件版本(完全没有冲突,忽略所有他们的更改),或者git checkout --theirs 来选择他们的文件版本(同样没有冲突并忽略我们的所有更改),或者git checkout -m重新创建合并冲突。如果有一个面向用户的git merge-file包装器,它可以从索引中提取所有三个版本,并让你使用--ours--theirs来像-X ours-X theirs一样作用于那个文件(这是这些标志在git merge-file中的含义)。请注意,这也应该允许你使用--union。请参见 git merge-file文档了解其作用描述。像--ours--theirs一样,它有点危险,应谨慎使用和观察;例如,在合并XML文件时它实际上并不起作用。

6我认为,“策略选项”这个名称不太合适,因为它听起来就像-s strategy参数,但实际上完全不同。 我使用的记忆方法是-s需要一个策略(Strategy),而-X需要一个扩展(eXtended)策略选项,传递给我们已选择的任何策略。

7您可以通过.gitattributesgit diff选项来控制差异,但我不确定前者如何影响内置合并策略,因为我实际上没有尝试过。

8在冲突合并期间,每个有冲突的文件的三个版本都在索引中,即使git checkout只有两个特殊语法。 您可以使用gitrevisions语法提取基本版本。


git checkout -b HEAD:这个命令现在可以工作了,是因为 Git 的某个新版本吗?还是我误解了你说的“HEAD 是 Git 中保留的名称”? - Ojasvi Monga
1
@joe:那应该是一个错误。在我的电脑上:git checkout -b HEAD会产生fatal: 'HEAD'不是一个有效的分支名称。 - torek

0
git merge Branch B

这个命令将B分支合并到当前分支(A)中

情况1。它应该将B分支的内容附加到A分支上。当文件名不同时或者B和A上相同文件的内容在不同行上不同时。

情况2。如果相同文件的内容在相同行上不同,则会出现合并冲突。


-2

你正在将更改合并到当前分支,因此假设你当前已经检出了分支A,那么你将会将来自分支B的更改合并到分支A中。


那么你的意思是Foo会被替换成Bar? - teddybear123
1
如果你的答案很短,你应该在回答中添加一些解释或者对问题进行评论。 - Nicolas Henneaux
这个答案是错误的:它取决于文件合并基础版本中的内容,就像 Philipp 所指出的那样。 - torek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接