Translating my Git workflow with local branches to Mercurial
It took me a while to really settle in to a Git workflow I like to use on a daily basis. It’s a pretty common workflow, and is centered around local topic branches and rebasing. It’s not actually much different than the workflow I used with SVN, except that I prefer rebase over merge. My typical Git workflow starts out with:
- git checkout –b “SomeTopic”
- git add .
- git commit –am “Commit message”
- <repeat last 2 steps as necessary>
- git checkout master
- git pull –rebase origin master
- git checkout SomeTopic
- git rebase master
- git checkout master
- git rebase SomeTopic (or merge, same thing)
- git push origin master
- git branch –d SomeTopic
In a nutshell, all work, and I mean ALL WORK, is done in a local branch. Every time. This is because I can never predict when other, unrelated work might come up. I do work in a local branch. When I want to bring that work back to master (basically, trunk), I first do a pull from origin back to master, rebasing my existing commits.
At this point, I should note that I rarely, rarely ever need to rebase upstream changes. If I pull work back to master, that means that I’m about to push. Otherwise, it can just stay in a local topic branch.
Anyway, I make sure that my master reflects the absolute latest upstream changes. When I’m ready to bring the local branch back in, INSTEAD OF MERGING, I rebase the branch on to master. All this means in practice is that the branch’s commits are re-played onto the master branch. Merging instead squashes all the branch’s work in to one single merge commit, which I’d rather avoid.
Side note – The git “rebase considered harmful” article is 3 years old. A lot of opinions have changed since then, so while its core arguments do apply (never rebase a pushed commit), rebase is a sharp, useful tool that does great things when used right.
Finally, I do a merge/rebase from master to the branch, which really just does a fast-forward merge. Because the SomeTopic pointer is a descendant of the master pointer, a merge/rebase is really just moving the master pointer up to the SomeTopic pointer. Once this is done, I push and I’m finished.
Translating this to Hg has been a little more difficult, however.
Combining Rebase and Bookmarks
Right now, I’m trying to use Bookmarks and Rebase to achieve this same workflow. The basic workflow I want is:
- All work is always isolated from any other work
- Pulling latest does not affect isolated work
- Isolated work, when rolled back in, has linear history preserved
I don’t really care how this is accomplished. So, I’m going from this article on the different ways of doing branching in Mercurial.
So my first try was using the Hg extensions Rebase and Bookmarks, which have both been included with Mercurial for a while now. On the surface, Rebase and Bookmarks seem very similar to Git’s rebase and branching model.
Mimicking git, I try:
- hg bookmark SomeTopic
- hg commit –Am “Some message”
Now I want to pull those changes back to master…but wait, there is no master! I don’t have a pointer to when I first made the branch, and even worse, the “default” branch is now sitting on my SomeTopic branch.
What I’d really like to do is do “hg checkout default”, then do an update, so that “default” always represents the upstream current state. But “default” has moved!
My next attempt was to create a “master” bookmark right before I created the “SomeTopic” bookmark, so that “master” mimics the Git master branch – that is, it’s merely a named pointer, nothing special.
I now want to do some more, unrelated work, so I:
- hg update master
- hg bookmark SomeOtherTopic
- hg commit –Am “Some other message”
At this point, here’s my tree:
As we can see here, I have my master bookmark marking where I diverged my bookmarks. This now more or less matches what I see in Git, with the exception of that “default” branch.
Let’s say that we now want to bring “SomeTopic” back to “master”. Really, I want to rebase “SomeTopic” on to “master”. Because “master” is a parent of SomeTopic, this should really just be a fast-forward merge. The master should just be moved up to SomeTopic.
If there were upstream changes, that would change the story a bit. Master would move up to those upstream changes, and all changes from where SomeTopic and master diverged would be replayed on top of master. One thing to note is that Git is very smart about fast-forward merges. If I rebase SomeTopic on to master, and master is still where it was, nothing would happen. I could then rebase master on to SomeTopic (or merge), and master would just move up.
In the picture above, I really just want “master” to move up to “SomeTopic”, then I can push “master” up. So let’s try to do an hg rebase:
- hg rebase –b SomeTopic –d master
I want to rebase SomeTopic on top of master. I get a message “nothing to rebase”. That’s fine, as I have nothing to do here anyway. “master” is in the direct ancestry of “SomeTopic”.
The next thing I want to do is move master to SomeTopic, which at this point should just be a fast-forward merge. But nothing I seem to do will allow me to move “master” up to “SomeTopic”. I try all of these:
- hg update master/hg merge –r SomeTopic <- “nothing to merge”
- hg rebase –b master –d SomeTopic <- “nothing to rebase”
Blarg. Even though I’ve configured my bookmark to automatically move forward, there doesn’t seem to be a way to do this myself. What I can do is:
- hg bookmark –d master
- hg update SomeTopic
- hg bookmark master
- hg bookmark –d SomeTopic
This basically fast-foward merges the master bookmark to SomeTopic, by…deleting it and re-creating it. If Mercurial supported a fast-forward merge here, that would be GREAT, but it doesn’t, so I have to jump through a bunch of hoops here. All of which I could batch up in to a “fast-forward” alias, but is still annoying, as Git just handles this automatically.
Anyway, this is now the state of things:
And now I want to push “master” up. But this will be interesting, I don’t want “SomeOtherTopic” to go out. So, I use:
- hg push –b master
Now only the “master” piece got pushed up. Let’s say we now want to get the SomeOtherTopic back in to the fold, AND, that there were upstream changes. In this case, we want to update our master to be include new changes, but without affecting “SomeOtherTopic”. We do this to update our master:
- hg update master
- hg pull –rebase –b default
This makes our local repository tree now:
Exactly what we wanted! The “master” bookmark got moved up, past “SomeOtherTopic”. Now, we just need to rebase SomeOtherTopic onto master:
- hg rebase –b SomeOtherTopic –d master
This means I’m replaying the SomeOtherTopic on top of master, resulting in the following tree:
Looking good! I now just follow the FF-merge-master-to-branch routine:
- hg update SomeOtherTopic
- hg bookmark –f master <- basically moves master to current location, better than delete/add
- hg bookmark –d SomeOtherTopic <- delete the bookmark (the work is integrated now)
- hg push –b master
Well, that’s it. It’s not completely like git branching, there are some caveats here and there, as git handles remotes different than Hg. Git also automatically handles fast-forward merges, but in practice, I don’t think that it’ll be a big deal.
I need to run with this with a team to really make sure it doesn’t corrupt things, but it seems to work so far.