Advanced Mercurial

I've been using Mercurial for roughly 3 years. By and large the basic pull/push/branch/merge/commit commands have done everything I've needed. Lately I've been feeling a little constrained, perhaps because git is opening my eyes to new concepts.

I had started to think that maybe Mercurial just wasn't that good. It turns out I just didn't know how to use it.

(A lot of these tips and tricks come from Steve Losh)

Secret Business

hg phase -fs

A changeset in mercurial has a phase associated with it: secret; draft; or public. A new (unpushed) commit creates a draft changeset; pushing it makes it public. This is usually all hidden away, but you can manually set the phase of you want. In particular, setting it to secret means that it will not be pushed when you push the rest of the repo. This (for example) will let you maintain a private bookmark without pushing a new head.

You can also set a new commit to secret using hg commit -s.

To make a changeset (and its ancestors) pushable again, change its phase to draft:

hg phase -d

Push like Git

hg push -r .

Mercurial uses '.' as shorthand for "the parent changeset of the working directory", ie usually the head of the branch you are on. Using -r to specify the revision to push, along with '.', means that you only push the current branch. Again, this is a way to maintain a local branch/bookmark.

Don't look stupid

hg commit --amend

Adds the changes in your working directory to the last (draft/unpushed) commit. So when you commit then see a stupid mistake, you can fix it and amend the commit. ie, no more commit -m typo

Avoid merge commits

hg rebase

Rebase takes a series of changesets and moves them to a different parent. In essence, it lets you clean up local history and avoid polluting the hg timeline with a bunch of merge commits.

Let's say you’re working on an issue on default. Once you're done, you pull and because someone else has committed to default, there's now two heads. Rather than just merge them in, you can rebase your changes on top of the other/public head:

hg rebase -m "Fixes issue 1765"

By default, rebase uses the parent of the working directory as the base, but you can specify it explicitly with -b <changeset>. You can also set the destination with -d.

Rebase starts at the “-b” revision, then works backwards until it finds a common ancestor with the destination. It takes all of the commits after that ancestor, 'replays' them as children of the destination destination changeset, then deletes the originals. Note that you might have to resolve multiple merge conflicts as hg works its way through the changesets.

If all that sounds crazy, think of it this way. If you have uncommitted changes in your working directory, pull then update (without -C), hg automatically attempts to "rebase" your uncommitted changes onto the new tip. This is just a fancy way of doing something similar with work you’ve locally committed.

Rebase is a plugin that needs to be enabled, and can only be used on draft/unpushed changesets.

Show progress

There’s a progress bundled extension for hg, which gives you a progress indicator for long-running operations. You just need to enable in [extensions] and also set verbose = False under [ui]

Shelve to change branches

hg shelve
hg unshelve

You do a bunch of work then realise you’re on the wrong branch. Often Mercurial will let you switch branches, but sometimes it can’t rebase your uncommitted changes cleanly onto the new branch.

hg shelve puts all the uncomitted changes in your working directory aside. It’s like hg update -C, except you keep a copy of your changes. hg unshelve re-applys the changes to your working directory, after you’ve done whatever else you like (pull, update, change branches etc).

Shelve is an extension that you need to enable.

Patch a changeset to another branch

hg export -o patch <revisions>
hg import patch

Lets say you commit a change on your dev branch then later realize that it needs to be on prod. You could copy it across manually, or you could create a patch. I've had bad luck creating a patch with TortoiseHg in the past, but the commandline version works perfectly. Note that this takes only the diff for the list of revisions you give it, so you need to make sure to include everything you need.

Update

I've since found a better way to copy specific changesets to another branch:

hg update <target branch>
hg graft <revision>

Not only is this cleaner, but graft has a bunch of useful options, like --edit which allows you to change the commit message.

Useful Stuff

I have a bunch of random useful extensions and aliases in my mercirial.ini/hgrc:

[extensions]
purge=
rebase= 
graphlog= 
progress=
shelve=

[ui]
verbose = False

[alias]
co = commit -m
secret = phase -fs
draft = phase -d
last = log -l 5 --style compact
preview = merge --preview 
addremove = addremove -s 100
untrack = rm -Af
amend = commit --amend
nudge = push -r .
glog = glog -l 10 --template "{rev}:[{branch} {bookmakrs}] {author|user}: \"{desc}\"\n"
repull = pull --rebase

Make it look pretty

If you’re using bash (on linux or cygwin), you can make your life a lot easier by showing the current branch and bookmark in the command prompt. Add this to your .bashrc:

hg_branch() {
            hg branch 2> /dev/null 
}

hg_bookmarks() {
            hg bookmarks 2>/dev/null | awk '/\*/ {print $2}'
}

DEFAULT="\e[37;40m"
PINK="\e[35;40m"
GREEN="\e[32;40m"
ORANGE="\e[33;40m"

export PS1="\n${GREEN}\w ${ORANGE}\$(hg_branch) ${PINK}\$(hg_bookmarks)${DEFAULT}\n$ "

This will give you a prompt that looks like this:

~work/tmp default dev
$
comments powered by Disqus