Mercurial is a distributed version control system which allows for very different workflows for your day to day work. In this post I will describe how to setup and use bookmark-based workflow in Mercurial. This is my current favorite workflow based on bookmarks which evolved while I was working on projects which used Mercurial as their primary version control system.
Main concepts
Since Git is present everywhere at the moment, it is important to introduce a few concepts and the Mercurial specific aspects and how they differ from Git. I will only focus on the differences which are relevant to this workflow.
Distributed Version Control System
Mercurial is a distributed version control system (DVCS) and as such does not need a central server. It is possible to use it entirely for a peer-to-peer based collaboration, although in this post I am assuming a team setup where a central server is used as an exchange for changes. I am also assuming that some web based tool is in place which provides the concept of a pull request as a transport mechanism for changes.
Branches and Bookmarks
Mercurial has the concept of a branch as well as a bookmark and differs in this aspect from Git.
A branch in Mercurial is recorded as part of the commit itself. A given commit is always part of exactly one branch. If no branch is recorded, it will be part of the default branch. This also means that branches are permanent, since they are recorded as part of the commits.
A branch in Git is just a reference to a given commit. A good analogy is to understand a branch as a pointer to a commit. This also means that a commit can be part of many branches and a branch can be deleted without affecting a commit. Due to that a branch is not part of the history.
Mercurial has the concept of a bookmark to bridge the gap. A bookmark is for the user somewhat similar to what a branch in Git is. It is not part of the history and it basically points to a given commit. This means that we can delete or change bookmarks without changing the underlying commits. And many bookmarks can point to the same commit. Mercurial has its own specific rules for how to update a bookmark, when a commit is added locally, so that the result is somewhat like branches in Git. I am calling it "somewhat like" Git branches, since there are still subtle differences which may cross your way.
Phases
In Mercurial each commit is in a specific phase. The relevant ones are draft and published. Usually you should not have to deal with phases too much. If you have to, then take this as an indicator that your setup should be further improved.
In general phases work like this:
- When you create a new commit, it will be in the phase draft. This means you can still change it without having to force Mercurial. Changes could be done by rebasing, history rewriting or amending a commit.
- Once you push a commit, it may be that it will transition into the phase published. It depends on the server configuration if this happens.
- When you pull in changes, the new commits will usually also be in the phase published.
- Mercurial will be picky about changing a commit, once it is in the phase published. The idea is that once you publish a commit, you should not change it anymore, otherwise you will create a mess for people who follow your project.
Setup
I am using the described workflow in a setup where I collaborate with a few people on the same codebase.
There is one central repository which represents the published commits and this one is usually not changed except for adding new commits to it. This means it makes up a good starting point for new work. I will call this repository upstream.
Each collaborator uses one or even multiple forks to prepare changes for integration into the upstream repository. I will call this repository fork in the following sections.
Workflow
This section describes the typical tasks around the development of a new feature or a bug fix.
Starting point
By default, I am using the latest state from the upstream repository as a base for any new work. I have added the URL of this repository inside of the file .hg/hgrc, so that I can use the following commands:
hg pull upstream
hg up -r `hg id -r default -i upstream`
These commands should leave me at the latest commit which is currently on the branch default in the upstream repository. The next step is to add a bookmark:
hg book feature-example
Working on the feature
Now I am basically adding changes. I like to commit often and preserve this way also the path on which I came to a solution, since I often find this being useful when I analyze a problem in the future.
To add changes, just commit:
# If you added new files, add them once, so that they will be tracked. hg add a-new-file.txt hg commit
The bookmark will be updated automatically each time when you add a commit. You can inspect the last commit by running the following command:
hg parent
Sharing changes
I prefer to work with a tool which provides the concept of a pull request based on a web interface for easy collaboration, in my case this is RhodeCode Enterprise which recently went open source.
The first step is to push changes up into my fork:
hg push -B feature-example
I am using -B
so that the bookmark will be added into my fork. I do not specify the name of the remote repository, since I added my fork as the default repository inside of the file .hg/hgrc.
Note that I configured my fork as not publishing. This has the nice side-effect that the commits will stay in the phase draft so that I can modify the history without having to use --force. I consider this a best practice, since I saw bad things happen because people got used to just use --force
if things don't work. The result was that even more things did not work anymore afterwards.
Integrate Changes Into Upstream
In my case this means creating a pull request via a web interface. I have created a shortcut in my web browser, so that I can type cl
to see the changelog and create a pull request from there.
A pull request basically automates the following operations:
- Have a local clone as a working area.
- Pull in changes from the fork.
- Merge the changes into the target branch, usually this branch is called default in a Mercurial repository.
- Push the result into the upstream repository.
Start on the next feature
Start again in the beginning of the workflow to implement the next feature.
Cleanup
I have had lengthy discussions about cleaning up my fork and my local clone. Some people prefer that and invest into cleaning bookmarks which are of no use anymore.
To put it simple: I don't clean up my fork. The reason is that I did not yet find a need for it. Even working full-time for two years on a project without any cleanup did not lead to any confusion on my end.
My assumption is that this works because I tend to integrate my changes very frequently with the upstream repository and have only very few things "open" at the same time. If case I have to search for a change because I don't remember a bookmark name, I will find it usually in the most recent 20 or so commits. Another aspect is consistent bookmark naming, mostly in the pattern of issue-NNN, so that there is rarely a need to search through all bookmarks.
Your habits might be different and a regular cleanup might be justified. I have never tried nor felt the need of it for myself.
Conclusions
Initially I did not get a very good start with Mercurial. My origins were in the Git world and I did initially not realize that Mercurial is different from Git. When just trying to apply my understanding of Git to Mercurial, I was constantly working against it.
Once I adopted to its specifics I found a workflow based on bookmarks, which fits my needs very well and proved to be useful for a few years by now.
Although Mercurial allows to use --force
in many cases to overcome obstacles, this seems to be the wrong thing to do in most of them. In my experience so far, it is often better to invest a few minutes to improve the configuration or the workflow, so that --force
is not needed for any regular operation.
If you have any suggestions on how to improve current workflow, please leave a comment below or tweet to @RhodeCode.
Thanks,
Johannes
Johannes Bornhold
Senior Python Developer
RhodeCode