My (work) Git Workflow

The problem: You want to track multiple patchsets against an upstream Subversion repository easily.

The solution: Use git-svn to track it and create topic branches for local changes.

Jump here for my actual workflow

A Not-So-Brief Introduction (with code!)

I started using git and git-svn a little over a year ago. I was an Amtrak regular at the time, so the ability to make offline commits seemed helpful. Plus, it was shiny, and I’d seen a demo of GitHub, so I had a hunch that it was going to be big.

One day, I sat down over lunch and watched most of Linus Torvalds’ git talk at Google. Until then, I hadn’t really considered DVCSes, staying in my comfortable little Subversion world, branching out to cvs and bzr as necessary. I don’t really remember what I got out of the talk, but somehow it inspired me to start reading up on how git could be used with svn, much like svk.

git-svn made its debut performance on our team’s IRC channel about a month later; I had been doing some major refactoring on a local git branch with lots of commits (squashing commits was mystery to be discovered later) and the time had come to merge it into trunk. The merge went fine (lucky, I guess), so I went for a git svn dcommit. And spammed the IRC channel for the next 30 seconds or so.

Flash forward to the following winter and new (remote) contributors to our project. It was time to start doing legitimate code reviews, in part to get the new folks up to speed with the code-base.

In the process of setting up Review Board, I discovered that it doesn’t play well with diffs generated with git. Bummer. They’re nice. I spent some time poking around RB’s source tree to see why that was the case. Long story short, no dice. However, on one of the message boards where the problem had been raised (no resolution, natch), I saw a snippet to transform git diffs into svn diffs, revision numbers and all. Bingo. After fiddling with it for a while, I ended up with the following, which has been working well. [Edit: updated gist with a more flexible version from Mike Pearce’s git-svn and ReviewBoard.]

Add the following to your ~/.gitconfig to enable git svn-diff. Awesome. It even looks like it belongs there. (git-svn-diff must be in your PATH.)

# ~/.gitconfig
[alias]
  svn-diff = !git-svn-diff
add git svn-diff as an alias for git-svn-diff

Bonus Points

Here are the relevant pieces of my ~/.bashrc to get tab completion and a colorized prompt that includes git branch information in it:

The Sizzle

This is essentially the git-svn variation of Josh Susser’s pure git workflow.

The Setup

Clone the target Subversion repository:

$ git svn clone svn+ssh://svn.host/path/to/repo -s
assuming a standard Subversion layout

Alternately, if you’re planning on sharing topic branches with fellow developers, have one person do the clone and pass it out (by copying it, to retain Subversion metadata).

Fixing a Bug / Implementing a Feature

Make sure you’re up-to-date:

[master] $ git svn rebase

Create a topic branch (I include a title to recognize it more easily) and check it out:

[master] $ git checkout -b bug-42-title

Attempt a bug fix (write a test, make it pass):

[bug-42-title] $ ...

Check it in:

[bug-42-title] $ git commit -a

Generate a patch for review:

[bug-42-title] $ git svn-diff > bug-42-title.patch
this will diff against the checked out trunk revision

Post it for review.

Time passes… Your diff hasn’t been reviewed, development continues

Update the tracking branch:

[master] $ git svn rebase

Rebase your topic branch against the current trunk:

[master] $ git checkout bug-42-title
[bug-42-title] $ git rebase master
[bug-42-title] $ # resolve conflicts; `git mergetool` is handy

Regenerate the patch:

[bug-42-title] $ git svn-diff > bug-42-title-2.patch

Post it for review.

Time passes… Your diff has been reviewed and been found wanting

Make changes to your topic branch:

[bug-42-title] $ ...

Check them in:

[bug-42-title] $ git commit -a

Regenerate the patch:

[bug-42-title] $ git svn-diff > bug-42-title-3.patch

Post it for review.

Landing a Reviewed Patch

You have a few options here.

You can apply the patch directly to the tracking branch:

[master] $ git apply bug-42-title-3.patch
[master] $ git commit -a

If you want to preserve history (i.e. multiple commits that tell a story), update the tracking branch and rebase your topic branch against it before merging:

[master] $ git svn rebase
[master] $ git checkout bug-42-title
[bug-42-title] $ git rebase master
[bug-42-title] $ # resolve conflicts
[bug-42-title] $ git checkout master
[master] $ git merge bug-42-title

If you want to get fancy (and remove your frustrated profanity), do an interactive rebase on the topic branch before merging:

[bug-42-title] $ git rebase -i
[bug-42-title] $ git checkout master
[master] $ git merge bug-42-title

Whew. Almost done. You’ll want to update the upstream Subversion repository, lest you risk your hard work being wasted:

[master] $ git svn dcommit
this will git svn rebase if necessary

If you don’t have write access to the upstream repository, submit the patch by mail instead.

Finally, once the patch has been merged, you can clean up your local repository by removing the topic branch:

[master] $ git branch -d bug-42-title

Topic branches needn’t to be limited to your own bug-fixes. git apply is quite liberal in what it understands, so you can grab diffs (from ReviewBoard, for example) and apply them to new branches to try out uncommitted functionality (or as an aid to the code reviewing process).

That’s it! You can keep as many topic branches active as you want (or need to), rebasing them against your tracking branch as necessary. I use this approach for bugs, features, and exploratory refactoring that may never see the light of day.

Working with Subversion Branches

Working with Subversion branches is remarkably straightforward, but it’s necessary to understand a few things about the repository layout and the branches that git-svn sets up when it does the initial clone.

First, if you did a git svn clone -s against a Subversion repository with a typical layout (trunk, branches/<branch>, tags/<tag>), a number of remote Git branches will have been created: one for trunk, one for each branch, and one for each tag (odd, but sensible given that Subversion doesn’t strictly do tags).

git branch will list local branches:

[master] $ git branch
  bug-42-title
* master

git branch -r will list Subversion branches (and tags):

[master] $ git branch -r
  stable-1.0
  tags/REL_1.0
  trunk

By default, your local master branch will be set up to track the remote trunk branch. Instead of using git pull (in a pure Git workflow) to update trunk to the current remote revision, use git svn rebase.

If you want to ensure that you have up-to-date versions of all Subversion branches, use git svn fetch to load upstream commits. This has the side-effect of causing master and trunk to be out-of-sync, so you’ll have to follow that with git svn rebase from the master branch. GitX will show you the current state of both local and remote branches, and thus is immensely useful when attempting to determine your current repository state.

In case you didn’t fully grok that last paragraph, where you would have used git svn rebase without branches, you should do the following (rebasing still works, but your branches won’t be up-to-date):

[master] $ git svn fetch
[master] $ git svn rebase

git svn rebase in this case is the equivalent to git rebase trunk, as trunk is the “SVN parent of the current HEAD” (from git help svn).

git svn dcommit will use svn to commit outstanding changes upstream (again, to the “SVN parent of the current HEAD”). The upstream branch in question depends on the Subversion branch that your current branch is tracking, usually using git branch --track <local> <remote>.

Subversion branches and “tags” can also be created with git-svn. For example, to tag and branch a 1.1 release:

[master] $ git svn branch -m "branching for 1.1" stable-1.1
[master] $ git svn branch -m "1.1 release" -t REL_1.1
[master] $ git branch -r
  stable-1.0
  stable-1.1
  tags/REL_1.0
  tags/REL_1.1
  trunk

Commit messages are necessary, as branching in Subversion is the equivalent to creating a changeset where a directory is copied.

Now that the branch has been created upstream, create a local tracking branch such that git svn dcommit will commit to the correct upstream branch.

[master] $ git branch --track stable-1.1 stable-1.1
[master] $ git checkout stable-1.1
[stable-1.1] $

Extra Credit

Convert Subversion “tags” into proper Git tags:

#!/bin/sh
#
# git-svn-convert-tags
# Convert Subversion "tags" into Git tags
for tag in `git branch -r | grep "  tags/" | sed 's/  tags\///'`; do
  git tag $tag refs/remotes/tags/$tag
done

Add the following to your ~/.gitconfig to enable git svn-convert-tags. (git-svn-convert-tags must be in your PATH.)

# ~/.gitconfig
[alias]
  svn-convert-tags = !git-svn-convert-tags