A large fraction of open source development projects and a good number of corporate projects use Subversion to manage their source code. It’s been around for more than a decade, and for most of that time was the de facto VCS choice for open-source projects. It’s also very similar in many ways to CVS, which was the big boy of the source-control world before that.
One of Git’s great features is a bidirectional bridge to Subversion called git svn
.
This tool allows you to use Git as a valid client to a Subversion server, so you can use all the local features of Git and then push to a Subversion server as if you were using Subversion locally.
This means you can do local branching and merging, use the staging area, use rebasing and cherry-picking, and so on, while your collaborators continue to work in their dark and ancient ways.
It’s a good way to sneak Git into the corporate environment and help your fellow developers become more efficient while you lobby to get the infrastructure changed to support Git fully.
The Subversion bridge is the gateway drug to the DVCS world.
The base command in Git for all the Subversion bridging commands is git svn
.
It takes quite a few commands, so we’ll show the most common while going through a few simple workflows.
It’s important to note that when you’re using git svn
, you’re interacting with Subversion, which is a system that works very differently from Git.
Although you can do local branching and merging, it’s generally best to keep your history as linear as possible by rebasing your work, and avoiding doing things like simultaneously interacting with a Git remote repository.
Don’t rewrite your history and try to push again, and don’t push to a parallel Git repository to collaborate with fellow Git developers at the same time. Subversion can have only a single linear history, and confusing it is very easy. If you’re working with a team, and some are using SVN and others are using Git, make sure everyone is using the SVN server to collaborate – doing so will make your life easier.
To demonstrate this functionality, you need a typical SVN repository that you have write access to.
If you want to copy these examples, you’ll have to make a writeable copy of an SVN test repository.
In order to do that easily, you can use a tool called svnsync
that comes with Subversion.
To follow along, you first need to create a new local Subversion repository:
$ mkdir /tmp/test-svn
$ svnadmin create /tmp/test-svn
Then, enable all users to change revprops – the easy way is to add a pre-revprop-change
script that always exits 0:
$ cat /tmp/test-svn/hooks/pre-revprop-change
#!/bin/sh
exit 0;
$ chmod +x /tmp/test-svn/hooks/pre-revprop-change
You can now sync this project to your local machine by calling svnsync init
with the to and from repositories.
$ svnsync init file:///tmp/test-svn \
http://your-svn-server.example.org/svn/
This sets up the properties to run the sync. You can then clone the code by running
$ svnsync sync file:///tmp/test-svn
Committed revision 1.
Copied properties for revision 1.
Transmitting file data .............................[...]
Committed revision 2.
Copied properties for revision 2.
[…]
Although this operation may take only a few minutes, if you try to copy the original repository to another remote repository instead of a local one, the process will take nearly an hour, even though there are fewer than 100 commits. Subversion has to clone one revision at a time and then push it back into another repository – it’s ridiculously inefficient, but it’s the only easy way to do this.
Now that you have a Subversion repository to which you have write access, you can go through a typical workflow.
You’ll start with the git svn clone
command, which imports an entire Subversion repository into a local Git repository.
Remember that if you’re importing from a real hosted Subversion repository, you should replace the file:///tmp/test-svn
here with the URL of your Subversion repository:
$ git svn clone file:///tmp/test-svn -T trunk -b branches -t tags
Initialized empty Git repository in /private/tmp/progit/test-svn/.git/
r1 = dcbfb5891860124cc2e8cc616cded42624897125 (refs/remotes/origin/trunk)
A m4/acx_pthread.m4
A m4/stl_hash.m4
A java/src/test/java/com/google/protobuf/UnknownFieldSetTest.java
A java/src/test/java/com/google/protobuf/WireFormatTest.java
…
r75 = 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae (refs/remotes/origin/trunk)
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/my-calc-branch, 75
Found branch parent: (refs/remotes/origin/my-calc-branch) 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae
Following parent with do_switch
Successfully followed parent
r76 = 0fb585761df569eaecd8146c71e58d70147460a2 (refs/remotes/origin/my-calc-branch)
Checked out HEAD:
file:///tmp/test-svn/trunk r75
This runs the equivalent of two commands – git svn init
followed by git svn fetch
– on the URL you provide.
This can take a while.
If, for example, the test project has only about 75 commits and the codebase isn’t that big, Git nevertheless must check out each version, one at a time, and commit it individually.
For a project with hundreds or thousands of commits, this can literally take hours or even days to finish.
The -T trunk -b branches -t tags
part tells Git that this Subversion repository follows the basic branching and tagging conventions.
If you name your trunk, branches, or tags differently, you can change these options.
Because this is so common, you can replace this entire part with -s
, which means standard layout and implies all those options.
The following command is equivalent:
$ git svn clone file:///tmp/test-svn -s
At this point, you should have a valid Git repository that has imported your branches and tags:
$ git branch -a
* master
remotes/origin/my-calc-branch
remotes/origin/tags/2.0.2
remotes/origin/tags/release-2.0.1
remotes/origin/tags/release-2.0.2
remotes/origin/tags/release-2.0.2rc1
remotes/origin/trunk
Note how this tool manages Subversion tags as remote refs.
Let’s take a closer look with the Git plumbing command show-ref
:
$ git show-ref
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/heads/master
0fb585761df569eaecd8146c71e58d70147460a2 refs/remotes/origin/my-calc-branch
bfd2d79303166789fc73af4046651a4b35c12f0b refs/remotes/origin/tags/2.0.2
285c2b2e36e467dd4d91c8e3c0c0e1750b3fe8ca refs/remotes/origin/tags/release-2.0.1
cbda99cb45d9abcb9793db1d4f70ae562a969f1e refs/remotes/origin/tags/release-2.0.2
a9f074aa89e826d6f9d30808ce5ae3ffe711feda refs/remotes/origin/tags/release-2.0.2rc1
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/remotes/origin/trunk
Git doesn’t do this when it clones from a Git server; here’s what a repository with tags looks like after a fresh clone:
$ git show-ref
c3dcbe8488c6240392e8a5d7553bbffcb0f94ef0 refs/remotes/origin/master
32ef1d1c7cc8c603ab78416262cc421b80a8c2df refs/remotes/origin/branch-1
75f703a3580a9b81ead89fe1138e6da858c5ba18 refs/remotes/origin/branch-2
23f8588dde934e8f33c263c6d8359b2ae095f863 refs/tags/v0.1.0
7064938bd5e7ef47bfd79a685a62c1e2649e2ce7 refs/tags/v0.2.0
6dcb09b5b57875f334f61aebed695e2e4193db5e refs/tags/v1.0.0
Git fetches the tags directly into refs/tags
, rather than treating them remote branches.
Now that you have a working directory, you can do some work on the project and push your commits back upstream, using Git effectively as an SVN client. If you edit one of the files and commit it, you have a commit that exists in Git locally that doesn’t exist on the Subversion server:
$ git commit -am 'Adding git-svn instructions to the README'
[master 4af61fd] Adding git-svn instructions to the README
1 file changed, 5 insertions(+)
Next, you need to push your change upstream.
Notice how this changes the way you work with Subversion – you can do several commits offline and then push them all at once to the Subversion server.
To push to a Subversion server, you run the git svn dcommit
command:
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M README.txt
Committed r77
M README.txt
r77 = 95e0222ba6399739834380eb10afcd73e0670bc5 (refs/remotes/origin/trunk)
No changes between 4af61fd05045e07598c553167e0f31c84fd6ffe1 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk
This takes all the commits you’ve made on top of the Subversion server code, does a Subversion commit for each, and then rewrites your local Git commit to include a unique identifier.
This is important because it means that all the SHA-1 checksums for your commits change.
Partly for this reason, working with Git-based remote versions of your projects concurrently with a Subversion server isn’t a good idea.
If you look at the last commit, you can see the new git-svn-id
that was added:
$ git log -1
commit 95e0222ba6399739834380eb10afcd73e0670bc5
Author: ben <ben@0b684db3-b064-4277-89d1-21af03df0a68>
Date: Thu Jul 24 03:08:36 2014 +0000
Adding git-svn instructions to the README
git-svn-id: file:///tmp/test-svn/trunk@77 0b684db3-b064-4277-89d1-21af03df0a68
Notice that the SHA-1 checksum that originally started with 4af61fd
when you committed now begins with 95e0222
.
If you want to push to both a Git server and a Subversion server, you have to push (dcommit
) to the Subversion server first, because that action changes your commit data.
If you’re working with other developers, then at some point one of you will push, and then the other one will try to push a change that conflicts.
That change will be rejected until you merge in their work.
In git svn
, it looks like this:
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: d5837c4b461b7c0e018b49d12398769d2bfc240a and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 f414c433af0fd6734428cf9d2a9fd8ba00ada145 c80b6127dd04f5fcda218730ddf3a2da4eb39138 M README.txt
Current branch master is up to date.
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.
To resolve this situation, you can run git svn rebase
, which pulls down any changes on the server that you don’t have yet and rebases any work you have on top of what is on the server:
$ git svn rebase
Committing to file:///tmp/test-svn/trunk ...
ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: eaa029d99f87c5c822c5c29039d19111ff32ef46 and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 65536c6e30d263495c17d781962cfff12422693a b34372b25ccf4945fe5658fa381b075045e7702a M README.txt
First, rewinding head to replay your work on top of it...
Applying: update foo
Using index info to reconstruct a base tree...
M README.txt
Falling back to patching base and 3-way merge...
Auto-merging README.txt
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.
Now, all your work is on top of what is on the Subversion server, so you can successfully dcommit
:
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M README.txt
Committed r85
M README.txt
r85 = 9c29704cc0bbbed7bd58160cfb66cb9191835cd8 (refs/remotes/origin/trunk)
No changes between 5762f56732a958d6cfda681b661d2a239cc53ef5 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk
Note that unlike Git, which requires you to merge upstream work you don’t yet have locally before you can push, git svn
makes you do that only if the changes conflict (much like how Subversion works).
If someone else pushes a change to one file and then you push a change to another file, your dcommit
will work fine:
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M configure.ac
Committed r87
M autogen.sh
r86 = d8450bab8a77228a644b7dc0e95977ffc61adff7 (refs/remotes/origin/trunk)
M configure.ac
r87 = f3653ea40cb4e26b6281cec102e35dcba1fe17c4 (refs/remotes/origin/trunk)
W: a0253d06732169107aa020390d9fefd2b1d92806 and refs/remotes/origin/trunk differ, using rebase:
:100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 e757b59a9439312d80d5d43bb65d4a7d0389ed6d M autogen.sh
First, rewinding head to replay your work on top of it...
This is important to remember, because the outcome is a project state that didn’t exist on either of your computers when you pushed. If the changes are incompatible but don’t conflict, you may get issues that are difficult to diagnose. This is different than using a Git server – in Git, you can fully test the state on your client system before publishing it, whereas in SVN, you can’t ever be certain that the states immediately before commit and after commit are identical.
You should also run this command to pull in changes from the Subversion server, even if you’re not ready to commit yourself.
You can run git svn fetch
to grab the new data, but git svn rebase
does the fetch and then updates your local commits.
$ git svn rebase
M autogen.sh
r88 = c9c5f83c64bd755368784b444bc7a0216cc1e17b (refs/remotes/origin/trunk)
First, rewinding head to replay your work on top of it...
Fast-forwarded master to refs/remotes/origin/trunk.
Running git svn rebase
every once in a while makes sure your code is always up to date.
You need to be sure your working directory is clean when you run this, though.
If you have local changes, you must either stash your work or temporarily commit it before running git svn rebase
– otherwise, the command will stop if it sees that the rebase will result in a merge conflict.
When you’ve become comfortable with a Git workflow, you’ll likely create topic branches, do work on them, and then merge them in.
If you’re pushing to a Subversion server via git svn
, you may want to rebase your work onto a single branch each time instead of merging branches together.
The reason to prefer rebasing is that Subversion has a linear history and doesn’t deal with merges like Git does, so git svn
follows only the first parent when converting the snapshots into Subversion commits.
Suppose your history looks like the following: you created an experiment
branch, did two commits, and then merged them back into master
.
When you dcommit
, you see output like this:
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M CHANGES.txt
Committed r89
M CHANGES.txt
r89 = 89d492c884ea7c834353563d5d913c6adf933981 (refs/remotes/origin/trunk)
M COPYING.txt
M INSTALL.txt
Committed r90
M INSTALL.txt
M COPYING.txt
r90 = cb522197870e61467473391799148f6721bcf9a0 (refs/remotes/origin/trunk)
No changes between 71af502c214ba13123992338569f4669877f55fd and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk
Running dcommit
on a branch with merged history works fine, except that when you look at your Git project history, it hasn’t rewritten either of the commits you made on the experiment
branch – instead, all those changes appear in the SVN version of the single merge commit.
When someone else clones that work, all they see is the merge commit with all the work squashed into it, as though you ran git merge --squash
; they don’t see the commit data about where it came from or when it was committed.
Branching in Subversion isn’t the same as branching in Git; if you can avoid using it much, that’s probably best.
However, you can create and commit to branches in Subversion using git svn
.
To create a new branch in Subversion, you run git svn branch [new-branch]
:
$ git svn branch opera
Copying file:///tmp/test-svn/trunk at r90 to file:///tmp/test-svn/branches/opera...
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/opera, 90
Found branch parent: (refs/remotes/origin/opera) cb522197870e61467473391799148f6721bcf9a0
Following parent with do_switch
Successfully followed parent
r91 = f1b64a3855d3c8dd84ee0ef10fa89d27f1584302 (refs/remotes/origin/opera)
This does the equivalent of the svn copy trunk branches/opera
command in Subversion and operates on the Subversion server.
It’s important to note that it doesn’t check you out into that branch; if you commit at this point, that commit will go to trunk
on the server, not opera
.
Git figures out what branch your dcommits go to by looking for the tip of any of your Subversion branches in your history – you should have only one, and it should be the last one with a git-svn-id
in your current branch history.
If you want to work on more than one branch simultaneously, you can set up local branches to dcommit
to specific Subversion branches by starting them at the imported Subversion commit for that branch.
If you want an opera
branch that you can work on separately, you can run
$ git branch opera remotes/origin/opera
Now, if you want to merge your opera
branch into trunk
(your master
branch), you can do so with a normal git merge
.
But you need to provide a descriptive commit message (via -m
), or the merge will say ``Merge branch opera'' instead of something useful.
Remember that although you’re using git merge
to do this operation, and the merge likely will be much easier than it would be in Subversion (because Git will automatically detect the appropriate merge base for you), this isn’t a normal Git merge commit.
You have to push this data back to a Subversion server that can’t handle a commit that tracks more than one parent; so, after you push it up, it will look like a single commit that squashed in all the work of another branch under a single commit.
After you merge one branch into another, you can’t easily go back and continue working on that branch, as you normally can in Git.
The dcommit
command that you run erases any information that says what branch was merged in, so subsequent merge-base calculations will be wrong – the dcommit
makes your git merge
result look like you ran git merge --squash
.
Unfortunately, there’s no good way to avoid this situation – Subversion can’t store this information, so you’ll always be crippled by its limitations while you’re using it as your server.
To avoid issues, you should delete the local branch (in this case, opera
) after you merge it into trunk.
The git svn
toolset provides a number of commands to help ease the transition to Git by providing some functionality that’s similar to what you had in Subversion.
Here are a few commands that give you what Subversion used to.
If you’re used to Subversion and want to see your history in SVN output style, you can run git svn log
to view your commit history in SVN formatting:
$ git svn log
------------------------------------------------------------------------
r87 | schacon | 2014-05-02 16:07:37 -0700 (Sat, 02 May 2014) | 2 lines
autogen change
------------------------------------------------------------------------
r86 | schacon | 2014-05-02 16:00:21 -0700 (Sat, 02 May 2014) | 2 lines
Merge branch 'experiment'
------------------------------------------------------------------------
r85 | schacon | 2014-05-02 16:00:09 -0700 (Sat, 02 May 2014) | 2 lines
updated the changelog
You should know two important things about git svn log
.
First, it works offline, unlike the real svn log
command, which asks the Subversion server for the data.
Second, it only shows you commits that have been committed up to the Subversion server.
Local Git commits that you haven’t dcommited don’t show up; neither do commits that people have made to the Subversion server in the meantime.
It’s more like the last known state of the commits on the Subversion server.
Much as the git svn log
command simulates the svn log
command offline, you can get the equivalent of svn annotate
by running git svn blame [FILE]
.
The output looks like this:
$ git svn blame README.txt
2 temporal Protocol Buffers - Google's data interchange format
2 temporal Copyright 2008 Google Inc.
2 temporal http://code.google.com/apis/protocolbuffers/
2 temporal
22 temporal C++ Installation - Unix
22 temporal =======================
2 temporal
79 schacon Committing in git-svn.
78 schacon
2 temporal To build and install the C++ Protocol Buffer runtime and the Protocol
2 temporal Buffer compiler (protoc) execute the following:
2 temporal
Again, it doesn’t show commits that you did locally in Git or that have been pushed to Subversion in the meantime.
You can also get the same sort of information that svn info
gives you by running git svn info
:
$ git svn info
Path: .
URL: https://schacon-test.googlecode.com/svn/trunk
Repository Root: https://schacon-test.googlecode.com/svn
Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029
Revision: 87
Node Kind: directory
Schedule: normal
Last Changed Author: schacon
Last Changed Rev: 87
Last Changed Date: 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009)
This is like blame
and log
in that it runs offline and is up to date only as of the last time you communicated with the Subversion server.
If you clone a Subversion repository that has svn:ignore
properties set anywhere, you’ll likely want to set corresponding .gitignore
files so you don’t accidentally commit files that you shouldn’t.
git svn
has two commands to help with this issue.
The first is git svn create-ignore
, which automatically creates corresponding .gitignore
files for you so your next commit can include them.
The second command is git svn show-ignore
, which prints to stdout the lines you need to put in a .gitignore
file so you can redirect the output into your project exclude file:
$ git svn show-ignore > .git/info/exclude
That way, you don’t litter the project with .gitignore
files.
This is a good option if you’re the only Git user on a Subversion team, and your teammates don’t want .gitignore
files in the project.
The git svn
tools are useful if you’re stuck with a Subversion server, or are otherwise in a development environment that necessitates running a Subversion server.
You should consider it crippled Git, however, or you’ll hit issues in translation that may confuse you and your collaborators.
To stay out of trouble, try to follow these guidelines:
-
Keep a linear Git history that doesn’t contain merge commits made by
git merge
. Rebase any work you do outside of your mainline branch back onto it; don’t merge it in. -
Don’t set up and collaborate on a separate Git server. Possibly have one to speed up clones for new developers, but don’t push anything to it that doesn’t have a
git-svn-id
entry. You may even want to add apre-receive
hook that checks each commit message for agit-svn-id
and rejects pushes that contain commits without it.
If you follow those guidelines, working with a Subversion server can be more bearable. However, if it’s possible to move to a real Git server, doing so can gain your team a lot more.