Given my current branch is Branch A
Branch A contains:
line 1 -- "i love you Foo"
Branch B contains:
line 1 -- "i love you Bar"
if i do a:
git merge Branch B
what would i get?
The modifier "dominant" is not defined by Git. The way you use the word appears to me to make an incorrect assumption, which I think makes the question un-answerable as is. With one small change, though, the answer becomes simple: it's neither. No branch is "dominant" here; both branches are equal partners, and the result of the merge is the same, whether you merge A into B, or B into A—but you can change this, in several ways.
There are quite a few useful points underneath, which this question exposes, so let's explore them. At the end, we'll see how to properly phrase the question, and what the possible answers are.
Each commit stores a complete copy of all the files in that commit, intact (albeit compressed). Some other version control systems start with an initial snapshot, then, for each commit, store a set of changes since a previous commit or changes since a previous snapshot. These other VCSes can therefore show you the changes easily (since that's what they have), but have a hard time getting the actual files (because they have to assemble lots of changes). Git takes the opposite approach, storing the files each time, and computing the changes only when you ask for them.
This doesn't make much difference in terms of usage, since given two snapshots, we can find a change, and given one snapshot and one change, we can apply the change to get a new snapshot. But it does matter somewhat, and I refer to these snapshots below. For (much) more on this, see How does git store files? and Are Git's pack files deltas rather than snapshots?
Meanwhile, each commit, in Git, also records a parent commit. These parent linkages form a backwards chain of commits, which we need since the commit IDs seem quite random:
4e93cf3 <- 2abedd2 <- 1f0c91a <- 3431a0f
The fourth commit "points back" to the third, which points back to the second, which points back to the first. (The first commit has no parent, of course, because it's the first commit.) This is how Git finds previous commits, given the latest or tip commits. The word tip does appear in the Git glossary, but only under the definition of branch.
The goal of any merge is to combine work. In Git, as in any other modern version control system, we can have different authors working on different branches, or "we" can be one person (one author, the royal "we" :-) ) working on different branches. In those various branches, we—whether "we" means one person or many—can make different changes to our files, with different intents and different outcomes.
Eventually, though, we decide we'd like to combine some of these in some way. Combining these different sets of changes, to achieve some particular result and—at least normally—record the fact that we did combine them, is a merge. In Git, this verb version of merge—to merge several branches—is done with git merge
, and the outcome is a merge, the noun form of merge.1 The noun can become an adjective: a merge commit is any commit with two or more parents. This is all defined properly in the Git glossary.
Each parent of a merge commit is a previous head (see below). The first such parent is the head that was HEAD
(see below as well). This makes the first parent of merge commits special, and is why git log
and git rev-list
have a --first-parent
option: this allows you to look at just the "main" branch, into which all "side" branches are merged. For this to work as desired, it's crucial that all merges (verb form) be performed carefully and with proper intent, which requires that none of them be performed via git pull
.
(This is one of several reasons that people new to Git should avoid the git pull
command. The importance, or lack thereof, of this --first-parent
property depends on how you are going to use Git. But if you are new to Git, you probably don't know yet how you are going to use Git, so you don't know whether this property will be important to you. Using git pull
casually screws it up, so you should avoid git pull
.)
1Confusingly, git merge
can also implement the action verb, but produce an ordinary, non-merge commit, using --squash
. The --squash
option actually suppresses the commit itself, but so does --no-commit
. In either case it's the eventual git commit
you run that makes the commit, and this is a merge commit unless you used --squash
when you ran git merge
. Why --squash
implies --no-commit
, when you can in fact run git merge --squash --no-commit
if you wanted it to skip the automatic commit step, is a bit of a mystery.
The git merge
documentation notes that there are five built-in strategies, named resolve
, recursive
, octopus
, ours
, and subtree
. I will note here that subtree
is just a minor tweak to recursive
, so perhaps it might be better to claim just four strategies. Moreover, resolve
and recursive
are actually pretty similar, in that recursive is simply a recursive variant of resolve
, which gets us down to three.
All three strategies work with what Git calls heads. Git does define the word head:
A named reference to the commit at the tip of a branch.
but the way Git uses this with git merge
does not quite match this definition either. In particular, you can run git merge 1234567
to merge commit 1234567
, even if it has no named reference. It is simply treated as if it were the tip of a branch. This works because the word branch itself is rather weakly defined in Git (see What exactly do we mean by "branch"?): in effect, Git creates an anonymous branch, so that you have an un-named reference to the commit that is the tip of this unnamed branch.
HEAD
The name HEAD
—which can also be spelled @
—is reserved in Git, and it always refers to the current commit (there is always a current commit).2 Your HEAD
may be either detached (pointing to a specific commit) or attached (containing the name of a branch, with the branch name in turn naming the specific commit that is therefore the current commit).
For all merge strategies, HEAD
is one of the heads to be merged.
The octopus
strategy is truly a bit different, but when it comes to resolving merge-able items, it works a lot like resolve
except that it cannot tolerate conflicts. That allows it to avoid stopping with a merge conflict in the first place, which thus allows it to resolve more than two heads. Except for its intolerance of conflicts and ability to resolve three or more heads, you can think of it as a regular resolve merge, which we'll get to in a moment.
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
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
]: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?
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
to find out what we changed, and:
git diff ba5eba5 BranchB
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 diff
s 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
With the default style, we see:
$ cat file.txt
<<<<<<< HEAD
i love you Foo
=======
i love you Bar
>>>>>>> BranchB
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With