├── .gitignore ├── spec ├── conversor_spec.rb └── spec_helper.rb ├── features ├── support │ └── env.rb ├── do_conversion.feature └── step_definitions │ └── steps_def.feature.rb ├── bin └── conversor.rb ├── README.md └── lib └── conversor.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /spec/conversor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Author:: Gonzalo Rodríguez-Baltanás Díaz 2 | # Licence:: See Licence.rdoc 3 | 4 | require 'conversor' 5 | 6 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | # Author:: Gonzalo Rodríguez-Baltanás Díaz 2 | 3 | $LOAD_PATH << File.expand_path('../../../lib', __FILE__) 4 | require 'conversor' -------------------------------------------------------------------------------- /features/do_conversion.feature: -------------------------------------------------------------------------------- 1 | Feature: Do conversion 2 | In order to update the destiny copy 3 | As a Conversor 4 | I want to commit every change from origin to destiny 5 | 6 | Scenario Outline: Do conversion 7 | Given there are some SVN repos like "" 8 | And we initiate the conversor with origin "" and destiny "" 9 | When I perform de conversion process 10 | Then both repos should have the same revision 11 | 12 | Scenarios: Origin has commit that arent on the destiny 13 | | origin | name_origin | name_destiny | destiny | 14 | | http://svn.github.com/Nerian/JPovray.git | origin | destiny | file:///tmp/Server_Repos/destiny | 15 | | http://svn.github.com/Nerian/DPovray.git | origin | destiny | file:///tmp/Server_Repos/destiny | 16 | 17 | -------------------------------------------------------------------------------- /features/step_definitions/steps_def.feature.rb: -------------------------------------------------------------------------------- 1 | class Output 2 | def messages 3 | @messages ||= [] 4 | end 5 | 6 | def puts(message) 7 | messages << message 8 | end 9 | end 10 | 11 | def output 12 | @output ||= Output.new 13 | end 14 | 15 | Given /^we initiate the conversor with origin "([^\"]*)" and destiny "([^\"]*)"$/ do |origin, destiny| 16 | @conversor = Conversor::Conversor.new(output, origin, destiny) 17 | end 18 | 19 | When /^I checkout destiny repo$/ do 20 | @conversor.checkout_destiny_repo() 21 | end 22 | 23 | Given /^the SVN Origin Repo is "([^\"]*)"$/ do |svn_address_origin| 24 | @conversor.svn_address_origin = svn_address_origin 25 | end 26 | 27 | Given /^the SVN destiny Repo is "([^\"]*)"$/ do |svn_address_destiny| 28 | @conversor.svn_address_destiny = svn_address_destiny 29 | end 30 | 31 | When /^I checkout origin repo$/ do 32 | @conversor.checkout_origin_repo() 33 | end 34 | 35 | Given /^there are some SVN repos like "([^\"]*)"$/ do |name| 36 | if not File.exist?("/tmp/Server_Repos") 37 | system("mkdir /tmp/Server_Repos") 38 | end 39 | system("rm -Rf /tmp/Server_Repos/"+name) 40 | system("svnadmin create /tmp/Server_Repos/"+name) 41 | end 42 | 43 | When /^I perform de conversion process$/ do 44 | @conversor.perform_conversion() 45 | end 46 | 47 | Then /^both repos should have the same revision$/ do 48 | @conversor.destiny_repo_online_revision.should == @conversor.final_revision_that_you_want_to_mirror 49 | end 50 | 51 | Then /^I should see a message "([^\"]*)"$/ do |message| 52 | result = File.exist?("/tmp/Server_Repos/"+@conversor.svn_origin_name) 53 | if result 54 | output.puts (message) 55 | end 56 | output.messages.should include(message) 57 | end 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /bin/conversor.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 3 | require 'optparse' 4 | require 'conversor' 5 | require 'ostruct' 6 | 7 | 8 | 9 | options = {} 10 | optparse = OptionParser.new do|opts| 11 | # Set a banner, displayed at the top 12 | # of the help screen. 13 | opts.banner = "Usage: conversor.rb --origin path_origin --destiny path_destiny ..." 14 | 15 | # Define the options, and what they do 16 | opts.on( '-o', '--origin ORIGIN', 'The repository from which you wan to to pull the commits' ) do |origin| 17 | options[:origin] = origin 18 | end 19 | 20 | opts.on( '-d', '--destiny DESTINY', 'The repository to which you want to copy the commits' ) do |destiny| 21 | options[:destiny] = destiny 22 | end 23 | 24 | # This displays the help screen, all programs are 25 | # assumed to have this option. 26 | opts.on( '-h', '--help', 'Display this screen' ) do 27 | puts opts 28 | exit 29 | end 30 | end 31 | 32 | begin 33 | optparse.parse! 34 | mandatory = [:origin, :destiny] # Enforce the presence of 35 | missing = mandatory.select{ |param| options[param].nil? } # the -o and -d switches 36 | if not missing.empty? # 37 | puts "Missing options: #{missing.join(', ')}" # 38 | puts optparse # 39 | exit 40 | else 41 | conversor = Conversor::Conversor.new(options[:origin], options[:destiny]) 42 | conversor.perform_conversion # 43 | end # 44 | rescue OptionParser::InvalidOption, OptionParser::MissingArgument # 45 | puts $!.to_s # Friendly output when parsing fails 46 | puts optparse # 47 | exit # 48 | end 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitHub Subversion Converter – GitSC 2 | ==================================== 3 | 4 | GitSC has nothing to do with StarCraft. GitSC is a ruby command line script that will allow you to transfer the commits from a GitHub SVN repo to a real repo anywhere. 5 | 6 | It really can transfer commits from SVN repos hosted anywhere, but GitSC is optimised to deal with GitHub SVN little nuances. 7 | 8 | It can keep transferring commits from origin to destiny after the initial transfer. It just pick where it left. 9 | 10 | What GitSC does is to commit changes one by one from the origin to destiny. In such process it performs some validations to deal with GitHub SVN little nuances, namely Phantom commits ( I created a fancy word, yeah! ^_^ ). So it is VERY slow. Transferring 50 commits can take 5 min. 11 | 12 | A Phantom commit happens when the previous commit is exactly the same as the commit we are trying to commit. SVN won't allow such a commit so it won't never happen on a real SVN server. But as I said, GitHub does a little magic behind the scene and it removes the .gitignore files. Many times you just make a commit whose only change was the .gitignore file. In such case, you have a phantom commit in svn fake repo. GitCS it is capable of dealing with this, rest assured. 13 | 14 | So if you just want to clone real SVN repos, not hosted at GitHub, I recommend you to check [svnsync](http://svnbook.red-bean.com/en/1.5/svn.ref.svnsync.html SVNSYNC). 15 | 16 | Current Status 17 | ==================================== 18 | 19 | * Transferring full GitHub repo to another SVN repo works. | it's done! 20 | * Transferring full GitHub repo to another SVN repo that already have commits | it's done! 21 | * Make commits retain the author name | it's done! 22 | * Make commits retain the commit message | it's done! 23 | * Create command line tool so GitSC can be used | it's done! 24 | * Think on how to deal with user names and passwords | on it! 25 | * Integrate with Hoe to make automatic Gem | on it! 26 | 27 | 28 | * Make commits have the right date it is not done. 29 | 30 | The last feature requires modifying hooks script on the server side, which sucks and I don't really need it. So chances are that I won't add this feature. Feel free to fork the project and do it yourself, I will accept a pull request. 31 | 32 | The line you have to touch in on the method "perform\_conversion\_operations" inside "lib/conversor.rb". The last lines of that method call the method "update\_destiny\_server\_commit\_date\_to\_origin\_commit\_date()". You just have to implement that. 33 | 34 | 35 | How to Use 36 | ==================================== 37 | Currently I just have the ruby class and cucumber tests. So it is not ready for use right now. 38 | 39 | But the expected way of using is: 40 | 41 | GitSC --origin *address* --destiny *address* 42 | 43 | Take note, if two persons do this a the same time you could end with many duplicated commits. So I recommend that if you are working with other people your team designate just one person to do it. 44 | 45 | 46 | 47 | Why are you doing this? 48 | ==================================== 49 | GitSC was born to fill a need. I had a couple of projects at school and it was mandatory to host the project at the faculty SVN servers. But I didn't want to use such useless version control system. I am a branch consuming addict. I can't live without superb branch support. SVN just doesn't give me that. 50 | 51 | At the same time I was learning ruby and reading Pragmatic Programmers's Behaviour Driven Development with Cucumber and RSpec. So it was the perfect occasion to apply the concepts. As I was developing this, I added a new trait to my personality. I am a test addict. Now I can't live without heavy testing :) 52 | 53 | Do you need help? 54 | =================================== 55 | 56 | Yes! The application needs to be able to deal with very different scenarios. So Tests are very much welcomed. Also, I am not a Ruby expert – give time – so any kind of code review would be very much appreciate. 57 | 58 | Thank you for your time. 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /lib/conversor.rb: -------------------------------------------------------------------------------- 1 | require 'find' 2 | require "rexml/document" 3 | 4 | module Conversor 5 | class Conversor 6 | 7 | # output is used for Cucumber testing. 8 | # svn_address_origin is the online address of the svn repo that we can to mirror from 9 | # svn_address_destiny is the online address of the svn repo that we want to mirror to 10 | # svn_origin_name is the name of the checkout origin repo. 11 | # svn_destiny_name is the name of the checkout destiny repo. 12 | attr_accessor :output, :svn_address_origin, :svn_address_destiny, :svn_origin_name, :svn_destiny_name, :final_revision_that_you_want_to_mirror, :log_file_of_origin 13 | 14 | def initialize(output=STDOUT, svn_address_origin=nil, svn_address_destiny=nil) 15 | @output = output 16 | @svn_origin_name = "origin" #Just the name, not the file path. 17 | @svn_destiny_name = "destiny" #Just the name, not the file path. 18 | @svn_address_origin = svn_address_origin #This is a complete PATH 19 | @svn_address_destiny = svn_address_destiny #This is a complete PATH 20 | @log_file_of_origin = "/tmp/log_file_of_origin.xml" 21 | 22 | checkout_origin_repo() 23 | checkout_destiny_repo() 24 | 25 | @final_revision_that_you_want_to_mirror = origin_repo_online_revision() 26 | end 27 | 28 | def checkout_origin_repo(revision=nil) 29 | system("rm -Rf /tmp/#{@svn_origin_name}") 30 | 31 | puts "==> Checking out SVN origin at: #{@svn_address_origin} \n" 32 | if not revision.nil? 33 | puts "=>checking out repo at #{@svn_address_origin} in /tmp/#{@svn_origin_name} revision: #{revision}" 34 | system("svn checkout #{@svn_address_origin} /tmp/#{@svn_origin_name} -r #{revision}") 35 | else 36 | puts "=>checking out repo at #{@svn_address_origin} in /tmp/#{@svn_origin_name}" 37 | system("svn checkout #{@svn_address_origin} /tmp/#{@svn_origin_name}") 38 | end 39 | end 40 | 41 | def checkout_destiny_repo(revision=nil) 42 | system("rm -Rf /tmp/#{@svn_destiny_name}") 43 | 44 | puts "==> Checking out SVN destiny at: #{@svn_address_destiny} \n" 45 | if not revision.nil? 46 | puts "checking out repo at #{@svn_address_destiny} in /tmp/#{@svn_destiny_name} revision #{revision}" 47 | system("svn checkout #{@svn_address_destiny} /tmp/#{@svn_destiny_name} -r #{revision}") 48 | else 49 | puts "checking out repo at #{@svn_address_destiny} in /tmp/#{@svn_destiny_name}" 50 | system("svn checkout #{@svn_address_destiny} /tmp/#{@svn_destiny_name}") 51 | end 52 | end 53 | 54 | =begin rdoc 55 | The algorithm do exactly this: 56 | 57 | Pick up what is the last revision in destiny. 58 | If that revision plus one is below the origin last revision, then it means we have commits to transfer. 59 | 60 | While the revision of both origin and destiny repo are not the same: 61 | Checkout the origin repo, at destiny revision number. 62 | Dump current working directory of origin into destiny 63 | Commit 64 | Update 65 | Repit with the next revision. 66 | =end 67 | def perform_conversion() 68 | 69 | # The revision that we want the destiny repo to have is the last revision in origin. 70 | revision_that_we_want_the_destiny_to_be_in = destiny_repo_online_revision().to_i + 1 71 | 72 | continue = true 73 | 74 | # Until the destiny online repo last revision is not the same as the last revision in origin 75 | # we will keep making commits. 76 | while(revision_that_we_want_the_destiny_to_be_in <= @final_revision_that_you_want_to_mirror.to_i and continue) 77 | 78 | # Perform the neccesary steps to transfer that specific commit from origin to destiny. 79 | puts "\n\n ====== Trying to apply revision #{revision_that_we_want_the_destiny_to_be_in.to_s}, final revision: #{@final_revision_that_you_want_to_mirror} ======\n\n" 80 | perform_conversion_operations(revision_that_we_want_the_destiny_to_be_in.to_s) 81 | 82 | # If the destiny online repo doesnt have the revision we wanted to apply, then something went wrong. 83 | if not destiny_repo_online_revision.to_i == revision_that_we_want_the_destiny_to_be_in 84 | puts "======>>> Something went wrong, check the log <<<======" 85 | puts "==> current destiny_repo_online_revision: #{destiny_repo_online_revision} svn_destiny_revision: #{revision_that_we_want_the_destiny_to_be_in}" 86 | continue = false 87 | else 88 | puts "\n ====== Done applying revision #{revision_that_we_want_the_destiny_to_be_in.to_s} ======\n" 89 | end 90 | revision_that_we_want_the_destiny_to_be_in = revision_that_we_want_the_destiny_to_be_in + 1 91 | end 92 | end 93 | 94 | 95 | def remove_files_from_destiny_repo_that_were_removed_in_origin_repo() 96 | # SVN Remove files that are in destiny but are not in origin 97 | list_of_files_that_should_be_removed = [] 98 | puts "\n\n====== Removing files that exist in destiny but are not in origin, which means they were removed and should be scheduled svn rm 'file' ======\n" 99 | Dir.glob(File.join("/tmp/#{@svn_destiny_name}", '**', '*')) do |file_path_destiny| 100 | if not file_path_destiny.include?(".svn") 101 | file_path_origin = file_path_destiny.gsub(svn_destiny_name, svn_origin_name) 102 | #Check if it doesnt exist in origin 103 | if not File.exist?(file_path_origin) 104 | #Schedule deletion by svn 105 | puts "==> The file #{file_path_destiny} does not exist in #{file_path_origin} and so is scheduled to removal in svn" 106 | system("svn remove "+file_path_destiny) 107 | list_of_files_that_should_be_removed.push(file_path_destiny) 108 | end 109 | end 110 | end 111 | 112 | list_of_files_that_should_be_removed.each do |name| 113 | system("rm -Rf #{name}") 114 | end 115 | puts "\n====== Done Removing files ======\n" 116 | end 117 | 118 | def remove_SVN_files_from_origin() 119 | # Find all .svn in origin and delete them 120 | puts "\n\n====== Removing .svn files from origin ======\n" 121 | Dir.glob("/tmp/#{@svn_origin_name}/**/.svn", File::FNM_DOTMATCH) do |file_path_origin| 122 | puts "==>Removing .svn file from"+ file_path_origin 123 | system("rm -Rf #{file_path_origin}") 124 | end 125 | puts "\n====== Done Removing .svn files ======\n" 126 | end 127 | 128 | 129 | def copy_files_from_origin_to_destiny() 130 | # Copy files that are in origin to destiny. 131 | puts "\n\n====== Copying files from origin to destiny ======\n" 132 | Dir.glob(File.join("/tmp/#{@svn_origin_name}", '**', '*')) do |file_path_origin| 133 | if not file_path_origin.include?(".svn") 134 | file_path_destiny = file_path_origin.gsub(svn_origin_name, svn_destiny_name) 135 | puts "==> Copying file from "+file_path_origin+" to destiny: "+file_path_destiny 136 | if File.directory?(file_path_origin) 137 | system("mkdir #{file_path_destiny}") 138 | puts "=> Done creating directory #{file_path_destiny} \n" 139 | else 140 | system("cp #{file_path_origin} #{file_path_destiny}") 141 | puts "=> Done copying file to #{file_path_destiny} \n" 142 | end 143 | end 144 | end 145 | puts "\n====== Done Copying files from origin to destiny ======\n" 146 | end 147 | 148 | 149 | def check_if_this_is_a_phantom_commit() 150 | # system("cd /tmp/"+@svn_destiny_name +" && "+"svn status | grep '^\?' | awk '{print $2}' | xargs svn add"+" && svn commit -m '"+revision_number_that_we_want_to_copy_to_destiny+"'"+ " && "+"svn update") 151 | if no_changes_to_update?() 152 | puts "==> This is a phantom commit. This means that the previous commit and this have exactly the same files. It is not possible to do a commit that doesn't change anything. This either means that either there was an svn error, check the current directory scheme of origin and destiny, or that you just commited a .gitignore file. So we are adding a file 'github_phantom_file' to the repo. This happens when you are using Git and push just a change to .gitignore. Github removes that kind of file, but still make it a new revision." 153 | system("touch /tmp/#{@svn_destiny_name}/github_phantom_file") 154 | end 155 | 156 | end 157 | 158 | def perform_conversion_operations(revision_number_that_we_want_to_copy_to_destiny) 159 | 160 | # Checkout a temporal repo that will contain the changes –– revision – that we want to apply to destiny 161 | checkout_origin_repo(revision_number_that_we_want_to_copy_to_destiny) 162 | 163 | # Get the information about the commit. We use svn log in origin to get the info. 164 | author = get_author_name(revision_number_that_we_want_to_copy_to_destiny) 165 | commit_message = get_commit_message(revision_number_that_we_want_to_copy_to_destiny) 166 | 167 | # In the event that the next revision removed files, we should 'svn rm name' before doing the copy. 168 | remove_files_from_destiny_repo_that_were_removed_in_origin_repo() 169 | 170 | # SVN files like '.svn' contain info about the state of the repo, revision, changes etc. It must not be 171 | # copied to the destiny repo or we would be overwriting the destiny repo own '.svn' files. 172 | # So we remove them from the temporal origin repo before copying from Origin to Destiny. 173 | remove_SVN_files_from_origin() 174 | 175 | # We copy all files from origin repo to Destiny repo. This essencially makes the working directory in destiny identical 176 | # to the working directory of origin, whose state is the the revision that we want to apply. 177 | # Later we just have to svn add * everything. SVN will automatically add what changed. 178 | copy_files_from_origin_to_destiny() 179 | 180 | # Show the current directory layout in origin and destiny 181 | # If they are the same, it means the we have a perfect copy. 182 | puts "\n\n ======= Current origin schema ======\n" 183 | list_directory("/tmp/#{svn_origin_name}") 184 | puts "\n-----------" 185 | puts "\n\n ======= Current destiny schema ======\n" 186 | list_directory("/tmp/#{svn_destiny_name}") 187 | puts "\n-----------" 188 | 189 | # Check if it is a phantom commit. Phantom commits are commit that had just changes to .gitignore file. 190 | # Github removes that file from the svn revision, so we are left with two subsequent revisions that are exactly 191 | # the identical. SVN commit with changes won't do anything. 192 | # To evade this problem, we make a fake file, .github file, so we have something to commit. 193 | # It will be erased in the next commit, so it won't cause any problem. 194 | check_if_this_is_a_phantom_commit() 195 | 196 | # The final stage is to 'svn add *' everything and commit to destiny online repo. If this goes well, we 197 | # would have succefully commited the intended revision. 198 | puts "\n ====== Start svn add * , commit, and update. Using username: #{author} ======\n" 199 | puts "Commit message: \n-----------\n#{commit_message}\n-----------" 200 | system("cd /tmp/#{@svn_destiny_name} && svn status | grep '^\?' | awk '{print $2}' | xargs svn add && svn commit --force-log --username #{author} -m '#{commit_message}' && svn update") 201 | 202 | #update_destiny_server_commit_date_to_origin_commit_date() 203 | 204 | puts "\n ====== End svn add * , commit, and update ======\n" 205 | 206 | end 207 | 208 | # Update the commit date – in destiny – so it is the same to the date in origin. 209 | def update_destiny_server_commit_date_to_origin_commit_date(revision_number) 210 | 211 | end 212 | 213 | def no_changes_to_update?() 214 | system("cd /tmp/#{@svn_destiny_name} && svn status >/tmp/svninfo2") 215 | if File.zero?("/tmp/svninfo2") 216 | return true 217 | else 218 | return false 219 | end 220 | end 221 | 222 | def origin_repo_online_revision() 223 | repo_online_revision(@svn_origin_name) 224 | end 225 | 226 | def destiny_repo_online_revision() 227 | repo_online_revision(@svn_destiny_name) 228 | end 229 | 230 | def repo_online_revision(repo) 231 | system("svn info /tmp/"+repo+" > /tmp/svninfo") 232 | string_file = "" 233 | File.open("/tmp/svninfo","r").each do |line| 234 | string_file += line 235 | end 236 | revision_line = /Revision: \d+/.match(string_file).to_s 237 | revision = /\d+/.match(revision_line).to_s 238 | end 239 | 240 | def list_directory(directory) 241 | Dir.glob( File.join(directory, '**', '*') ) { |file| puts file } 242 | end 243 | 244 | def get_author_name(commit_number) 245 | system("cd /tmp/#{@svn_origin_name} && svn log --xml -r #{commit_number} >#{@log_file_of_origin}") 246 | file = File.open("#{@log_file_of_origin}", "r") 247 | log = REXML::Document.new(file) 248 | author = log.root.elements[1].elements["author"].text 249 | end 250 | 251 | def get_commit_message(commit_number) 252 | system("cd /tmp/#{@svn_origin_name} && svn log --xml -r #{commit_number} >#{@log_file_of_origin}") 253 | file = File.open("#{@log_file_of_origin}", "r") 254 | log = REXML::Document.new(file) 255 | msg = log.root.elements[1].elements["msg"].text 256 | msg = msg.gsub(/'|"/, " ") 257 | end 258 | end 259 | end --------------------------------------------------------------------------------