Skip to content

Instantly share code, notes, and snippets.

@henderea
Last active August 29, 2015 14:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save henderea/6e779b66be3580c9a584 to your computer and use it in GitHub Desktop.
Save henderea/6e779b66be3580c9a584 to your computer and use it in GitHub Desktop.
A Ruby script for doing a svn relocate when using git-svn
#!/usr/bin/env ruby
Signal.trap('SIGINT') {
Process.waitall
puts
exit 1
}
require 'readline'
require 'fileutils'
def puts_header(str)
l = str.length
puts "\n\e[1m#{'#' * (l + 4)}\n# #{str} #\n#{'#' * (l + 4)}\e[0m\n\n"
end
# Credit for this mdoule (other than a few modifications) goes to http://www.sanityinc.com/articles/relocating-git-svn-repositories/ and https://gist.github.com/purcell/591602
module CopyBranch
module_function
# There is a 'git' gem, but I didn't want to figure out how to call it to get the functionality I needed. The following
# methods were first tested on the command line, and originally came from this blog post:
# http://www.sanityinc.com/articles/relocating-git-svn-repositories.
# find git branches whose latest commit hasn't been checked into svn
def local_branches(repo_path)
result = []
%x[(cd "#{repo_path}" && git branch | cut -c3-)].split("\n").each do |branch|
result << branch unless %x[(cd #{repo_path} && git log -1 --pretty=full #{branch})] =~ /git-svn-id:/
end
result
end
# return the git commit sha for the newest commit that *has* been checked into svn
def newest_svn_commit_on_branch(repo_path, branch)
%x[(cd "#{repo_path}" && git rev-list -n1 #{branch} --grep=git-svn-id:)].strip
end
# return the svn revision number corresponding to a git commit sha
def find_svn_rev_for_commit(repo_path, commit)
%x[(cd "#{repo_path}" && git svn find-rev #{commit})].strip
end
# return the git commit sha for the commit corresponding to an svn revision
def find_commit_for_svn_rev(repo_path, svn_rev, base_branch)
%x[(cd "#{repo_path}" && git rev-list #{base_branch} --grep="git-svn-id:.*@#{svn_rev}")].strip
end
def branch_exists?(repo_path, branch)
%x[(cd "#{repo_path}" && git branch -M #{branch} #{branch} &> /dev/null)]
$? == 0
end
# create a branch on a git repo from a certain git commit and checkout that branch
def create_branch_and_checkout(repo_path, commit, branch)
%x[(cd "#{repo_path}" && git checkout -b #{branch} #{commit})]
end
# return a patch containing every commit in a branch on a repo, starting from a certain commit
def create_patch(repo_path, commit, branch)
%x[(cd "#{repo_path}" && git format-patch --stdout #{commit}..#{branch})]
end
# apply a patch to the current branch of a repo
def apply_patch(repo_path, patch)
%x[(cd "#{repo_path}" && echo #{patch} | git am)]
end
# create a patch from the src repo and apply it to the dest repo
def copy_branch_commits(src_repo_path, src_branch, src_branch_point, dest_repo_path)
%x[(cd "#{src_repo_path}" && git format-patch --stdout #{src_branch_point}..#{src_branch}) | (cd "#{dest_repo_path}" && git am)]
end
def copy_branch_itr(src_repo_path, src_branch, base_branch, dest_repo_path)
src_branch_point = newest_svn_commit_on_branch(src_repo_path, src_branch)
raise "Couldn't find an svn commit on branch '#{src_branch}' in repo '#{src_repo_path}'" if src_branch_point.empty?
svn_revision = find_svn_rev_for_commit(src_repo_path, src_branch_point)
raise "Couldn't extract the svn revision for commit '#{src_branch_point}' in repo '#{src_repo_path}'" if svn_revision.empty?
dest_branch_point = find_commit_for_svn_rev(dest_repo_path, svn_revision, base_branch)
raise "Couldn't find the git commit containing svn revision '#{svn_revision}' in repo '#{dest_repo_path}'" if dest_branch_point.empty?
create_branch_and_checkout(dest_repo_path, dest_branch_point, src_branch)
copy_branch_commits(src_repo_path, src_branch, src_branch_point, dest_repo_path)
end
def copy_branch(src, dest, base_branch, branch_name = nil)
src_repo = File.expand_path(src)
dest_repo = File.expand_path(dest)
[src_repo, dest_repo].each do |path|
unless File.exist?(path) && File.directory?(path)
puts "directory #{path} doesn't exist or isn't a directory"
exit 1
end
end
if branch_name
if branch_exists?(dest_repo, branch_name)
puts "** Skipped branch [#{branch_name}] because it already exists"
else
copy_branch_itr(src_repo, branch_name, base_branch, dest_repo)
end
else
local_branches(src_repo).each do |branch|
if branch_exists?(dest_repo, branch)
puts "** Skipped branch [#{branch}] because it already exists"
else
puts "Copying branch #{branch}"
copy_branch_itr(src_repo, branch, base_branch, dest_repo)
puts " Copy successful"
end
end
end
end
end
# Must be called with one command-line arg.
# Example: git-svn-relocate.rb https://new.server
if ARGV.count < 1
puts "Please invoke this script with one command-line argument (new SVN URL)."
exit 1
end
initial_branch = nil
branches_str = `git branch --list --no-color 2>&1`.chomp
branches = branches_str.split(/\n/).map { |b|
rv = b.gsub(/^[*]?\s*(.+)\s*/, '\1')
initial_branch = rv if b.start_with?('*')
rv
}
branches = branches - [initial_branch]
branches << initial_branch
initial_branch_real = initial_branch
puts "Local branches: \n#{branches.join("\n")}\n\n"
puts_header 'Checking for un-pushed changes in local branches'
has_unpushed = []
branches.each { |b|
`git checkout #{b} 2>&1`
result = `git log --oneline @{u}..HEAD 2>&1`.chomp
unless result.strip.empty?
puts "Branch #{b} has un-pushed changes!"
has_unpushed << b
end
}
create_copy = false
unless has_unpushed.empty?
if has_unpushed.include?(initial_branch)
initial_branch = (branches - has_unpushed).first
if initial_branch.nil?
puts "You have no branches without un-pushed changes! This script requires at least one branch with all changes pushed in order to work. Exiting."
exit 1
end
puts "Initial branch #{initial_branch_real} had unpushed changes. Using #{initial_branch} instead."
`git checkout #{initial_branch} 2>&1`
end
puts_header 'IMPORTANT NOTE!'
puts "If you have local commits that have not been pushed to the svn server, even if they are in a different branch, this script may not work properly \e[1mand may mess up your repository\e[0m."
puts "Please cancel and do the following:\n1) manually back up the un-pushed changes\n2) delete the branches with un-pushed changes\n3) run this script again\n4) manually restore the un-pushed changes and any deleted branches.\n\n"
puts "This script can attempt to perform these steps automatically. It will create a copy of the repo that you can choose whether or not to delete at the end of the operation.\n\n"
ans = Readline.readline("Continue and attempt automatically? (y/n) ", true).chomp
unless ans.downcase == 'y' || ans.downcase == 'yes'
exit 1
end
create_copy = true
else
puts "No unpushed changes\n\n"
ans = Readline.readline("Create repo copy? (y/n) ", true).chomp
create_copy = ans.downcase == 'y' || ans.downcase == 'yes'
end
if create_copy
puts_header 'Creating copy of repo'
cur_dir = Dir.getwd
dir_name = cur_dir[(cur_dir.rindex('/')+1)..-1]
new_dir = File.expand_path("../#{dir_name}.backup-#{Random.rand}")
FileUtils.cp_r(cur_dir, new_dir)
end
oldUrl_init = `git config --get svn-remote.svn.url`.chomp
newUrl_init = ARGV[0]
puts "old URL from git config: \e[1m#{oldUrl_init}\e[0m"
loop do
puts "new URL provided: \e[1m#{newUrl_init}\e[0m"
ans = Readline.readline("continue with this URL? (y/n) ", true).chomp
unless ans.downcase == 'y' || ans.downcase == 'yes'
nurl = Readline.readline("enter new URL (enter to cancel): ", true).chomp
if nurl && !nurl.strip.empty?
newUrl_init = nurl.strip
else
break
end
else
break
end
end
unless has_unpushed.empty?
puts_header 'Deleting branches with un-pushed changes from main repo'
has_unpushed.each { |hu| `git branch -D #{hu} 2>&1` }
end
# Prepare URLs for regex search and replace.
oldUrl = oldUrl_init.gsub(/[.]/, '\\\.')
newUrl = newUrl_init.gsub(/[&]/, '\\\&')
oldUrl2 = oldUrl_init.gsub(%r{//(.+?[@])}, '//').gsub(/[.]/, '\\\.')
newUrl2 = newUrl_init.gsub(%r{//(.+?[@])}, '//').gsub(/[&]/, '\\\&')
filter="sed \"s|git-svn-id: #{oldUrl2}|git-svn-id: #{newUrl2}|g\""
puts_header "beginning git filter-branch"
system("git filter-branch --msg-filter '#{filter}' -- --all")
puts_header "beginning .git/config url change"
system("sed -i.backup -e 's|#{oldUrl}|#{newUrl}|g' .git/config")
puts_header "beginning removing .git/svn"
system('rm -rf .git/svn')
puts_header "beginning git svn rebase"
system('git svn rebase')
unless has_unpushed.empty?
puts_header 'Copying branches from copy back to main version'
has_unpushed.each { |hu|
loop do
begin
puts "preparing to copy branch \e[1m#{hu}\e[0m"
base_branch = initial_branch
loop do
ans = Readline.readline("Base off of branch \e[1m#{base_branch}\e[0m? (y/n) ", true).chomp
if ans.downcase == 'y' || ans.downcase == 'yes'
break
end
nbranch = Readline.readline("Please enter the branch to base off of (enter to use already selected one): ", true).chomp
if nbranch && !nbranch.strip.empty?
base_branch = nbranch.strip
else
break
end
end
puts "beginning copying branch \e[1m#{hu}\e[0m based off of branch \e[1m#{base_branch}\e[0m"
CopyBranch.copy_branch(new_dir, cur_dir, base_branch, hu)
break
rescue StandardError => e
puts "copying branch \e[1m#{hu}\e[0m based off of branch \e[1m#{base_branch}\e[0m failed: #{e.message}"
puts e.backtrace.join("\n")
end
end
}
end
`git checkout #{initial_branch_real} 2>&1`
loop do
begin
puts_header 'Fetching latest contents from svn to finish rebuilding repo.'
ans = Readline.readline("Start fetch at a specific revision? (y/n) ", true).chomp
if ans.downcase == 'y' || ans.downcase == 'yes'
rev = Readline.readline("What svn revision should it start at? (enter or <= 0 to start at the beginning) ", true).chomp
unless rev.strip.empty? || rev.strip.to_i <= 0
system("git svn fetch svn -r #{rev.strip}:HEAD")
break
end
end
system('git svn fetch svn')
break
rescue Exception => e
ans = Readline.readline("\n\nDid fetch freeze and you want to try fetching again (n to exit)? (y/n) ", true).chomp
unless ans.downcase == 'y' || ans.downcase == 'yes'
exit 0
end
end
end
if new_dir
puts "\n\nRepo copy is located at #{new_dir}"
ans = Readline.readline("Delete repo copy? (y/n) ", true).chomp
unless ans.downcase == 'y' || ans.downcase == 'yes'
exit 0
end
FileUtils.rm_r(new_dir)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment