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
.
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.gitInitialized 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:
Finally, push to the Git remote origin:
$ git push origin masterCounting 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
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
First, check that master
/trunk
is up to date.
$ git pull origin masterFrom 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 dcommitCommitting 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
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
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
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
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
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
.
[…] 本文参考了这篇文章:Using Git and Subversion Together。 […]