A light branching strategy with mercurial and bookmarks
Posted on 2015-07-22 in Programmation
I am a contributor to nbpython the netbeans plugin for python. Since the DCVS used is mercurial and I have almost exclusively used git before I add to adapt my workflow to work properly.
What I want to do:
- create a branch from the current default branch (the equivalent of master in git) to work on my contributions. This branch is temporary, used for development and code review (so it must be pushed and have an easy way to update to it) and should be deleted once the code is merge in the main trunck.
- the code must be "rebased" in git terminology above the latest commit in default to preserve a linear history.
If you are coming from a git background, you must know that mercurial has four branching mechanism available:
- branching by cloning: you just do a hg clone of the repo in another folder. This is far from efficient in term of disk space and network usage but it is the older branching mechanism and I only present it for the sake of completeness. I don't see the point to use it now.
- anonymous branch: to create such a branch, you go to a previous revision and make a new commit. The only way to switch to this branch is with the commit hash (hence the name anonymous). In git, you just cannot do that. If you go to a commit that is associated with a branch, git warns you that you are in detached HEAD and that the commit you do will be discarded at some point (probably in the next garbage collection). In mercurial, you can do this as it allows multiple heads.
- named branch: again, there is no equivalent in git. Theses branches are global and permanent: you can close them so they are not listed by default with hg branches (which list all the named branches) but you cannot delete them. You create them with hg branch <branch-name>. Furthermore, the name of the branch will be stored with the commit metadata. This look good for long lasting branch for some important features but this is too heavy for what I am looking for.
- bookmarks: you can bookmark any change in mercurial. If a bookmark is active and you do a commit, the bookmark will be updated to point to this commit. Nothing is stored as metadata. This what looks more like the branch in git and this is what I want.
Using bookmarks
You can easily:
- navigate to a bookmark with hg up <bookmark> (this is the command you use to go to any revision let it be a commit, a branch or a bookmark)
- list them with hg bookmarks
- delete them with hg bookmark -d <bookmark-name>. This will only delete the bookmark, not the commits associated with it which are still accessible with their hashes (even if they are not merged in a named branch). See below to learn how to do that.
- push them to the remote repository with hg push -B <bookmark-name. This will create the bookmark if it doesn't exist or update it if it is already present.
One disturbing side effect when you use bookmark is that a branch will have multiple heads. So I use the rebase extension and do a hg pull --rebase to get the changes from http://hg.netbeans.org/main/contrib. If you used mercurial before you may have done instead hg pull and then hg update (and eventually hg merge to solve conflict) or used the fetch extension and did hg fetch which just does all this in one command.
So, how do I work?
Before doing anything, I created a main bookmark at the current state of default so that I could go back there easily. You should do the same. Once this is done, here's what I do:
- I do a hg pull --rebase netbeans to get the latest code from hg.netbeans.org (my default remote point to my bitbucket clone so I don't push by error a bookmark on netbeans).
- I create a bookmark with hg bookmark <feature>.
- I work and commit normally.
- I do a hg push -B <feature> to push my patches to my bitbucket repo so the other contributors can review my work.
Once my changes are validated, I must rebase my changes to the top of default. The fist way I found out was to export my changes with hg export -r <list of revisions to export> > file.patch and then do a hg up main and finally hg import < file.patch. This worked but the graft extension makes it easier. graft is the equivalent of cherry-pick. So here's what I do:
- hg up main
- hg graft -r <list of revision to export>
- hg push -r main netbeans to push the new revision under the bookmark main to hg.netbeans.org to the default branch without creating the bookmark.
Now I want to delete the branch. To do that, I use the strip extension. It deletes a commit and all its children. So a hg strip -r <fist commit of bookmarked branch will to the trick. But it has an option to delete a bookrmark and all its parents (until it reaches the original parent of course). So I do: hg strip -B <feature>.
Other mercurial tips
Here are some tips I learned working with mercurial. There are too few of them for a dedicated article yet. I will update this list when I learn more of them.
- To do the equivalent of git rebase -i, use hg histedit -r <revision>.
- To discard the last commit: hg strip --keep. This will remove the last commit from history but let the files like they were after this commit.
- To get hg st to display the relative path of files, add this to your .hgrc:
[alias] __mystatus = status st = !hg __mystatus $($HG root) $HG_ARGS
Some extensions that I think are a must have:
pager: by default, mercurial don't page its output. So for instance if you have lots of commits and do a hg log, they will all be printed at once. Not very practical. pager allow you to give the result to less.
progress: to view the progress of network operation (like push, pull, clone). Sadly not enabled by default. Once the extension is enabled, you must configure it:
[pager] pager = LESS='FRX' less
To activate an extension, add it to the [extensions] section of your .hgrc like this:
[extensions] progress =
You can view my complete .hgrc here.
Conclusion
Coming from git, mercurial is a bit disturbing at first. The main critic I have is that even for things that I consider to be basic like a less pager or to view progress of some operations you must enable some extensions. However, despite being different, it is just a matter of habit to be at ease with mercurial. If you haven't tried it yet, you really should:
- it works great
- I think that the command names are better and less confusing
- it has nice local number to identify commits. So instead of doing hg up -r <hash> you can just do hg up -r 78.
- it has the tip which identify the last commit you made
- and many more.