Merging in Mercurial: A Practical Primer
Merging, and more specifically conflict resolution, in Mercurial can be a sticking point for new and casual users. The fetch extension can decrease some repetitive merging, but sooner or later conflicts will occur. For example, if you have a project with a web developer and designer collaborating in parallel on a task, at some point you'll have conflicting changes. While it's great that Mercurial now comes with sample configuration settings for a variety of merge tools, they're mostly GUI-based, which isn't going to help when logged-in to a headless Linux server. Ever try walking a casual user, over instant messenger, through the process of resolving their conflicts in a 3-pane Vim window? Mercurial's internal:merge merger is a simple alternative. It inserts good, old-fashioned conflict markers. You use whichever editor you like to resolve the conflicts, and you tell Mercurial when everything is resolved. We'll walk through this process starting with creating and conflict and finishing with resolving it.
Setup a New Repository
To begin, we'll create an example repository:
$ hg init hello
$ cd hello
$ echo "print 'hello world'" > hello.py
$ python hello.py
hello world
$ hg ci --config ui.username='Able <able@example.com>' -A -m 'getting started'
adding hello.py
Let's see what we've got:
$ hg log -p
changeset: 0:e15827879a00
tag: tip
user: Able <able@example.com>
date: Wed Apr 21 19:09:02 2010 -0400
summary: getting started
diff --git a/hello.py b/hello.py
new file mode 100755
--- /dev/null
+++ b/hello.py
@@ -0,0 +1,1 @@
+print 'hello world'
Create Conflicting Changes
Now that we've got a base version, let's create a conflict. Pretending to be two other users, we'll create conflicting changes to Able's file. First, Baker will add a line with the current date and time.
$ cd ..
$ hg clone hello hello_baker
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd hello_baker
$ echo print \"baker says: it is now $(date)\" >> hello.py
$ hg ci --config ui.username="Baker <baker@example.com>" -m "baker's time"
$ python hello.py
hello world
baker says: it is now Wed Apr 21 19:20:34 EDT 2010
Second, Charlie will do the same but with a different date & time.
$ cd ..
$ hg clone hello hello_charlie
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd hello_charlie
$ echo print \"charlie says: it is now $(date)\" >> hello.py
$ hg ci --config ui.username="Charlie <charlie@example.com>" -m "charlie's time"
$ python hello.py
hello world
charlie says: it is now Wed Apr 21 19:22:15 EDT 2010
Now we've got 3 repositories with 3 different versions:
A / \ B C
- Able's original 1-line file
- Baker's version, which adds a line to Able's
- Charlie's version, which ads a different line to Able's
Pull Changes & Resolve Conflicts
Able's in charge of consolidating the changes, which we know will conflict. He starts with Baker's:
$ cd ../hello
$ hg pull -u ../hello_baker
pulling from ../hello_baker
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg tip
changeset: 1:786404a3c8b9
tag: tip
user: Baker <baker@example.com>
date: Wed Apr 21 19:20:45 EDT 2010
summary: baker's time
At this point, Able and Baker both have the same repository. Both consist of 2 changesets: Able's original and Baker's addition. When Able pulls from Charlie, he's going to get a conflict, because a human must resolve the conflicting changes made to line 2 of hello.py.
$ hg pull -u ../hello_charlie
pulling from ../hello_charlie
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
not updating, since new heads added
(run 'hg heads' to see heads, 'hg merge' to merge)
hg heads lists the changesets that require merging:
$ hg heads
changeset: 2:8c0dc6a6e5fe
tag: tip
parent: 0:e15827879a00
user: Charlie <charlie@example.com>
date: Wed Apr 21 19:22:25 EDT 2010
summary: charlie's time
changeset: 1:786404a3c8b9
user: Baker <baker@example.com>
date: Wed Apr 21 19:20:45 EDT 2010
summary: baker's time
$ hg merge --config ui.merge=internal:merge
merging hello.py
warning: conflicts during merge.
merging hello.py failed!
0 files updated, 0 files merged, 0 files removed, 1 files unresolved
use 'hg resolve' to retry unresolved file merges or 'hg update -C' to abandon
With internal:merge, Mercurial inserts conflict markers showing the conflicting portions. You can pick one, the other, or neither.
$ cat hello.py
print 'hello world'
<<<<<<< local
print "charlie says: it is now Wed Apr 21 19:22:15 EDT 2010"
=======
print "baker says: it is now Wed Apr 21 19:20:34 EDT 2010"
>>>>>>> other
In this case, Able doesn't like Baker or Charlie's solution and does things his own way:
$ hg diff
diff --git a/hello.py b/hello.py
--- a/hello.py
+++ b/hello.py
@@ -1,2 +1,2 @@
print 'hello world'
-print "charlie says: it is now Wed Apr 21 19:22:15 EDT 2010"
+print 'able says: use "date" to check the time'
$ hg ci --config ui.username='Able <able@example.com>' -m 'use date!'
abort: unresolved merge conflicts (see hg resolve)
Oops, it's easy to forget about hg resolve! While Able resolve the situation, Mercurial needs to be informed. This is a good time to mention hg resolve plays several roles. In addition to marking files as resolved, it can list files needed resolution, and mark files as unresolved. Let's mark all files as resolved and then commit:
$ hg resolve -m -a
$ hg ci --config ui.username='Able <able@example.com>' -m 'use date!'
Able's committed the merge. Notice this changeset has two parents -- the two heads have been merged into one. For refugees from other VCS, having to commit a merge can be a surprise, but it's great! It means until you've committed the merge you haven't actually committed yourself to anything. You're free to experiment, redo the merge if things don't go smoothly, etc... and since your work was committed before you began merging your original work is also safe.
$ hg tip
hg tip
changeset: 3:fa1b367d9adb
tag: tip
parent: 2:8c0dc6a6e5fe
parent: 1:786404a3c8b9
user: Able <able@example.com>
date: Wed Apr 21 19:24:15 2010 -0400
summary: use date!
To use internal:merge all the time, add it to your ~/.hgrc.
[ui]
merge=internal:merge
More "internals"
Here are few more merge tools Mercurial has up its sleeves:
- internal:prompt: have Mercurial ask you whether "keep local" or "take other" -- used by default for unmergable files (e.g. images)
- internal:other: take other version -- useful if you need to merge and want to accept the other version as-is
- internal:local: keep local version -- useful if you want to merge and keep your version as-is
Tips & Tricks
hg glog (graphlog) is an extension that shows a text-based graphical representation of history.
$ hg glog -r -3:
@ changeset: 3:fa1b367d9adb
|\ tag: tip
| | parent: 2:8c0dc6a6e5fe
| | parent: 1:786404a3c8b9
| | user: Able <able@example.com>
| | date: Wed Apr 21 19:24:15 2010 -0400
| | summary: use date!
| |
| o changeset: 2:8c0dc6a6e5fe
| | parent: 0:e15827879a00
| | user: Charlie <charlie@example.com>
| | date: Wed Apr 21 19:22:25 EDT 2010
| | summary: charlie's time
| |
o | changeset: 1:786404a3c8b9
|/ user: Baker <baker@example.com>
| date: Wed Apr 21 19:20:45 EDT 2010
| summary: baker's time
|
You can browse your repository using Mercurial's built-in web server, which has graphical log view.
$ hg server -v -a 127.0.0.0 -p 8888
listening at http://localhost:8888/ (bound to 127.0.0.1:8888)
and open http://127.0.0.1:8888/ in your browser.
Other Resources
- hginit: Merging - part 4 of Joel Spolsky's 6-part Mercurial tutorial.
- Chapter 3. A tour of Mercurial: merging work from Mercurial: The Definite Guide.