rakelib/scm.rb
author Jan Vrany <jan.vrany@fit.cvut.cz>
Fri, 21 Sep 2018 23:14:41 +0100
changeset 255 6d6880749905
parent 249 20b718d60bba
child 263 dd183640f63e
permissions -rw-r--r--
Add `tmp` to `.hgignore`

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
  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
    return if @cvs_rsh_set
    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?
      # 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
        ssh_in_path ? (ssh = 'ssh') : (ssh = %q{"c:\msys64\usr\bin\ssh.exe"} if File.exist? %q{c:\msys64\usr\bin\ssh.exe})

        # 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
          if ENV['SSH_AUTH_SOCK']
            # 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
      ssh = 'ssh' unless ssh_configured
    end
    ENV['CVS_RSH'] = ssh if ssh
    cvs_rsh_set = true
  end


  public

  class CheckoutException < Exception
  end # class CheckoutException


  def self._check_type(type)
    raise CheckoutException.new("Unknown version control system type (#{type})") if type != :cvs and type != :svn and type != :git and type != :hg
  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
    unless File.exist? wc
      self.checkout(repository, directory, **kwargs)
      return
    end

    case type
      when :svn
        _update_svn(repository, directory, branch, root, **kwargs)
      when :cvs
        _update_cvs(repository, directory, branch, root, **kwargs)
      when :git
        _update_git(repository, directory, branch, root, **kwargs)
      when :hg
        _update_hg(repository, directory, branch, root, **kwargs)
      else
        error("Type #{type} not found")
    end
  end

  def self._update_hg(repository, directory, branch, root, **kwargs)

    wc = root / directory
    separator = kwargs[:separator] || '.'
    revision = kwargs[:revision]
    url = "#{repository.canonical}/#{directory.gsub('/', separator)}" unless directory.nil?
    hg = HG::Repository.new(wc)
    begin
      paths = hg.paths
      if repository.staging
        unless paths.has_key? 'staging'
          paths['staging'] = "#{repository.staging}/#{directory.gsub('/', separator)}"
          hg.paths = paths
        end
        hg.pull('staging')
      end
      if repository.upstream
        unless paths.has_key? 'upstream'
          paths['upstream'] = "#{repository.upstream}/#{directory.gsub('/', separator)}"
          hg.paths = paths
        end
        hg.pull('upstream')
      end
      unless 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.
      unless revision
        revision = hg.bookmark
        unless revision
          bookmarks = hg.bookmarks(branch)
          if bookmarks.has_key? BUILD_BRANCH
            revision = BUILD_BRANCH            
          elsif bookmarks.has_key? 'master'
            revision = 'master'
          else
            if hg.heads(branch, include_secret: false).size > 1
              raise CheckoutException.new("HG: Cannot checkout #{directory}: directory. The ->#{branch}<- branch has multiple heads but no bookmark named 'master'! (All other branches are ignored)")
            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
    raise CheckoutException.new("SVN: Cannot update #{wc}") unless (sh %W{svn --non-interactive --trust-server-cert update}, cwd: wc)
  end

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


    unless 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
        _checkout_svn(repository, directory, branch, root, **kwargs)
      when :cvs
        _checkout_cvs(repository, directory, branch, root, **kwargs)
      when :git
        _checkout_git(repository, directory, branch, root, **kwargs)
      when :hg
        _checkout_hg(repository, directory, branch, root, **kwargs)
      else
        error("Type #{type} not found")
    end

  end

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

  def self._checkout_hg(repository, directory, branch, root, **kwargs)
    separator = kwargs[:separator] || '.'
    revision = kwargs[:revision]

    paths = {}
    if repository.canonical
      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
      paths['upstream'] = "#{repository.upstream}/#{directory.gsub('/', separator)}"
    end
    if repository.staging
      paths['staging'] = "#{repository.staging}/#{directory.gsub('/', separator)}"
    end
    paths['default'] = paths['staging'] || paths['upstream'] || paths['canonical']

    begin
      hg = HG::Repository.init(root / directory)
      # Configure path aliases.
      #
      # Set the repository as non-publishing, This way when cloning from a staging
      # repo changes in draft phase would remain drafs. This is  essential to
      # employ evolve extension and being able to fix & evolve changes in clones
      # (on a CI server, for instance) and being able to push back without need to
      # fiddle around phases.
      #
      # The downside is that we cannot do an `uncompressed` pull. This is the price
      # we have to pay.
      hg.config_set(
          phases: {'publish' => 'false'},
          paths: paths
      )

      hg.pull('staging') if repository.staging
      hg.pull('upstream') if repository.upstream
      hg.pull('canonical') if repository.canonical
      # 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.
      unless revision
        bookmarks = hg.bookmarks(branch)
        if bookmarks.has_key? BUILD_BRANCH
            revision = BUILD_BRANCH            
        elsif bookmarks.has_key? 'master'
          revision = 'master'
        else
          if hg.heads(branch).size > 1
            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
      raise Exception.new('CVS only support date spec as revision: option (YYYY-MM-DD)') unless revision.match(/^\d{4}-([0]\d|[1][012])-([012]\d|[3][01])$/)
      revision_arg = " -D #{revision}"
    end
    ensure_cvs_rsh_set
    unless 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)
  error("checkout: No repository found (#{repo_name})") unless repo
  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)
  error("update: No repository found (#{repo_name})") unless repo
  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