假设我的当前分支是分支A
分支A包含:
line 1 -- "i love you Foo"
分支B包含:
line 1 -- "i love you Bar"
如果我做a:
git merge Branch B
我会得到什么?
假设我的当前分支是分支A
分支A包含:
line 1 -- "i love you Foo"
分支B包含:
line 1 -- "i love you Bar"
如果我做a:
git merge Branch B
我会得到什么?
同时,在Git中,每个提交还记录了一个父提交。这些父链接形成了一条向后的提交链,我们需要这些链接,因为提交ID似乎非常随机:
4e93cf3 <- 2abedd2 <- 1f0c91a <- 3431a0f
git merge
完成的,结果是一个合并,即合并的名词形式。1 名词可以变成形容词:任何具有两个或多个父节点的提交都是合并提交。所有这些都在Git词汇表中得到了正确的定义。
每个合并提交的父提交都是之前的一个head(见下文)。第一个这样的父提交是HEAD
所在的head(也见下文)。这使得合并提交的第一个父提交很特殊,这就是为什么git log
和git 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 merge
文档指出有五种内置的策略,分别命名为resolve
、recursive
、octopus
、ours
和subtree
。这里需要注意的是,subtree
只是对recursive
的微小调整,因此也许更好的做法是声称只有四种策略。此外,resolve
和recursive
实际上非常相似,因为递归只是resolve
的递归变体,这样就只剩下三种了。
所有三种策略都使用Git所谓的heads。Git定义了head这个词:
一个分支的顶部指向的提交,可以通过名称引用。git merge
时并不完全符合这个定义。特别是,您可以运行git merge 1234567
来合并提交1234567
,即使它没有命名引用。它被简单地视为一个分支的顶部,这是有效的,因为在Git中“分支”这个词本身的定义比较弱(参见我们所说的“分支”究竟是什么?):实际上,Git创建了一个匿名分支,因此您有一个未命名的引用,指向该未命名分支的顶部提交。HEAD
。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 diff base HEAD
和git diff base theirs
来查找更改,其中base
是合并基础提交*
的ID。然后Git结合这些更改。
3技术上,合并基础被定义为由提交之间的单向弧连接形成的有向无环图或DAG的最低公共祖先(LCA)。 提交是图中的顶点/节点。 LCA在树中很容易,但DAG比树更一般化,因此有时不存在单个LCA。 这就是递归
合并与解决
合并不同的地方:解决
通过随意选择这些“最佳”祖先节点之一来工作,而递归
选择所有这些节点,并将它们合并形成一种虚假提交:一个虚拟合并基础。细节超出了本答案的范围,但我在其他地方已经展示了它们。
现在我们终于得到了你问题的答案:
为了回答这个问题,我们需要再获得一些信息:合并基础中有什么? 你在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
上改变了什么?不是两个分支中的内容,而是自从基础以来每个人都改变了什么?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
设置为diff3
(git config merge.conflictStyle diff3
或git -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
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 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您可以通过.gitattributes
或git diff
选项来控制差异,但我不确定前者如何影响内置合并策略,因为我实际上没有尝试过。
8在冲突合并期间,每个有冲突的文件的三个版本都在索引中,即使git checkout
只有两个特殊语法。 您可以使用gitrevisions
语法提取基本版本。
git checkout -b HEAD
会产生fatal: 'HEAD'不是一个有效的分支名称。
- torekgit merge Branch B
这个命令将B分支合并到当前分支(A)中
情况1。它应该将B分支的内容附加到A分支上。当文件名不同时或者B和A上相同文件的内容在不同行上不同时。
情况2。如果相同文件的内容在相同行上不同,则会出现合并冲突。
你正在将更改合并到当前分支,因此假设你当前已经检出了分支A,那么你将会将来自分支B的更改合并到分支A中。