Table of Contents
We've now covered cloning a repository, making changes in a repository, and pulling or pushing changes from one repository into another. Our next step is merging changes from separate repositories.
Merging is a fundamental part of working with a distributed revision control tool. Here are a few cases in which the need to merge work arises.
Alice and Bob each have a personal copy of a repository for a project they're collaborating on. Alice fixes a bug in her repository; Bob adds a new feature in his. They want the shared repository to contain both the bug fix and the new feature.
Cynthia frequently works on several different tasks for a single project at once, each safely isolated in its own repository. Working this way means that she often needs to merge one piece of her own work with another.
Because we need to merge often, Mercurial makes the process easy. Let's walk through a merge. We'll begin by cloning yet another repository (see how often they spring up?) and making a change in it.
$
cd ..
$
hg clone hello my-new-hello
updating working directory 2 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd my-new-hello
# Make some simple edits to hello.c.$
my-text-editor hello.c
$
hg commit -m 'A new hello for a new day.'
We should now have two copies of
hello.c
with different contents. The
histories of the two repositories have also diverged, as
illustrated in Figure 3.1, “Divergent recent histories of the my-hello and my-new-hello
repositories”. Here is a copy of our
file from one repository.
$
cat hello.c
/* * Placed in the public domain by Bryan O'Sullivan. This program is * not covered by patents in the United States or other countries. */ #include <stdio.h> int main(int argc, char **argv) { printf("once more, hello.\n"); printf("hello, world!\"); printf("hello again!\n"); return 0; }
And here is our slightly different version from the other repository.
$
cat ../my-hello/hello.c
/* * Placed in the public domain by Bryan O'Sullivan. This program is * not covered by patents in the United States or other countries. */ #include <stdio.h> int main(int argc, char **argv) { printf("hello, world!\"); printf("hello again!\n"); return 0; }
We already know that pulling changes from our my-hello
repository will have no
effect on the working directory.
$
hg pull ../my-hello
pulling from ../my-hello searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge)
However, the hg pull command says something about “heads”.
Remember that Mercurial records what the parent of each change is. If a change has a parent, we call it a child or descendant of the parent. A head is a change that has no children. The tip revision is thus a head, because the newest revision in a repository doesn't have any children. There are times when a repository can contain more than one head.
In Figure 3.2, “Repository contents after pulling from my-hello into my-new-hello”, you can
see the effect of the pull from my-hello
into my-new-hello
. The history that
was already present in my-new-hello
is untouched, but
a new revision has been added. By referring to Figure 3.1, “Divergent recent histories of the my-hello and my-new-hello
repositories”, we can see that the
changeset ID remains the same in the new
repository, but the revision number has
changed. (This, incidentally, is a fine example of why it's
not safe to use revision numbers when discussing changesets.)
We can view the heads in a repository using the hg heads command.
$
hg heads
changeset: 6:b6fed4f21233 tag: tip parent: 4:2278160e78d4 user: Bryan O'Sullivan <[email protected]> date: Tue May 05 06:55:53 2009 +0000 summary: Added an extra line of output changeset: 5:5218ee8aecf3 user: Bryan O'Sullivan <[email protected]> date: Tue May 05 06:55:55 2009 +0000 summary: A new hello for a new day.
What happens if we try to use the normal hg update command to update to the new tip?
$
hg update
abort: crosses branches (use 'hg merge' or 'hg update -C')
Mercurial is telling us that the hg update command won't do a merge; it won't update the working directory when it thinks we might want to do a merge, unless we force it to do so. (Incidentally, forcing the update with hg update -C would revert any uncommitted changes in the working directory.)
To start a merge between the two heads, we use the hg merge command.
$
hg merge
merging hello.c 0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit)
We resolve the contents of hello.c
This updates the working directory so that it
contains changes from both heads, which
is reflected in both the output of hg
parents and the contents of
hello.c
.
$
hg parents
changeset: 5:5218ee8aecf3 user: Bryan O'Sullivan <[email protected]> date: Tue May 05 06:55:55 2009 +0000 summary: A new hello for a new day. changeset: 6:b6fed4f21233 tag: tip parent: 4:2278160e78d4 user: Bryan O'Sullivan <[email protected]> date: Tue May 05 06:55:53 2009 +0000 summary: Added an extra line of output$
cat hello.c
/* * Placed in the public domain by Bryan O'Sullivan. This program is * not covered by patents in the United States or other countries. */ #include <stdio.h> int main(int argc, char **argv) { printf("once more, hello.\n"); printf("hello, world!\"); printf("hello again!\n"); return 0; }
Whenever we've done a merge, hg parents will display two parents until we hg commit the results of the merge.
$
hg commit -m 'Merged changes'
We now have a new tip revision; notice that it has both of our former heads as its parents. These are the same revisions that were previously displayed by hg parents.
$
hg tip
changeset: 7:ecb0e17b2a4e tag: tip parent: 5:5218ee8aecf3 parent: 6:b6fed4f21233 user: Bryan O'Sullivan <[email protected]> date: Tue May 05 06:55:56 2009 +0000 summary: Merged changes
In Figure 3.3, “Working directory and repository during merge, and following commit”, you can see a representation of what happens to the working directory during the merge, and how this affects the repository when the commit happens. During the merge, the working directory has two parent changesets, and these become the parents of the new changeset.
We sometimes talk about a merge having sides: the left side is the first parent in the output of hg parents, and the right side is the second. If the working directory was at e.g. revision 5 before we began a merge, that revision will become the left side of the merge.
Most merges are simple affairs, but sometimes you'll find yourself merging changes where each side modifies the same portions of the same files. Unless both modifications are identical, this results in a conflict, where you have to decide how to reconcile the different changes into something coherent.
Figure 3.4, “Conflicting changes to a document” illustrates an instance of two conflicting changes to a document. We started with a single version of the file; then we made some changes; while someone else made different changes to the same text. Our task in resolving the conflicting changes is to decide what the file should look like.
Mercurial doesn't have a built-in facility for handling conflicts. Instead, it runs an external program, usually one that displays some kind of graphical conflict resolution interface. By default, Mercurial tries to find one of several different merging tools that are likely to be installed on your system. It first tries a few fully automatic merging tools; if these don't succeed (because the resolution process requires human guidance) or aren't present, it tries a few different graphical merging tools.
It's also possible to get Mercurial to run a
specific program or script, by setting the
HGMERGE
environment variable to the name of your
preferred program.
My preferred graphical merge tool is kdiff3, which I'll use to describe the features that are common to graphical file merging tools. You can see a screenshot of kdiff3 in action in Figure 3.5, “Using kdiff3 to merge versions of a file”. The kind of merge it is performing is called a three-way merge, because there are three different versions of the file of interest to us. The tool thus splits the upper portion of the window into three panes:
At the left is the base version of the file, i.e. the most recent version from which the two versions we're trying to merge are descended.
In the middle is “our” version of the file, with the contents that we modified.
On the right is “their” version of the file, the one that from the changeset that we're trying to merge with.
In the pane below these is the current result of the merge. Our task is to replace all of the red text, which indicates unresolved conflicts, with some sensible merger of the “ours” and “theirs” versions of the file.
All four of these panes are locked together; if we scroll vertically or horizontally in any of them, the others are updated to display the corresponding sections of their respective files.
For each conflicting portion of the file, we can choose to resolve the conflict using some combination of text from the base version, ours, or theirs. We can also manually edit the merged file at any time, in case we need to make further modifications.
There are many file merging tools available, too many to cover here. They vary in which platforms they are available for, and in their particular strengths and weaknesses. Most are tuned for merging files containing plain text, while a few are aimed at specialised file formats (generally XML).
In this example, we will reproduce the file modification history of Figure 3.4, “Conflicting changes to a document” above. Let's begin by creating a repository with a base version of our document.
$
cat > letter.txt <<EOF
>
Greetings!
>
I am Mariam Abacha, the wife of former
>
Nigerian dictator Sani Abacha.
>
EOF
$
hg add letter.txt
$
hg commit -m '419 scam, first draft'
We'll clone the repository and make a change to the file.
$
cd ..
$
hg clone scam scam-cousin
updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd scam-cousin
$
cat > letter.txt <<EOF
>
Greetings!
>
I am Shehu Musa Abacha, cousin to the former
>
Nigerian dictator Sani Abacha.
>
EOF
$
hg commit -m '419 scam, with cousin'
And another clone, to simulate someone else making a change to the file. (This hints at the idea that it's not all that unusual to merge with yourself when you isolate tasks in separate repositories, and indeed to find and resolve conflicts while doing so.)
$
cd ..
$
hg clone scam scam-son
updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd scam-son
$
cat > letter.txt <<EOF
>
Greetings!
>
I am Alhaji Abba Abacha, son of the former
>
Nigerian dictator Sani Abacha.
>
EOF
$
hg commit -m '419 scam, with son'
Having created two different versions of the file, we'll set up an environment suitable for running our merge.
$
cd ..
$
hg clone scam-cousin scam-merge
updating working directory 1 files updated, 0 files merged, 0 files removed, 0 files unresolved$
cd scam-merge
$
hg pull -u ../scam-son
pulling from ../scam-son 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)
In this example, I'll set
HGMERGE
to tell Mercurial to use the
non-interactive merge command. This is
bundled with many Unix-like systems. (If you're following this
example on your computer, don't bother setting
HGMERGE
. You'll get dropped into a GUI file
merge tool instead, which is much preferable.)
$
export HGMERGE=merge
$
hg merge
merging letter.txt merge: warning: conflicts during merge merging letter.txt failed! 0 files updated, 0 files merged, 0 files removed, 1 files unresolved use 'hg resolve' to retry unresolved file merges or 'hg up --clean' to abandon$
cat letter.txt
Greetings! <<<<<<< /tmp/tour-merge-conflictgW7-1Z/scam-merge/letter.txt I am Shehu Musa Abacha, cousin to the former ======= I am Alhaji Abba Abacha, son of the former >>>>>>> /tmp/letter.txt~other.c6Rq0s Nigerian dictator Sani Abacha.
Because merge can't resolve the conflicting changes, it leaves merge markers inside the file that has conflicts, indicating which lines have conflicts, and whether they came from our version of the file or theirs.
Mercurial can tell from the way merge exits that it wasn't able to merge successfully, so it tells us what commands we'll need to run if we want to redo the merging operation. This could be useful if, for example, we were running a graphical merge tool and quit because we were confused or realised we had made a mistake.
If automatic or manual merges fail, there's nothing to prevent us from “fixing up” the affected files ourselves, and committing the results of our merge:
$
cat > letter.txt <<EOF
>
Greetings!
>
I am Bryan O'Sullivan, no relation of the former
>
Nigerian dictator Sani Abacha.
>
EOF
$
hg resolve -m letter.txt
$
hg commit -m 'Send me your money'
$
hg tip
changeset: 3:6f17ad930bf5 tag: tip parent: 1:cef8fbca9a6f parent: 2:dc8f64391590 user: Bryan O'Sullivan <[email protected]> date: Tue May 05 06:55:57 2009 +0000 summary: Send me your money
The process of merging changes as outlined above is straightforward, but requires running three commands in sequence.
hg pull -u hg merge hg commit -m 'Merged remote changes'
In the case of the final commit, you also need to enter a commit message, which is almost always going to be a piece of uninteresting “boilerplate” text.
It would be nice to reduce the number of steps needed, if
this were possible. Indeed, Mercurial is distributed with an
extension called fetch
that
does just this.
Mercurial provides a flexible extension mechanism that lets people extend its functionality, while keeping the core of Mercurial small and easy to deal with. Some extensions add new commands that you can use from the command line, while others work “behind the scenes,” for example adding capabilities to Mercurial's built-in server mode.
The fetch
extension adds a new command called, not surprisingly, hg fetch. This extension acts as a
combination of hg pull -u,
hg merge and hg commit. It begins by pulling
changes from another repository into the current repository. If
it finds that the changes added a new head to the repository, it
updates to the new head, begins a merge, then (if the merge
succeeded) commits the result of the merge with an
automatically-generated commit message. If no new heads were
added, it updates the working directory to the new tip
changeset.
Enabling the fetch
extension is easy. Edit the
.hgrc
file in your home
directory, and either go to the extensions
section or create an
extensions
section. Then
add a line that simply reads
“fetch=
”.
[extensions] fetch =
(Normally, the right-hand side of the
“=
” would indicate where to find
the extension, but since the fetch
extension is in the standard
distribution, Mercurial knows where to search for it.)
During the life of a project, we will often want to change the layout of its files and directories. This can be as simple as renaming a single file, or as complex as restructuring the entire hierarchy of files within the project.
Mercurial supports these kinds of complex changes fluently, provided we tell it what we're doing. If we want to rename a file, we should use the hg rename[2] command to rename it, so that Mercurial can do the right thing later when we merge.
We will cover the use of these commands in more detail in the section called “Copying files”.
[2] If you're a Unix user, you'll be glad to know that the hg rename command can be abbreviated as hg mv.