Skip to content

Instantly share code, notes, and snippets.

@boneskull
Last active June 4, 2021 19:39
Show Gist options
  • Save boneskull/46b77dcba2cf10f5337c to your computer and use it in GitHub Desktop.
Save boneskull/46b77dcba2cf10f5337c to your computer and use it in GitHub Desktop.
How to use Mercurial bookmarks

How to Use Mercurial Bookmarks

Note: This document is specific to the FocusVision development environment, however it's mostly applicable elsewhere.

by Christopher Hiller

Mercurial branches are not "cheap". Unlike Git, to create or destroy a branch, you actually have to commit a changeset to the repository. Branches are great for…something…but whatever that is, we're not doing it.

Enter Bookmarks.

Bookmarks work similarly to the Git notion of branches. To create a bookmark, you do not create a changeset. A bookmark is simply a reference to a changeset. It ires repository meta-data, and by default stays "local" (in your working copy).

You can push and pull bookmarks themselves from remote repositories, if you wish. In effect, you can create one remote repository that everyone works from this way (instead of stable, dev, rc etc.).

Let's get started.

Setup

Since version 1.8 of Mercurial, the bookmark command is part of core, and no longer an extension. If you do not have Mercurial >=2.9 installed, however, upgrade for crying out loud. beacon-sdk (or our Vagrant box) now includes a new package.

  1. Clone a repository (and save the path):

    $ hg clone ssh://xxx//home/branches/core/stable beacon 
    destination directory: beacon 
    requesting all changes 
    adding changesets 
    adding manifests 
    adding file changes 
    added 59238 changesets with 101360 changes to 13523 files 
    updating to branch default 
    6797 files updated, 0 files merged, 0 files removed, 0 files unresolved 
    $ cd beacon
    $ echo "stable = ssh://xxx//home/branches/core/stable" >> .hg/hgrc 
  2. Create a bookmark:

    $ cd beacon 
    $ hg bookmark -i core/stable 

    We've created a bookmark at our current changeset, which is the tip of our clone of stable. The -i (--inactive) flag tells Mercurial not to move this bookmark automatically unless it is explicitly updated to. You'll see why next step.

    You can give bookmarks any unique name you want. There is only one namespace for bookmarks, so if you want to organize them in some manner, you can put a / in the bookmark name. I recommend using a prefix so you do not confuse your remotes with bookmarks.

  3. Pull a different repository, and bookmark that:

    $ echo "dev = ssh://xxx//home/branches/core/dev" >> .hg/hgrc 
    $ hg pull dev
    pulling from ssh://xxx//home/branches/core/dev 
    searching for changes 
    adding changesets 
    adding manifests 
    adding file changes 
    added 122 changesets with 155 changes to 59 files 
    (run 'hg update' to get a working copy) 
    $ hg update 
    54 files updated, 0 files merged, 0 files removed, 0 files unresolved 
    $ hg bookmark -i core/dev 

    You can only have one active bookmark at any time. When you use update to navigate to a bookmark, that bookmark is considered active. When a bookmark is active, any descendant changeset applied on top of that bookmark will cause the bookmark to move. That means: when you commit over an active bookmark or update to a descendant of an active bookmark, the bookmark will reference your new changeset.

    Because we automatically merge stable into dev (and other automatic merges), dev will always be a descendant of stable. If you update from an active core/stable bookmark to the tip of dev, your core/stable bookmark will move, which is undesirable.

    In this case, if we had not originally used the -i (--inactive) flag, our core/stable bookmark would be automatically updated to the tip of dev, which is bad. It may help to think of these two commands as equivalent; they both create an active bookmark:

    $ hg bookmark -i foo && hg update -r foo 
    $ hg bookmark foo 

    TL;DR: use the -i or --inactive flag when using hg bookmark unless you plan to commit over that bookmark right away.

  4. Bask in the glory of your bookmarks:

    $ hg bookmark 
    core/dev 59359:89f7168f314d 
    core/stable 59237:d7dbf9f25270 

    We now have two bookmarks.

    Unlike Git, Mercurial does not "track" remote repositories with bookmarks. The bookmark represents the tip of your stable repository, not the remote. If you lose your place somehow (i.e. screw up), you can force a "reset" of a bookmark to a remote's tip:

    $ hg bookmark core/stable -f -r $(hg identify stable)

    
    

Working

  1. First, understand where you are:

    $ hg identify 
    89f7168f314d tip core/dev 
  2. Oops. We need to work on an FT ticket right now, so let's go ahead and update to the core/stable bookmark.

    $ hg update -r core/stable 
    49 files updated, 0 files merged, 5 files removed, 0 files unresolved 
  3. Make changes as per usual:

    $ touch butts 
    $ hg add butts 
    $ hg commit -m 'nothing to do w/ butts' 
    created new head 

    OMG a new head! New heads are bad; doesn't Mercurial warn me about them all the time??

    Yes, you now have a new head. And no, they are not bad. We have two heads because we made a commit on top of core/stable, which is not the tip; whenever you commit on top of anything that's not tip, you get a new head.

    Do not fear; this is how it's supposed to work.

  4. OK, ready to push, right?

    $ hg push stable 
    pushing to ssh://xxx//home/branches/core/stable 
    searching for changes 
    abort: push creates new remote head 14b723cad087! 
    (merge or see "hg help push" for details about pushing new heads) 

    Yeah, you don't want to do that. When you hg push without a revision, you're essentially telling Mercurial to "push this entire repository including all heads".

    To do this properly, you need to tell Mercurial what revision to push. And since bookmarks are really just aliases to revisions, you simply issue:

    $ hg push -r core/stable stable 

    I'm not going to actually do this because I don't want an empty file named butts anywhere on our cloud servers, but that's how it works.

    Sometimes creating a remote head is what you want to do, but not yet. If we decide to cut back on the number of remote repositories we work with, we may have repo(s) with bookmarks, and likely many heads. For example, each "alpha branch" would have its own bookmark and head. When it's time to merge the "alpha" back into the "main" bookmark, the second head would be destroyed, but the bookmark would remain.

  5. I did what you said and it still aborted with a warning about new remote heads.

    That means you probably just need to pull again.

    $ hg pull --rebase stable 

    To those of you unfamiliar with rebase, know this: fetch is abhorred by the Mercurial community (and me too), in part because it creates a bunch of godawful "automatic merge" changesets. It fills your repo up with cruft. Using --rebase, "merge" changesets only happen when they absolutely must.

    WTF is a pull --rebase?

    In a nutshell: You take your new, local changesets, set them aside, then grab some new changesets, then put your changesets back on top of the ones you pulled. It will attempt to automatically merge, but w/o a new "automatic merge" changeset. If there are merge conflicts, they will be resolved just like any other merge.

    You can use pull --rebase in any situation where you'd use fetch, except one: you already pushed the changesets you're trying to rebase. Like this:

    • hg pull from dev
    • hg commit
    • hg commit
    • hg push to some alpha
    • hg pull --rebase from dev BZZZZT nope.

    You will receive an immutable changeset error. At this point, you'll have to merge and likely manually commit. This is for your own good; if someone had pulled from your alpha before you rebased, then you pushed your rebased changes to that alpha, their repo would be hosed, because the history would be different.

  6. Now I need to work in dev.

    $ hg up -r core/dev 
    54 files updated, 0 files merged, 1 files removed, 0 files unresolved 

    Commit commit commit.

  7. I need to pull in some alpha or something.

    $ hg pull ssh://xxx//home/branches/alpha/xtabs-refactor 
    pulling from ssh://xxx//home/branches/alpha/xtabs-refactor 
    searching for changes 
    adding changesets 
    adding manifests 
    adding file changes 
    added 188 changesets with 835 changes to 301 files (+1 heads) 
    (run 'hg heads .' to see heads, 'hg merge' to merge) 

    If you see this: (run 'hg heads .' to see heads, 'hg merge' to merge), it means you can't just update without a merge first. In pratical terms, dev needs to be merged into xtabs-refactor:

    $ hg merge -r tip 
    merging hermes/plugins/report/styles/config.style 
    163 files updated, 1 files merged, 113 files removed, 0 files unresolved 
    (branch merge, don't forget to commit) 
    $ hg commit -m 'merging dev -> xtabs-refactor' 
  8. I really f'd something up.

    hg strip is great, as long as you didn't push your filth. In that case, you'll probably have to hg backout.

Caveats

deviant hates bookmarks and expects you to always be deploying tip. This will not always be the case. Until we have some reasonable solution, if you need to deploy and you are not at the tip (find out by issuing a hg id), then pass the -S flag to deviant deploy.

Further Reading

@xianghongai
Copy link

hg update --clean

@agentgt
Copy link

agentgt commented Jul 12, 2019

In Mercurial 4.0 (I think its that version) a new extension called bookflow was added.

This extension is not very well known yet and is kind of not really documented well but it basically avoids the whole bookmark moving issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment