What is the dominant branch when doing a git merge

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.

Reminder: Git stores snapshots with parent linkage

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 a merge

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.


Git merge strategies

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.

One head is always 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.


How resolve/recursive merge works

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.


For resolve/recursive, there is no dominant branch by default

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 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

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.


How you can set dominance

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.

Leave a Comment