Better git-svn Through Aliases: git up & git dci

I’ve been using git-svn for almost a year now, and have settled on a
low-friction workflow that has been working really well. First, a few
notes about how I work with Git and Subversion…

  1. For the most part, we avoid Subversion branching and merging, so I optimize for working in trunk.
  2. Forget about using Git push and pull for
    collaborating with others. git-svn stores extra metadata that isn’t
    pushed with the rest of the repository, so git-svn operations will fail
    on cloned repos. Furthermore, each repository’s git-svn commits are
    unique, so pulling from another repository will fetch its parallel
    history (if you have to do this, git rebase -i can help
    filter out the overlapping commits). That said, I do use push/pull to
    collaborate with myself across machines, though I always operate against
    Subversion from one machine.
  3. All substantial work is done in local topic branches, with master
    reserved exclusively for tracking what’s in (or to be immediately
    committed to) Subversion.

git up

First up is the alias that stands in for svn update, to make sure I’m always working from the latest changes:

!git svn fetch && git push .
remotes/trunk:master && git push -f origin master:master
&& git rebase master

Update Nov. 30, 2010: After reports that people were having issues calling git up from master, I’ve modified the alias slightly:

!git svn fetch && git svn rebase -l && git push . remotes/trunk:master && git push -f origin master

If this is your first alias, the command to set the alias would be:

git config alias.up "!git svn fetch && git
svn rebase -l && git push . remotes/trunk:master && git
push -f origin master"

Step by step, this alias does the following:

  1. Fetch the latest from Subversion. I use svn fetch instead of svn rebase because the former also fetches from Subversion branches; we’ll do our own rebase later.
  2. Rebase my current branch against its Subversion parent. -l skips a remote fetch, since we just did one.
  3. Update my local master branch to match Subversion’s trunk. If you didn’t use --std-layout, you might need to replace remotes/trunk with remotes/git-svn or whatever your git-svn ref is.
  4. Push master to my origin remote, which serves as a backup and allows
    me to collaborate with myself between machines (if you don’t have a Git
    remote, feel free to omit this).
  5. Rebase my current branch against master (and therefore Subversion).

The key is item #2: automating the synchronization of master and
Subversion means I never have to think about it again. I can either
rebase a topic branch against master to get reasonably fresh commits, or
use git up to grab the latest (mostly when preparing to
commit into Subversion). Without this alias, I tended to waste a fair
amount of time switching back to master periodically just to make sure
it’s up-to-date enough.

When do I git up?

You can use git up pretty much any time you want — on
master, on a topic branch or even with a detached HEAD — as long as your
working copy and index are clean. If you have work in progress but need
Subversion’s latest, you can either git stash or make a temporary commit and then git reset HEAD^
after the update. I used to favor the former, but am starting to prefer
the latter because I tend to be undisciplined about cleaning up stashes
that git stash pop didn’t delete due to merge conflicts.

git dci

Satisfied with this abstraction for pulling changes from Subversion, I
then applied the same logic to committing into Subversion:

!git svn dcommit && git push . remotes/trunk:master && git push -f origin master && git checkout master

The dci alias (short for dcommit) does the following:

  1. Commit my local changes into Subversion, generating a Subversion
    commit for each new Git commit. If a modified file has also changed in
    Subversion since your last update, the dcommit will fail — git up, resolve conflicts and try again.
  2. Keep master in sync with Subversion…
  3. …and origin.
  4. And finally, switch back to master, which will now reference the
    latest git-svn commit. From here I can either delete my finished topic
    branch, or rebase against master (git rebase master <em>branch-name</em>) and get back to work.

Again, by automating most of what I was already doing I can be
confident that I will always come out of a dcommit in a well-known,
consistent state from which I can proceed without extra thought.

When do I dci?

git dci should be used when you want to commit HEAD and
its uncommitted ancestors into Subversion. In my experience, this
usually falls into one of three scenarios:

1. Entire Branch

You have a topic branch and want to replay all of its commits into
Subversion one by one. In this case, simply checkout the branch and call
git dci:

git dci Entire Branch

I use this most often, as I prefer granular commits and a linear history.

Update Dec. 4, 2010: Donn pointed out that I gloss over my use of an lg alias, which provides a concise graph of history. The alias is described here.

2. Squashed Branch

The exception to my linear history preference is if the build would
break between intermediate commits. For example, I might upgrade a
dependency in one commit and then fix the build in the next commit. One
could certainly use a squash merge or interactive rebase, but sometimes I
prefer to keep the granular history in Git.

So how do we accomplish this with git-svn? Well git-svn essentially
treats merge commits as the sum of their parts, relative to the previous
Subversion commit — it squashes for us. To make our single Subversion
commit, we’ll just switch to master and use git merge --no-ff:

git merge --no-ff

The --no-ff flag forces the creation of a merge commit
even though we should be able to fast-forward (if we up’d first, that
is). You can make this the default behavior for master by setting
branch.master.mergeoptions (I use --no-ff --no-commit).

Once we have our merge commit, we again use git dci to push all its changes into Subversion:

git dci After Merge

Note that the dev2 commits remain untouched by git-svn, and Subversion has the combined changes:

git-svn Merge Commit in Subversion

3. Detached HEAD

The final scenario is really no different from either #1 or #2, but it’s worth pointing out that you can use git dci
from any HEAD, not just on a branch. For example, suppose I have a few
refactoring commits that I created as part of feature work which I would
like to share with the team now while I finish up the feature. In this
case, I can checkout (or merge from) the last of the commits I want to
share:

Checkout Detached HEAD

And git dci from the detached HEAD to save those changes:

git dci from Detached HEAD

Note that git dci left me on master. Now to continue on
dev3, I just rebase against master so Git is aware that the dci’d
commits have been updated with git-svn metadata:

git rebase master dev3

If I want to get even more sophisticated, I could cherry-pick then
dci individual commits, or I could create a copy of the branch and use
interactive rebase to exclude the commits that I don’t want to dci yet.
Just remember to rebase the topic branch against master when you’re
done.

If you have any additional git-svn tips or questions, please let me know.

Related Articles:

    About Keith Dahlby

    I'm a .NET developer, Git enthusiast and language geek from Cedar Rapids, IA. I work as a software guru at J&P Cycles and studied Human-Computer Interaction at Iowa State University.
    This entry was posted in git, git-svn. Bookmark the permalink. Follow any comments here with the RSS feed for this post.