Thursday, May 24, 2012

Hg Bookmarks Made Me Sad

Hg's branches are "permanent and global", meaning they are fixed to the changeset, can't be deleted, and can't be left behind when you push changes on that branch.

This is in contrast to git's branches, which are temporary and are not part of the changesets.  I think of them as pointers.

It can be nice to have branch names on your commits, because it adds some meaningful context to the commits.  It makes understanding your history very easy.  The only downside that I am aware of is the potential for name collisions.  Someone might try to create a branch using a name that someone else had already used.  In which case you should really just use a different name...  If there are other downsides, I don't know what they are.

However, it has always been the recommendation of the Mercurial team that branches be used for long lived branches, not short term topic branches.  Pre-2.0 they probably would have recommended using local clones, now they recommend using Bookmarks.  I've found local clones less than ideal for my workflow, which typically looks like this:
  1. Get latest changes
  2. Decide what task to work on next
  3. Create a branch-line to work on that task
  4. Hack hack hack
  5. If it takes awhile (>8hrs), merge default into the branch-line
  6. If I have to go home for the night and I'm not done, push branch to remote as backup
  7. Push branch-line to remote, have someone do a code review
  8. Land branch-line on default, close branch-line
Some things to note about this:
  • The branches are small and generally short lived, one per "topic"
  • I want to push them to the remote for backup purposes (in case my computer fries over night or something)
  • I want to push them to remote so others can collaborate
This is why named-branches are so much more convenient for me than local clones.

However, in a recent release of Mercurial, they added a new notification message when you create a branch which says: "(branches are permanent and global, did you want a bookmark?)"  So they couldn't be much more clear and in my face about the fact they think I should be using bookmarks for my workflow instead of named-branches.

Bookmarks are basically Mercurial's version of git's temporary short lived branches.  It means I'll lose the nice branch names on my commits in history.  But I wont have to worry about name conflicts.  This already doesn't seem like a worthwhile trade, but I'm willing to take the Mercurial dev's word for it and try it out.  Sadly I found them, in their current state (2.2.1), to be bug prone and impractical.  For the remainder of this post, I'd like to explain what I don't like about them as they are now.  But since I don't want to have to explain the basics here, you should go read about them first:  I'd like to throw this one little caveat in before I start, which is to say that it's totally possible I am miss using these things.  I sincerely hope that's the case and someone will point out a better way to me.  But I couldn't find any good real workflow examples of bookmarks, so I had to figure it out on my own.

Must create my own 'master' bookmark, everyone on the team must use this bookmark
When I create my first bookmark and commit on it, I've just created two heads on the default branch. I can easily find the head of my topic branch, it has a bookmark, but how do I find the head of the mainline?

Worse, say I publish the bookmark to the remote server and you do a pull -u.  You will be updated to my topic bookmark, because it's the tip.  That is NOT what either of us wanted.  I created a topic branch because I didn't want it to get in your way.  In fact, you shouldn't have to be aware of my branch at all!

So bookmarks are broken before we even get out of the gate.  The work around is to create a 'master' bookmark that points at the mainline head.  Everyone on the team will have to aware of this bookmark, and they'll have to be careful to always update to it.

Must merge 'master' with 'master@1' and bookmark -d 'master@1'
The next problem happens when you and I have both advanced the master bookmark.  In the simplest case, maybe we both just added a new changeset directly on master.  Lets say you push first, and I pull.  If we weren't using bookmarks, hg would notify me when I pulled that there were multiple heads on my branch and it would suggest I do a merge.  So I'd merge your update with my update and be back to only one head on the branch.

With bookmarks, it's more confusing.  Hg will notify me that it detected a divergent bookmark, and it will rename your master bookmark to master@1 and leave it where it was.  It will leave mine named master and leave it where it was.  Now I have to "hg merge master@1; hg bookmark -d master@1;"

As a side note here, I was curious how git handles this problem, since git's branches are implemented so similarly to hg's bookmarks.  The core difference is that git wont let you pull in divergent changes from a remote into your branch without doing a merge.  It's conceptually similar to renaming the bookmark to master@1, since what git technically does is pull the changes into a "remote tracking branch" (that's a simplification, but close enough), and then merge that remote tracking branch onto your branch.  But it has a totally different feel when you're actually using it.

Can't hg push, or it will push my changes without my bookmark
This is the most devastating issue.  If I have created a new topic bookmark and committed on it, and then I do "hg push", it's going to push my changes to the remote without my bookmark!  The bookmarks only get pushed when you explicitly push them with "hg push -B topic".  Which means if I'm using bookmarks, I can't ever use the hg push command without arguments, or I'm going to totally confuse everyone else on the team with all these anonymous heads.

It's true that as long as the team is using the master bookmark and their own topic bookmarks, they shouldn't really have any problems here...  But it's still totally confusing, and totally not what I wanted.

The Mercurial team feels very very very strongly about maintaining backwards compatibility.  So it's probably a pipe dream to hope that this might change.  But I have two suggestions on how these problems might be mitigated.  These suggestions probably suck, but here they are anyway.

Hg up should prefer heads without bookmarks
If I do a pull -u and it brings down a new head, but that head has a bookmark, hg up should update to the head WITHOUT the bookmark.  This would allow me to use bookmarks without them getting in the way of other members of the team.

I think it would also allow me to not have to create the 'master' bookmark.  When I wanted to land a topic bookmark, I would just do: "hg up default; hg merge topic; hg ci -m "merged topic";"  Since "default" is the branch name, hg would prefer the head without bookmarks, which would be the mainline.

Hg push should warn if pushing a head with a bookmark
This would be consistent with hg's treatment of branches.  When you hg push, if you have a new branch, it aborts and warns you that you're about to publish a new branch.  You have to do hg push --new-branch.  I think it should do the same thing for bookmarks.  This would prevent me from accidentally publishing my topic bookmarks.

I <3 Hg
I really like Mercurial.  Even in the hg vs. git battle, I tend to prefer hg.  I love how intuitive it's commands are, I love how awesome it's help is, I love it's "everything is just a changeset in the DAG" model (vs. git's "you can only see one branch at a time, what's a DAG?" model).  And that's why bookmarks are making me sad.  Every time I create a branch, hg tells me I'm doing it wrong, but bookmarks are way too unfriendly right now (unless I'm missing something huge [it wouldn't be the first time]).

I still strongly recommend Hg.  If you're still using CVS, or Subversion, or heaven help you TFS, you should take a look at Mercurial.

And if you're a Mercurial expert (or a Mercurial developer!) please help me understand how to use bookmarks correctly!

PS.  I thought about drawing little graph pictures to help explain the issues I laid out here, but I don't have a decent drawing tool at my disposal, and I didn't think this rant really deserved anymore time than I already put in.  Hopefully you were able to make sense out of all these words.


  1. Seems like I'm not the only one who has problems using bookmarks. I ended up doing stuff like 'hg push -r master trunk' (since using 'hg push -r master default' does not work for some reason) to kind of simulate git's "push only current branch" behaviour.

    Very nice post, maybe we'll see some improvements in hg 2.3+ :-)

  2. I'm afraid you're not missing anything big... the problems you describe are real in Mercurial 2.2. I ran into them as well when I wrote my bookmarks guide:

    Luckily, some of the problems have been fixed already: Mercurial 2.3 will automatically delete the divergent bookmark when you merge, for example. I've tried to point out the changes in my guide linked above.

  3. You can create a default bookmark, which will then be the preferred head to update to on a new clone. This does not alleviate all pain, though.

    hg bookmark default -f

    Warning before pushing a new head with a bookmark would be nice, too. It warns anyway, so it could give a cleaner warning.

    But different from that warning, I think that you are doing nothing wrong (and missing nothing), if you use named branches for short-term features. After all, you can just close them (hg commit --close-branch).

  4. Is it better "hg update" should stay on the same bookmarked tip? If current is on an anonymous changeset, then should update to anonymous tip.

    Say, after pull but before update, if you're on a "branch" bookmarked as A, and if no divergent A incoming, then update should go to the latest A, if has divergent A incoming, then nothing should happen on working dir when update. If you're on a "branch" which has no bookmark, then update should go to the "tip" without bookmarks.

  5. This comment has been removed by the author.

  6. But to make the behavior of update command consistent on whether the "branch" you're working on with or without bookmark before you issue it, maybe should update to divergent A if you were on A?

    Or, do nothing when there's "divergent anonymous bookmark" incoming.