In the last post, we looked at a workflow very common in the Git sphere: utilizing local branches to create segregated workspaces for individual topic branches. As far as I can tell, this seems to be the preferred day-to-day workflow for Git users, as its first-class local branching support makes it completely seamless to create segregated areas of work.
This workflow is completely possible in Hg, but does take little more set up and one or two extra steps that Git doesn’t force you to make. The idea behind this workflow is that I can’t predict when unrelated work comes in. Because local branches and local commits are so cheap, there’s really no reason to throw any code away, ever. With local topic branches, I can keep mini-spikes around without affecting anyone.
However, there are scenarios where the likelihood of unrelated work is small, so the need for topic branches tends to diminish. With Hg, the limitations of its model of local branches, bookmarks and rebasing tends to lessen the full benefits of local topic branches.
For those cases where I’m truly confident that I’m working on a continuous main line of work, I’ll use a slightly different workflow than one that uses topic branches.
The simplified workflow
In the simplified workflow, it is nearly identical to the normal centralized workflows (except most operations are local). When I want to start work for the day, I’ll:
- work work work
- hg commit –Am “Working on some stuff”
- work work work
- hg ci –Am “Working on some more stuff”
- work work work
- hg ci –Am “Finished my stuff”
At this point, I’ve finished some logical set of work, and I’d like to push my work upstream. My local repository now looks like:
Unlike the previous workflow, my “master” bookmark moves along, instead of always pointing at the latest pulled commit. It’s still important that this bookmark sticks around, as we’ll see soon. Now that I want to push, I want to first pull down incoming commits. Let’s suppose that someone else also made some commits on another repository and already pushed. The server repository shows this:
Note that we don’t see our bookmarks here, as by default bookmarks don’t get pushed upstream. When we pull from upstream, we’ll get the commit from the “otherdude” developer. So, I’ll:
- hg pull –rebase
Instead a “pull/merge/update” workflow, which generates noisy merge commits, I’ll rebase my three commits against the upstream changes. Rebase simply replays my three commits against the incoming tip. That would mean that I expect to see that the parent of “Working on some stuff” to be the “otherdude” commit instead of the “Finishing work on a feature” commit. After the pull and rebase, my local repository is now:
This is what we expected, our commits that originally came after the “Finishing work on a feature” commit got moved AFTER the “otherdude” commit. This produces a nice clean timeline that makes localizing bugs and merging changes a lot easier. With a regular pull/merge workflow, you’re merging all 3 commits at once. With a rebase, I merge one commit at a time, making the potential merges much smaller. Each merge also modifies each commit, instead of one gigantic merge commit with all changes coming in at once.
Anyway, I’ve pull upstream changes, so now I’m ready to push:
- hg push –b master
I only want to push that mainline branch, “master”, just like my previous workflows. By pushing only my “master” branch, I can transfer back and forth between my simplified, mainline workflow and topic branch workflow very easily, with neither conflicting with the other. In Git, only the current branch is ever pushed by default, but in Hg, it’s the opposite, so I have to a bit more explicit.
Comparing rebase and merge
Just to show what a merge looks like in comparison, let’s say “otherdude” doesn’t rebase before he commits his additional work. He has some more commits:
Now he wants to push these two commits up. However, the other user already pushed rebased commits, so now the server looks like:
Instead of doing a rebase on pull, he does a regular pull and update. Because other commits have gone in, he’ll need to do a merge:
- hg pull –u
- hg merge
- hg ci –Am “Stupid merge commit”
He tries to pull and update, but since there were commits there, two heads get created and he needs to merge in his changes. This causes an extra merge AND commit step, and now uglies up the history:
So the silly thing about this is the stupid merge commit contains ALL changes from the other two outgoing commits, yet all three commits get pushed! This also becomes really ugly over time, especially when you have overlapping commits and merges:
I’m not sure human beings are meant to comprehend this picture, so I’ll take the rebased workflow with its clean, linear history any day of the week. With the simplified workflow, rebasing is actually simpler than the pull/merge/commit workflow. Rebase is good, whether we work in topic branches or not.