├── .gitignore ├── VERSION.yml ├── lib ├── svn2git.rb └── svn2git │ └── migration.rb ├── test ├── commands_test.rb ├── test_helper.rb └── escape_quotes_test.rb ├── Rakefile ├── MIT-LICENSE ├── bin └── svn2git ├── svn2git.gemspec ├── ChangeLog.markdown └── README.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | .idea 3 | .ruby-version 4 | -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :major: 2 3 | :minor: 4 4 | :patch: 0 5 | :build: 6 | -------------------------------------------------------------------------------- /lib/svn2git.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/svn2git/migration' 2 | 3 | -------------------------------------------------------------------------------- /test/commands_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(__FILE__, '..', 'test_helper')) 2 | 3 | class CommandsTest < Minitest::Test 4 | def test_checkout_svn_branch 5 | actual = Svn2Git::Migration.checkout_svn_branch('blah') 6 | 7 | assert_equal 'git checkout -b "blah" "remotes/svn/blah"', actual 8 | end 9 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift "#{File.dirname(__FILE__)}/../lib" 2 | 3 | require 'rubygems' 4 | require 'svn2git' 5 | require 'minitest/autorun' 6 | 7 | if Minitest.const_defined?('Test') 8 | # We're on Minitest 5+. Nothing to do here. 9 | else 10 | # Minitest 4 doesn't have Minitest::Test yet. 11 | Minitest::Test = MiniTest::Unit::TestCase 12 | end -------------------------------------------------------------------------------- /test/escape_quotes_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(__FILE__, '..', 'test_helper')) 2 | 3 | class EscapeQuotesTest < Minitest::Test 4 | def test_identity 5 | expected = 'A string without any need to escape.' 6 | actual = Svn2Git::Migration.escape_quotes(expected) 7 | 8 | assert_equal expected, actual 9 | end 10 | 11 | def test_escape_single_quotes 12 | actual = Svn2Git::Migration.escape_quotes("Here's a message with 'single quotes.'") 13 | 14 | assert_equal "Here\\'s a message with \\'single quotes.\\'", actual 15 | end 16 | 17 | def test_escape_double_quotes 18 | actual = Svn2Git::Migration.escape_quotes('Here is a message with "double quotes."') 19 | 20 | assert_equal 'Here is a message with \\"double quotes.\\"', actual 21 | end 22 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rubygems/package_task' 4 | 5 | begin 6 | require 'jeweler' 7 | Jeweler::Tasks.new do |spec| 8 | spec.name = "svn2git" 9 | spec.summary = "A tool for migrating svn projects to git" 10 | spec.authors = ["James Coglan", "Kevin Menard"] 11 | spec.homepage = "https://github.com/nirvdrum/svn2git" 12 | spec.email = "nirvdrum@gmail.com" 13 | spec.license = 'MIT' 14 | spec.add_development_dependency 'minitest' 15 | end 16 | Jeweler::GemcutterTasks.new 17 | 18 | rescue LoadError 19 | puts "Jeweler not available. Install it with: gem install jeweler" 20 | end 21 | 22 | desc 'Test the rubber plugin.' 23 | Rake::TestTask.new(:test) do |t| 24 | t.libs << 'lib' 25 | t.libs << 'test' 26 | t.pattern = 'test/**/*_test.rb' 27 | t.verbose = true 28 | end 29 | 30 | desc 'Default: run unit tests.' 31 | task :default => :test -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 James Coglan, Kevin Menard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /bin/svn2git: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright (c) 2008 James Coglan 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'rubygems' 24 | require 'svn2git' 25 | 26 | migration = Svn2Git::Migration.new(ARGV) 27 | migration.run! 28 | -------------------------------------------------------------------------------- /svn2git.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | # stub: svn2git 2.4.0 ruby lib 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "svn2git" 9 | s.version = "2.4.0" 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 12 | s.require_paths = ["lib"] 13 | s.authors = ["James Coglan", "Kevin Menard"] 14 | s.date = "2016-10-30" 15 | s.email = "nirvdrum@gmail.com" 16 | s.executables = ["svn2git"] 17 | s.extra_rdoc_files = [ 18 | "ChangeLog.markdown", 19 | "README.markdown" 20 | ] 21 | s.files = [ 22 | "ChangeLog.markdown", 23 | "MIT-LICENSE", 24 | "README.markdown", 25 | "Rakefile", 26 | "VERSION.yml", 27 | "bin/svn2git", 28 | "lib/svn2git.rb", 29 | "lib/svn2git/migration.rb", 30 | "svn2git.gemspec", 31 | "test/commands_test.rb", 32 | "test/escape_quotes_test.rb", 33 | "test/test_helper.rb" 34 | ] 35 | s.homepage = "https://github.com/nirvdrum/svn2git" 36 | s.licenses = ["MIT"] 37 | s.rubygems_version = "2.5.1" 38 | s.summary = "A tool for migrating svn projects to git" 39 | 40 | if s.respond_to? :specification_version then 41 | s.specification_version = 4 42 | 43 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 44 | s.add_development_dependency(%q, [">= 0"]) 45 | else 46 | s.add_dependency(%q, [">= 0"]) 47 | end 48 | else 49 | s.add_dependency(%q, [">= 0"]) 50 | end 51 | end 52 | 53 | -------------------------------------------------------------------------------- /ChangeLog.markdown: -------------------------------------------------------------------------------- 1 | # 2.4.0 - 2016-10-30 2 | 3 | This release introduces the ability to supply a password for SVN repositories that can't authenticate by other means. 4 | It also adds the ability to specify the '--branches' and '--tags' arguments multiple times to better support those with 5 | more complicated SVN repository layouts. 6 | 7 | * Added support for the '--password' option for authentication (thanks edpbx). 8 | * Added the ability to specify the '--branches' and '--tags' arguments multiple times (thanks pdf). 9 | * Fixed a problem with processing of the '--exclude' argument (improper quoting internally) (thanks pdf). 10 | 11 | # 2.3.2 - 2014-06-08 12 | 13 | This is a bugfix release. It fixes issues running with Windows using MRI ruby and fixes a problem with Ruby 1.8.7. 14 | 15 | * Removed open4 dependency. svn2git no longer has any runtime dependencies and things work well on Windows again. 16 | * Fixed an issue with Ruby 1.8.7, which doesn't implicitly require the 'thread' library meaning classes that library weren't in scope. 17 | 18 | 19 | # 2.3.1 - 2014-05-14 20 | 21 | This is a critical bugfix release if you're running git >= 1.8.3.2. In the days of svn2git 1.x we supported syncing 22 | local branches with upstream by tracking the branch as we set them up. This allowed one to checkout the branch and 23 | issue a "git pull" to fetch the changes. git-svn ceased allowing this in 1.8.3.2, which broke svn2git with that 24 | version of git and all subsequent versions. The rationale seemed to be in order to prevent pushing changes from 25 | git-svn back up and breaking the remote link, but this was never something svn2git supported anyway. 26 | 27 | Acknowledging the new reality of upstream, the old behavior is retained but deprecated for users of git < 1.8.3.2. 28 | We'll be removing the establishment of remote tracking SVN branches in the 2.5.0 release. If you wish to sync back 29 | with upstream, run `svn2git --rebase`. If you're on git >= 1.8.3.2 your only option for resynchronizing is to 30 | use `svn2git --rebase`. 31 | 32 | Many thanks to ktdreyer for modernizing the test suite and Daniel Ruf (DanielRuf) for pushing on the git compatibility 33 | issue. 34 | 35 | * Fixed creating local branches for remote SVN branches in git >= 1.8.3.2. 36 | * Fixed verbose logging of sub-process STDERR stream. 37 | * Added MIT license metadata to gemspec. 38 | * Switched to minitest to get tests working on Ruby 1.9+ with minitest 5+ installed. 39 | 40 | 41 | # 2.3.0 - 2014-05-14 42 | 43 | This release passes STDIN through to the underlying git-svn process, allowing users to interact with the 44 | git-svn prompt. Principally, it will allow users to choose what to do when prompted about unverified 45 | SSL certificates. 46 | 47 | * Pass STDIN through to the underlying git-svn process so users can respond to prompts. 48 | 49 | # 2.2.5 - 2014-03-09 50 | 51 | This is a bugfix release. It improves handling of quotes in SVN commit messages. 52 | 53 | 54 | * Fixed an with single quote escaping (thanks aucl). 55 | * Escape double quotes (e.g., if they appear in a commit message) before passing to the shell (thanks aucl). 56 | 57 | # 2.2.4 - 2014-03-08 58 | 59 | There was a permissions problem with some of the files packed in 2.2.3. This was caught immediately after the gem 60 | was pushed, so it was yanked as it simply wouldn't work for anyone. 2.2.4 contains everything 2.2.3 did, but with 61 | proper packaging. 62 | 63 | # 2.2.3 - 2014-03-08 64 | 65 | This is a bugfix release. First change done by FeeJai. 66 | 67 | * Fixed an issue with password protected svn-repositories. The prompt to enter the password is now displayed. 68 | * Fixed an issue with server certificates. If the certificate is untrusted, the prompt to confirm or deny the certificate is now shown. 69 | * Fixed an issue with using the `--local` flag for `git config` in git versions < 1.7.4. 70 | 71 | 72 | # 2.2.2 - 2012-10-07 73 | 74 | This is an overdue bugfix release. No new features were added, but several long-standing bugs fixed by the community 75 | have been merged. Many thanks to Edson de Lima (edsonlima), Rudger (Rud5G), Ben Wolfe (bwolfe), CyberTech, PowerKiKi, and Philipp Riemer (ruderphilipp) for the pull requests. 76 | 77 | * Fixed an issue working with repositories that contained a space in the name (thanks edsonlima). 78 | * Fixed an issue working with tags that contain a hyphen (thanks Rud5G). 79 | * Fixed an issue with fixing tags during a rebase (thanks PowerKiKi). 80 | * Double-quote git-svn commands working with tags to avoid issues with special strings (thanks CyberTech). 81 | * Improved the documentation example of fetching the author list for an SVN repository (thanks bwolfe). 82 | * Set the git committer date for tags in a more cross-platform manner (thanks CyberTech). 83 | * Improved documentation formatting (thanks ruderphilipp). 84 | 85 | # 2.2.1 - 2012-02-25 86 | 87 | This is a critical bugfix release if your repository has tags. Thanks to David Zülke (dzuelke) for the patches making up this release. 88 | 89 | * Added the ability to specify an end revision for migration (thanks dzuelke). 90 | * Fixed an issue with initial conversion if the repo had tags (thanks dzuelke). 91 | 92 | # 2.2.0 - 2012-01-25 93 | 94 | Thanks to Craig Hobbs (craigahobbs) and Simon Chiang (thinkerbot) for the patches making up this release. 95 | It rounds out our tag support by handling tags with special characters and preserving original tag author info. 96 | 97 | * Fixed an issue with not quoting tag names (thanks craigahobbs and thinkerbot) 98 | * Fixed an issue whereby the person running the svn2git conversion became the author of every tag (i.e., we lost the 99 | original tag committer info) (thanks thinkerbot) 100 | 101 | # 2.1.2 - 2011-12-28 102 | 103 | * Fixed a regression in improperly quoting branch names (thanks ziangsong). 104 | 105 | # 2.1.1 - 2011-12-27 106 | 107 | * Fixed SVN branch detection (thanks thinkerbot). 108 | * Stop processing when a git subprocess fails (thanks thinkerbot). 109 | * Fixed an issue with SVN branches containing shell special characters (thanks sleicht). 110 | 111 | # 2.1.0 - 2011-04-03 112 | 113 | Thanks to Francois Rey (fmjrey), Sven Axelsson (svenax), and Julian Taylor (juliantaylor) for submitting all the patches 114 | that comprise this release. svn2git now works with a much wider array SVN repositories because of their efforts. 115 | 116 | * Added --no-minimize-url option for migrating specific subprojects from an SVN repo containing several projects (thanks fmjrey). 117 | * Added --username option for migrating password-protected repositories (thanks svenax). 118 | * Added --revision option for specifying the revision to start importing from (thanks svenax). 119 | * Fixed compatibility with older versions of git (thanks juliantaylor). 120 | 121 | # 2.0.0 - 2010-05-29 122 | 123 | This release adds the oft requested incremental SVN update support. If you run svn2git with the `--rebase` option on an existing 124 | repository that you've converted with svn2git, it will fetch new branches & tags from SVN and update existing ones. There are 125 | two important things to note: 126 | 127 | * This will not work on already converted repositories because the tracking information isn't set up correctly. You could do that 128 | yourself, but it's probably a lot easier to do the conversion over. 129 | * svn2git now maintains remote tracking information. If this is a problem for you because you don't want any links to the SVN server 130 | you can either stick with a 1.x release of svn2git or simply clone the repo created with svn2git, which will lose the tracking information. 131 | 132 | A great deal of thanks to Nathaniel McCallum (npmccallum) for coming up with an elegant solution and then providing the patch for this release. 133 | 134 | # 1.3.3 - 2010-03-31 135 | 136 | Thanks to Jeff Ramnani (jramnani) for finding a problem with with the --excludes tag and providing a patch. 137 | 138 | * Fix error when using '--exclude' option. 139 | 140 | # 1.3.2 - 2010-03-12 141 | 142 | Thanks to Rajit Singh (rajit) for finding a problem with quoting in tag comments that were causing issues with svn2git's internal 143 | quoting and providing a patch. 144 | 145 | * Deal cleanly with any single quotes found in tag comments so that the 'git tag' commands run correctly. 146 | 147 | # 1.3.1 - 2009-06-09 148 | 149 | Thanks to KUBO Atsuhiro (iteman) for finding a problem with the tagging process and providing a patch. 150 | 151 | * Fixed a problem with creating actual git tags when the SVN tags path was named anything other than 'tags.' 152 | 153 | # 1.3.0 - 2009-06-09 154 | 155 | Many thanks to Malte S. Stretz (mss) for the patches making up most of this release. 156 | 157 | * Fixed a problem where tags didn't get the original date and time. 158 | * New switch --exclude which can be used to specify a PCRE pattern to exclude paths from the import. 159 | * New switches --no{trunk,branches,tags} to skip import of those. 160 | * Improved docs. 161 | 162 | # 1.2.4 - 2009-05-04 163 | 164 | * No changes. I ran the jeweler command twice inadvertently. Tearing down the release would be more harmful than helpful. 165 | 166 | # 1.2.3 - 2009-05-04 167 | 168 | * Yanked out the code referencing the gem by name. This shouldn't be necessary at all. 169 | 170 | # 1.2.2 - 2009-05-04 171 | 172 | * Updated the reference gem in the binary to use this one and not the one on RubyForge. 173 | 174 | # 1.2.1 - 2009-04-19 175 | 176 | * Fixed a problem with the svn2git binary not loading command-line args properly. 177 | 178 | # 1.2.0 - 2009-04-17 179 | 180 | * Reworked command-line options so they work similarly to every other app in the world. 181 | * Better error messaging when no URL provided. 182 | * Improved docs. 183 | 184 | # 1.1.1 - 2009-04-15 185 | 186 | * Started using Jeweler for gem management. 187 | * Fixed issue with not loading up RubyGems appropriately. 188 | 189 | # 1.1.0 - 2009-01-02 190 | 191 | * First release since nirvdrum fork. 192 | 193 | * Fixed issues with handling of tags and branches. 194 | * Added better logging of output from git-svn. 195 | * Wrap external command processing to capture failures. 196 | 197 | # 1.0.0 - 2008-07-19 198 | 199 | * Forked version from jcoglan. 200 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | svn2git 2 | ======= 3 | 4 | _svn2git_ is a tiny utility for migrating projects from Subversion to Git 5 | while keeping the trunk, branches and tags where they should be. It uses 6 | git-svn to clone an svn repository and does some clean-up to make sure 7 | branches and tags are imported in a meaningful way, and that the code checked 8 | into master ends up being what's currently in your svn trunk rather than 9 | whichever svn branch your last commit was in. 10 | 11 | Examples 12 | -------- 13 | 14 | Say I have this code in svn: 15 | 16 | trunk 17 | ... 18 | branches 19 | 1.x 20 | 2.x 21 | tags 22 | 1.0.0 23 | 1.0.1 24 | 1.0.2 25 | 1.1.0 26 | 2.0.0 27 | 28 | git-svn will go through the commit history to build a new git repo. It will 29 | import all branches and tags as remote svn branches, whereas what you really 30 | want is git-native local branches and git tag objects. So after importing this 31 | project I'll get: 32 | 33 | $ git branch 34 | * master 35 | $ git branch -a 36 | * master 37 | 1.x 38 | 2.x 39 | tags/1.0.0 40 | tags/1.0.1 41 | tags/1.0.2 42 | tags/1.1.0 43 | tags/2.0.0 44 | trunk 45 | $ git tag -l 46 | [ empty ] 47 | 48 | After svn2git is done with your project, you'll get this instead: 49 | 50 | $ git branch 51 | * master 52 | 1.x 53 | 2.x 54 | $ git tag -l 55 | 1.0.0 56 | 1.0.1 57 | 1.0.2 58 | 1.1.0 59 | 2.0.0 60 | 61 | Finally, it makes sure the HEAD of master is the same as the current trunk of 62 | the svn repo. 63 | 64 | Installation 65 | ------------ 66 | 67 | Make sure you have git, git-svn, and ruby installed. svn2git is a ruby wrapper around git's native SVN support through git-svn. It is possible to have git installed without git-svn installed, so please do verify that you can run `$ git svn` successfully. For a Debian-based system, the installation of the prerequisites would look like: 68 | 69 | $ sudo apt-get install git-core git-svn ruby 70 | 71 | Once you have the necessary software on your system, you can install svn2git through rubygems, which will add the `svn2git` command to your PATH. 72 | 73 | $ sudo gem install svn2git 74 | 75 | 76 | Usage 77 | ----- 78 | 79 | ### Initial Conversion ### 80 | 81 | There are several ways you can create a git repo from an existing 82 | svn repo. The differentiating factor is the svn repo layout. Below is an 83 | enumerated listing of the varying supported layouts and the proper way to 84 | create a git repo from a svn repo in the specified layout. 85 | 86 | 1. The svn repo is in the standard layout of (trunk, branches, tags) at the 87 | root level of the repo. 88 | 89 | $ svn2git http://svn.example.com/path/to/repo 90 | 91 | 2. The svn repo is NOT in standard layout and has only a trunk and tags at the 92 | root level of the repo. 93 | 94 | $ svn2git http://svn.example.com/path/to/repo --trunk dev --tags rel --nobranches 95 | 96 | 3. The svn repo is NOT in standard layout and has only a trunk at the root 97 | level of the repo. 98 | 99 | $ svn2git http://svn.example.com/path/to/repo --trunk trunk --nobranches --notags 100 | 101 | 4. The svn repo is NOT in standard layout and has no trunk, branches, or tags 102 | at the root level of the repo. Instead the root level of the repo is 103 | equivalent to the trunk and there are no tags or branches. 104 | 105 | $ svn2git http://svn.example.com/path/to/repo --rootistrunk 106 | 107 | 5. The svn repo is in the standard layout but you want to exclude the massive 108 | doc directory and the backup files you once accidently added. 109 | 110 | $ svn2git http://svn.example.com/path/to/repo --exclude doc --exclude '.*~$' 111 | 112 | 6. The svn repo actually tracks several projects and you only want to migrate 113 | one of them. 114 | 115 | $ svn2git http://svn.example.com/path/to/repo/nested_project --no-minimize-url 116 | 117 | 7. The svn repo is password protected. 118 | 119 | $ svn2git http://svn.example.com/path/to/repo --username <> 120 | 121 | If this doesn't cooperate and you need to specify a password on the command-line: 122 | 123 | $ svn2git http://svn.example.com/path/to/repo --username <> --password <> 124 | 125 | 8. You need to migrate starting at a specific svn revision number. 126 | 127 | $ svn2git http://svn.example.com/path/to/repo --revision <> 128 | 129 | 9. You need to migrate starting at a specific svn revision number, ending at a specific revision number. 130 | 131 | $ svn2git http://svn.example.com/path/to/repo --revision <>:<> 132 | 133 | 10. Include metadata (git-svn-id) in git logs. 134 | 135 | $ svn2git http://svn.example.com/path/to/repo --metadata 136 | 137 | The above will create a git repository in the current directory with the git 138 | version of the svn repository. Hence, you need to make a directory that you 139 | want your new git repo to exist in, change into it and then run one of the 140 | above commands. Note that in the above cases the trunk, branches, tags options 141 | are simply folder names relative to the provided repo path. For example if you 142 | specified trunk=foo branches=bar and tags=foobar it would be referencing 143 | http://svn.example.com/path/to/repo/foo as your trunk, and so on. However, in 144 | case 4 it references the root of the repo as trunk. 145 | 146 | ### Repository Updates ### 147 | 148 | As of svn2git 2.0 there is a new feature to pull in the latest changes from SVN into your 149 | git repository created with svn2git. This is a one way sync, but allows you to use svn2git 150 | as a mirroring tool for your SVN repositories. 151 | 152 | The command to call is: 153 | 154 | $ cd && svn2git --rebase 155 | 156 | Authors 157 | ------- 158 | 159 | To convert all your svn authors to git format, create a file somewhere on your 160 | system with the list of conversions to make, one per line, for example: 161 | 162 | jcoglan = James Coglan 163 | stnick = Santa Claus 164 | 165 | Then pass an _authors_ option to svn2git pointing to your file: 166 | 167 | $ svn2git http://svn.example.com/path/to/repo --authors ~/authors.txt 168 | 169 | Alternatively, you can place the authors file into `~/.svn2git/authors` and 170 | svn2git will load it out of there. This allows you to build up one authors 171 | file for all your projects and have it loaded for each repository that you 172 | migrate. 173 | 174 | If you need a jump start on figuring out what users made changes in your 175 | svn repositories the following command sequence might help. It grabs all 176 | the logs from the svn repository, pulls out all the names from the commits, 177 | sorts them, and then reduces the list to only unique names. So, in the end 178 | it outputs a list of usernames of the people that made commits to the svn 179 | repository which name on its own line. This would allow you to easily 180 | redirect the output of this command sequence to `~/.svn2git/authors` and have 181 | a very good starting point for your mapping. 182 | 183 | $ svn log --quiet | grep -E "r[0-9]+ \| .+ \|" | cut -d'|' -f2 | sed 's/ //g' | sort | uniq 184 | 185 | Or, for a remote URL: 186 | 187 | $ svn log --quiet http://path/to/root/of/project | grep -E "r[0-9]+ \| .+ \|" | cut -d'|' -f2 | sed 's/ //g' | sort | uniq 188 | 189 | Debugging 190 | --------- 191 | 192 | If you're having problems with converting your repository and you're not sure why, 193 | try turning on verbose logging. This will print out more information from the 194 | underlying git-svn process. 195 | 196 | You can turn on verbose logging with the `-v` or `--verbose` flags, like so: 197 | 198 | $ svn2git http://svn.yoursite.com/path/to/repo --verbose 199 | 200 | Options Reference 201 | ----------------- 202 | 203 | $ svn2git --help 204 | Usage: svn2git SVN_URL [options] 205 | 206 | Specific options: 207 | --rebase Instead of cloning a new project, rebase an existing one against SVN 208 | --username NAME Username for transports that needs it (http(s), svn) 209 | --password PASS Password for transports that needs it (http(s), svn) 210 | --trunk TRUNK_PATH Subpath to trunk from repository URL (default: trunk) 211 | --branches BRANCHES_PATH Subpath to branches from repository URL (default: branches); can be used multiple times 212 | --tags TAGS_PATH Subpath to tags from repository URL (default: tags); can be used multiple times 213 | --rootistrunk Use this if the root level of the repo is equivalent to the trunk and there are no tags or branches 214 | --notrunk Do not import anything from trunk 215 | --nobranches Do not try to import any branches 216 | --notags Do not try to import any tags 217 | --no-minimize-url Accept URLs as-is without attempting to connect to a higher level directory 218 | --revision START_REV[:END_REV] 219 | Start importing from SVN revision START_REV; optionally end at END_REV 220 | -m, --metadata Include metadata in git logs (git-svn-id) 221 | --authors AUTHORS_FILE Path to file containing svn-to-git authors mapping (default: ~/.svn2git/authors) 222 | --exclude REGEX Specify a Perl regular expression to filter paths when fetching; can be used multiple times 223 | -v, --verbose Be verbose in logging -- useful for debugging issues 224 | 225 | -h, --help Show this message 226 | 227 | FAQ 228 | --- 229 | 230 | 1. Why don't the tags show up in the master branch? 231 | 232 | The tags won't show up in the master branch because the tags are actually 233 | tied to the commits that were created in svn when the user made the tag. 234 | Those commits are the first (head) commit of branch in svn that is 235 | associated with that tag. If you want to see all the branches and tags 236 | and their relationships in gitk you can run the following: gitk --all 237 | 238 | For further details please refer to FAQ #2. 239 | 240 | 2. Why don't you reference the parent of the tag commits instead? 241 | 242 | In svn you are forced to create what are known in git as annotated tags. 243 | It just so happens that svn annotated tags allow you to commit change 244 | sets along with the tagging action. This means that the svn annotated tag 245 | is a bit more complex then just an annotated tag it is a commit which is 246 | treated as an annotated tag. Hence, for there to be a true 1-to-1 mapping 247 | between git and svn we have to transfer over the svn commit which acts as 248 | an annotated tag and then tag that commit in git using an annotated tag. 249 | 250 | If we were to reference the parent of this svn tagged commit there could 251 | potentially be situations where a developer would checkout a tag in git 252 | and the resulting code base would be different than if they checked out 253 | that very same tag in the original svn repo. This is only due to the fact 254 | that the svn tags allow changesets in them, making them not just annotated 255 | tags. 256 | 257 | -------------------------------------------------------------------------------- /lib/svn2git/migration.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require 'pp' 3 | require 'timeout' 4 | require 'thread' 5 | 6 | module Svn2Git 7 | DEFAULT_AUTHORS_FILE = "~/.svn2git/authors" 8 | 9 | class Migration 10 | 11 | attr_reader :dir 12 | 13 | def initialize(args) 14 | @options = parse(args) 15 | if @options[:rebase] 16 | show_help_message('Too many arguments') if args.size > 0 17 | verify_working_tree_is_clean 18 | elsif @options[:rebasebranch] 19 | show_help_message('Too many arguments') if args.size > 0 20 | verify_working_tree_is_clean 21 | else 22 | show_help_message('Missing SVN_URL parameter') if args.empty? 23 | show_help_message('Too many arguments') if args.size > 1 24 | @url = args.first.gsub(' ', "\\ ") 25 | end 26 | end 27 | 28 | def run! 29 | if @options[:rebase] 30 | get_branches 31 | elsif @options[:rebasebranch] 32 | get_rebasebranch 33 | else 34 | clone! 35 | end 36 | fix_branches 37 | fix_tags 38 | fix_trunk 39 | optimize_repos 40 | end 41 | 42 | def parse(args) 43 | # Set up reasonable defaults for options. 44 | options = {} 45 | options[:verbose] = false 46 | options[:metadata] = false 47 | options[:nominimizeurl] = false 48 | options[:rootistrunk] = false 49 | options[:trunk] = 'trunk' 50 | options[:branches] = [] 51 | options[:tags] = [] 52 | options[:exclude] = [] 53 | options[:revision] = nil 54 | options[:username] = nil 55 | options[:password] = nil 56 | options[:rebasebranch] = false 57 | 58 | if File.exists?(File.expand_path(DEFAULT_AUTHORS_FILE)) 59 | options[:authors] = DEFAULT_AUTHORS_FILE 60 | end 61 | 62 | 63 | # Parse the command-line arguments. 64 | @opts = OptionParser.new do |opts| 65 | opts.banner = 'Usage: svn2git SVN_URL [options]' 66 | 67 | opts.separator '' 68 | opts.separator 'Specific options:' 69 | 70 | opts.on('--rebase', 'Instead of cloning a new project, rebase an existing one against SVN') do 71 | options[:rebase] = true 72 | end 73 | 74 | opts.on('--username NAME', 'Username for transports that needs it (http(s), svn)') do |username| 75 | options[:username] = username 76 | end 77 | 78 | opts.on('--password PASSWORD', 'Password for transports that need it (http(s), svn)') do |password| 79 | options[:password] = password 80 | end 81 | 82 | opts.on('--trunk TRUNK_PATH', 'Subpath to trunk from repository URL (default: trunk)') do |trunk| 83 | options[:trunk] = trunk 84 | end 85 | 86 | opts.on('--branches BRANCHES_PATH', 'Subpath to branches from repository URL (default: branches); can be used multiple times') do |branches| 87 | options[:branches] << branches 88 | end 89 | 90 | opts.on('--tags TAGS_PATH', 'Subpath to tags from repository URL (default: tags); can be used multiple times') do |tags| 91 | options[:tags] << tags 92 | end 93 | 94 | opts.on('--rootistrunk', 'Use this if the root level of the repo is equivalent to the trunk and there are no tags or branches') do 95 | options[:rootistrunk] = true 96 | options[:trunk] = nil 97 | options[:branches] = nil 98 | options[:tags] = nil 99 | end 100 | 101 | opts.on('--notrunk', 'Do not import anything from trunk') do 102 | options[:trunk] = nil 103 | end 104 | 105 | opts.on('--nobranches', 'Do not try to import any branches') do 106 | options[:branches] = nil 107 | end 108 | 109 | opts.on('--notags', 'Do not try to import any tags') do 110 | options[:tags] = nil 111 | end 112 | 113 | opts.on('--no-minimize-url', 'Accept URLs as-is without attempting to connect to a higher level directory') do 114 | options[:nominimizeurl] = true 115 | end 116 | 117 | opts.on('--revision START_REV[:END_REV]', 'Start importing from SVN revision START_REV; optionally end at END_REV') do |revision| 118 | options[:revision] = revision 119 | end 120 | 121 | opts.on('-m', '--metadata', 'Include metadata in git logs (git-svn-id)') do 122 | options[:metadata] = true 123 | end 124 | 125 | opts.on('--authors AUTHORS_FILE', "Path to file containing svn-to-git authors mapping (default: #{DEFAULT_AUTHORS_FILE})") do |authors| 126 | options[:authors] = authors 127 | end 128 | 129 | opts.on('--exclude REGEX', 'Specify a Perl regular expression to filter paths when fetching; can be used multiple times') do |regex| 130 | options[:exclude] << regex 131 | end 132 | 133 | opts.on('-v', '--verbose', 'Be verbose in logging -- useful for debugging issues') do 134 | options[:verbose] = true 135 | end 136 | 137 | opts.on('--rebasebranch REBASEBRANCH', 'Rebase specified branch.') do |rebasebranch| 138 | options[:rebasebranch] = rebasebranch 139 | end 140 | 141 | opts.separator "" 142 | 143 | # No argument, shows at tail. This will print an options summary. 144 | # Try it and see! 145 | opts.on_tail('-h', '--help', 'Show this message') do 146 | puts opts 147 | exit 148 | end 149 | end 150 | 151 | @opts.parse! args 152 | options 153 | end 154 | 155 | def self.escape_quotes(str) 156 | str.gsub(/'|"/) { |c| "\\#{c}" } 157 | end 158 | 159 | def escape_quotes(str) 160 | Svn2Git::Migration.escape_quotes(str) 161 | end 162 | 163 | def self.checkout_svn_branch(branch) 164 | "git checkout -b \"#{branch}\" \"remotes/svn/#{branch}\"" 165 | end 166 | 167 | private 168 | 169 | def clone! 170 | trunk = @options[:trunk] 171 | branches = @options[:branches] 172 | tags = @options[:tags] 173 | metadata = @options[:metadata] 174 | nominimizeurl = @options[:nominimizeurl] 175 | rootistrunk = @options[:rootistrunk] 176 | authors = @options[:authors] 177 | exclude = @options[:exclude] 178 | revision = @options[:revision] 179 | username = @options[:username] 180 | password = @options[:password] 181 | 182 | if rootistrunk 183 | # Non-standard repository layout. The repository root is effectively 'trunk.' 184 | cmd = "git svn init --prefix=svn/ " 185 | cmd += "--username='#{username}' " unless username.nil? 186 | cmd += "--password='#{password}' " unless password.nil? 187 | cmd += "--no-metadata " unless metadata 188 | if nominimizeurl 189 | cmd += "--no-minimize-url " 190 | end 191 | cmd += "--trunk='#{@url}'" 192 | run_command(cmd, true, true) 193 | 194 | else 195 | cmd = "git svn init --prefix=svn/ " 196 | 197 | # Add each component to the command that was passed as an argument. 198 | cmd += "--username='#{username}' " unless username.nil? 199 | cmd += "--password='#{password}' " unless password.nil? 200 | cmd += "--no-metadata " unless metadata 201 | if nominimizeurl 202 | cmd += "--no-minimize-url " 203 | end 204 | cmd += "--trunk='#{trunk}' " unless trunk.nil? 205 | unless tags.nil? 206 | # Fill default tags here so that they can be filtered later 207 | tags = ['tags'] if tags.empty? 208 | # Process default or user-supplied tags 209 | tags.each do |tag| 210 | cmd += "--tags='#{tag}' " 211 | end 212 | end 213 | unless branches.nil? 214 | # Fill default branches here so that they can be filtered later 215 | branches = ['branches'] if branches.empty? 216 | # Process default or user-supplied branches 217 | branches.each do |branch| 218 | cmd += "--branches='#{branch}' " 219 | end 220 | end 221 | 222 | cmd += @url 223 | 224 | run_command(cmd, true, true) 225 | end 226 | 227 | run_command("#{git_config_command} svn.authorsfile #{authors}") unless authors.nil? 228 | 229 | cmd = "git svn fetch " 230 | unless revision.nil? 231 | range = revision.split(":") 232 | range[1] = "HEAD" unless range[1] 233 | cmd += "-r #{range[0]}:#{range[1]} " 234 | end 235 | unless exclude.empty? 236 | # Add exclude paths to the command line; some versions of git support 237 | # this for fetch only, later also for init. 238 | regex = [] 239 | unless rootistrunk 240 | regex << "#{trunk}[/]" unless trunk.nil? 241 | tags.each{|tag| regex << "#{tag}[/][^/]+[/]"} unless tags.nil? or tags.empty? 242 | branches.each{|branch| regex << "#{branch}[/][^/]+[/]"} unless branches.nil? or branches.empty? 243 | end 244 | regex = '^(?:' + regex.join('|') + ')(?:' + exclude.join('|') + ')' 245 | cmd += "--ignore-paths='#{regex}' " 246 | end 247 | run_command(cmd, true, true) 248 | 249 | get_branches 250 | end 251 | 252 | def get_branches 253 | # Get the list of local and remote branches, taking care to ignore console color codes and ignoring the 254 | # '*' character used to indicate the currently selected branch. 255 | @local = run_command("git branch -l --no-color").split(/\n/).collect{ |b| b.gsub(/\*/,'').strip } 256 | @remote = run_command("git branch -r --no-color").split(/\n/).collect{ |b| b.gsub(/\*/,'').strip } 257 | 258 | # Tags are remote branches that start with "tags/". 259 | @tags = @remote.find_all { |b| b.strip =~ %r{^svn\/tags\/} } 260 | 261 | end 262 | 263 | def get_rebasebranch 264 | get_branches 265 | @local = @local.find_all{|l| l == @options[:rebasebranch]} 266 | @remote = @remote.find_all{|r| r.include? @options[:rebasebranch]} 267 | 268 | if @local.count > 1 269 | pp "To many matching branches found (#{@local})." 270 | exit 1 271 | elsif @local.count == 0 272 | pp "No local branch named \"#{@options[:rebasebranch]}\" found." 273 | exit 1 274 | end 275 | 276 | if @remote.count > 2 # 1 if remote is not pushed, 2 if its pushed to remote 277 | pp "To many matching remotes found (#{@remotes})" 278 | exit 1 279 | elsif @remote.count == 0 280 | pp "No remote branch named \"#{@options[:rebasebranch]}\" found." 281 | exit 1 282 | end 283 | pp "Local branches \"#{@local}\" found" 284 | pp "Remote branches \"#{@remote}\" found" 285 | 286 | @tags = [] # We only rebase the specified branch 287 | 288 | end 289 | 290 | def fix_tags 291 | current = {} 292 | current['user.name'] = run_command("#{git_config_command} --get user.name", false) 293 | current['user.email'] = run_command("#{git_config_command} --get user.email", false) 294 | 295 | @tags.each do |tag| 296 | tag = tag.strip 297 | id = tag.gsub(%r{^svn\/tags\/}, '').strip 298 | subject = run_command("git log -1 --pretty=format:'%s' \"#{escape_quotes(tag)}\"").chomp("'").reverse.chomp("'").reverse 299 | date = run_command("git log -1 --pretty=format:'%ci' \"#{escape_quotes(tag)}\"").chomp("'").reverse.chomp("'").reverse 300 | author = run_command("git log -1 --pretty=format:'%an' \"#{escape_quotes(tag)}\"").chomp("'").reverse.chomp("'").reverse 301 | email = run_command("git log -1 --pretty=format:'%ae' \"#{escape_quotes(tag)}\"").chomp("'").reverse.chomp("'").reverse 302 | run_command("#{git_config_command} user.name \"#{escape_quotes(author)}\"") 303 | run_command("#{git_config_command} user.email \"#{escape_quotes(email)}\"") 304 | 305 | original_git_committer_date = ENV['GIT_COMMITTER_DATE'] 306 | ENV['GIT_COMMITTER_DATE'] = escape_quotes(date) 307 | run_command("git tag -a -m \"#{escape_quotes(subject)}\" \"#{escape_quotes(id)}\" \"#{escape_quotes(tag)}\"") 308 | ENV['GIT_COMMITTER_DATE'] = original_git_committer_date 309 | 310 | run_command("git branch -d -r \"#{escape_quotes(tag)}\"") 311 | end 312 | 313 | ensure 314 | # We only change the git config values if there are @tags available. So it stands to reason we should revert them only in that case. 315 | unless @tags.empty? 316 | current.each_pair do |name, value| 317 | # If a line was read, then there was a config value so restore it. 318 | # Otherwise unset the value because originally there was none. 319 | if value.strip != '' 320 | run_command("#{git_config_command} #{name} \"#{value.strip}\"") 321 | else 322 | run_command("#{git_config_command} --unset #{name}") 323 | end 324 | end 325 | end 326 | end 327 | 328 | def fix_branches 329 | svn_branches = @remote - @tags 330 | svn_branches.delete_if { |b| b.strip !~ %r{^svn\/} } 331 | 332 | if @options[:rebase] 333 | run_command("git svn fetch", true, true) 334 | end 335 | 336 | svn_branches.each do |branch| 337 | branch = branch.gsub(/^svn\//,'').strip 338 | if @options[:rebase] && (@local.include?(branch) || branch == 'trunk') 339 | lbranch = branch 340 | lbranch = 'master' if branch == 'trunk' 341 | run_command("git checkout -f \"#{lbranch}\"") 342 | run_command("git rebase \"remotes/svn/#{branch}\"") 343 | next 344 | end 345 | 346 | next if branch == 'trunk' || @local.include?(branch) 347 | 348 | if @cannot_setup_tracking_information 349 | run_command(Svn2Git::Migration.checkout_svn_branch(branch)) 350 | else 351 | status = run_command("git branch --track \"#{branch}\" \"remotes/svn/#{branch}\"", false) 352 | 353 | # As of git 1.8.3.2, tracking information cannot be set up for remote SVN branches: 354 | # http://git.661346.n2.nabble.com/git-svn-Use-prefix-by-default-td7594288.html#a7597159 355 | # 356 | # Older versions of git can do it and it should be safe as long as remotes aren't pushed. 357 | # Our --rebase option obviates the need for read-only tracked remotes, however. So, we'll 358 | # deprecate the old option, informing those relying on the old behavior that they should 359 | # use the newer --rebase otion. 360 | if status =~ /Cannot setup tracking information/m 361 | @cannot_setup_tracking_information = true 362 | run_command(Svn2Git::Migration.checkout_svn_branch(branch)) 363 | else 364 | unless @legacy_svn_branch_tracking_message_displayed 365 | warn '*' * 68 366 | warn "svn2git warning: Tracking remote SVN branches is deprecated." 367 | warn "In a future release local branches will be created without tracking." 368 | warn "If you must resync your branches, run: svn2git --rebase" 369 | warn '*' * 68 370 | end 371 | 372 | @legacy_svn_branch_tracking_message_displayed = true 373 | 374 | run_command("git checkout \"#{branch}\"") 375 | end 376 | end 377 | end 378 | end 379 | 380 | def fix_trunk 381 | trunk = @remote.find { |b| b.strip == 'trunk' } 382 | if trunk && ! @options[:rebase] 383 | run_command("git checkout svn/trunk") 384 | run_command("git branch -D master") 385 | run_command("git checkout -f -b master") 386 | else 387 | run_command("git checkout -f master") 388 | end 389 | end 390 | 391 | def optimize_repos 392 | run_command("git gc") 393 | end 394 | 395 | def run_command(cmd, exit_on_error=true, printout_output=false) 396 | log "Running command: #{cmd}\n" 397 | 398 | ret = '' 399 | @stdin_queue ||= Queue.new 400 | 401 | # We need to fetch input from the user to pass through to the underlying sub-process. We'll constantly listen 402 | # for input and place any received values on a queue for consumption by a pass-through thread that will forward 403 | # the contents to the underlying sub-process's stdin pipe. 404 | @stdin_thread ||= Thread.new do 405 | loop { @stdin_queue << $stdin.gets.chomp } 406 | end 407 | 408 | # Open4 forks, which JRuby doesn't support. But JRuby added a popen4-compatible method on the IO class, 409 | # so we can use that instead. 410 | IO.popen("2>&1 #{cmd}") do |output| 411 | threads = [] 412 | 413 | threads << Thread.new(output) do |output| 414 | # git-svn seems to do all of its prompting for user input via STDERR. When it prompts for input, it will 415 | # not terminate the line with a newline character, so we can't split the input up by newline. It will, 416 | # however, use a space to separate the user input from the prompt. So we split on word boundaries here 417 | # while draining STDERR. 418 | output.each(' ') do |word| 419 | ret << word 420 | 421 | if printout_output 422 | $stdout.print word 423 | else 424 | log word 425 | end 426 | end 427 | end 428 | 429 | # Simple pass-through thread to take anything the user types via STDIN and passes it through to the 430 | # sub-process's stdin pipe. 431 | Thread.new do 432 | loop do 433 | user_reply = @stdin_queue.pop 434 | 435 | # nil is our cue to stop looping (pun intended). 436 | break if user_reply.nil? 437 | 438 | stdin.puts user_reply 439 | stdin.close 440 | end 441 | end 442 | 443 | threads.each(&:join) 444 | 445 | # Push nil to the stdin_queue to gracefully exit the STDIN pass-through thread. 446 | @stdin_queue << nil 447 | end 448 | 449 | if exit_on_error && $?.exitstatus != 0 450 | $stderr.puts "command failed:\n#{cmd}" 451 | exit -1 452 | end 453 | 454 | ret 455 | end 456 | 457 | def log(msg) 458 | print msg if @options[:verbose] 459 | end 460 | 461 | def show_help_message(msg) 462 | puts "Error starting script: #{msg}\n\n" 463 | puts @opts.help 464 | exit 465 | end 466 | 467 | def verify_working_tree_is_clean 468 | status = run_command('git status --porcelain --untracked-files=no') 469 | unless status.strip == '' 470 | puts 'You have local pending changes. The working tree must be clean in order to continue.' 471 | exit -1 472 | end 473 | end 474 | 475 | def git_config_command 476 | if @git_config_command.nil? 477 | status = run_command('git config --local --get user.name', false) 478 | 479 | @git_config_command = if status =~ /unknown option/m 480 | 'git config' 481 | else 482 | 'git config --local' 483 | end 484 | end 485 | 486 | @git_config_command 487 | end 488 | 489 | end 490 | end 491 | 492 | --------------------------------------------------------------------------------