… An Edge Case Tutorial for the Obsessed

This post proposes an arcane Goldbergian solution to a problem you may not have. It’s about version control software and a way to use a single folder on your computer as both a Git repository and a Subversion working copy.

If that confuses you, chances are you’ve got something better to do with your time. Honestly. If Google brought you here while you were planning your vacation, you’ll need to tweak your search query and try again.

If you’re still with me, you probably came here looking for trouble, so grab a cup of your favorite beverage and read on to see how (and why) you might want to consider this hybrid hyper-versioning setup.

Background

Git logo Many high-profile software projects have begun to switch from centralized version control systems (VCSs) to distributed solutions such as Git. There are many good reasons which are beyond the scope of this post, but offline access is one of the most compelling motivations. With Git you can work offline while traveling, for example, or stay productive when your client’s flaky VPN goes AWOL again — sound familiar, anyone?

You can develop away in the seclusion of a remote mountain getaway and push your changes upstream when you (or the servers) return to the grid. Once you get used to this workflow, it’s pretty hard to go back to the centralized approach.

Many distributed VCSs such as Bazaar or Mercurial can import existing Subversion repositories and provide offline access to project history and other version-control goodies, but it’s usually a one-way street — there’s no way back. In many cases, however, switching to another VCS is simply not an option — especially if you don’t own the repository you’re contributing to (hello, freelancers).

This is one of Git’s greatest strengths: you can actively participate in ongoing development hosted on a remote Subversion repository using the git-svn bridge as a Subversion client. You can push any local changes you’ve made in Git back to Subversion, and pull in updates from the SVN server to your local Git repository, ready for your next weekend workfest at the cottage. Your teammates continue on in Subversion, blissfully unaware of your extra-subversive affair.

“OK, so I get the Git thing, but why combine the two?”

If git-svn is the gateway drug that gets you hooked on Git, think of this approach as a sort of methadone — a substitute drug for recovering Subversion addicts.

Maybe you’ve gotten attached to one of those slick Subversion frontends like Versions or Cornerstone or an uglier, practical cross-platform cousin such as SmartSVN or the Syncro SVN Client included in <oXygen/>. Maybe you just really like to rename files in a GUI & commit without hunting for a cheat sheet of command-line syntax. Maybe you find it really annoying that Git leaves empty folders strewn around your disk when you remove their children.

Whatever the reason, if you’re working [in|for] a Subversion shop, you might want to keep an SVN working copy around just in case you need it.

“But do I really want two sets of project files on my computer?”

No.

“Wasn’t version control supposed to prevent this kind of insanity?”

Yes.

A Word of Caution

The solution I’m suggesting here is not for the faint of heart. Purists from both the Git and Subversion camps would surely consider the following folly at best, and perhaps even a Really-Bad-Idea®.

(If you do decide to try this at home, please remember to wash your hands afterwards…)

The Proposal

The basic idea is to create a new local Git repository by importing the contents of the remote Subversion repository and then move Git’s tracking data to an existing SVN working copy on your computer: “one working copy to rule them all.”

We’ll throw in a few extra hooks for convenience, but more on that later. Here’s how we start:

  1. Clone the remote Subversion repository to a new folder on your computer with a command such as: $ git svn clone --prefix svn/ -s <svnrepo> --authors-file=users.txt
  2. Copy the .git/ subfolder with the tracking metadata from your new Git repository to your SVN working copy.
  3. In Subversion, add .git to the svn:ignore properties (or global ignore).
  4. Tell Git to exclude .svn/ folders with .gitignore, or set the core.excludesfile property in your global Git config.

Now Git treats the project folder as a local repository with a remote tracking branch that syncs with the Subversion server and SVN still sees that very same folder as a working copy. Each is delightfully unaware of the other’s presence.

At least in theory…

And Here There be Dragons

Because neither Git nor Subversion expect anyone else to be mucking about with their pristine files, any time you commit with either Git or Subversion, the other will complain that the fileset in your working folder has been modified (until you pat them on the head and tell them that everything is OK).

For example, if you make changes, commit them in Git and push the revised content upstream via git svn dcommit, the tracked files in the working copy are current, but SVN looks at the last committed state of each file in the .svn/text-base/ area and thinks they’re dirty, since it doesn’t yet know that the files have since been updated in the remote repository.

Likewise, Git views files as dirty after any SVN commits. Both systems are purposely careful about clobbering any locally modified files with stuff from the server (and rightly so, but in our case, we know it’s OK).

So what to do?

Essentially, we have to tell each VCS to disregard any files in the working copy that it thinks are modified, and get the latest changes from the remote Subversion repository anyway.

In Git’s case, that’s:

$ git checkout HEAD --force
$ git svn rebase

If your last commit was in Git, and you want Subversion to catch up, use:

$ svn revert
$ svn update

But all this manual bunnystomping gets tedious quickly, so we need a way to automate it.

Enter hooks.

The Solution

Both Subversion and Git support so-called “hook” scripts. These scripts can be set to run whenever certain events take place, such as right before a commit (pre-commit hook) or right afterwards (post-commit).

We can take advantage of these and similar mechanisms to automatically update the status information of one VCS when we commit with the other.

SVN Hooks vs. Client-Side Actions

In Subversion, hooks are associated with the repository, which means any hooks defined there affect all users of the repository. Normally, that’s a good thing, but we don’t want that now.

Cornerstone icon Instead, we want a script that will run locally any time we commit to this particular Subversion repository, but that won’t affect any other users. This is one area where Cornerstone shines: “Client-Side Post-Commit Actions”. This Cornerstone feature allows us to call a shell script to automatically perform operations after committing changes to the repository (just select a script file in the commit view and it will run when the commit completes).

So we could set up a script to force a checkout of HEAD and then rebase from SVN as suggested above.

But there’s actually a more elegant (and safer) way to deal with the modified files in the working copy before rebasing. Just in case any of the changes were important, we can use git stash to save any modified tracked files and staged changes to a temporary branch, so we can restore them later if necessary.

git stash save "Changes stashed before rebase by SVN post-commit script"
git svn rebase

Actually, in a post-commit script you’ll need to specify the absolute path to your Git binaries, but that path might not be the same on your system as on mine. You can copy my script and adjust the paths for your system — get the gist of it here.

Git Hooks vs. Aliases

In Git, we could use the standard hook mechanism, since the local repository is our own and any hooks we define there won’t affect anyone else. (Even if the repository is later cloned, hooks aren’t replicated this way, so we’re safe.)

But as of this writing (Git v1.6.6.1), there isn’t a dedicated post-dcommit hook, and in fact no hooks are currently run directly by git svn. However, dcommit uses git-rebase internally, so git-rebase can run the pre-rebase hook. But that would affect any rebase operation in our repository, which might have unintended side effects.

In general, Git proponents recommend treating hooks as a last resort to be used only if there is no other way to accomplish the task. But in our case, there is.

Since all we want to do is run two simple Subversion commands whenever we run a certain Git command, we can set up a shell alias (in ~/.bash_profile) to customize Git’s behavior and use that instead of the standard Git command.

Enter this all on one line (I’ve broken it up here for readability):

alias svnpush="git svn dcommit;
               /usr/local/bin/svn revert --recursive .;
               /usr/local/bin/svn update --force"

Note: In the alias example above, I’ve hardcoded the absolute path to my preferred SVN package. Subversion may be installed elsewhere on your system. You can find out where by entering which svn at the command line prompt.

This alias creates a “virtual” command called svnpush (you could call it anything), which will run the Git command svn dcommit followed by the subversion commands revert and update. (The --recursive and --force options ensure that all files are updated from the server, regardless of what’s in the way.)

Now we can use the new command svnpush to run all three commands at once. Any time we use this alias, our latest Git commits are sent to the upstream Subversion server and the contents of the .svn subdirectories in the working copy are updated to reflect the newer state in the remote repository.

That’s it!

Now we get all the Git goodness and can still fall back on our Subversion habits if we feel so inclined.

Certainly one day graphical frontends will arise to make Git versioning more accessible to newcomers and provide better usability for users of all levels. A few are available today, but they have a long way to go before they provide full coverage of the command-line operations and mature to the level we’re used to in the Subversion ecosystem.

At the moment, the field is still wide open for a well-crafted interface that’s polished enough to bring the power of Git to the masses. Any takers? Panic? Sofa? Zennaware? Until then, with this setup we can continue using the fine Subversion tools we have now and get acquainted with the raw power of Git’s command line when necessary.

If you’d like to learn more about Git, I highly recommend Travis Swicegood’s excellent book entitled Pragmatic Version Control Using Git.”

If you have any questions or comments about this technique, feel free to leave a comment below.