Saturday, 28 June 2008

Shelving looms

For a feature that I'm currently working on, I decided to try out the loom plugin. Looms have been around for a little while, but I just hadn't gotten around to trying them out.

We have code reviews of all work that is to be merged. Part of this process is to try to limit changes to 800 lines of unified diff. We have found that when the branches have more changes than this the time to review the branch increases non-linearly with the increase in line count. In the past in order to break up a "chunky" branch I would branch from trunk for the first part:

$ bzr cbranch trunk feature-part-1

(I use cbranch as I don't have my working trees in my repository. This is another story to write about. cbranch is found in bzrtools.)

Once this part was complete, I would branch from that for part-2:

$ bzr cbranch feature-part-1 feature-part-2

Complications come in when I want to bring an updated trunk into my branch for part-2, as it makes getting a diff of changes much more difficult as I can no longer generate a diff simply. This problem propagates if I need three or four parts to implement the feature.

Enter looms. Looms provide a new branch format for Bazaar. To convert your branch to the new format, you use the command loomify. You can then create threads of your loom. Each thread is like another branch.

So, the process goes something like this:

$ bzr cbranch trunk my-new-feature
$ cd my-new-feature
$ bzr loomify
$ hack, commit, hack, commit, hack, commit
$ bzr create-thread next-part-of-feature

Creating a thread is like creating a new branch which has the same revisions as the last thread.

$ hack, commit, hack, commit, et al

However what happened after several hours of hacking away, and several diversions in the code that needed fixing, I checked out the size of the branch.

$ bzr diff -r thread:

(The loom plugin adds a revision specifier to easily allow things like this to see all the changes that were introduced by the current thread.)

Oh, damn. The resulting size was about 1100 lines. Now while our 800 line limit is set in stone, it is considered a bit rude if you could have broken it up but didn't. Stepping through the diff I identified three distinct chunks of work that could be broken out for review. My question was "now that I have all this in a loom, and I have a vested interest in keeping the loom as the current work is based on an earlier thread, how the hell am I going to break this work up?" Shelve to the rescue. Shelve is also found in bzrtools. I also wanted to have the threads named reasonably, and unfortunately there is no easy way to rename a thread right now. I wanted to have the threads named alpha, bravo and charlie (well, not really, but you get the picture). The first step is to create alpha and get rid of the current thread.

$ bzr create-thread alpha
$ bzr down-thread

create-thread takes you to the new thread too, so using down-thread to go back to the thread I was working on before.

$ bzr combine-thread

This effectively discards the current thread. The assumption is that the changes from this thread had been merged into the lower thread through merging another branch. This hasn't happened in this case, and discarding is what we want here. So now I'm at the state where I was before except my thread is now called "alpha". Now to break out the changes for alpha.

$ bzr create-thread bravo
$ bzr down-thread

I created a thread bravo. This is also at the state where all three parts are there and working. Next I went back to "alpha" thread. Now we use shelve using the revision specifier that looms introduce. Shelve by default will just allow you to shelve (or put to one side) the uncommitted changes.

$ bzr shelve -r thread:

Now I get lots of questions. Do I want to shelve each of the chunks. I shelve all changes that are unrelated to the alpha feature. What I'm left with after this command is a working tree as it would be if I had just written the alpha feature on a clean thread. I checked the results with:

$ bzr diff -r thread:

Looks good, so commit.

$ bzr commit -m "Shelved changes unrelated to alpha."

Now for the magic.

$ bzr up-thread

This takes me back to "bravo". However up-thread also merges in the changes from the thread below. Now my tree is showing that all the changes relating to bravo and charlie have been removed. The actual merge magic is done with this command:

$ bzr revert .

Take a special note of the dot. Without the dot the revert would revert the entire merge. I don't want this. I just want to revert the changes to the tree. I need bzr to remember that I have merged the changes from the thread below. This is exactly what the "revert ." does. The changes to the three are reverted, but the merge isn't. Next you need to commit.

$ bzr commit -m "Merge from alpha while splitting up the changes."

Now I have the alpha thread with just the changes needed to implement alpha, and a bravo thread that appears to introduced the bravo and charlie features. I also have a .shelf directory (created by shelve). Since I have no intention of unshelving these changes (as they are already there), I delete this directory. I'm not sure if this is strictly necessary, but I like to run a clean shop.

To break apart the bravo and charlie features I repeated the process. The end result was three separate threads that each appear to introduce a single feature.


One point of caution. Sometimes in the breaking apart, you don't always get a clean break. In these situations you need to keep more than you need (i.e. don't shelve that change), and once the revert and commit is done, then go back to the earlier thread and clean up. If you try to do it earlier, the changes will be thrown away in the revert dot command, and then it just gets messier.

All in all I'm really enjoying working with looms. I currently have about eight threads, and will probably need another four or five to finish the feature, but this way it is dead easy to keep the changes small and distinct and simple to review.


Stephen said...

I was looking for a way to reorder the threads in my loom, specifically, to move a thread further away from the base.

After reading your post, I had the confidence in loom/shelve to actually try it, and it worked out great!

* "bzr up-thread" to the top (or wherever you want to insert the new thread)
* "bzr create-thread new"
* "bzr down-thread" to the existing location
* "bzr shelve", and get rid of everything
* "bzr up-thread" && "bzr commit" at each intermediate level
* "bzr revert ." && "bzr commit" in the new thread location.

Thanks for the inspiration.

Stephen Warren said...

Oops. Forgot the final steps!
* "bzr down-thread" to the original location.
* "bzr combine-thread" to get rid of the original thread.