aragost Trifork: Mercurial Kick Start Exercises


Mercurial Bookmarks

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

Introduction

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.

Bookmark Basics

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" 

Adding a Bookmark

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:

alice-pre-bookmark.png

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:

alice-greetings-bookmark.png

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:

alice-inactive-greetings.png

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:

alice-active-traveling.png

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
alice-traveling-moved.png

Updating to a Bookmark

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.

Renaming and Deleting a Bookmark

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

Pushing a Feature Branch

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)
alice-two-heads.png

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.

Sharing 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.

Incoming and Outgoing Bookmarks

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:

carla-with-bookmark.png

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

Working with a Published Bookmark

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.

Divergent Bookmarks

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.

Automatic Bookmark Renaming

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:

alice-divergent-bookmarks.png

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.

Merging Divergent Bookmarks

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:

alice-merged-bookmarks.png

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

Merging a Feature Branch

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.

Merging

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:

alice-before-merge.png

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:

alice-after-merge.png

Deleting the Bookmark

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.

Summary

Bookmarks give you a lightweight way to track multiple lines of development (heads) in your repository. The main features of bookmarks are:

The main commands you use to work with bookmarks are:

Exercises

  1. Have one person in the group create a repository — the others clone this.

  2. Create a bookmark named after yourself and make some commits.

  3. Publish your bookmark to the central repository and pull the others’ branches into your own repository.

  4. 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.

  5. Update to one of the other branches and make a commit. Push this back to the server and experiment with managing divergent bookmarks.

  6. Enable the rebase extension and try rebasing a feature branch on top of the main line of development. The bookmarks should follow along automatically.

History

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:

Mercurial 1.1 (December 2008)
Bookmarks are introduced as a standard extension.
Mercurial 1.6 (June 2010)
Pushable bookmarks. Previously, bookmarks were local-only labels for commits. With this version, bookmarks can be shared by publishing them to a remote server from which others can pull them.
Mercurial 1.8 (March 2011)
The bookmarks feature “graduated” and became part of core Mercurial.
Mercurial 2.1 (February 2012)
Divergent bookmarks are renamed when pulled into the local repository. This lets you distinguish between foo (your bookmark) and foo@alice (Alice’s bookmark).

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.