Customising Mercurial Like a Pro

Consider the following Mercurial screenshot:

hg-wip

This might look bewildering at first. It’s a custom hg wip command, a variation of hg log. You can see the DAG on the left, what looks like commit summaries in white, and what looks like usernames in mauve. This is not what stock Mercurial looks like, but in my opinion, this looks great. It has so much information packed into such a small space, all colour-coded for your convenience. Let’s take a look.

Current commit

hg-wip-current

The current commit is simply where you’re standing. It’s the commit that hg diff or hg status would use as reference for telling you how the working copy has changed. All that I did for hg wip was to highlight more clearly what this commit is. Note in the DAG that this commit is marked with an “@”.

Bookmarks

hg-wip-book

I have chosen to display bookmarks in green. Recall that bookmarks are simply movable markers that follow along with the commits that they’re pointing to. They’re very similar to git branches. Here I am using them to refer to each of my feature branches. There is a special “@” bookmark that indicates where upstream development is.

Branches

hg-wip-branches

In cyan we see the branch names. In this case there are only two commits on the stable branch. Branch names are permanent names tattooed on commits. This is useful for when you want to have a permanent record of where a commit was done.

Phases

hg-wip-phases

This part is one of my favourites. Mercurial phases indicate whether commits have been published yet or not, and whether they should be shared or not. Commits that have been published and therefore immutable are in orange. The commits I’ve been working on but haven’t been accepted into upstream yet are yellow drafts. Finally, a couple of local patches that should never be shared are secret commits in blue. I am using local revision numbers to identify commits, but it would also be possible to use global hashes.

Selective about which commits to show

Another cool thing about my hg wip command is that it only shows what I consider interesting information about my work-in-progress commits, hence the name of the command. To be precise, it only shows my draft and secret commits, current open heads of development, and the minimum common commits. No other intermediate commits are shown. Actually, usually I don’t even need the extra newlines, and I prefer this more compact format:

compact-hg-wip

So, how is this command built? It actually depends on a number of different Mercurial features, all useful on their own. All of the following snippets go into your ~/.hgrc config file.

Revsets

Revsets are a very useful language for selecting a subset of commits. Here you can see me talking about them. The revset I used for hg wip was

[revsetalias]
wip = (parents(not public()) or not public() or . or head()) and (not obsolete() or unstable()^) and not closed()

It looks like quite a mouthful, so let’s pick it apart:

parents(not public())
Take the parents of all the non-public commits (i.e. drafts and secrets).
or not public() or . or head()
Also take all non-public commits, the current commit (that’s “.”), and all heads.
and (not obsolete() or unstable()^)
But exclude obsolete and unstable commits (this only matters if you’re using the experimental Evolve extension).
and not closed()
And also exclude any closed branch heads

The nice thing is that all of this complicated selection is also fast!

Templating the output

The next step is to show the output in the format that we want it. That’s what the templating engine is for. The template I used here was

[templates]
wip = '{label("log.branch", ifeq(branch, "default", "", branch))} {label("changeset.{phase}", rev)} {label("grep.user", author|user)}{label("log.tag", if(tags," {tags}"))} {bookmarks % "{ifeq(bookmark, currentbookmark, label('log.activebookmark', bookmark), label('log.bookmark', bookmark))} "}\n{label(ifcontains(rev, revset('parents()'), 'desc.here'),desc|firstline)}'

Whoa! That’s even worse than the revset. Unfortunately, due to the nature of the templating engine, inserting whitespace to make this more readable would also make this extra whitespace show up in the final output.

If we are willing to define more intermediate templates and move them to an external template file, it can actually be made readable:

wbrch = '{label("log.branch",
                 ifeq(branch, "default",
                      "",
                      branch))}'
wcset = '{label("changeset.{phase}",
                rev)}'
wuser = '{label("grep.user",
              author|user)}'
wtags = '{label("log.tag",
              if(tags," {tags}"))}'
wbook = '{bookmarks % "{ifeq(bookmark, currentbookmark,
                             label('log.activebookmark', bookmark),
                             label('log.bookmark', bookmark))} "
}'
wdesc = '{label(ifcontains(rev, revset('parents()'), 'desc.here'),
              desc|firstline)}'
changeset = '{wbrch} {wcset} {wuser} {wtags} {wbook}\n{wdesc}'

Okay, this is a bit better, but it requires using an extra config file. The syntax is a bit tricky, but most of it is self-explanatory. Special keywords in {braces} get expanded to their corresponding values. There are a few functions like if() and ifcontains() to selectively output extra information if that information exists. For example, I use ifeq() to hide the branch name if this name is “default”.

Once you’re in a function, keywords are already available. For example, rev gives you the revision number, and if you wanted global hashes instead, you could change that to node or shortest(node). You can even use a revset in the templating engine! In this case, for selecting the current commits and comparing it to the revision being printed. We use the parents() revset to select the current commit(s), because there may be more than one current commit if there’s an uncommitted merge state on the working directory. For template keywords that return a list of things, such as bookmark, we can iterate over elements of that list using “%” and then select different labels depending if the bookmark is the current bookmark or not.

But what is this label function? Ah, well, labelling is how the colour extension decides how to colourise the output.

Colourful Mercurial

The finishing touches are now to pick colours for all of the labels we defined. For this we enable the colour extension and we configure it:

[extensions]
color=

[color]
mode=terminfo

#Custom colours
color.orange=202
color.lightyellow=191
color.darkorange=220
color.brightyellow=226

#Colours for each label
log.branch=cyan
log.summary=lightyellow
log.description=lightyellow
log.bookmark=green
log.tag=darkorange
log.activebookmark = green bold underline

changeset.public=orange bold
changeset.secret=blue bold
changeset.draft=brightyellow bold

desc.here=bold blue_background

Here we set the mode to terminfo so that we can use 256 colours. Most modern terminals can support it, but sometimes you have to specify the $TERM enivronment variable to be xterm-256color or something similar. Then you can define your own colours from the 256 available, referring to the numbers on this chart. If you dislike my colour choices, this is where you can configure them.

Define the command

The final part is to just turn this on:

[alias]
wip = log --graph --rev=wip --template=wip

This defines the hg wip command to be an alias to hg log together with the parameters for displaying the graph, using the wip revset, and the wip template.

And voilà, fancy shmancy colourful hg command! Here is the complete addition to your ~/.hgrc all at once, for delicious copypasta:

[revsetalias]
wip = (parents(not public()) or not public() or . or head()) and (not obsolete() or unstable()^) and not closed()

[templates]
wip = '{label("log.branch", ifeq(branch, "default", "", branch))} {label("changeset.{phase}", rev)} {label("grep.user", author|user)}{label("log.tag", if(tags," {tags}"))} {bookmarks % "{ifeq(bookmark, currentbookmark, label('log.activebookmark', bookmark), label('log.bookmark', bookmark))} "}\n{label(ifcontains(rev, revset('parents()'), 'desc.here'),desc|firstline)}'

[extensions]
color=

[color]
mode=terminfo

#Custom colours
color.orange=202
color.lightyellow=191
color.darkorange=220
color.brightyellow=226

#Colours for each label
log.branch=cyan
log.summary=lightyellow
log.description=lightyellow
log.bookmark=green
log.tag=darkorange
log.activebookmark = green bold underline

changeset.public=orange bold
changeset.secret=blue bold
changeset.draft=brightyellow bold

desc.here=bold blue_background

[alias]
wip = log --graph --rev=wip --template=wip

Acknowledgements

This command comes from ideas cobbled together from Steven Losh, Augie Fackler, and Sean Farley. They are all great contributors to Mercurial, and they have taught me so much! Thanks, guys!