Despite the growth of Git, Subversion (also known as SVN) remains widely used across many projects. Many older open source projects still continue to use Subversion, as do many internal corporate projects.
Thankfully Git provides functionality to allow you to access existing Subversion repositories through the git svn
command. This also provides a good way of learning Git if you still have to use Subversion repositories, and a good migration path for projects that currently use Subversion and wish to use Git.
In this chapter you’ll learn about Git’s Subversion integration by learning the following topics:
-
How to import an existing Subversion repository into a Git repository
-
How to fetch from and push changes to a Subversion repository for Git
-
How to access a GitHub repository using a Subversion client
Let’s start by importing the "Google Search Appliance connector for Lotus Notes" Subversion repository from Google Code. It was selected because it has a small history, but still contains branches and tags so we can see how they’re used.
Note
|
How can I install
git-svn ?git svn is usually installed as part of the default Git installation (and is in all of the official Git installers). If it hasn’t been then when you run git svn you’ll see a message resembling: WARNING: You called a Git command named 'svn', which does not exist. In this case you’ll need to install git-svn separately. This can be done by installing git-svn (or similar) with your package manager; for example, on Debian/Ubuntu run apt-get install git-svn .
|
-
Change to the directory where you want the new
google-notes
repository to be created; for example,cd /Users/mike/
to create the new local repository in/Users/mike/GitInPracticeRedux
. -
Run
git svn clone --prefix=origin/ --stdlayout http://google-enterprise-connector-notes.googlecode.com/svn/ google-notes
. The output should resemble the following:
# git svn clone --prefix=origin/ --stdlayout (1)
http://google-enterprise-connector-notes.googlecode.com/svn/ (2)
google-notes (3)
Initialized empty Git repository in /Users/mike/google-notes/.git/
r1 = ec0d880208029f06e2181ce2241d5f950b77f8c2 (4)
(refs/remotes/origin/trunk)
A domino_stylesheet.xslt (5)
r2 = ad41618c83d7dfd2768c8668bfdce0a63ea6cfca
(refs/remotes/origin/trunk)
A google-access-control-1_0.ntf (5)
...
r381 = 750dd5077c4122bcc3a8a17a65088c0ebd85a2b6 (6)
(refs/remotes/origin/trunk)
Checked out HEAD:
http://google-enterprise-connector-notes.googlecode.com/svn/trunk
r381 (7)
-
clone command
-
SVN repository
-
Directory name
-
First revision
-
Added file
-
Last revision
-
HEAD position
From the Subversion repository clone output:
-
"clone command (1)" shows the
git svn clone
command used to clone the entire Subversion repository. The--prefix
argument is only needed in Git versions before 2.0 (which was released on May 28, 2014) and means that the Subversion remote branches will all be created underorigin/
(like a normal Git remote repository would do). The--stdlayout
flag is used to say that the Subversion repository is in the usual format—has thebranches
,tags
, andtrunk
as the subfolders of the root of the repository and they should all be imported. -
"SVN repository (2)" shows the HTTP URL of the Subversion repository that we’ve cloned.
-
"Directory name (3)" shows the name of the local directory which will contain the new Git repository created from the Subversion repository.
-
"First revision (4)" shows the first commit/revision (
r1
) of the Subversion repository being mapped and imported as a new commit/SHA-1 (ec0d88…
) into the local Git repository. -
"Added file (5)" shows a new file added in the first imported commit.
-
"Last revision (6)" shows the last commit/revision (
r381
) of the Subversion repository being mapped and imported as a new commit/SHA-1 (750dd5…
) into the local Git repository. -
"HEAD position (7)" shows that the current
HEAD
pointer position is pointing to the tip of thetrunk
remote branch (which is Subversion’sr381
).
Please note that if you’re using an older version of git
you may see a warning message resembling the following:
Using higher level of URL:
http://google-...code.com/svn/google-notes =>
http://google-...code.com/svn
W: Ignoring error from SVN, path probably does not exist:
(160013): Filesystem has no item:
Could not resolve path /google-notes for history
W: Do not be alarmed at the above message git-svn is just searching
aggressively for old history.
This may take a while on large repositories
As it states in the output: don’t be alarmed by this. This is just git svn
trying to access a higher-level directory than we’ve specified and being denied access. It’s not a problem; this is why the message was removed from later versions.
You have successfully imported a Subversion repository into a local Git repository.
Recall from 02-RemoteGit.adoc that we use git clone
to initially download an entire repository. Subversion’s usual equivalent to git clone
is svn checkout
, which only checks out the most recent revision. As we’re importing the Subversion repository into Git, we need to download all the previous revisions and all the branches and tags. Each new Git commit has metadata stored in the commit message containing the mapping between Git and Subversion commits.
You can see from the listing that git svn
is iterating through all the revisions in the Subversion repository and creating Git commits locally from the changes. This may take a long time on large repositories, but will only need to be done once. After git svn clone
has finished, you now have a complete local copy of a Subversion repository.
Let’s see an example of how git-svn
formats a commit message:
# git show -s --format=%B master
Log server and database in Lotus Notes errors (b/13059110)
Changes:
...
git svn-id:
http://google-enterprise-connector-notes.googlecode.com/svn/trunk@381 (1)
43735464-b96a-11de-8be4-e1425cda3908 (2)
-
SVN URL
-
UUID
From the Subversion commit in Git repository output:
-
"SVN URL (1)" shows the full URL for this Subversion commit, including the Subversion branch (
trunk
) and revision (@381
). Thetrunk
is automatically mapped to themaster
Git branch. -
"UUID (2)" shows a unique
git svn
identifier for this Subversion repository. This is used to ensure that the repository at the URL remains the same and is not replaced with another, which could cause errors when you tried to update.
You may have also noticed that the clone output sometimes mentioned branches. Here’s a sample that was cut from Subversion repository clone output:
Found merge parent (svn:mergeinfo prop): (1)
ae01454731b1603701c59b78c3a2a2801eb4115f (2)
r378 = 677696fd7befaa4212e760d62ab281780469ea00
(refs/remotes/origin/3.2.x) (3)
M projects/version.properties
r379 = 818430013a86360963676c8ff979cf59b64121ef
(refs/remotes/origin/3.2.x)
Found possible branch point:
http://google-enterprise-...googlecode.com/svn/branches/3.2.x => (4)
http://google-enterprise-...googlecode.com/svn/tags/3.2.4, 379 (5)
Found branch parent: (refs/remotes/origin/tags/3.2.4) (6)
818430013a86360963676c8ff979cf59b64121ef (7)
Following parent with do_switch
Successfully followed parent
-
Merge found
-
Branch parent
-
Commit branch
-
Branch URL
-
Tag URL
-
Tag found
-
Tag parent
From the clone branch detection output:
-
"Merge found (1)" shows that
git svn
found one of the parent commits of a merge by looking at thesvn:mergeinfo
Subversion property on the commit. -
"Branch parent (2)" shows the SHA-1 of the found parent commit.
-
"Commit branch (3)" shows that the found parent commit is for the
3.2.x
branch. -
"Branch URL (4)" shows the (abbreviated) URL for the branch that was used to create the tag commit.
-
"Tag URL (5)" shows the (abbreviated) URL and revision number for the tag commit.
-
"Tag found (6)" shows the parent commit that was found for the
3.2.4
tag commit. -
"Tag parent (7)" shows the SHA-1 of the found tag commit.
Let’s examine the structure of the Subversion repository by running git branch --remote
to view all the Git remote branches created by git svn
:
# cd google-notes/
# git branch --remote
origin/2.6.x
...
origin/3.2.x (1)
origin/Notes-Connector
origin/dev (2)
origin/tags/1.0.0
origin/tags/2.8.4 (3)
origin/tags/2.8.4@273 (4)
...
origin/tags/3.2.4
origin/tags/builds
origin/trunk
-
3.2 branch
-
Work branch
-
Branch tag
-
Duplicate tag
From the remote branch output:
-
"Minor branch (1)" shows the stable 3.2 release branch named
3.2.x
. This will be used to create more patch tags in the 3.2 series; for example,3.2.4
. -
"Work branch (2)" shows a named branch used for development work named
dev
. -
"Branch tag (3)" shows the branch for the
2.8.4
tag. Note that this has been imported as a branch and not a native Git tag. This will be explained later. -
"Duplicate tag (4)" shows the duplicate
2.8.4
tag named2.8.4@273
. This is because it was revision273
and the other2.8.4
is at revision274
.
Note
|
Why are there tags in the branch output?
You may have noticed that tags from git svn aren’t the same as normal Git tags but instead are just branches with a tags/ prefix. This is because in Subversion, the only difference between a tag and a branch is that of principle. Generally you don’t update tags in Subversion but it’s possible and has happened in this repository. The reason there is a duplicated 2.8.4 tag (named 2.8.4@273 ) is because there a commit was made to create the 2.8.4 tag and another commit was made on it. This wouldn’t really be possible in Git; you’d need to use git tag --force to forcefully update the tag and then the previous tag would be lost. This is the reason why git svn doesn’t import the Subversion tags as native Git tags. If you wanted to create native Git tags from these anyway, you could use the git branch --remote --list 'origin/tags/*' to only show Subversion tags and then create Git tags manually. For example, to create the 3.2.4 tag, you could run git tag 3.2.4 origin/tags/3.2.4 .
|
Recall from 03-FilesystemInteractions.adoc that .gitignore
files contain a list of patterns of paths for Git to ignore in a repository.
Subversion uses the svn:ignore
property on directories instead. These aren’t imported by git svn
into a .gitignore
file automatically. This is because doing so would require adding a file to the repository.
You can export the svn:ignore
property values to a .gitignore
file by using the git svn show-ignore
command:
# git svn show-ignore
# /projects/ (1)
/projects/build (2)
/projects/install
/projects/downloads
# /projects/notes-client/
...
-
Directory comment
-
Directory ignore
From the Subversion ignore rules output:
-
"Directory comment (1)" shows a
.gitignore
comment line (comments are prefixed with#
) for theprojects
directory’ssvn:ignore
property value. -
"Directory ignore (2)" shows an ignore rule for the
projects
directory to ignore a file or directory namedbuild
.
You can use the git svn show-ignore
output to write a .gitignore
file by running git svn show-ignore > .gitignore
. The >
redirects the output from the command from the terminal into the .gitignore
file. You can then add and commit this file to the repository to share these rules with anyone else using git svn
.
In some cases you may not want people to know you’re using git svn
, or not want to commit a .gitignore
file to a Subversion repository. In this case you could just omit the .gitignore
file or not add it to any commits, but this could get irritating when files aren’t ignored. Instead you can make use of the .git/info/exclude
we saw in 01-LocalGit.adoc, which operates like a local .gitignore
file for a single repository. This file handily also uses the same syntax as .gitignore
. You can write the ignore rules to it by running git svn show-ignore > .git/info/exclude
.
To update a Subversion repository, you need to use the git svn
command; you can’t use git fetch
or git pull
because git svn
hasn’t set up any remote Git repository references for you, as it doesn’t use the same transport mechanism. When working ,locally you use git
commands as normal.
Git SVN add/commit/dcommit/rebase/checkout cycle shows the git svn
cycle we’ll look at in this section. As in the local workflow in 01-LocalGit.adoc, files are modified, added, committed, and can be checked out. But in comparison to 02-RemoteGit.adoc, the remote repository is a Subversion repository so it requires different commands.
The equivalents to git fetch
and git pull --rebase
for Subversion repositories are git svn fetch
and git svn rebase
. There’s no equivalent to git pull
without --rebase
. This is because Git and Subversion handle merges differently, so it’s important to avoid merge commits on updates, as they won’t (and shouldn’t) be seen by other users of the Subversion repository.
If you run git svn rebase
on the master
branch and there are no new commits the output will be:
# git svn rebase
Current branch master is up to date.
If there was a single new revision (r2
) the output might resemble:
# git svn rebase
M README.txt (1)
r2 = 685b522aebec94dc75d725c34c092d9be5f3fc39 (remotes/origin/trunk) (2)
First, rewinding head to replay your work on top of it... (3)
Fast-forwarded master to remotes/origin/trunk. (4)
-
Modified file
-
New revision
-
Rebase begin
-
Fast-forward
From the one new Subversion revision output:
-
"Modified file (1)" shows that a file named
README.txt
was modified in the new revision. -
"New revision (2)" shows the new revision number (
r2
) and the new commit SHA-1 (685b52…
). -
"Rebase begin (3)" shows the beginning of the
git rebase
operation thatgit svn rebase
is running to rebase any commits made on this branch on top of the newly received commits. -
"Fast-forward (4)" shows that this
git rebase
was a fast-forward of theHEAD
pointer to the latest new commit, as there were no local commits that needed to be rebased.
Let’s look at the metadata of a commit imported from a Subversion repository:
# git show -s --format=short master
commit 750dd5077c4122bcc3a8a17a65088c0ebd85a2b6
Author: [email protected] (1)
<[email protected]@43735464-b96a-11de-8be4-e1425cda3908> (2)
Log server and database in Lotus Notes errors (b/13059110)
-
Author name
-
Author email
From the Subversion commit metadata in Git repository output:
-
"Author name (1)" shows an email address instead of the author name. This is the username of the user in the Subversion repository (which happens to be an email address in this case).
-
"Author email (2)" shows the author email address. In
git-svn
these are created from the username by appending the username with@
followed by the UUID for the Subversion repository.
It’s possible to use a Subversion authors mapping file by passing the --author-file
(or -A
) flag to git svn clone
when you first clone a Subversion repository.
The authors file has the following syntax:
mikemcquaid = Mike McQuaid <[email protected]>
If passed a valid file with this format, when git svn
reads a new revision, it looks up the username in this file. If the username is mikemcquaid
it will replace the author (or committer) name and email address with those specified in the file. If it can’t find an entry in the file, it will stop the clone (or rebase) and you need to add the new author’s details to the file.
As git svn
creates a Git repository from a Subversion repository, you can still use all the graphical tools you’re used to.
Additionally, GitX provides an additional column to display the Subversion revision number:
The Subversion revision number is shown in the Git SVN Revision
column in GitX on import Subversion repository.
With what you’ve already learned this section (cloning a Subversion repository, creating real Git tags, mapping authors) you can create a Git repository that contains all the information from a Subversion repository in the typical Git format.
This may be useful if you want to migrate a project from Subversion to Git; you can import the entire history, migrate the tags, and git push
it to a new repository. Even if you want to remove all references to the original Subversion repository, you could even use git filter-branch
(introduced in 06-RewritingHistoryAndDisasterRecovery.adoc) to remove all the git-svn
Subversion references from commit messages or otherwise reformat them.
Remember that svn commit
actually does the equivalent of a git commit
and a git push
to the remote server. As the repository created by git svn
is a normal Git repository, you can change files and commit as you might do with any other Git repository. The only differences are when you want to push your changes to the Subversion repository, and if you want to interact with remote branches.
To push all the unpushed commits on the current branch to a Subversion repository you use the git svn dcommit
command.
-
Change directory to a Git SVN repository; on my system,
cd /Users/mike/GitSVN/
. -
Make some changes to a file such as
README.txt
file and commit them:git commit --message "README.txt: improve grammar." README.txt
. -
Run
git svn dcommit
.
The output from these commands should resemble:
# git commit --message "README.txt: improve grammar." README.txt
[master bcd0a70] README.txt: improve grammar. (1)
1 file changed, 1 insertion(+), 1 deletion(-)
# git svn dcommit
Committing to http://svntest.com/svntest/ ...
M README.txt
Committed r3 (2)
M README.txt
r3 = da4cc700b6d5fe07ead532a34195b438680e7a71 (remotes/origin/trunk) (3)
No changes between bcd0a70923a9b53cd98ccaeee1567ca95bb579c0 and (4)
remotes/origin/trunk
Resetting to the latest remotes/origin/trunk (5)
-
New commit
-
Push success
-
New revision
-
Commit diff
-
Trunk reset
From the Subversion push output:
-
"New commit (1)" shows the commit subject and SHA-1 of the new commit.
-
"Push success (2)" shows that the new Subversion revision was committed successfully.
-
"New revision (3)" shows the new commit that was created from the new Subversion revision. Recall that commits all contain their revision numbers and repository UUIDs, which requires rewriting the commit message. Also recall that rewriting the commit message changes the SHA-1 of a commit. As a result, this new commit SHA-1 doesn’t match the SHA-1 in "new commit (1)," although the actual changes are the same.
-
"Commit diff (4)" shows
git svn
checking that there are no differences between the commit that was just created and the commit the Subversion repository returned. -
"trunk reset (5)" shows that the
HEAD
andmaster
branch pointers are being updated to the new commit. The old commit is still accessible from before it was rewritten and the new commit was created with the Subversion metadata.
You have successfully committed and pushed changes to a Subversion repository.
You can see that git svn dcommit
also has to do some rewriting of commits, similar to git svn rebase
. This is because the commit messages store additional metadata that can only be obtained from the Subversion server. The Subversion server may have had additional commits in the meantime, which means the revision number may differ from the last one that was seen. If this has happened, a rebase may need to be done by git svn dcommit
after receiving the new commit from the server.
Subversion doesn’t have the concept of local branches or tags. If a branch or tag needs to be created in Subversion then the Subversion client has to speak to the server.
As we have a local Git repository containing the contents of the Subversion repository, we’re not bound by the same constraints. We can create local branches and tags and use them as we wish, and everything is fairly simple unless you want to send or receive commits from the Subversion server.
Recall that both git svn rebase
and git svn dcommit
perform rebasing operations on updates. As a result, it becomes difficult to correctly handle merges between Subversion branches with git svn
. You can read how to do this in git svn --help
using the --mergeinfo
flag but I won’t be covering it in this book.
Note
|
How should I collaborate when using Git SVN?
What I’d advise is that you use local branches only for your own work, and not for collaboration with others. When you’re finished with a local branch and wish to merge it, you should rebase the contents into the branch you wish to include it in. This means that others won’t see your merge commits but you can still use the cheap local branches and history rewriting in Git. If you want to interact with Subversion remote branches or tags, you should instead use the git svn branch and git svn tag commands. These are copies of Subversion’s svn branch and svn tag commands and take the same parameters and use the same syntax.
|
So far this chapter has been concerned with accessing Subversion repositories using Git. This assumes a development team that is mostly using Subversion and a few users or single user using Git. Incidentally, this is how I learned Git originally; I worked on Subversion projects but used Git locally.
What if the situation were reversed and the majority of people on the project wanted to use Git and a minority wanted to use Subversion? This is made better if you host your Git repository on GitHub, as GitHub provides a Subversion interface for every Git repository.
Let’s try checking out the GitInPracticeRedux
repository from earlier chapters using svn checkout
.
You wish to check out the GitInPracticeRedux
repository from earlier chapters using Subversion.
-
Change to the directory where you want the new
GitInPracticeReduxSVN
repository to be created; for examplecd /Users/mike/
to create the new local repository in/Users/mike/GitInPracticeReduxSVN
. -
Run
svn co https://github.com/MikeMcQuaid/GitInPracticeRedux GitInPracticeReduxSVN
. The output should resemble the following:
# svn co https://github.com/MikeMcQuaid/GitInPracticeRedux
A GitInPracticeRedux/branches
A GitInPracticeRedux/branches/inspiration
A GitInPracticeRedux/branches/inspiration/.gitignore
A GitInPracticeRedux/branches/inspiration/00-Preface.asciidoc (1)
...
A GitInPracticeRedux/branches/v0.1-release/00-Preface.asciidoc (2)
...
A GitInPracticeRedux/tags/v0.1/00-Preface.asciidoc (3)
...
A GitInPracticeRedux/trunk/00-Preface.asciidoc (4)
A GitInPracticeRedux/trunk/01-IntroducingGitInPractice.asciidoc
A GitInPracticeRedux/trunk/02-AdvancedGitInPractice.asciidoc
Checked out revision 26. (5)
-
Inspiration branch
-
V0.1-release branch
-
V0.1 tag
-
Trunk
-
Latest revision
From the checkout GitHub repository with Subversion partial output:
-
"Inspiration branch (1)" shows the
00-Preface.asciidoc
file in theinspiration
branch. -
"V0.1-release branch (2)" shows the
00-Preface.asciidoc
file in thev0.1-release
branch. -
"C0.1 tag (3)" shows the
00-Preface.asciidoc
file in thev0.1
tag. -
"Trunk (4)" shows the
00-Preface.asciidoc
file intrunk
(which is actually the renamedmaster
branch). -
"Latest revision (5)" shows the latest revision number for the repository
(r26)
.
You have checked out the GitInPracticeRedux
repository using Subversion.
As you can see, the Git repository has been transformed into the traditional Subversion layout with trunk
, branches
, and tags
folders in the root. Typically you’d use svn co https://github.com/MikeMcQuaid/GitInPracticeRedux/trunk
instead and switch to the current branch of choice using svn switch
.
You can svn commit
, svn branch
, and use any other Subversion commands with this repository and they’re mapped on the GitHub servers into the corresponding Git commands.
You can read more about the GitHub Subversion integration at https://help.github.com/articles/support-for-subversion-clients; the current implementation-specific details are beyond the scope of this book and not necessary for typical use.
If you’re already using or considering GitHub, I’d strongly recommend using the GitHub repository through Subversion rather than a Subversion repository through git-svn
. This is because Subversion’s functionality is effectively a subset of Git’s functionality, so using GitHub’s Subversion support won’t limit Git users as much (if at all) compared to using Git users using git svn
. For example, you can happily merge branches using Git and push them when using GitHub’s Subversion integration, whereas when using git-svn
then, as mentioned in Commit and push to an SVN repository from a Git repository, you should do branch merges using Subversion’s tools instead.
If you’re not using GitHub, there are tools such as SubGit (http://subgit.com) that are beyond the scope of this book but may enable you to work in teams with some users using Git and others using Subversion.