lib/svn2git/migration.rb in svn2git-2.2.5 vs lib/svn2git/migration.rb in svn2git-2.3.0

- old
+ new

@@ -1,7 +1,9 @@ require 'optparse' require 'pp' +require 'open4' +require 'timeout' module Svn2Git DEFAULT_AUTHORS_FILE = "~/.svn2git/authors" class Migration @@ -338,26 +340,82 @@ def run_command(cmd, exit_on_error=true, printout_output=false) log "Running command: #{cmd}" ret = '' + @mutex ||= Mutex.new + @stdin_queue ||= Queue.new - cmd = "2>&1 #{cmd}" - IO.popen(cmd) do |stdout| - if printout_output - stdout.each_char do |character| - $stdout.print character - ret << character - end - else + # 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. + status = (defined?(JRUBY_VERSION) ? IO : Open4).popen4(cmd) do |pid, stdin, stdout, stderr| + threads = [] + + threads << Thread.new(stdout) do |stdout| stdout.each do |line| - log line - ret << line + @mutex.synchronize do + ret << line + + if printout_output + $stdout.print line + else + log line + end + end end end + + threads << Thread.new(stderr) do |stderr| + # 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. + stderr.each(' ') do |word| + @mutex.synchronize do + ret << word + + if printout_output + $stdout.print word + else + log word + end + 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(stdin) do |stdin| + loop do + 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 + 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) + # JRuby's open4 doesn't return a Process::Status object when invoked with a block, but rather returns the + # block expression's value. The Process::Status is stored as $?, so we need to normalize the status + # object if on JRuby. + status = $? if defined?(JRUBY_VERSION) + + if exit_on_error && (status.exitstatus != 0) $stderr.puts "command failed:\n#{cmd}" exit -1 end ret