Code Vigorous


navigation
home
github
mozillians
email
about
Dustin J. Mitchell

More time with Mercurial

22 Jun 2015

As I described in my last post, I have been using a Mercurial workflow for projects that are in Mercurial repositories.

For the most part it’s been going well, but I’ve enountered a few issues that I haven’t been able to resolve.

Remote Refs

Within git’s very generic concept of “refs” (names for commits) is hte concept of a “remote ref”. When you fetch a remote repository, Git not only brings the commits from that repository into the local repository, but also copies (some of) the remote repositories refs, namespacing them locally. For exapmle, if I fetch the “upstream” repository, I’ll get remote refs like “upstream/master” or “upsream/production”. This makes it easy to manipulate my local history with reference ot the upstream repository. For example rebasing on top of the latest commits fetched from upstream is as simple as git rebase upstream/master.

Mercurial falls down a little bit here. In its default configuration, when it pulls from a remote repository, hg will not update any bookmarks or branches. It just dumps the new commits into the local repository.

For a simple upstream repository with one head, the tip refspec is actually (for once!) useful here – right after an hg pull, tip refers to the equivalent of upstream/master. But in a more complex repository – say, one with staging and production branches, or multiple release branches – tip is whatever branch was most recently updated. Not a very useful concept. In this case, you’re left looking at the output of hg log -G or hg heads to try to discern which head on the branch is yours, and which is from upstream.

The hgremotenames extension purports to fix this issue, and I’ve started using it on one of the more complex repositories I work with (https://hg.mozilla.org/build/puppet, which has three upstream repositories that occasionally perform multidirectional merges). It doesn’t seem to be updating bookmarks as promised, but that may be because there are no new refs downloaded on hg pull.

Don’t Treat Me Like a Child

Mercurial’s paternal attitude has improved dramatically over the last few years, and -f (the equivalent of a teenager’s sighing eye-roll, I suppose) works wonders at convincing it to shut up and let me do what I want to do.

However, there are still a few spots where Mercurial insists that I must not want to do the thing I want to do. In particular, hg histedit will not let you edit a forked history if some of those forks might be orphaned by the edit. For example:

o  changeset:   3:21f32b086ce3
|  parent:      1:237cb0b291ce
|  user:        Dustin J. Mitchell <dustin@mozilla.com>
|  date:        Mon Jun 22 10:05:40 2015 -0400
|  summary:     more frobnication
|
| @  changeset:   2:3f6141f7e80b
|/   user:        Dustin J. Mitchell <dustin@mozilla.com>
|    date:        Mon Jun 22 10:05:13 2015 -0400
|    summary:     try: -b all -t all
|
o  changeset:   1:237cb0b291ce
|  user:        Dustin J. Mitchell <dustin@mozilla.com>
|  date:        Mon Jun 22 10:04:57 2015 -0400
|  summary:     frobnicate
|
o  changeset:   0:2de5e87806b1
   user:        Dustin J. Mitchell <dustin@mozilla.com>
   date:        Mon Jun 22 10:04:43 2015 -0400
   summary:     base

Here I’ve commited some additional frobnication after my latest push to try, and I no longer want the try-syntax commit. Yet:

dustin@ramanujan ~/tmp/hgrepo $ hg histedit 0
abort: cannot edit history that would orphan nodes

The fix here is fairly straightforward – just manually strip the try commit. In fact, I wrote a short script to push to try for me and strip the commit afterward.

Rather than simply bombing out, I’d prefer that Mercurial either warned me and required -f, or just kept the existing history for those nodes. The result might then be:

  @  changeset:   4:1259be5a2faa
  |  user:        Dustin J. Mitchell <dustin@mozilla.com>
  |  date:        Mon Jun 22 10:10:24 2015 -0400
  |  summary:     more frobnication (rebased)
  |
  o  changeset:   3:f77d4ba31d4b
  |  parent:      0:2de5e87806b1
  |  user:        Dustin J. Mitchell <dustin@mozilla.com>
  |  date:        Mon Jun 22 10:10:04 2015 -0400
  |  summary:     frobnicate (rebased)
  |
o |  changeset:   2:21f32b086ce3
| |  parent:      1:237cb0b291ce
| |  user:        Dustin J. Mitchell <dustin@mozilla.com>
| |  date:        Mon Jun 22 10:05:40 2015 -0400
| |  summary:     try: foo
| |
o |  changeset:   1:237cb0b291ce
|/   user:        Dustin J. Mitchell <dustin@mozilla.com>
|    date:        Mon Jun 22 10:04:57 2015 -0400
|    summary:     frobnicate
|
o  changeset:   0:2de5e87806b1
   user:        Dustin J. Mitchell <dustin@mozilla.com>
   date:        Mon Jun 22 10:04:43 2015 -0400
   summary:     base

Aggressively Stripping History

Don’t laugh, but sometimes Mercurial goes too far in destroying history.

When I put a large change up for review, I like to do so in a series of commits that tell a story. That helps the reviewer follow along, and can help to segregate large, boring changes (replace all sessionId with sessionToken) from smaller, more important changes (generate a new session token instead of an id). But like any good writer, I don’t create the story in that order: hacking is more organic, as I try out ideas and update the design.

Once I have a working, tested final product that I’m happy with, I usually want to go back and re-structure the story (rebase). That may mean changing the order of commits, squashing multiple commits together, or even moving hunks between commits. Git’s rebase and Mercurial’s histedit both support this this quite well.

But I want the end product to be byte-for-byte identical: the final commit’s tree should be the same before and after the rebase. In Git, this is easy: either capture the pre-rebase sha1, or look it up in the reflog after the rebase is complete, and use git diff to compare the rebase result to the pre-rebase tree. Git’s reflog keeps the now-unnecessary commits “alive” for a short time, allowing this kind of comparison.

Mercurial, by comparison, immediately strips the old commits from the history. You can retrieve the commits from the backup bundle in an emergency, but they aren’t handily kept around for this kind of comparison.

Conflict Resolution

Finally something I just haven’t figured out yet. When a Mercurial operation encounters a merge conflict, it pops up a three-way merge application (vim in my case - I don’t have an X11 connection for any of the graphical tools). However, it’s not entirely clear what has already been merged, and what each of the three files represents, particularly in a histedit operation. On several occasions, I’ve unintentionally dropped hunks during the conflict-resolution process. It’s also not entirely clear how to abort an operation when a conflict occurs: if I save the file and exit, it seems to assume that I’ve resolved the conflict and proceed merrily along.

I very much like the conflict markers that Git or Subversion will insert into a file. In this case, the operation completes and I’m left at the command line with some hints about the files that contain conflict markers. Those files are as merged as possible, with only the ambiguous hunks remaining. Both sides of the conflict are present in the file, so in most cases the fix is simply to delete some lines.

I haven’t been able to find a way to use conflict markers with Mercurial. Is there a merge tool I can configure that will simply add markers and drop me at a shell?