Alex headshot

AlBlue’s Blog

Macs, Modularity and More

Git Tip of the Week: Merging Revisited

2011, git, gtotw

This week's Git Tip of the Week is about merging. You can subscribe to the feed if you want to receive new instalments automatically.


We previously looked at merging back in April, but we touched on the the index last week, and the index (or stage) numbers, which play a part in merges. This week, we'll look at what that means with different kinds of merges. If you're not familliar with merges, take a look at the previous post first.

Merging allows you to bring together two (or more) commits into a single merge commit, whilst at the same time bringing together all of the different files combined in the two. If there are merge conflicts, these need to be resolved individually. Merges where one of the commits is an ancestor of the current commit are referred to as fast-forward merges, and do not result in a merge commit being created. When you pull from a remote source, you're actually doing a fetch and a merge in one step – though usually, you can just fast-forward on when you do.

For the purposes of this post, I'll create three branches called ‘Alex’, ‘Bob’ and ‘master’, in homage to Alice and Bob. Each branch will have a file called Hello which will contain the text “Hello, my name is Alex” (c.f. Bob) as well as an AlexsFile, BobsFile etc.


(master) $ git ls-tree Alex
100644 blob ada4c4c4f33cd190fe40769d5ca9826adb9fb7ce	AlexsFile
100644 blob ca4eef2f4e3f1fe92028176cb547b590a08c2259	Hello
(master) $ git ls-tree Bob
100644 blob eea826732acee08a8cf83445e3b98cf58f11ce5c	BobsFile
100644 blob ca4eef2f4e3f1fe92028176cb547b590a08c2259	Hello
(master) $ git ls-tree master
100644 blob ca4eef2f4e3f1fe92028176cb547b590a08c2259	Hello

Strategies

When you create a merge, you have the option of saying how the merge will be processed. The usual strategy is recursive. This means Git will walk each directory (tree) and find out which files have differences compared to the base revision, and then use the one with changes. (If both have changes, the new file's contents are merged textually, and if there's a problem with that, a conflict ensues.) This is why you see the message “Merge made by recursive” after you do the operation:


(master) $ git checkout Alex
Switched to branch 'Alex'
(Alex) $ git merge Bob
Merge made by recursive.
 BobsFile |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 BobsFile
(Alex) $ git log --oneline --graph
*   d612aab Merge branch 'Bob' into Alex
|\  
| * 8afb2d3 Bob's File
* | 4abf59e Alex's File
|/  
* 5cba624 Hello
(Alex) $ git show d612aa
commit d612aab9858289ed027230d3b9a7b2a7a5e75945
Merge: 4abf59e 8afb2d3
…
    Merge branch 'Bob' into Alex

The commit d612aa… is a merge node in that it brings two separate branches together. Since neither is an ancestor of the other, they can't be fast-forwarded, and as such, the merge node is created. We can determine the commits' parents with HEAD^1 and HEAD^2:


(Alex) $ git rev-parse HEAD^1
4abf59ef73c186e93db25e8b7bc4423fbd11bbd0
(Alex) $ git rev-parse HEAD^2
8afb2d368ce26ca71cec539c31400c7001a18efc

It's even easier when there are no changes to be merged:


(Alex) $ git checkout master
Switched to branch 'master'
(master) $ git merge Bob
Updating 5cba624..8afb2d3
Fast-forward
 BobsFile |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 BobsFile

The fast-forward here indicates that master is behind Bob, and so we can simply move the pointer forwards – as a result, we don't need to create a merge node.

So, two strategies that Git uses are to do a fast-forward or merge made by recursive. This covers 99% of the merges that you'll need to do; but it's worth noting that Git has a few more tricks up it's sleeve which can help in certain situations.

Octopus Merge

The examples above all have one or two parents. However, a Git merge node is capable of representing more than two heads in a merge, and it uses a strategy called octopus. This is selected by default when you merge more than two branches:


(master) $ git reset --hard 5cba624b94a7a622183c960697867c8bba73aa91
HEAD is now at 5cba624 Hello
(master) $ date > NewFile
(master) $ git add NewFile
(master) $ git commit -m "New File"
[master 598ad85] New File
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 NewFile
(master) $ git merge Alex Bob
Trying simple merge with Alex
Trying simple merge with Bob
Merge made by octopus.
 AlexsFile |    1 +
 BobsFile  |    1 +
 2 files changed, 2 insertions(+), 0 deletions(-)
 create mode 100644 AlexsFile
 create mode 100644 BobsFile
(master) $ git log --oneline --graph
*-.   5a2aa0d Merge branches 'Alex' and 'Bob'
|\ \  
| | * 8afb2d3 Bob's File
| * | 4abf59e Alex's File
| |/  
* | 598ad85 New File
|/  
* 5cba624 Hello
(master) $ git show
commit 5a2aa0da3d3b3365703d710dad8aeebc0770b8ef
Merge: 598ad85 4abf59e 8afb2d3
…
    Merge branches 'Alex' and 'Bob'
(master) $ git rev-parse HEAD^1 HEAD^2 HEAD^3
598ad850114e1f7445ee8b02e93ee23060439560
4abf59ef73c186e93db25e8b7bc4423fbd11bbd0
8afb2d368ce26ca71cec539c31400c7001a18efc

In this case, we have three commits merging together into the merge node; the master (which had diverged), and the Alex and Bob branches from before. Since our merge node now has three parents, we can use git rev-parse to convert the HEAD^1/2/3 into the first, second and third heads.

What if the files are conflicted?


(master) $ git checkout Alex
Switched to branch 'Alex'
(Alex) $ echo Hello, my name is Alex > Hello 
(Alex) $ git commit -a -m "My name is Alex"
[Alex c2cb955] My name is Alex
 1 files changed, 1 insertions(+), 1 deletions(-)
(Alex) $ git checkout Bob
Switched to branch 'Bob'
(Bob) $ echo Hello, my name is Bob > Hello  
(Bob) $ git commit -a -m "My name is Bob"
[Bob 7cb6225] My name is Bob
 1 files changed, 1 insertions(+), 1 deletions(-)
(Bob) $ git checkout master
(master) $ git merge Alex Bob
Trying simple merge with Alex
Trying simple merge with Bob
Simple merge did not work, trying automatic merge.
Auto-merging Hello
ERROR: content conflict in Hello
fatal: merge program failed
Automatic merge failed; fix conflicts and then commit the result.
(master|MERGING) $ git ls-files --stage
100644 ada4c4c4f33cd190fe40769d5ca9826adb9fb7ce 0	AlexsFile
100644 eea826732acee08a8cf83445e3b98cf58f11ce5c 0	BobsFile
100644 ca4eef2f4e3f1fe92028176cb547b590a08c2259 1	Hello
100644 a5a820416bae2c7b77340e5b2120aab9595d2bfc 2	Hello
100644 98b16693fe64acb9d002af1fe5f5162d58bd40b4 3	Hello
100644 09d774502f97ba9a46f25f8f11601b653c376828 0	NewFile
(master|MERGING) $ 

We can launch a three-way diff tool with git mergetool:


(master|MERGING) $ git mergetool
(master|MERGING) $ git mergetool
merge tool candidates: opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse ecmerge p4merge araxis bc3 emerge vimdiff
Merging:
Hello

Normal merge conflict for 'Hello':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff): 
…
(master|MERGING) $ git commit -a -m "Merged"
[master 999a938] Merged

Note that the Octopus merge does not handle conflicts of more than 2 files. If you do, you end up with a different error mesage:


(master) $ echo Hello World > Hello 
(master) $ git commit -a -m "Hello World"
[master fe79e59] Hello World
 1 files changed, 1 insertions(+), 1 deletions(-)
(master) $ git merge Alex Bob
Trying simple merge with Alex
Simple merge did not work, trying automatic merge.
Auto-merging Hello
ERROR: content conflict in Hello
fatal: merge program failed
Automated merge did not work.
Should not be doing an Octopus.
Merge with strategy octopus failed.

Ours

Finally, the last strategy that's worth knowing about is the ours strategy. This takes any number of heads, and creates a merge node, but without actually doing any changes. In other words, a git diff HEAD^1 will always return empty for an ours strategy:


(master) $ git diff HEAD^1
(master) $ git log --oneline --graph --decorate
*-.   c5f84cc (HEAD, master) Merge branches 'Alex' and 'Bob'
|\ \  
| | * 7cb6225 (Bob) My name is Bob
| | * 8afb2d3 Bob's File
| * | c2cb955 (Alex) My name is Alex
| * | 4abf59e Alex's File
| |/  
* | 598ad85 New File
|/  
* 5cba624 Hello
(master) $ git rev-parse HEAD^{tree}
78784bb4dea678c157d8711bc56c5478a74588c3
(master) $ git rev-parse HEAD^1^{tree}
78784bb4dea678c157d8711bc56c5478a74588c3

We can verify that these are identical, since tree pointed to by HEAD is the same as the tree pointed to by HEAD^1 (i.e. the parent). The suffix ^{tree} is used to show the tree associated with the commit.

The --decorate argument to git log adds the branch names to the output, which can be useful in showing where merges have come from. The --graph argument is mostly of use with the --oneline argument; although you can run a git --graph the full commit messages tend to hide the structure of the graph.

The ours strategy is only really useful if you want to encode a set of previous commits but not have them affect the current master (say, because you've cherry picked some of the contents and don't want to take other parts, but preserve them in the history as-is somehow).

Merge Message and Fast Forwards

The merge message will be created automatically, based on the names of the branches you are merging. However, it's possible to pass a -m option, like with git commit, to supply an additional message. This can be useful if the merge message needs additional information encoded (such as which bug(s) were fixed).

It's also possible to force a merge, even if one isn't necessary. If you have topic-based branches, it can be useful to denote that the work was carried out on a separate branch before being merged back into master. Running git merge --no-ff will create a merge node, whether or not the branch can be fast-forwarded it or not. Since the merge commit has the branch name you're merging from as part of the commit, you can end up with descriptive names to show the feature having been completed:


(master) $ git checkout -b "bug12345"
Switched to a new branch 'bug12345'
(bug12345) $ echo BugFix >> Hello 
(bug12345) $ git commit -a -m "Fixing bug 12345"
[bug12345 e2bd64e] Fixing bug 12345
 1 files changed, 2 insertions(+), 0 deletions(-)
(bug12345) $ git checkout master
Switched to branch 'master'
(master) $ git merge bug12345 # without --no-ff
Updating 999a938..e2bd64e
Fast-forward
 Hello |    2 ++
 1 files changed, 2 insertions(+), 0 deletions(-)
(master) $ git reset --hard HEAD^
HEAD is now at 999a938 Merged
(master) $ git merge --no-ff bug12345 # with --no-ff
Merge made by recursive.
 Hello |    2 ++
 1 files changed, 2 insertions(+), 0 deletions(-)
(master) $ git log --oneline --graph --decorate
*   4a905c8 (HEAD, master) Merge branch 'bug12345'
|\  
| * e2bd64e (bug12345) Fixing bug 12345
|/  
*-.   999a938 Merged

Being able to merge with a --no-ff can be useful when using some kinds of commit workflows, where many branches are used to develop individual features and then brought into a master branch subsequently. It's also worth noting the --merges argument allows you to filter just the merges in a repository:


(master) apple[merge] $ git log --oneline --merges --decorate
4a905c8 (HEAD, master) Merge branch 'bug12345'
999a938 Merged

Next time, we'll look at what we can do with the different Git workflows using merge --no-ff.


Come back next week for another instalment in the Git Tip of the Week series.