class Svn2Git::Migration

Attributes

dir[R]

Public Class Methods

checkout_svn_branch(branch) click to toggle source
# File lib/svn2git/migration.rb, line 163
def self.checkout_svn_branch(branch)
  "git checkout -b \"#{branch}\" \"remotes/svn/#{branch}\""
end
escape_quotes(str) click to toggle source
# File lib/svn2git/migration.rb, line 155
def self.escape_quotes(str)
  str.gsub(/'|"/) { |c| "\\#{c}" }
end
new(args) click to toggle source
# File lib/svn2git/migration.rb, line 13
def initialize(args)
  @options = parse(args)
  if @options[:rebase]
     show_help_message('Too many arguments') if args.size > 0
     verify_working_tree_is_clean
  elsif @options[:rebasebranch]
     show_help_message('Too many arguments') if args.size > 0
     verify_working_tree_is_clean
  else
     show_help_message('Missing SVN_URL parameter') if args.empty?
     show_help_message('Too many arguments') if args.size > 1
     @url = args.first.gsub(' ', "\\ ")
  end
end

Public Instance Methods

escape_quotes(str) click to toggle source
# File lib/svn2git/migration.rb, line 159
def escape_quotes(str)
  Svn2Git::Migration.escape_quotes(str)
end
parse(args) click to toggle source
# File lib/svn2git/migration.rb, line 42
def parse(args)
  # Set up reasonable defaults for options.
  options = {}
  options[:verbose] = false
  options[:metadata] = false
  options[:nominimizeurl] = false
  options[:rootistrunk] = false
  options[:trunk] = 'trunk'
  options[:branches] = []
  options[:tags] = []
  options[:exclude] = []
  options[:revision] = nil
  options[:username] = nil
  options[:password] = nil
  options[:rebasebranch] = false

  if File.exist?(File.expand_path(DEFAULT_AUTHORS_FILE))
    options[:authors] = DEFAULT_AUTHORS_FILE
  end


  # Parse the command-line arguments.
  @opts = OptionParser.new do |opts|
    opts.banner = 'Usage: svn2git SVN_URL [options]'

    opts.separator ''
    opts.separator 'Specific options:'

    opts.on('--rebase', 'Instead of cloning a new project, rebase an existing one against SVN') do
      options[:rebase] = true
    end

    opts.on('--username NAME', 'Username for transports that needs it (http(s), svn)') do |username|
      options[:username] = username
    end

    opts.on('--password PASSWORD', 'Password for transports that need it (http(s), svn)') do |password|
      options[:password] = password
    end

    opts.on('--trunk TRUNK_PATH', 'Subpath to trunk from repository URL (default: trunk)') do |trunk|
      options[:trunk] = trunk
    end

    opts.on('--branches BRANCHES_PATH', 'Subpath to branches from repository URL (default: branches); can be used multiple times') do |branches|
      options[:branches] << branches
    end

    opts.on('--tags TAGS_PATH', 'Subpath to tags from repository URL (default: tags); can be used multiple times') do |tags|
      options[:tags] << tags
    end

    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
      options[:rootistrunk] = true
      options[:trunk] = nil
      options[:branches] = nil
      options[:tags] = nil
    end

    opts.on('--notrunk', 'Do not import anything from trunk') do
      options[:trunk] = nil
    end

    opts.on('--nobranches', 'Do not try to import any branches') do
      options[:branches] = nil
    end

    opts.on('--notags', 'Do not try to import any tags') do
      options[:tags] = nil
    end

    opts.on('--no-minimize-url', 'Accept URLs as-is without attempting to connect to a higher level directory') do
      options[:nominimizeurl] = true
    end

    opts.on('--revision START_REV[:END_REV]', 'Start importing from SVN revision START_REV; optionally end at END_REV') do |revision|
      options[:revision] = revision
    end

    opts.on('-m', '--metadata', 'Include metadata in git logs (git-svn-id)') do
      options[:metadata] = true
    end

    opts.on('--authors AUTHORS_FILE', "Path to file containing svn-to-git authors mapping (default: #{DEFAULT_AUTHORS_FILE})") do |authors|
      options[:authors] = authors
    end

    opts.on('--exclude REGEX', 'Specify a Perl regular expression to filter paths when fetching; can be used multiple times') do |regex|
      options[:exclude] << regex
    end

    opts.on('-v', '--verbose', 'Be verbose in logging -- useful for debugging issues') do
      options[:verbose] = true
    end

    opts.on('--rebasebranch REBASEBRANCH', 'Rebase specified branch.') do |rebasebranch|
      options[:rebasebranch] = rebasebranch
    end

    opts.separator ""

    # No argument, shows at tail.  This will print an options summary.
    # Try it and see!
    opts.on_tail('-h', '--help', 'Show this message') do
      puts opts
      exit
    end
  end

  @opts.parse! args
  options
end
run!() click to toggle source
# File lib/svn2git/migration.rb, line 28
def run!
  if @options[:rebase]
    get_branches
  elsif @options[:rebasebranch]
    get_rebasebranch
  else
    clone!
  end
  fix_branches
  fix_tags
  fix_trunk
  optimize_repos
end

Private Instance Methods

clone!() click to toggle source
# File lib/svn2git/migration.rb, line 169
def clone!
  trunk = @options[:trunk]
  branches = @options[:branches]
  tags = @options[:tags]
  metadata = @options[:metadata]
  nominimizeurl = @options[:nominimizeurl]
  rootistrunk = @options[:rootistrunk]
  authors = @options[:authors]
  exclude = @options[:exclude]
  revision = @options[:revision]
  username = @options[:username]
  password = @options[:password]

  if rootistrunk
    # Non-standard repository layout.  The repository root is effectively 'trunk.'
    cmd = "git svn init --prefix=svn/ "
    cmd += "--username='#{username}' " unless username.nil?
    cmd += "--password='#{password}' " unless password.nil?
    cmd += "--no-metadata " unless metadata
    if nominimizeurl
      cmd += "--no-minimize-url "
    end
    cmd += "--trunk='#{@url}'"
    run_command(cmd, true, true)

  else
    cmd = "git svn init --prefix=svn/ "

    # Add each component to the command that was passed as an argument.
    cmd += "--username='#{username}' " unless username.nil?
    cmd += "--password='#{password}' " unless password.nil?
    cmd += "--no-metadata " unless metadata
    if nominimizeurl
      cmd += "--no-minimize-url "
    end
    cmd += "--trunk='#{trunk}' " unless trunk.nil?
    unless tags.nil?
      # Fill default tags here so that they can be filtered later
      tags = ['tags'] if tags.empty?
      # Process default or user-supplied tags
      tags.each do |tag|
        cmd += "--tags='#{tag}' "
      end
    end
    unless branches.nil?
      # Fill default branches here so that they can be filtered later
      branches = ['branches'] if branches.empty?
      # Process default or user-supplied branches
      branches.each do |branch|
        cmd += "--branches='#{branch}' "
      end
    end

    cmd += @url

    run_command(cmd, true, true)
  end

  run_command("#{git_config_command} svn.authorsfile #{authors}") unless authors.nil?

  cmd = "git svn fetch "
  unless revision.nil?
    range = revision.split(":")
    range[1] = "HEAD" unless range[1]
    cmd += "-r #{range[0]}:#{range[1]} "
  end
  unless exclude.empty?
    # Add exclude paths to the command line; some versions of git support
    # this for fetch only, later also for init.
    regex = []
    unless rootistrunk
      regex << "#{trunk}[/]" unless trunk.nil?
      tags.each{|tag| regex << "#{tag}[/][^/]+[/]"} unless tags.nil? or tags.empty?
      branches.each{|branch| regex << "#{branch}[/][^/]+[/]"} unless branches.nil? or branches.empty?
    end
    regex = '^(?:' + regex.join('|') + ')(?:' + exclude.join('|') + ')'
    cmd += "--ignore-paths='#{regex}' "
  end
  run_command(cmd, true, true)

  get_branches
end
fix_branches() click to toggle source
# File lib/svn2git/migration.rb, line 328
def fix_branches
  svn_branches = @remote - @tags
  svn_branches.delete_if { |b| b.strip !~ %r{^svn\/} }

  if @options[:rebase]
     run_command("git svn fetch", true, true)
  end

  svn_branches.each do |branch|
    branch = branch.gsub(/^svn\//,'').strip
    if @options[:rebase] && (@local.include?(branch) || branch == 'trunk')
       lbranch = branch
       lbranch = 'master' if branch == 'trunk'
       run_command("git checkout -f \"#{lbranch}\"")
       run_command("git rebase \"remotes/svn/#{branch}\"")
       next
    end

    next if branch == 'trunk' || @local.include?(branch)

    if @cannot_setup_tracking_information
      run_command(Svn2Git::Migration.checkout_svn_branch(branch))
    else
      status = run_command("git branch --track \"#{branch}\" \"remotes/svn/#{branch}\"", false)

      # As of git 1.8.3.2, tracking information cannot be set up for remote SVN branches:
      # http://git.661346.n2.nabble.com/git-svn-Use-prefix-by-default-td7594288.html#a7597159
      #
      # Older versions of git can do it and it should be safe as long as remotes aren't pushed.
      # Our --rebase option obviates the need for read-only tracked remotes, however.  So, we'll
      # deprecate the old option, informing those relying on the old behavior that they should
      # use the newer --rebase otion.
      if status =~ /fatal:.+'#{branch}'.+/
        @cannot_setup_tracking_information = true
        run_command(Svn2Git::Migration.checkout_svn_branch(branch))
      else
        unless @legacy_svn_branch_tracking_message_displayed
          warn '*' * 68
          warn "svn2git warning: Tracking remote SVN branches is deprecated."
          warn "In a future release local branches will be created without tracking."
          warn "If you must resync your branches, run: svn2git --rebase"
          warn '*' * 68
        end

        @legacy_svn_branch_tracking_message_displayed = true

        run_command("git checkout \"#{branch}\"")
      end
    end
  end
end
fix_tags() click to toggle source
# File lib/svn2git/migration.rb, line 290
def fix_tags
  current = {}
  current['user.name']  = run_command("#{git_config_command} --get user.name", false)
  current['user.email'] = run_command("#{git_config_command} --get user.email", false)

  @tags.each do |tag|
    tag = tag.strip
    id      = tag.gsub(%r{^svn\/tags\/}, '').strip
    subject = run_command("git log -1 --pretty=format:'%s' \"#{escape_quotes(tag)}\"").chomp("'").reverse.chomp("'").reverse
    date    = run_command("git log -1 --pretty=format:'%ci' \"#{escape_quotes(tag)}\"").chomp("'").reverse.chomp("'").reverse
    author  = run_command("git log -1 --pretty=format:'%an' \"#{escape_quotes(tag)}\"").chomp("'").reverse.chomp("'").reverse
    email   = run_command("git log -1 --pretty=format:'%ae' \"#{escape_quotes(tag)}\"").chomp("'").reverse.chomp("'").reverse
    run_command("#{git_config_command} user.name \"#{escape_quotes(author)}\"")
    run_command("#{git_config_command} user.email \"#{escape_quotes(email)}\"")

    original_git_committer_date = ENV['GIT_COMMITTER_DATE']
    ENV['GIT_COMMITTER_DATE'] = escape_quotes(date)
    run_command("git tag -a -m \"#{escape_quotes(subject)}\" \"#{escape_quotes(id)}\" \"#{escape_quotes(tag)}\"")
    ENV['GIT_COMMITTER_DATE'] = original_git_committer_date

    run_command("git branch -d -r \"#{escape_quotes(tag)}\"")
  end

ensure
  # 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.
  unless @tags.empty?
    current.each_pair do |name, value|
      # If a line was read, then there was a config value so restore it.
      # Otherwise unset the value because originally there was none.
      if value.strip != ''
        run_command("#{git_config_command} #{name} \"#{value.strip}\"")
      else
        run_command("#{git_config_command} --unset #{name}")
      end
    end
  end
end
fix_trunk() click to toggle source
# File lib/svn2git/migration.rb, line 380
def fix_trunk
  trunk = @remote.find { |b| b.strip == 'trunk' }
  if trunk && ! @options[:rebase]
    run_command("git checkout svn/trunk")
    run_command("git branch -D master")
    run_command("git checkout -f -b master")
  else
    run_command("git checkout -f master")
  end
end
get_branches() click to toggle source
# File lib/svn2git/migration.rb, line 252
def get_branches
  # Get the list of local and remote branches, taking care to ignore console color codes and ignoring the
  # '*' character used to indicate the currently selected branch.
  @local = run_command("git branch -l --no-color").split(/\n/).collect{ |b| b.gsub(/\*/,'').strip }
  @remote = run_command("git branch -r --no-color").split(/\n/).collect{ |b| b.gsub(/\*/,'').strip }

  # Tags are remote branches that start with "tags/".
  @tags = @remote.find_all { |b| b.strip =~ %r{^svn\/tags\/} }

end
get_rebasebranch() click to toggle source
# File lib/svn2git/migration.rb, line 263
def get_rebasebranch
      get_branches 
      @local = @local.find_all{|l| l == @options[:rebasebranch]}
      @remote = @remote.find_all{|r| r.include? @options[:rebasebranch]}

  if @local.count > 1 
    pp "To many matching branches found (#{@local})."
    exit 1
  elsif @local.count == 0
        pp "No local branch named \"#{@options[:rebasebranch]}\" found."
    exit 1
  end

  if @remote.count > 2 # 1 if remote is not pushed, 2 if its pushed to remote
    pp "To many matching remotes found (#{@remotes})"
    exit 1
  elsif @remote.count == 0
        pp "No remote branch named \"#{@options[:rebasebranch]}\" found."
    exit 1
  end
      pp "Local branches \"#{@local}\" found"
      pp "Remote branches \"#{@remote}\" found"

  @tags = [] # We only rebase the specified branch

end
git_config_command() click to toggle source
# File lib/svn2git/migration.rb, line 479
def git_config_command
  if @git_config_command.nil?
    status = run_command('git config --local --get user.name', false)

    @git_config_command = if status =~ /error: .+\s.+git config \[.+/m
                            'git config'
                          else
                            'git config --local'
                          end
  end

  @git_config_command
end
log(msg) click to toggle source
# File lib/svn2git/migration.rb, line 461
def log(msg)
  print msg if @options[:verbose]
end
optimize_repos() click to toggle source
# File lib/svn2git/migration.rb, line 391
def optimize_repos
  run_command("git gc")
end
run_command(cmd, exit_on_error=true, printout_output=false) click to toggle source
# File lib/svn2git/migration.rb, line 395
def run_command(cmd, exit_on_error=true, printout_output=false)
  log "Running command: #{cmd}\n"

  ret = ''
  @stdin_queue ||= Queue.new

  # We need to fetch input from the user to pass through to the underlying sub-process.  We'll constantly listen
  # for input and place any received values on a queue for consumption by a pass-through thread that will forward
  # the contents to the underlying sub-process's stdin pipe.
  @stdin_thread ||= Thread.new do
    loop { @stdin_queue << $stdin.gets.chomp }
  end

  # Open4 forks, which JRuby doesn't support.  But JRuby added a popen4-compatible method on the IO class,
  # so we can use that instead.
  IO.popen("2>&1 #{cmd}") do |output|
    threads = []

    threads << Thread.new(output) do |output|
      # git-svn seems to do all of its prompting for user input via STDERR.  When it prompts for input, it will
      # not terminate the line with a newline character, so we can't split the input up by newline.  It will,
      # however, use a space to separate the user input from the prompt.  So we split on word boundaries here
      # while draining STDERR.
      output.each(' ') do |word|
        ret << word

        if printout_output
          $stdout.print word
        else
          log word
        end
      end
    end

    # Simple pass-through thread to take anything the user types via STDIN and passes it through to the
    # sub-process's stdin pipe.
    Thread.new do
      loop do
        begin
          user_reply = @stdin_queue.pop

          # nil is our cue to stop looping (pun intended).
          break if user_reply.nil?

          $stdin.puts user_reply
          $stdin.close
        rescue IOError
          $stdout.print "No input requested.\n"
        end
      end
    end

    threads.each(&:join)

    # Push nil to the stdin_queue to gracefully exit the STDIN pass-through thread.
    @stdin_queue << nil
  end

  if exit_on_error && $?.exitstatus != 0
    $stderr.puts "command failed:\n#{cmd}"
    exit -1
  end

  ret
end
show_help_message(msg) click to toggle source
# File lib/svn2git/migration.rb, line 465
def show_help_message(msg)
  puts "Error starting script: #{msg}\n\n"
  puts @opts.help
  exit
end
verify_working_tree_is_clean() click to toggle source
# File lib/svn2git/migration.rb, line 471
def verify_working_tree_is_clean
  status = run_command('git status --porcelain --untracked-files=no')
  unless status.strip == ''
    puts 'You have local pending changes.  The working tree must be clean in order to continue.'
    exit -1
  end
end