rakelib/scm.rb
author Jan Vrany <jan.vrany@fit.cvut.cz>
Sat, 14 Jan 2017 00:15:19 +0000
changeset 117 f163911eb7bc
parent 102 fc572bd895f2
child 120 2c4e52c4c930
permissions -rw-r--r--
Win32: Workarounds for bugs in eXept's Win32 makefiles as of 2017-01-13 As of 2017-01-13, mingwmake fails due to a slasg/backslash mess in OUTDIR and OUTDIR_SLASH variables (presumably). As a consequence, `objmingw` directory is either not created at all or an attemptp to create it fails because of invalid command syntax. To (temporarily, I hope) workaround it, checkout older makefiles (from 2017-01-01) which seems to work. How could eXept mess up things so badly without noticing?

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 repository.upstream then        
        if not paths.has_key? 'upstream'           
          paths['upstream'] = "#{repository.upstream}/#{directory.gsub('/', separator)}"
          hg.paths = paths
        end
        hg.pull('upstream')
      end
      if not paths.has_key? 'canonical'
        paths['canonical'] = "#{repository.canonical}/#{directory.gsub('/', separator)}"
        hg.paths = paths
      end
      hg.pull('canonical')
      # 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 = {}
    if repository.canonical then
      paths['canonical'] = "#{repository.canonical}/#{directory.gsub('/', separator)}"
    else
      raise Exception.new("Repository named #{repository.name} does not define mandatory canonical repository URL")
    end
    if repository.upstream then
      paths['upstream'] = "#{repository.upstream}/#{directory.gsub('/', separator)}"
    end
    if repository.staging then
      paths['staging'] = "#{repository.staging}/#{directory.gsub('/', separator)}"
    end

    begin
      if repository.staging then
        paths['default'] = paths['staging']
        hg = HG::Repository.clone(paths['staging'], root / directory, noupdate: true)        
        hg.paths = paths
        hg.pull('upstream') if paths['upstream'] 
        hg.pull('canonical')
      elsif repository.upstream then
        paths['default'] = paths['upstream']
        hg = HG::Repository.clone(paths['upstream'], root / directory, noupdate: true)        
        hg.paths = paths
        hg.pull('canonical')
      else
        paths['default'] = paths['canonical']
        hg = HG::Repository.clone(paths['canonical'], root / directory, noupdate: true)
        hg.paths = paths
      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)    
    revision =  kwargs[:revision] || nil
    revision_arg = ''
    if revision then
      if not revision.match(/^\d\d\d\d-\d\d-\d\d$/) 
        raise Exception.new("CVS only support date spec as revision: option (YYYY-MM-DD)")
      end
      revision_arg = " -D #{revision}"
    end
    ensure_cvs_rsh_set()
    if not sh "cvs -z 9 -d #{repository.canonical} co #{revision_arg} #{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