鉴于我目前的分支是分支A.
分支A包含:
line 1 -- "i love you Foo"
Run Code Online (Sandbox Code Playgroud)
分支B包含:
line 1 -- "i love you Bar"
Run Code Online (Sandbox Code Playgroud)
如果我这样做:
git merge Branch B
Run Code Online (Sandbox Code Playgroud)
我会得到什么?
tor*_*rek 13
修饰语"显性"不是由Git定义的.在我看来,你使用这个词的方式做出了一个不正确的假设,我认为这个问题是不可回答的.然而,通过一个小的改变,答案变得简单:它既不是.这里没有分支"占主导地位"; 两个分支都是相等的伙伴,并且合并的结果是相同的,无论是将A合并到B,还是将B合并到A中,但是您可以通过多种方式更改它.
下面有很多有用的点,这个问题暴露出来,所以让我们来探索它们.最后,我们将看到如何正确地表达问题,以及可能的答案是什么.
每个提交都存储完整个提交中所有文件的完整副本(尽管是压缩的).其他一些版本控制系统以初始快照开始,然后,对于每次提交,存储自上次提交以来的一组更改或自上一个快照以来的更改.因此,这些其他VCS可以轻松地向您显示更改(因为这是他们拥有的),但很难获得实际文件(因为他们必须组装大量更改).Git采用相反的方法,每次都存储文件,并且仅在您要求时才计算更改.
这在使用方面没有太大区别,因为给定两个快照,我们可以找到更改,并且给定一个快照和一个更改,我们可以应用更改来获取新快照.但它确实有点重要,我在下面提到这些快照.有关这方面的更多内容,请参阅git如何存储文件?并且Git的包文件是否是快照而不是快照?
同时,Git中的每个提交也记录父提交.这些父链接形成了一个向后提交链,我们需要它,因为提交ID似乎很随机:
4e93cf3 <- 2abedd2 <- 1f0c91a <- 3431a0f
Run Code Online (Sandbox Code Playgroud)
第四个提交"指回"到第三个,它指向第二个,它指向第一个.(当然,第一次提交没有父级,因为它是第一次提交.)这是Git在给定最新或提示提交的情况下找到先前提交的方式.单词提示确实出现在Git词汇表中,但仅限于分支的定义.
任何合并的目标都是结合工作.在Git中,与任何其他现代版本控制系统一样,我们可以让不同的作者在不同的分支上工作,或者"我们"可以是在不同分支上工作的一个人(一个作者,皇家"我们":-)).在那些不同的分支中,我们 - 无论"我们"是指一个人还是多个人 - 可以对我们的文件进行不同的更改,具有不同的意图和不同的结果.
但最终,我们决定以某种方式将其中一些结合起来.结合这些不同的组变化,实现一些特定的结果和至少正常记录的事实,我们也将它们结合起来,是一个合并.在Git中,这个动词版本合并,合并几个分支,与完成git merge,其结果是一个合并,合并的名词形式.1 名词可以成为形容词:合并提交是指具有两个或更多父母的任何提交.这些都在Git词汇表中正确定义.
合并提交的每个父级都是前一个头(见下文).第一个这样的父母是头HEAD(也见下文).这使得merge的第一个parent提交特殊,这就是为什么git log并git rev-list有一个--first-parent选项:这允许你只查看"main"分支,所有"side"分支都合并到该分支中. 为了使其按照需要工作,所有合并(动词形式)必须仔细执行并具有适当的意图,这要求不通过任何一个执行git pull.
(这是Git新手应该避免git pull命令的几个原因之一.这个--first-parent属性的重要性或缺乏取决于你将如何使用Git.但如果你是Git的新手,你可能不会还知道你将如何使用Git的,所以你不知道该物业是否对你很重要. 用git pull随便螺丝它,所以你应该避免git pull.)
1令人困惑的是,git merge也可以实现动作动词,但生成普通的非合并提交,使用--squash.该--squash选项实际上会抑制提交本身,但同样如此--no-commit.在任何一种情况下,它都是git commit你运行的最终提交,这是一个合并提交,除非你--squash在运行时使用git merge.为什么--squash意味着--no-commit,git merge --squash --no-commit如果你想让它跳过自动提交步骤,你实际上可以运行,这有点神秘.
该git merge文件指出,有五个内置策略,命名resolve,recursive,octopus,ours,和subtree.我将在此处注意到这subtree只是一个小小的调整recursive,所以也许最好只声称四个策略.此外,resolve和recursive实际上是非常类似的,就是递归是一个简单的递归变型resolve,这会让我们减少到3.
所有这三个策略什么混帐要求工作头. Git确实定义了head这个词:
对分支顶端的提交的命名引用.
但Git使用它的方式git merge也不完全符合这个定义.特别是,即使没有命名引用,您也可以运行git merge 1234567合并提交1234567.它只是被视为分支的尖端.这是有效的,因为单词分支本身在Git中定义相当弱(请参阅"分支"到底是什么意思?):实际上,Git创建了一个匿名分支,因此您有一个未命名的提交引用这个未命名的分支的提示.
HEAD名称HEAD- 也可以拼写@- 在Git中保留,它总是引用当前提交(总是有当前提交).2 您HEAD可以分离(指向特定提交)或附加(包含分支的名称,分支名称依次命名特定提交,因此是当前提交).
对于所有合并策略,HEAD是要合并的头之一.
该octopus战略是一个真正的有点不同,但是当它涉及到解决的合并,能项目,它的工作原理很像resolve,只是它无法容忍冲突.这允许它首先避免以合并冲突停止,从而允许它解决两个以上的头.除了不容忍冲突和解决三个或更多头的能力之外,你可以把它看作是一个常规的解决方案合并,我们马上就会解决这个问题.
The ours strategy is wholly different: it completely ignores all other heads. There are never any merge conflicts because there are no other inputs: the result of the merge, the snapshot in the new HEAD, is the same as whatever was in the previous HEAD. This, too, allows this strategy to resolve more than two heads—and gives us a way to define "dominant head" or "dominant branch", as well, although now the definition is not particularly useful. For the ours strategy, the "dominant branch" is the current branch—but the goal of an ours merge is to record, in history, that there was a merge, without actually taking any of the work from the other heads. That is, this kind of merge is trivial: the verb form of "to merge" does nothing at all, and then the resulting noun form of "a merge" is a new commit whose first parent has the same snapshot, with the remaining parents recording the other heads.
2There is one exception to this rule, when you are on what Git calls variously an "unborn branch" or an "orphan branch". The example most people encounter most often is the state a newly created repository has: you are on branch master, but there are no commits at all. The name HEAD still exists, but the branch name master does not exist yet, as there is no commit it can point-to. Git resolves this sticky situation by creating the branch name as soon as you create the first commit.
You can get yourself into it again at any time using git checkout --orphan to create a new branch that does not actually exist yet. The details are beyond the scope of this answer.
The remaining (non-ours) kinds of merge are the ones we usually think of when we talk about merging. Here, we really are combining changes. We have our changes, on our branch; and they have their changes, on their branch. But since Git stores snapshots, first we have to find the changes. What, precisely, are the changes?
The only way Git can produce a list of our changes and a list of their changes is to first find a common starting point. It must find a commit—a snapshot—that we both had and both used. This requires looking through the history, which Git reconstructs by looking at the parents. As Git walks back through the history of HEAD—our work—and of the other head, it eventually finds a merge base: a commit we both started from.3 These are often visually obvious (depending on how carefully we draw the commit graph):
o--o--o <-- HEAD (ours)
/
...--o--*
\
o--o <-- theirs
Run Code Online (Sandbox Code Playgroud)
Here, the merge base is commit *: we both started from that commit, and we made three commits and they made two.
Since Git stores snapshots, it finds the changes by running, in essence, git diff base HEAD and git diff base theirs, with base being the ID of the merge base commit *. Git then combines these changes.
3The merge base is technically defined as the Lowest Common Ancestor, or LCA, of the Directed Acyclic Graph or DAG, formed by the commits' one-way arcs linking each commit to its parent(s). The commits are the vertices/nodes in the graph. LCAs are easy in trees, but DAGs are more general than trees, so sometimes there is no single LCA. This is where recursive merge differs from resolve merge: resolve works by picking one of these "best" ancestor nodes essentially arbitrarily, while recursive picks all of them, merging them to form a sort of pretend-commit: a virtual merge base. The details are beyond the scope of this answer, but I have shown them elsewhere.
Now we finally get to the answer to your question:
Given my current branch is Branch A
Branch A contains [in file
file.txt]:Run Code Online (Sandbox Code Playgroud)line 1 -- "i love you Foo"Branch B contains:
Run Code Online (Sandbox Code Playgroud)line 1 -- "i love you Bar"if i do a:
Run Code Online (Sandbox Code Playgroud)git merge BranchBwhat would i get?
To answer this, we need one more piece of information: What's in the merge base? What did you change on BranchA, and what did they change on BranchB? Not what's in the two branches, but rather, what did each of you change since the base?
Let's suppose we find the ID of the merge base,4 and it's (somehow) ba5eba5. We then run:
git diff ba5eba5 HEAD
Run Code Online (Sandbox Code Playgroud)
to find out what we changed, and:
git diff ba5eba5 BranchB
Run Code Online (Sandbox Code Playgroud)
to find out what they changed. (Or, similarly, we use git show ba5eba5:file.txt and look at line 1, although just doing the two git diffs is easier.) Obviously at least one of us changed something, otherwise line 1 would be the same in both files.
If we changed line 1 and they didn't, the merge result is our version.
If we didn't change line 1, and they did, the merge result is their version.
If we both changed line 1, the merge result is that the merge fails, with a merge conflict. Git writes both lines into the file and stops the merge with an error, and makes us clean up the mess:
Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt
Run Code Online (Sandbox Code Playgroud)
With the default style, we see:
$ cat file.txt
<<<<<<< HEAD
i love you Foo
=======
i love you Bar
>>>>>>> BranchB
Run Code Online (Sandbox Code Playgroud)
If we set merge.conflictStyle to diff3 (git config merge.conflictStyle diff3 or git -c merge.conflictStyle=diff3 merge BranchB or similar),5 Git writes not only our two lines, but also what was there originally:
$ cat file.txt
<<<<<<< HEAD
i love you Foo
||||||| merged common ancestors
original line 1
=======
i love you Bar
>>>>>>> BranchB
Run Code Online (Sandbox Code Playgroud)
Note, by the way, that Git doesn't look at any of the intermediate commits. It simply compares the merge base to the two heads, with two git diff commands.
4This presupposes that there is a single merge base. That's usually the case, and I don't want to get into virtual merge bases here except in some of these footnotes. We can find all the merge bases with git merge-base --all HEAD BranchB, and it usually prints just one commit ID; that's the (single) merge base.
5I use this diff3 style, setting it in my --global Git configuration, because I find it good, when resolving conflicts, to see what was in the merge base. I don't like having to find the merge base and check it out; and for a truly recursive merge, when Git constructed a virtual merge base, there's nothing to check out. Admittedly, when there is a virtual merge base, this can get quite complicated, as there can be merge conflicts in the virtual merge base! See Git - diff3 Conflict Style - Temporary merge branch for an example of this.
Let's define dominant head, for the purpose of handling merge conflicts or potential merge conflicts, as the version whose changes are preferred automatically. There is an easy way, in the recursive and resolve strategies, to set this.
Of course, Git gives us the -s ours merge strategy, which eliminates even the potential of merge conflicts. But if we didn't change line 1 and they did, this uses our line 1 anyway, so that's not what we want. We want to take either our or their line 1 if only we or only they changed it; we just want Git to prefer our head, or their head, for line 1 in the case where we both changed it.
These are the -X ours and -X theirs strategy-option arguments.6 We still use the recursive or resolve strategy, just as before, but we run with -X ours to tell Git that, in the case of a conflict, it should prefer our change. Or, we run with -X theirs to prefer their change. In either case, Git doesn't stop with a conflict: it takes the preferred (or dominant) change and presses on.
These options are somewhat dangerous, because they depend on Git getting the context right. Git knows nothing about our code or text files: it's just doing a line-by-line diff,7 trying to find a minimal set of instructions: "Delete this line here, add that one there, and that will take you from the version in the base commit to the version in one of the heads." Admittedly, this is true of "no preferred/dominant head" merges as well, but in those cases, we don't take one of their changes, and then override another nearby "their" change with ours, which is where we likely to hit trouble, especially in the kinds of files I work with.
In any case, if you run a regular git merge without one of these -X arguments, you'll get a conflict as usual. You can then run git checkout --ours on any one file to pick out our version of the file (with no conflicts at all, ignoring all of their changes), or git checkout --theirs to pick out their version of the file (again with no conflicts and ignoring all of our changes), or git checkout -m to re-create the merge conflicts. It would be nice if there were a user-oriented wrapper for git merge-file that would extract all three versions from the index8 and let you use --ours or --theirs to act like -X ours or -X theirs just for that one file (this is what those flags mean, in git merge-file). Note that this should also let you use --union. See the git merge-file documentation for a description of what this does. Like --ours and --theirs it's a bit dangerous and should be used with care and observation; it doesn't really work when merging XML files, for instance.
6The name "strategy option" is, I think, a bad choice, since it sounds just like the -s strategy argument, but is actually entirely different. The mnemonic I use is that -s takes a Strategy, while -X takes an eXtended strategy option, passed on to whatever strategy we already chose.
7You can control the diff through .gitattributes or options to git diff, but I'm not sure how the former affects the built in merge strategies, as I have not actually tried it.
8During a conflicted merge, all three versions of each conflicted file are there in the index, even though git checkout only has special syntax for two of them. You can use gitrevisions syntax to extract the base version.
| 归档时间: |
|
| 查看次数: |
1360 次 |
| 最近记录: |