Mercurial Best Practices

This project uses Mercurial as it Source Code Manager (SCM). If you are used to traditional SCMs such as Subversion, you have to learn that Mercurial is a Distributed SCM (DSCM), which operates in a more sophisticated way. This document is not intended as a full Mercurial tutorial (we advice about reading the official tutorial at Selenic), but as an introduction which also describes the best practices we have defined for our project.

Please send any question or comment to dev at jrawio dot kenai dot com.

Introduction

The basic concept of a DSCM is that there are multiple, complete repositories. While with Subversion there is only one repository (typically at the forge) and you check out files from it, choosing a specific branch or tagged version, with Mercurial you always have a complete clone of the repository on your computer; in function of what you need to do, you extract out of it the desired branch into a working directory. That's why you don't “check out” from a Mercurial repository, but you entirely clone it:

fritz% hg clone https://kenai.com/hg/jrawio~src
destination directory: jrawio~src
updating working directory
361 files updated, 0 files merged, 0 files removed, 0 files unresolved

You'll note that a hidden directory named .hg is created in your target directory: that's where the clone has been stored. To select the desired branch to work with, you use the update command:

fritz% hg update -c default 
77 files updated, 0 files merged, 0 files removed, 0 files unresolved

or

fritz% hg update -C default
77 files updated, 0 files merged, 0 files removed, 0 files unresolved

This will prepare the required files in your working directory. If you specify the uppercase -C option, any local uncommitted change will be lost; with the lowercase -c option, an error message will be printed. Of course, we advice about using the latter version most of the time. Note that what other SCMs calls the “trunk” is instead named the “default” branch in Mercurial; it's the branch used for building the official releases. The update command works both with branches and single revision numbers or tags:

fritz% hg update -C 612
56 files updated, 0 files merged, 7 files removed, 0 files unresolved
fritz% hg update -C 1.5.2
37 files updated, 0 files merged, 7 files removed, 0 files unresolved

The id command let you see to which changeset the working directory is set to:

fritz% hg update -C 612
70 files updated, 0 files merged, 0 files removed, 0 files unresolved
fritz% hg id
22cdc4017acf+ (1.6)
fritz% hg update -C 1.5.2
93 files updated, 0 files merged, 7 files removed, 0 files unresolved
fritz% hg id
bda51f8fea52+ 1.5.2
fritz% hg update -C default
120 files updated, 0 files merged, 0 files removed, 0 files unresolved
fritz% hg id
13cc733c3fd6
fritz% hg update -C tip
27 files updated, 0 files merged, 9 files removed, 0 files unresolved
fritz% hg id
3f1e2f3aea37 (1.6) tip

As you can see, the id command shows you the changeset id, the branch if any (within parentheses) and the tags, if any.
You can see which tags are defined in the project with the tags command:

fritz% hg tags
tip                              694:f937409f32e9
1.5.4                            692:6e1d38eff017
1.5.3                            638:707fa855b850
1.5.2                            568:bda51f8fea52
1.5.1                            373:c4b375a8ef3c

When you perform local changes, you commit them as you would do with other SCMs:

fritz% hg commit -m "Commit message."

Changes are committed only to your local repository, so other people won't see them. Thanks to the fact that changes are locally committed, you can even rollback the last commit with:

fritz% hg rollback
rolling back last transaction

Rollback can be done only once after every commit. If you need to erase a higher number of commits, you can use the strip command:

fritz% hg strip -f 687
81 files updated, 0 files merged, 0 files removed, 0 files unresolved
saving bundle to /private/tmp/src/.hg/strip-backup/8823e28e8eb5-backup

You can check which outgoing changes are pending with the outgoing command:

fritz% hg outgoing
comparing with https://kenai.com/hg/jrawio~src
searching for changes
changeset:   689:166e784caab1
parent:      687:70982f05332b
user:        fabriziogiudici <fabrizio dot giudici at tidalwave dot it>
date:        Tue Aug 25 13:21:58 2009 +0200
summary:     Changed version number.

changeset:   690:51f2a49661cf
tag:         tip
user:        fabriziogiudici <fabrizio dot giudici at tidalwave dot it>
date:        Tue Aug 25 13:22:13 2009 +0200
summary:     Updated the license.

The latest change in a branch is called the “tip” and can be see with the tip command:

fritz% hg tip
changeset:   694:f937409f32e9
tag:         tip
user:        hudson
date:        Sun Aug 23 21:12:51 2009 +0200
summary:     [maven-release-plugin] prepare for next development iteration

Note that revision numbers in Mercurial are quite different from Subversion. The simple incremental number (e.g. 694) is only valid in your local repository and for a limited time; it might change every time you synchronize with the remote repository, if other people made changes too. The good revision identifier that is always valid is the alphanumeric string (e.g. f937409f32e9). Mercurial uses SHA-1 hashes for identifying any revision; what you're seeing is the leading part of a SHA-1 hash, which is handier than the full thing, still keeping very low chances of collisions.

You can synchronize your changes (so others can see them) with the push command:

fritz% hg push
pushing to https://kenai.com/hg/jrawio~src
searching for changes
adding changesets
adding manifests
adding file changes
added 7 changesets with 72 changes to 78 files (+1 heads)

which will update the remote repository. This operation might fail if somebody else have performed incompatible changes in the meantime. In this case, you'll be forced to first sync your local repository with them, merge them and at last performing the push. You can see whether there are incoming changes with the incoming command:

fritz% hg incoming
comparing with https://kenai.com/hg/jrawio~src
searching for changes
changeset:   692:6e1d38eff017
tag:         1.5.4
user:        hudson
date:        Sun Aug 23 21:12:15 2009 +0200
summary:     [maven-release-plugin] prepare release 1.5.4

changeset:   693:6a22b7d32402
user:        hudson
date:        Sun Aug 23 21:12:35 2009 +0200
summary:     [maven-release-plugin]  copy for tag 1.5.4

changeset:   694:f937409f32e9
tag:         tip
user:        hudson
date:        Sun Aug 23 21:12:51 2009 +0200
summary:     [maven-release-plugin] prepare for next development iteration

and you get the remote changes with the pull command:

fritz% hg pull
pulling from https://kenai.com/hg/jrawio~src
searching for changes
adding changesets
adding manifests
adding file changes
added 8 changesets with 18 changes to 4 files
(run 'hg update' to get a working copy)

You can see the history of the project with the log command:

fritz% hg log --limit 2
changeset:   694:f937409f32e9
tag:         tip
user:        hudson
date:        Sun Aug 23 21:12:51 2009 +0200
summary:     [maven-release-plugin] prepare for next development iteration

changeset:   693:6a22b7d32402
user:        hudson
date:        Sun Aug 23 21:12:35 2009 +0200
summary:     [maven-release-plugin]  copy for tag 1.5.4

We advice about always using the --limit option, otherwise Mercurial will dump the whole history of the project since the beginning.

All the described commands but push, pull, outgoing and incoming don't need a network connection, which facilitates the work in “roaming” mode. Furthermore, since commits are local, they are very fast - this facilitates a good working practice, that is “commit often” - we advice about committing every few minutes of work and pushing batch of changes when they make sense; at least once per day. For what concerns best commit practices, please read further about the branch management.
Consider that it's even possible to “collapse” multiple local commits into a single one, with a single description, before pushing them; this procedure has been described by Martin Fowler as “squash commit”. We don't recommend trying it until you have some deeper knowledge about how Mercurial works.

Branching and "branch-per-feature"

One of the major benefits of Mercurial over other SCMs is the much greater easiness of managing branches. Since you have the whole repository cloned on your computer, you can create, modify, close branches and switching the working area from a branch to another without requiring a network connection; this also means that all operations are fast.

To see which are the branches currently open in your project, use the branches command:

fritz% hg branches
default                      694:f937409f32e9
fix-jrw-246-1                637:cbf229d7afce
1.6                          621:f9584b14a8e4
2.0                          587:d4139f45eafa
fix-JRW-120                  562:a82b7ed21b94
fix-JRW-6                    560:eb9031bec74c
fix-JRW-162-and-JRW-194      367:e75735d2055c

The command also outputs the identifier of the latest changes performed on each branch. To see which branch you're working with, use the branch command:

fritz% hg branch
default

We have already told you that to switch the workspace from a branch to another you use the update command:

fritz% hg update -c default 
77 files updated, 0 files merged, 0 files removed, 0 files unresolved

or

fritz% hg update -C default
77 files updated, 0 files merged, 0 files removed, 0 files unresolved

You can create a new branch with the branch command and one argument:

fritz% hg branch my-new-branch
marked working directory as branch my-new-branch
fritz% hg commit -m "Created my-new-branch."

Please note that if you don't perform the commit, the branch is not really created (the concept is that the branch is created by the first commit which targets it).

The best practice adopted by all projects by Tidalwave is “branch-per-feature” - while it needs some more reasoning by the committers, it is extremely powerful and efficient. The concept is that every work that must be done for a bug fix or a new feature is performhed within a specific branch. For instance, if you are going to work on issue JRW-234, you'd create branch "fix-JRW-234". Occasionally it might happen that two issues appear to be tightly related since from the beginning, so you could create a single branch for both (e.g. "fix-JRW-234-and-JRW-235"). Very trivial changes (such as fixing a typo in a log, or adding a comment) can be done directly on the default branch - but we insist on that “very trivial”. In doubt, it's always better to branch.

Branches will be merged back to the default branch only when the work is completed - which means that all the related tests pass.

The advantages of this policy are:

  1. Different people can work at different issues without interfering with each other work.
  2. Issues requiring long time to be fixed won't jeopardize the road map. For instance, if a release is going to happen in a week, we don't need that all the work in progress is completed. Simply, issues that are not completed yet won't be merged. This also allows to have a fixed release plan (e.g. one release every two weeks, or every month) independently of the current status of work. This is especially handy for issues that require deep investigation and are hard to fix.
  3. It is possible to anticipate work on milestones requiring extensive refactoring or API changes. For instance, it is possible to start working on a v2.0 branch, while at the same time regular bug fixing goes on with v1.5, 1.6 and so on. Also, it is possible to work with bug fixes that might require extensive probing code to find out the problem - the probing code can be even committed, and later removed just before the work is finished.

Merging to the default branch is performed with the following sequence of commands:

fritz% hg update -c default
77 files updated, 0 files merged, 0 files removed, 0 files unresolved
fritz% hg merge -f -r fix-JRW-234
3 files updated, 0 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)
fritz% hg commit -m "Merged changes from branch fix-JRW-234"
fritz% hg update -c fix-JRW-234
127 files updated, 0 files merged, 6 files removed, 0 files unresolved
fritz% hg commit --close-branch -m "Closed branch fix-JRW-234"
created new head
fritz% hg update -c default
120 files updated, 0 files merged, 0 files removed, 0 files unresolved

In general, branching of course could not be straightforward in case of conflicts. We advice about using an IDE with Mercurial support (such as NetBeans 6.7.1) which provide visual aids for reviewing and resolving conflicts.

Especially with long-running branches, it is advisable to periodically merge with the default branch, for instance at every release; in this way it is possible to prevent excessive divergence from occurring, thus reducing the chances and the impact of conflicts when the final merge is performed. The following sequence of commands can be used for this task:

fritz% hg update -c fix-JRW-234
77 files updated, 0 files merged, 0 files removed, 0 files unresolved
fritz% hg merge -f -r default
3 files updated, 0 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)
fritz% hg commit -m "Merged changes from the default branch."

Creating and submitting patches

Patches can be created in two ways, by means of the diff and export commands. For instance, these commands creates a patch to apply to the default branch in order to incorporate changes made in the working directory:

fritz% hg diff -g -r default > mypatch.diff
fritz% hg export -g fix-JRW-234 -o mypatch.diff

While diff only dumps the changes, without references to the user who made them and the commit messages, export produces all these items of information. This means that if you produce a patch, submit to us and have your commit messages and your name listed in the project history - this works without requiring that you are a direct committer registered with the project forge.

We advice about using -g to use the “git” diff format: this extends the default GNU format with support for renaming, creating and deleting files, binary files and filesystem flags.

Suggested Mercurial configuration

Global Mercurial options can be configured in a .hgrc file placed under your home directory. You can find below an example of our suggested configuration:

fritz% cat $HOME/.hgrc
[ui]
username=fabriziogiudici <fabrizio dot giudici at tidalwave dot it>

[extensions]
hgext.mq =
hgext.keyword=

[auth]
kenai.prefix = https://kenai.com
kenai.username = fabriziogiudici
kenai.password = ***************

[keyword]
**.java =
**.xml =
**.properties =

[defaults]
log = --limit 10

[diff]
git = true 

The relevant settings are, from top to bottom:

  1. the user name as it will appear in commit messages
  2. the enablement of the MQ extensions (for the strip command and the support of “squash commits”)
  3. the authentication information if you are a committer registered with the forge
  4. the keyword expansion in sources
  5. some useful defaults for commands, such as log
  6. the selection of “git” as the default diff format