rakelib/scm.rb
author Jan Vrany <jan.vrany@fit.cvut.cz>
Thu, 24 Nov 2016 20:47:41 +0000
changeset 85 6d918f722075
parent 81 2a1efb99c83d
child 88 112075e99cef
permissions -rw-r--r--
Do not tamper with SSH configuration if SSH agent is not running. Sigh, we should not tamper with SSH configuration wildly. User may have her ssh and mercurial properly configured to use `plink.exe` and `pageant`. If we just start using `ssh.exe` clone/pull might not work beause `ssh.exe` cannot talk to `pageant`. So, if we don't fine OpenSSH's style of agent, don't use `ssh.exe` event if available.

require 'rakelib/hglib'

module Rake
end

module Rake::StX
end

# Cross-platform way of finding an executable in the $PATH.
#
#   which('ruby') #=> /usr/bin/ruby
def which(cmd)
  exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
  ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
    exts.each { |ext|
      exe = File.join(path, "#{cmd}#{ext}")
      return exe if File.executable?(exe) && !File.directory?(exe)
    }
  end
  return nil
end

cvs_rsh_set = false

module Rake::Stx::SCM
  # Not quite sure why following code
  #
  #     include RakeFileUtils
  #
  # does not make extra methods (like `sh`) defined in `extensions.rb`
  # visible here. To workaround, define them here. Ugly...
  module_function
  def sh(cmd, cwd: Dir.pwd, &block)
    begin 
      return RakeFileUtils::sh(cmd, cwd: cwd, &block)
    rescue
      return false      
    end
  end

  # Make sure CVS_RSH environment variable is properly set. Prefer MSYS2 ssh.exe
  # over plink.exe. For details, see `hglib.rb`, method `sshconf()`  
  module_function
  def ensure_cvs_rsh_set()        
    if @cvs_rsh_set then
      return
    end
    ssh = nil
    ssh_configured = ENV['CVS_RSH']    
    ssh_in_path = which('ssh') ? true : false
    plink_in_path = which('plink') ? true : false
    if Gem.win_platform? then                        
      # If CVS_RSH is not set or is set to plink.exe, try to change to 
      # MSYS2 ssh.exe as it gives better performance on (fast) LANs.      
      if /^.*[pP]link(\.exe)?"?\s*(-ssh)?\s*(-2)?$/ =~ ssh_configured then            
        if ssh_in_path then
          ssh = 'ssh'
        else 
          if (File.exist? "c:\\msys64\\usr\\bin\\ssh.exe") then              
            ssh = "\"c:\\msys64\\usr\\bin\\ssh.exe\""
          end
        end
        # Sigh, we should not tamper with SSH configuration wildly. User may have
        # her ssh and mercurial properly configured to use `plink.exe` and `pageant`.
        # If we just start using `ssh.exe` clone/pull might not work beause
        # `ssh.exe` cannot talk to `pageant`. So, if we don't find OpenSSH's 
        # style of agent, don't use `ssh.exe` event if available.
        if ssh then
          if ENV['SSH_AUTH_SOCK'] then
            # Good, OpenSSH agent running but still, be nice and  tell the 
            # user SSH configuration has been tampered wirh. 
            info("Setting CVS_RSH=\"#{ssh}\" for faster transfers")
          else 
            # No agent, no fun. Be nice and give user a hit
            warn("Not using CVS_RSH=\"#{ssh}\" option because SSH agent is not running")
            warn("For faster CVS checkout over LAN, consider using ssh-agent or ssh-pageant (if you want to use PuTTY's pageant)")
            ssh = nil
          end
        end
      end       
    else                 
      if not ssh_configured then
        ssh = "ssh"
      end
    end
    if ssh then 
      ENV['CVS_RSH'] = ssh
    end
    cvs_rsh_set = true
  end


  public

  class CheckoutException < Exception
  end # class CheckoutException


  def self._check_type(type)
    if (type != :cvs and type != :svn and type != :git and type != :hg)
      raise CheckoutException.new("Unknown version control system type (#{type})")
    end
  end

  def self.update(repository, directory, **kwargs)
    type = repository.type
    url = repository.canonical
    self._check_type(type)    
    root = kwargs[:root] || BUILD_DIR
    branch = kwargs[:branch]
    if branch == nil
      if type == :svn
        branch = 'trunk'
      elsif type == :hg
        branch = 'default'
      end
    end

    wc = root / directory
    if (! File.exist? wc)
      self.checkout(repository, directory, **kwargs)
      return
    end
    case type
      when :svn    then _update_svn(repository, directory, branch, root, **kwargs)
      when :cvs    then _update_cvs(repository, directory, branch, root, **kwargs)
      when :git    then _update_git(repository, directory, branch, root, **kwargs)
      when :hg     then _update_hg(repository, directory, branch, root, **kwargs)
    end
  end

  def self._update_hg(repository, directory, branch, root, **kwargs)
    
    wc = root / directory
    separator = kwargs[:separator] || '.'
    revision =  kwargs[:revision] 
    if directory != nil then      
      url = "#{repository.canonical}/#{directory.gsub('/', separator)}"
    end
    hg = HG::Repository.new(wc)
    begin
      paths = hg.paths
      if repository.staging then
        if not paths.has_key? 'staging'           
          paths['staging'] = "#{repository.staging}/#{directory.gsub('/', separator)}"
          hg.paths = paths
        end
        hg.pull('staging')
      end
      if not paths.has_key? 'canonical'
        paths['canonical'] = "#{repository.canonical}/#{directory.gsub('/', separator)}"
        hg.paths = paths
      end
      hg.pull('default')
      if paths['default'] != paths['canonical'] then
        hg.pull('canonical')
      end
      # If revision is not specified, then look for an active bookmark
      # and update to it. If no bookmark is active, then look for bookmark
      # `master`. If it exist, then update to `master`. If it 
      # does not, then update to tip or throw an error.
      # The error is thrown if there's no bookmark `master` and
      # branch has multiple heads since it's not clear which 
      # head rev to use.
      if not revision then
        revision = hg.bookmark()
        if not revision then
          bookmarks = hg.bookmarks(branch)
          if (bookmarks.has_key? 'master') then
            revision = 'master'
          else
            if (hg.heads(branch).size > 1) then
              raise CheckoutException.new("HG: Cannot checkout #{directory}: branch #{branch} has multiple heads but no bookmark named 'master'!")
            end            
          end          
        end
      end
      hg.update(revision || branch)
    rescue Exception => ex 
      raise CheckoutException.new("HG: Cannot update #{wc}: #{ex.message}")
    end
  end


  def self._update_svn(repository, directory, branch, root, **kwargs)
    wc = root / directory
    if not sh %W{svn --non-interactive --trust-server-cert update}, cwd: wc
      raise CheckoutException.new("SVN: Cannot update #{wc}")
    end
  end

  def self._update_cvs(repository, directory, branch, root, **kwargs)
    ensure_cvs_rsh_set()
    wc = root / directory
    if File.directory? wc
      if not sh %W{cvs -z 9 update -A -d}, cwd: wc
        raise CheckoutException.new("CVS: Cannot update #{wc}")
      end
    else
      if not sh %W{cvs -z 9 update -A #{File.basename(wc)}}, cwd: File.dirname(wc)
        raise CheckoutException.new("CVS: Cannot update #{wc}")
      end
    end
  end

  def self.checkout(repository, directory, **kwargs)    
    type = repository.type
    url = repository.canonical
    self._check_type(type)
    
    root = kwargs[:root] || BUILD_DIR
    branch = kwargs[:branch]
    if branch == nil
      if type == :svn
        branch = 'trunk'
      elsif type == :hg
        branch = 'default'
      end
    end

    wc = root / directory
    if (File.exist? wc)
      self.update(repository, directory, **kwargs)
      return
    end

    if (not File.exists? File.dirname(wc))
      begin
        FileUtils.mkdir_p(File.dirname(wc))
      rescue => ex
        raise CheckoutException.new("Cannot create directory for working copy (#{ex})")
      end
    end
    case type
      when :svn    then _checkout_svn(repository, directory, branch, root, **kwargs)
      when :cvs    then _checkout_cvs(repository, directory, branch, root, **kwargs)
      when :git    then _checkout_git(repository, directory, branch, root, **kwargs)
      when :hg     then _checkout_hg(repository, directory, branch, root, **kwargs)      
    end

  end

  def self._checkout_svn(repository, directory, branch, root, **kwargs)    
    url = "#{repository.canonical}/#{directory}/#{branch}"
    if not sh %W{svn --non-interactive --trust-server-cert co #{url} #{directory}}, cwd: root
      raise CheckoutException.new("SVN: Cannot checkout from #{url}")
    end
  end

  def self._checkout_hg(repository, directory, branch, root, **kwargs)    
    separator = kwargs[:separator] || '.'
    revision =  kwargs[:revision] 
    
    paths = { 'default' => "#{repository.upstream}/#{directory.gsub('/', separator)}",
              'canonical' => "#{repository.canonical}/#{directory.gsub('/', separator)}" }            
    if repository.staging then
      paths['staging'] = "#{repository.staging}/#{directory.gsub('/', separator)}"
    end
    
    begin
      if repository.staging then
        hg = HG::Repository.clone(paths['staging'], root / directory, noupdate: true)
        hg.paths = paths
        hg.pull('default')  
      else
        hg = HG::Repository.clone(paths['default'], root / directory, noupdate: true)
        hg.paths = paths
      end
      
      if paths['default'] != paths['canonical'] then
        hg.pull('canonical')
      end
      # If revision is not specified, then look for bookmark
      # `master`. If it exist, then check out `master`. If it 
      # does not, then checkout tip or throw an error.
      # The error is thrown if there's no bookmark `master` and
      # branch has multiple heads since it's not clear which 
      # head rev to use.
      if not revision then
        bookmarks = hg.bookmarks(branch)
        if (bookmarks.has_key? 'master') then
          revision = 'master'
        else
          if (hg.heads(branch).size > 1) then
            raise CheckoutException.new("HG: Cannot checkout #{directory}: branch #{branch} has multiple heads but no bookmark named 'master'!")
          end            
        end          
      end

      hg.update(revision || branch)
    #rescue Exception => e
    #  raise CheckoutException.new("HG: Cannot clone from #{url}: #{e.message}")
    end
  end

  def self._checkout_cvs(repository, directory, branch, root, **kwargs)    
    ensure_cvs_rsh_set()
    if not sh %W{cvs -z 9 -d #{repository.canonical} co #{directory}}, cwd: root
      raise CheckoutException.new("CVS: Cannot checkout #{directory}from #{repository.url}")
    end
  end
end # module Rake::Stx::SCM

def checkout(repo_name, directory, **kwargs)
  # repository should be symbolic name
  repo = Rake::Stx::Configuration::Repository::find(repo_name)
  if not repo then
      error("checkout(): No repository found (#{repo_name})")
  end  
  kwargs[:separator] = repo.separator
  Rake::Stx::SCM.checkout(repo, directory, **kwargs)  
end

def update(repo_name, directory, **kwargs)
  # repository should be symbolic name
  repo = Rake::Stx::Configuration::Repository::find(repo_name)
  if not repo then  
    error("update(): No repository found (#{repo_name})")
  end  
  kwargs[:separator] = repo.separator
  Rake::Stx::SCM.update(repo, directory, **kwargs)
end

def cvs(url, directory, **kwargs)
  repo = Rake::Stx::Configuration::Repository.new(:type => :cvs, :url => url)
  Rake::Stx::SCM.checkout(repo, directory, **kwargs)
end

def svn(url, directory, **kwargs)
  repo = Rake::Stx::Configuration::Repository.new(:type => :svn, :url => url)
  Rake::Stx::SCM.checkout(repo, directory, **kwargs)
end

def hg(url, directory, **kwargs)
  repo = Rake::Stx::Configuration::Repository.new(:type => :hg, :url => url)
  Rake::Stx::SCM.checkout(repo, directory, **kwargs)
end

def git(url, directory, **kwargs)
  repo = Rake::Stx::Configuration::Repository.new(:type => :git, :url => url)
  Rake::Stx::SCM.checkout(repo, directory, **kwargs)
end