There are two main ways to organize branches in Mercurial: named branches and bookmarks. Bookmarks are used for short-term feature or topic branches whereas named branches are used for long-term branches.
Named branches work well in a setting where you want to track the context in which each changeset was made; the branch name is embedded in the changeset and you can refer to it years later. Because the branch name is embedded in the history, named branches are great when you need an audit trail and want to be able to see who did what and when.
There are contexts where named branches work less well: if you don’t need or want to track the branch name after the fact, then named branches aren’t for you. Maybe you just want to experiment a bit without committing yourself to a given branch name, or perhaps you want the ability to change the branch name later.
If that is your use case, then Mercurial’s bookmarks will help.
Note
You should use Mercurial 2.1 or later for the examples in this section.
Contents
We will explore bookmarks in detail below, but put briefly, a Mercurial bookmark lets you associate a new name to a changeset. This is handy when you are working on several different things at the same time — such as two different feature branches.
Tags also add new names to existing changesets, but unlike tags, bookmarks are mutable and transient: you can move, rename, or delete them and they are not stored in history. This means that there is no audit trail for bookmarks.
Imagine that Alice, Bob, and Carla are writing a phrase book together. In the project, they will be working on phrases in different categories, and they will sometimes have to work on different categories at the same time. So they will naturally have several different ongoing branches of development at the same time. Bookmarks let them keep track of these branches in a lightweight fashion.
Carla is the boss, so she starts off by creating a repository that will be used for the project:
carla$ hg init phrases carla$ cd phrases carla$ echo "The Phrase Book Project" > README.txt carla$ hg add adding README.txt carla$ hg commit -m "Added a README"
Alice and Bob each have their own clone and push/pull to/from Carla’s repository, which acts as a central repository. Typically, the central repository will be hosted on some company server and the clones will be on the developers’ own machines. But for simplicity, we will place all three repositories on the same filesystem.
Alice and Bob can now make a clone of the repository Carla just made. For Alice it looks like this:
alice$ hg clone ../carla/phrases destination directory: phrases updating to branch default 1 files updated, 0 files merged, 0 files removed, 0 files unresolved alice$ cd phrases
They will be working on phrases in different categories and will start by collecting just the English phrases. Carla asks Alice to add phrases used for greetings:
alice$ echo "Hello!" > greetings.txt alice$ hg add adding greetings.txt alice$ hg commit -m "First greeting"
Carla now suddenly asks Alice to begin looking at phrases for traveling. Alice is already working on the greetings, and she would like to separate the two tasks — mixing up changes related to greetings with changes about traveling will make them harder to review later. Alice will therefore hg update back to revision 0 and start a new feature branch there. But in order to remember where she was, she will make a bookmark first. At the moment her repository looks like this:
Her greetings–related change is only identified by being on the default branch and by being the tip changeset. Both of these identifiers will move around when she begins working on a new branch, so she needs something more stable — a bookmark. She makes the bookmark:
alice$ hg bookmark greetings
The changeset now has a bookmark associated with it:
She can see the bookmark using hg bookmarks:
alice$ hg bookmarks * greetings 1:0b89bcda3dcf
The asterisk (*) indicates that the bookmark is active, which means that it will move along if she makes a new commit. Because the active bookmark moves along when you commit, it will always point to the head of the branch you’re working on.
Alice wants to create a new branch for the traveling phrases. The branch should start at revision 0, so she updates to this revision first. This makes the greetings bookmark inactive since she has moved away from this branch of development:
alice$ hg update 0 0 files updated, 0 files merged, 1 files removed, 0 files unresolved alice$ hg bookmarks greetings 1:0b89bcda3dcf
The bookmark is left in place:
This means that Alice can always run hg update greetings to get back to it again. For now, she’ll be clever and create a new bookmark called traveling for the new branch Carla has asked her to work on. The new bookmark is automatically made active:
alice$ hg bookmark traveling alice$ hg bookmarks greetings 1:0b89bcda3dcf * traveling 0:4326a390b9b6
The active bookmark can also be seen in TortoiseHg:
Now, when Alice creates a new commit, the traveling bookmark will, well…, travel with the commit:
alice$ echo "When does the bus arrive?" > traveling.txt alice$ hg add traveling.txt alice$ hg commit -m "Started on traveling phrases" created new head
Now that Alice has bookmarks on revision 1 and 2, she can use the bookmark names in all commands that expect a revision number. That is, she can now use 1 (the local revision number), 0b89bcda3dcf (the global changeset ID), and greetings (the bookmark) interchangeably:
alice$ hg log -r 1 changeset: 1:0b89bcda3dcf bookmark: greetings user: Alice <alice@example.net> date: Tue May 01 10:20:35 2012 +0000 summary: First greeting alice$ hg log -r 0b89bcda3dcf changeset: 1:0b89bcda3dcf bookmark: greetings user: Alice <alice@example.net> date: Tue May 01 10:20:35 2012 +0000 summary: First greeting alice$ hg log -r greetings changeset: 1:0b89bcda3dcf bookmark: greetings user: Alice <alice@example.net> date: Tue May 01 10:20:35 2012 +0000 summary: First greeting
Just like tags and branch names, bookmarks work with all commands — hg diff, hg merge, etc. Updating looks like this:
alice$ hg update greetings 1 files updated, 0 files merged, 1 files removed, 0 files unresolved alice$ hg bookmarks * greetings 1:0b89bcda3dcf traveling 2:f1cd0e213eec alice$ hg update traveling 1 files updated, 0 files merged, 1 files removed, 0 files unresolved alice$ hg bookmarks greetings 1:0b89bcda3dcf * traveling 2:f1cd0e213eec
Notice how the bookmark she updates to becomes the currently active bookmark. A new commit will advance the currently active bookmark, so hg update traveling is enough to prepare Alice’s working copy for working on the traveling branch.
The distinguishing feature of bookmarks compared to named branches is that they exist outside of the history. Here, “history” means the immutable changesets that cannot be changed without changing their changeset IDs. Bookmarks are not stored in the changesets and they are not part of the computation of changeset IDs. They can therefore be deleted and renamed at will.
Imagine that Alice adds a new bookmark:
alice$ hg bookmark some-name alice$ hg bookmarks greetings 1:0b89bcda3dcf * some-name 2:f1cd0e213eec traveling 2:f1cd0e213eec
She can now rename the bookmark:
alice$ hg bookmark --rename some-name new-name alice$ hg bookmarks greetings 1:0b89bcda3dcf * new-name 2:f1cd0e213eec traveling 2:f1cd0e213eec
She can also delete it:
alice$ hg bookmark --delete new-name alice$ hg bookmarks greetings 1:0b89bcda3dcf traveling 2:f1cd0e213eec
There is now no trace of the deleted bookmark; nobody can see that it ever existed. This makes bookmarks good for tracking small feature branches that don’t need to be recorded in long-term history in the same way that named branches are recorded.
After deleting the new-name bookmark, there are no active bookmarks. We will continue working on the traveling branch, so we make that bookmark active again:
alice$ hg update traveling 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
While Alice has been working, Carla has been busy too. She updated the README with the authors:
carla$ echo "by Alice, Bob, and Carla" >> README.txt carla$ hg commit -m "Added authors"
This means that Alice’s traveling feature branch will become a second head when she pushes. Mercurial will thus abort the push by default:
alice$ hg push -r traveling pushing to /home/carla/phrases searching for changes abort: push creates new remote head f1cd0e213eec! (you should pull and merge or use push -f to force)
To investigate the situation, Alice will pull and look at the repository in TortoiseHg:
alice$ hg pull pulling from /home/carla/phrases searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads .' to see heads, 'hg merge' to merge)
There are no less than three heads! Pushing the traveling head will indeed create two heads in the remote repository and Mercurial will guard against that since multiple unnamed heads can be confusing: users who clone the repository won’t know which head they should use as a basis for their own work. When there is only one head, there is no question — you just clone and begin working.
Above, Mercurial suggests to merge the heads or use -f to force the push. Forcing things is normally a sign that you’re not using the tool as designed, but this is one of the few situations where you do want to add the -f flag. The reason is that Alice’s work on the traveling branch isn’t finished yet, and so she doesn’t want to mix the changes on the branch with the changes on the main line. If there had been an important “bugfix” on the main line, then she could have merged it into her branch, but here she doesn’t bother.
So Alice will forcibly push the feature branch to the remote repository:
alice$ hg push -f -r traveling pushing to /home/carla/phrases searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads)
Here it’s very important that Alice restricts the push to only push the branch she wants! She has two feature branches that don’t exist in Carla’s repository, and the -f flag tells Mercurial to blindly push all outgoing changesets. Some care is needed to publish just one out of many private feature branches. Here, the +1 heads printed tells Alice that she did it right — she pushed a single new branch to the repository.
How do things look from Carla’s perspective? She now has two anonymous heads in her repository:
carla$ hg heads changeset: 2:f1cd0e213eec tag: tip parent: 0:4326a390b9b6 user: Alice <alice@example.net> date: Tue May 01 10:20:40 2012 +0000 summary: Started on traveling phrases changeset: 1:4b18431b805d user: Carla <carla@example.net> date: Tue May 01 10:20:45 2012 +0000 summary: Added authors
We call them anonymous since there are no names attached to them — they are on the same named branch and have no bookmarks:
carla$ hg bookmarks no bookmarks set
Since Alice intends to continue working on the branch, it would have been convenient if her bookmark had been pushed too. Originally (before Mercurial 1.6), bookmarks were strictly local, but it is now possible to push and pull bookmarks.
Bookmarks can be pushed and pulled between repositories. This works over any of the protocols used by Mercurial: SSH, HTTP, and local filesystem access. The ability to push a bookmark is governed by the same rules that govern normal push access. If you can push a changeset to a repository, then you can also push a bookmark to it.
Alice can easily fix her earlier mistake. First, she uses hg outgoing -B to see if there are any bookmarks in the local repository that aren’t present in the repository on the server (similarly, hg incoming -B will tell you about any bookmarks present on the server that do not exist in the local repository):
alice$ hg outgoing -B comparing with /home/carla/phrases searching for changed bookmarks greetings 0b89bcda3dcf traveling f1cd0e213eec
She has two bookmarks locally that don’t exist on the server. To push a bookmark she simply needs to use -B instead of -r when pushing:
alice$ hg push -B traveling pushing to /home/carla/phrases searching for changes no changes found exporting bookmark traveling
This will make Mercurial push the changeset pointed to by traveling and export the bookmark to the server. Since she has already pushed the changesets, only the bookmark is pushed here. Carla’s repository now looks much more tidy than before:
Alice will publish her greetings branch in the same way, and do it right this time:
alice$ hg push -f -B greetings pushing to /home/carla/phrases searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) exporting bookmark greetings
Now that Alice has published her traveling bookmark, she can keep working and periodically push back to Carla. This will automatically advance the bookmark on the server — no more -B flags needed:
alice$ echo "Two tickets, please!" >> traveling.txt alice$ hg commit -m "A ticket phrase" alice$ hg push pushing to /home/carla/phrases searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files updating bookmark traveling
Notice the updating bookmark traveling message at the bottom. This is what tells Alice that the bookmark is synchronized in the two repositories.
Alice and Bob can now work together on the feature branch. First, Bob will pull the branch from Carla. Bob needs to explicitly import the bookmark with -B the first time he pulls a feature branch:
bob$ hg pull -B traveling pulling from /home/carla/phrases searching for changes adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files (run 'hg update' to get a working copy) importing bookmark traveling bob$ hg bookmarks traveling 2:0098cff6f6d8
Note
This behavior will change in Mercurial 2.3. Starting with that version, Mercurial will automatically import remote bookmarks on changesets you pull. That means that Bob can just run hg pull to get all incoming changesets and bookmarks from Carla.
Bob will now check out the branch, make a change, and push it back:
bob$ hg update traveling 1 files updated, 0 files merged, 0 files removed, 0 files unresolved bob$ echo "Where is the train station?" >> traveling.txt bob$ hg commit -m "Asking for a train station" bob$ hg push pushing to /home/carla/phrases searching for changes note: unsynced remote changes! adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files updating bookmark traveling
Notice how the traveling bookmark was updated automatically on the server. In general, Mercurial will take care to synchronize a bookmark on push and pull as soon as it exists both locally and remotely. There is an additional constraint: hg push will only update the remote bookmark if it is an ancestor of the local bookmark. If the bookmarks have diverged, then someone else has worked on the branch and the two heads should be merged before pushing. We’ll look at this situation next.
Bob has just pushed a changeset on the traveling branch, but Alice has not pulled it yet. Before pulling it she makes her own commit on the branch:
alice$ echo "How much for a ticket?" >> traveling.txt alice$ hg commit -m "Asking for the ticket price"
As usual in Mercurial, she will notice the new commit from Bob when she tries to push back to the server:
alice$ hg push pushing to /home/carla/phrases searching for changes abort: push creates new remote head e0c765eadfc4! (you should pull and merge or use push -f to force)
Here she wants to pull and merge — she’s not about to publish a new feature branch, so it would be wrong to use the -f flag.
When Alice pulls, she will get the changeset from Bob:
alice$ hg pull pulling from /home/carla/phrases searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) divergent bookmark traveling stored as traveling@default (run 'hg heads .' to see heads, 'hg merge' to merge)
In addition to the normal hg pull output, Mercurial printed a message about storing a divergent bookmark as traveling@default. In TortoiseHg the situation looks like this:
What happened is that Mercurial detected that traveling on the server had diverged from traveling locally. As explained above, divergence means that the bookmarks are siblings — neither is the ancestor of the other. The remote bookmark was renamed to traveling@default. The @default comes from the default path. If Alice had
[paths] carla = /home/carla/phrases
in her .hg/hgrc file, then the bookmark would have been renamed to traveling@carla.
Like normal, Alice will now merge with the other head. Normally, she can just run hg merge and Mercurial will automatically select the other head on the branch as the obvious merge candidate. This doesn’t work with bookmarks:
alice$ hg merge abort: branch 'default' has 4 heads - please merge with an explicit rev (run 'hg heads .' to see heads)
She has to merge with the divergent bookmark manually:
alice$ hg merge "traveling@default" merging traveling.txt warning: conflicts during merge. merging traveling.txt incomplete! (edit conflicts, then use 'hg resolve --mark') 0 files updated, 0 files merged, 0 files removed, 1 files unresolved use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
There is a conflict since both Alice and Bob added a new line after the first line. Alice resolves the conflict by incorporating both of their changes in the file:
alice$ cat traveling.txt When does the bus arrive? Two tickets, please! <<<<<<< local How much for a ticket? ======= Where is the train station? >>>>>>> other alice$ hg resolve --tool internal:local traveling.txt alice$ echo "Where is the train station?" >> traveling.txt
She checks the diff and since it looks okay, she commits the merge:
alice$ hg diff diff -r e0c765eadfc4 traveling.txt --- a/traveling.txt Tue May 01 10:21:00 2012 +0000 +++ b/traveling.txt Tue May 01 10:21:03 2012 +0000 @@ -1,3 +1,4 @@ When does the bus arrive? Two tickets, please! How much for a ticket? +Where is the train station? alice$ hg commit -m "Merged with Bob"
After the merge, the divergent bookmark is still around:
alice$ hg bookmarks greetings 1:0b89bcda3dcf * traveling 7:0e1a5b84f635 traveling@default 6:888ae5a9e614
Alice no longer needs the bookmark, so she deletes it:
alice$ hg bookmarks --delete "traveling@default"
The repository now looks like this:
Note
Mercurial 2.3 will improve this: hg merge will now automatically select a divergent bookmark as the merge candidate. After the merge is committed, the divergent bookmark is removed automatically.
When she pushes back to Carla, the traveling bookmark will move forward. Before the push, it was pointing at 888ae5a9e614, and since 0e1a5b84f635 is a descendant, the bookmark can move forward on the server:
alice$ hg push pushing to /home/carla/phrases searching for changes adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 1 files updating bookmark traveling
Alice and Bob have now collected three phrases about traveling, and they decide that the traveling branch can be merged back into the main line.
Alice will do the merge. She starts by pulling from Carla to make sure she has the latest changes, and then she updates to the unnamed head where Carla was editing the README.txt file:
alice$ hg pull pulling from /home/carla/phrases searching for changes no changes found alice$ hg update -r "head() and not bookmark()" 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
In the update command, Alice used a revision set to select the changeset that is a head with no bookmark. She can see in TortoiseHg that she ended up at the correct changeset:
She now simply merges with the traveling branch:
alice$ hg merge traveling 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) alice$ hg commit -m "Merged with traveling branch"
The result is as you would expect:
The branch has been merged and the bookmark is no longer needed. Alice can therefore delete it:
alice$ hg bookmark --delete traveling
She can also push the merge changeset back:
alice$ hg push pushing to /home/carla/phrases searching for changes adding changesets adding manifests adding file changes added 1 changesets with 0 changes to 0 files (-1 heads)
Even though she deleted the bookmark locally and pushed the merge changeset, the bookmark still exists on the server:
alice$ hg incoming -B comparing with /home/carla/phrases searching for changed bookmarks traveling 0e1a5b84f635
To delete it on the server, Alice runs
alice$ hg push -B traveling pushing to /home/carla/phrases searching for changes no changes found deleting remote bookmark traveling
Specifying a bookmark that doesn’t exist locally but does exist on the server will delete the bookmark from the server. This is similar to when she published the bookmark in the first place: hg push -B NAME will make NAME point to the same thing on the server as it points to locally — if NAME doesn’t exist locally, it will also not exist on the server.
Alice could also have pushed the merge and deleted the bookmark in a single operation. We can illustrate this by merging the greetings branch too:
alice$ hg merge greetings 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) alice$ hg commit -m "Merged greetings branch" alice$ hg bookmark --delete greetings alice$ hg push -B greetings -r . pushing to /home/carla/phrases searching for changes adding changesets adding manifests adding file changes added 1 changesets with 0 changes to 0 files (-1 heads) deleting remote bookmark greetings
Here, -B greetings is what makes Mercurial delete the remote bookmark, and -r . is what makes it push the newly created merge changeset: the . (dot) revision is a shorthand for the parent revision of the working copy. Without the -r . flag, hg push would only have pushed up to the greetings changeset — and thus excluded the merge changeset.
Bookmarks give you a lightweight way to track multiple lines of development (heads) in your repository. The main features of bookmarks are:
Mutability: Bookmarks live outside of the changeset history and can be added, renamed, and deleted without leaving a trace. This makes bookmarks good for tracking feature or topic branches.
Shareability: Bookmarks can be pushed and pulled between repositories. When a bookmark has been published, Mercurial will keep it in sync going forward. Since bookmarks are not automatically published, you can keep them private until you decide to share them.
The main commands you use to work with bookmarks are:
hg bookmarks: List all bookmarks.
hg bookmark NAME: Create a new bookmark called NAME.
hg update NAME: Update to NAME and make the bookmark active.
hg incoming -B: See bookmarks that exist in the remote repository but do no exist in the local repository.
hg push -B NAME: Publish a feature branch NAME and publish the bookmark. If NAME doesn’t exist locally, delete the NAME bookmark in the remote repository.
hg pull -B NAME: Pull a feature branch NAME along with the bookmark.
Have one person in the group create a repository — the others clone this.
Create a bookmark named after yourself and make some commits.
Publish your bookmark to the central repository and pull the others’ branches into your own repository.
See the revision set documentation and try a query like hg log -r "ancestors(NAME) - ancestors(.)" to see changesets on the branch NAME that have not yet been merged into your current revision. Revision sets also work in TortoiseHg.
Update to one of the other branches and make a commit. Push this back to the server and experiment with managing divergent bookmarks.
Enable the rebase extension and try rebasing a feature branch on top of the main line of development. The bookmarks should follow along automatically.
The first versions of Mercurial had neither named branches nor bookmarks. Named branches were introduced in Mercurial 0.9.2 (December 2006) and bookmarks were introduced two years later. Over the years, bookmarks have gained features and become more stable. Noticeable events up to Mercurial 2.2 include:
Because the bookmarks feature has had a long history, there are quite a few guides out there that now contain outdated information. You can compare the publication date of a guide with the table above to see which features the guide cannot possibly know about.