Using Git and Subversion Together

Many organizations use Subversion (or SVN) as their version control system (VCS) of choice. Often, SVN is so thoroughly baked into the organization’s processes—with build scripts, commit hooks, custom tooling, etc.—that it would be prohibitively expensive to change to a different VCS.

There’s nothing wrong with Subversion, but sometimes you want the power of Git. I personally prefer Git’s ability to commit locally and its branching model over SVN. Git can bidirectionally interact with Subversion repositories with the git svn command.

To demonstrate how, I will give repeatable, step-by-step instructions. The only prerequisite is an existing SVN repository. I will be using an SVN repository on my local machine for demonstration purposes. (These are the instructions I followed for setting up a local SVN repository: Creating Local SVN Repository (Home Repository).)

Setting Up the Remote Git Repository

The constant we must maintain between the SVN and Git repository is: The Git repository’s master branch should always be equivalent to the SVN trunk, commit for commit.

To start, let’s assume we have an existing SVN repository called project.

Pre-existing SVN repository

Pre-existing SVN repository

If we were to checkout this repository and look at it’s commit history, it might look something like this:

$ svn co file:///Users/justin/svn_repo/project/
A    project/branches A    project/tags A    project/trunk A    project/trunk/file.txt A    project/trunk/bugFix.txt Checked out revision 3.

$ cd project/ ; svn log

------------------------------------------------------------------------ r3 | justin | 2015-08-16 10:00:10 -0400 (Sun, 16 Aug 2015) | 1 line Fix a bug ------------------------------------------------------------------------ r2 | justin | 2015-08-16 09:59:12 -0400 (Sun, 16 Aug 2015) | 1 line Add source files ------------------------------------------------------------------------ r1 | justin | 2015-08-16 09:58:09 -0400 (Sun, 16 Aug 2015) | 1 line Initial repo setup ------------------------------------------------------------------------

First, we’ll set up the remote Git repository. It will allow us to share our changes with our team before they’re committed to the SVN repository. Log into the remote machine and create a bare Git repository. In this example, my Git remote will be on the same machine:

$ cd ~/git_remote
$ git init --bare project.git
Initialized empty Git repository in /Users/justin/git_remote/project.git/

On your local machine, clone the newly initialized Git repository (again, in this example, just another file location on my local machine).

$ cd ~/git_local
$ git clone file:///Users/justin/git_remote/project.git/
Cloning into 'project'... warning: You appear to have cloned an empty repository. Checking connectivity... done

Next, we will copy the SVN repository commits into our local Git repository. In the current directory (i.e., ~/git_local/), type the following:

$ git svn clone -s file:///Users/justin/svn_repo/project/
r1 = 77824f37cf61b95d29e64f5757533e3436a546f8 (refs/remotes/trunk) A     file.txt r2 = 24c1a4e4efcf1cc1a3c4954bc3a68f75008a407b (refs/remotes/trunk) A     bugFix.txt r3 = bab2f4a2259b4242e53f4c7c5b01685056607830 (refs/remotes/trunk) Checked out HEAD: file:///Users/justin/svn_repo/project/trunk r3

The effect of that command can be seen in two places. One, the local Git history should look something like this:

$ git log --oneline --decorate --all --graph
* bab2f4a (HEAD, trunk, master) Fix a bug * 24c1a4e Add source files * 77824f3 Initial repo setup

Second, the /.git/config file will have the following appended to it:

[svn-remote "svn"]
    url = file:///Users/justin/svn_repo/project
    fetch = trunk:refs/remotes/trunk
    branches = branches/*:refs/remotes/*
    tags = tags/*:refs/remotes/tags/*

The commits can be visualized like so:

Clone from SVN to local Git

Clone from SVN to local Git


Finally, push to the Git remote origin:

$ git push origin master
Counting objects: 8, done. Delta compression using up to 4 threads. Compressing objects: 100% (4/4), done. Writing objects: 100% (8/8), 699 bytes | 0 bytes/s, done. Total 8 (delta 2), reused 0 (delta 0) To file:///Users/justin/git_remote/project.git/ * [new branch]      master -> master

The history and visualization is:

$ git log --oneline --decorate --all --graph
* bab2f4a (HEAD, trunk, origin/master, master) Fix a bug * 24c1a4e Add source files * 77824f3 Initial repo setup
Push to remote Git

Push to remote Git

Git remote master is now in sync with SVN trunk, commit for commit. However, if a commit is made to trunk or master, it must be manually synced to the Git or SVN repository, respectively. That’s what the next sections will go over.

Set Up Local Git Repository

Now that the remote Git master is in sync with SVN trunk, let’s go through the steps as if we don’t have a local Git repository, i.e., the perspective of your teammates. Fortunately, these steps are similar to the initial setup, but they must be followed each time the remote Git repository is cloned.

First, clone from the remote Git repository:

$ cd ~/git_local_2
$ git clone file:///Users/justin/git_remote/project.git/
Cloning into 'project'... remote: Counting objects: 8, done. remote: Compressing objects: 100% (4/4), done. remote: Total 8 (delta 2), reused 0 (delta 0) Receiving objects: 100% (8/8), done. Resolving deltas: 100% (2/2), done. Checking connectivity... done

Then, in the same directory, link it with the SVN repository:

$ git svn clone -s file:///Users/justin/svn_repo/project/
r1 = 77824f37cf61b95d29e64f5757533e3436a546f8 (refs/remotes/trunk) A     file.txt r2 = 24c1a4e4efcf1cc1a3c4954bc3a68f75008a407b (refs/remotes/trunk) A     bugFix.txt r3 = bab2f4a2259b4242e53f4c7c5b01685056607830 (refs/remotes/trunk)

If the remote Git repository and your local repository were set up correctly, both master and trunk should point to the exact same commit:

$ cd project/

$ git log --oneline --decorate --all --graph

* bab2f4a (HEAD, trunk, origin/master, origin/HEAD, master) Fix a bug * 24c1a4e Add source files * 77824f3 Initial repo setup

If master and trunk point to different commits, likely one of the git svn init steps was done incorrectly.

Workflow

We’re now ready to enjoy the fruits of our labor. From here, create a feature branch off of master (or any other SVN branch) push it to remote, and from there follow whatever Git workflow you and your team agree upon. The only qualification is that, before putting changes back on master/trunk, all branches must be merged, rebased, or otherwise accounted for. This is because the entire feature branch will be rebased to the tip of master/trunk during this process.

For simplicity, let’s assume that no commits have been made to master/trunk (we’ll go over that detail below). Let’s also assume that our feature branch has one commit (it could also be a series of commits, as long as the history is linear). This situation is depicted below.

$ git log --oneline --decorate --all --graph
* bd3cfdd (HEAD, origin/feature, feature) Add new feature * bab2f4a (trunk, origin/master, origin/HEAD, master) Fix a bug * 24c1a4e Add source files * 77824f3 Initial repo setup
Git feature branch

Git feature branch

First, check that master/trunk is up to date.

$ git pull origin master
From file:///Users/justin/git_repo/project * branch            master     -> FETCH_HEAD Already up-to-date.
$ git svn fetch $

Of course, there was no new history (as per our assumptions), but we did it anyway because it’s a good habit.

Next, push the feature branch from local to SVN.

$ git checkout feature
$ git svn dcommit
Committing to file:///Users/justin/svn_repo/project/trunk ... A     feature.txt Committed r4 A     feature.txt r4 = 1e8f83e78ad541a59805ccd396f89ffa15d9667b (refs/remotes/trunk) No changes between bd3cfddd858f4b176d2fb59105d135ad5ef08639 and refs/remotes/trunk Resetting to the latest refs/remotes/trunk

And the resultant log and depiction looks like:

$ git log --oneline --decorate --all --graph
* 1e8f83e (HEAD, trunk, feature) Add new feature | * bd3cfdd (origin/feature) Add new feature |/ * bab2f4a (origin/master, origin/HEAD, master) Fix a bug * 24c1a4e Add source files * 77824f3 Initial repo setup

dcommit from local Git to SVN

dcommit from local Git to SVN


Note that the dcommit command rebased the feature branch. This is why it’s important that anyone else working off of the feature branch has already committed and pushed their changes, and any branches off of the feature branch are carefully rebased after this process.

We must keep trunk and master in sync, so the next step is to merge master and push to origin.

$ git checkout master
Switched to branch 'master'
$ git merge feature
Updating bab2f4a..1e8f83e
Fast-forward
feature.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 feature.txt
$ git push origin master
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 417 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To file:///Users/justin/git_repo/project.git/
bab2f4a..1e8f83e  master -> master

Which creates the following history and depiction:

$ git log --oneline --decorate --all --graph
* 1e8f83e (HEAD, trunk, origin/master, origin/HEAD, master, feature) Add new feature | * bd3cfdd (origin/feature) Add new feature |/ * bab2f4a Fix a bug * 24c1a4e Add source files * 77824f3 Initial repo setup
merge local Git master with feature

merge local Git master with feature

Finally, the feature branch should be deleted.

$ git branch -d feature
Deleted branch feature (was 1e8f83e).
$ git push origin --delete feature
To file:///Users/justin/git_repo/project.git/
- [deleted]         feature
$ git log --oneline --decorate --all --graph
* 1e8f83e (HEAD, trunk, origin/master, origin/HEAD, master) Add new feature
* bab2f4a Fix a bug
* 24c1a4e Add source files
* 77824f3 Initial repo setup
delete feature branches

delete feature branches

When trunk has Advanced Beyond the Feature Branch

This is the most common scenario you’re likely to run into. Suppose you’re about to merge a new feature back to master/trunk. As always, you first call git pull origin master and git svn fetch.

$ git pull origin master
From file:///Users/justin/git_repo/project
* branch            master     -> FETCH_HEAD
Already up-to-date.
$ git svn fetch
M     file.txt
r5 = ae4df2e75ae787ef74d8351742fc6c07528a09c2 (refs/remotes/trunk)
$ git log --oneline --decorate --all --graph
* a4e025f (HEAD, origin/feature, feature) Add cooler new feature
| * ae4df2e (trunk) Fix another bug
|/
* 1e8f83e (origin/master, origin/HEAD, master) Add new feature
* bab2f4a Fix a bug
* 24c1a4e Add source files
* 77824f3 Initial repo setup
Trunk has advanced beyond master

Trunk has advanced beyond master

This time, trunk is ahead of the Git commit on which the feature branch is based. Handing this situation is easy. First, rebase the feature branch onto the trunk commit:

$ git rebase trunk feature
First, rewinding head to replay your work on top of it...
Applying: Add cooler new feature
$ git log --oneline --decorate --all --graph
* 9f5e826 (HEAD, feature) Add cooler new feature
* ae4df2e (trunk) Fix another bug
| * a4e025f (origin/feature) Add cooler new feature
|/
* 1e8f83e (origin/master, origin/HEAD, master) Add new feature
* bab2f4a Fix a bug
* 24c1a4e Add source files
* 77824f3 Initial repo setup
Rebase feature branch

Rebase feature branch

Finally, follow the same instructions from above:

$ git svn dcommit
Committing to file:///Users/justin/svn_repo/project/trunk ...
M     feature.txt
Committed r6
M     feature.txt
r6 = b16c5f99570244c9329acf5c0f33206d94e1d497 (refs/remotes/trunk)
No changes between 9f5e826e9175389dd079cc25ff1931768c8d8956 and refs/remotes/trunk
Resetting to the latest refs/remotes/trunk
$ git checkout master
Switched to branch 'master'
$ git merge feature
Updating 1e8f83e..b16c5f9
Fast-forward
feature.txt | 1 +
file.txt    | 1 +
2 files changed, 2 insertions(+)
$ git push origin master
Counting objects: 9, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 664 bytes | 0 bytes/s, done.
Total 6 (delta 2), reused 0 (delta 0)
To file:///Users/justin/git_repo/project.git/
1e8f83e..b16c5f9  master -> master
$ git branch -d feature
Deleted branch feature (was b16c5f9).
$ git push origin --delete feature
To file:///Users/justin/git_repo/project.git/
- [deleted]         feature
$ git log --oneline --decorate --all --graph
* b16c5f9 (HEAD, trunk, origin/master, origin/HEAD, master) Add cooler new feature
* ae4df2e Fix another bug
* 1e8f83e Add new feature
* bab2f4a Fix a bug
* 24c1a4e Add source files
* 77824f3 Initial repo setup

Final Thoughts

You must always call git svn fetch before calling git svn docommit. If you do not, extra commits will be added to trunk, and your history will be messed up.

SVN branches

  • If the SVN repository has branches or tags, they can be handled the same way trunk is handled.
  • Git svn can be used to create branches, but I wouldn’t recommend using this feature.
  • Git cannot merge SVN branches, so do not merge sync’ed SVN branches in Git.

Branch Naming

The name master and trunk are just conventions. With an authoritative SVN repository and subserviant Git repository, it might be more clear to use the branch name trunk instead of master.

One thought on “Using Git and Subversion Together

  1. […] 本文参考了这篇文章:Using Git and Subversion Together。 […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s