rakelib/hglib.rb
author Jan Vrany <jan.vrany@fit.cvut.cz>
Fri, 04 Nov 2016 00:22:47 +0000
changeset 72 3e832d54a4af
parent 71 68c8cccbdec5
child 73 d2fd92cae180
permissions -rwxr-xr-x
Added support for checking out a particular revision ...or tag or bookmark. For now it is implemented only for Mercurial SCM. If revision is not specified, then bookmark `master` is checked out. If there's no such bookmark and branch has multiple heads, then error is thrown. If it has only one head, that one is checked out.

# This file is not a standalone script. It is a kind
# of lightweight Mercurial library used by other scripts.

require 'uri'
require 'open3'
require 'shellwords'

# Following hack is to make hglib.rb working wit both jv:scripts and
# Smalltalk/X rakefiles. 
begin
  require 'rakelib/inifile'
rescue
  begin
    require 'inifile'
  rescue LoadError => ex
    $LOGGER.error("Cannot load package 'inifile'")
    $LOGGER.error("Run 'gem install inifile' to install it")
    exit 1
  end
end

if not $LOGGER then
  if STDOUT.tty? then
    require 'logger'
    $LOGGER = Logger.new(STDOUT)
    $LOGGER.level = Logger::INFO
  else 
    require 'syslog/logger'
    $LOGGER = Syslog::Logger.new($0)    
  end
end

module HG
  @@config = nil

  GLOBAL_OPTIONS= [:'cwd', :'repository', :'noninteractive', :'config', :'debug', :'debugger',
       :'encoding',:'encodingmode',:'traceback',:'time', :'profile',:'version', :'help',
       :'hidden' ]
  # Execute `hg` command with given positional arguments and
  # keyword arguments turned into command options. For example, 
  #
  #     HG::hg("heads", "default", cwd: '/tmp/testrepo')
  #
  # will result in executing
  # 
  #     hg --cwd '/tmp/testrepo' heads default
  #
  # In addition if block is passed, then the block is evaluate with
  # `hg` command exit status (as Process::Status) and (optionally)
  # with contents of `hg` command stdout and stderr. 
  # If no block is given, an exception is raised when `hg` command 
  # exit status IS NOT zero.
  def self.hg(command, *args, **options, &block)
    g_opts = []
    c_opts = []
    options.each do | k , v |       
      if v != false and v != nil
        o = k.size == 1 ? "-#{k}" : "--#{k}"                
        if GLOBAL_OPTIONS.include? k then                  
          if v.kind_of?(Array)
            v.each do | e |
              g_opts << o << (e == true ? '' : e)  
            end
          else
            g_opts << o << (v == true ? '' : v)
          end
        else
          if v.kind_of?(Array)
            v.each do | e |
              c_opts << o << (e == true ? '' : e)  
            end
          else
            c_opts << o << (v == true ? '' : v)
          end
        end
      end
    end
    c_opts.reject! { | e | e.size == 0 }
    cmd = ['hg'] + g_opts + [command] + c_opts + args      
    cmd_info = cmd.shelljoin.
                gsub(/username\\=\S+/, "username\\=***").
                gsub(/password\\=\S+/, "password\\=***")
    $LOGGER.debug("executing: #{cmd_info}")
    if defined? RakeFileUtils then
      puts cmd_info
    end
    if block_given? then
      stdout, stderr, status = Open3.capture3(*cmd)
      case block.arity
      when 1        
        yield status
      when 2        
        yield status, stdout
      when 3        
        yield status, stdout, stderr
      else
        raise Exception.new("invalid arity of given block")
      end
    else
      if not system(*cmd) then
        raise Exception.new("command failed: #{cmd.join(' ')}")
      end
    end    
  end

  def self.config()
    if @@config == nil
      files = Dir.glob('/etc/mercurial/hgrc.d/*.rc') + 
          [ '/etc/mercurial/hgrc' ,
          hgrc() ]  
      @@config = IniFile.new()
      files.each do | file |
        if File.exist?(file)
          $LOGGER.debug("Loading global config from \"#{file}\"")
          @@config.merge!(IniFile.new(:filename => file))
        end
      end  
    end
    return @@config
  end

  def self.hgrc()
  	return File.expand_path('~/.hgrc')
  end

  class Repository
    attr_accessor :path, :config

    # Clone a repository from given `uri` to given `directory`. 
    # Returns an `HG::Repository` instance representing the repository
    # clone. 
    # If `noupdate` is true, working copy is not updated, i.e., will be
    # empty. Use this when you're going to issue `update(rev)` shortly after.
    #
    def self.clone(uri, directory, noupdate: false)
      if noupdate then
        HG::hg("clone", uri, directory, noupdate: true)
      else
        HG::hg("clone", uri, directory)
      end
      return HG::Repository.new(directory)
    end

    # Initializes an empty repository in given directory. Returns an 
    # `HG::Repository` instance representing the created (empty) repository.
    def self.init(directory)
      HG::hg("init", directory)
      return HG::Repository.new(directory)
    end

    # Like HG::hg, but passes --cwd @path
    def hg(command, *args, **options, &block)
      options[:cwd] = @path
      HG::hg(command, *args, **options, &block)
    end

    def hgrc() 
      return File.join(@path, '.hg', 'hgrc')
    end

    def initialize(directory)
      @path = directory
      config_file = hgrc()      
      if File.exist? ( config_file ) 
        $LOGGER.debug("Loading repository config from \"#{config_file}\"")
        @config = HG::config().merge(IniFile.new(:filename => config_file))
      else
        @config = HG::config()
      end
    end

    # Return a hashmap with defined paths (alias => uri)
    def paths() 
      return @config['paths'].clone
    end

    # Set paths for given repository
    def paths=(paths)
      config = IniFile.new(:filename => self.hgrc())
      config['paths'] = paths
      config.write()
    end

    def log(revset, template = "{node|short}\n")      
      log = []
      hg("log", rev: revset, template: template) do | status, out |     
        if status.success?
          puts out
          log = out.split("\n")
        end
      end
      return log
    end

    # Return changeset IDs of all head revisions. 
    # If `branch` is given, return only heads in given
    # branch.
    def heads(branch = nil) 
      if branch then
        return log("head() and branch('#{branch}')")
      else
        return log("head()")
      end
    end

    # Return name of an active bookmark or nil if no bookmark
    # is active
    def bookmark() 
      filename = File.join(@path, '.hg', 'bookmarks.current')
      if File.exist? filename then
        file = File.open(filename, "r")
        begin
          bookmark = file.read.chomp
        ensure
          file.close()
        end
        return bookmark
      else
        return nil
      end
    end

    # Return a hash "bookmark => revision" of all 
    # bookmarks. 
    def bookmarks(branch = nil)
      revset  = "bookmark()"
      revset += " and branch('#{branch}')" if branch
      bookmarks = {}
      self.log(revset, "{bookmarks}|{node|short}\n").each do | line |
        bookmark, changesetid = line.split("|")
        bookmarks[bookmark] = changesetid
      end
      return bookmarks
    end

    def pull(remote = 'default', user: nil, pass: nil, rev: nil, bookmarks: [])
      authconf = []
      if pass != nil then
        if user == nil then
          raise Exception.new("Password given but not username! Use user: named param to specify username.")
        end
        # If user/password is provided, make sure we don't have
        # username in remote URI. Otherwise Mercurial won't use 
        # password from config!        
        uri = URI.parse(self.paths[remote] || remote)
        uri.user = nil
        uri = uri.to_s
        uri_alias = if self.paths.has_key? remote then remote else 'xxx' end
        authconf << "auth.#{uri_alias}.prefix=#{uri}"
        authconf << "auth.#{uri_alias}.username=#{user}"        
        authconf << "auth.#{uri_alias}.password=#{pass}"        
      end
      hg("pull", remote, config: authconf, rev: nil) do | status |
        if not status.success? then
          raise Exception.new("Failed to pull from #{remote} (exit code #{status.exitstatus})")
        end
      end
    end

    def push(remote = 'default', user: nil, pass: nil, rev: nil)
      authconf = []
      if pass != nil then
        if user == nil then
          raise Exception.new("Password given but not username! Use user: named param to specify username.")
        end
        # If user/password is provided, make sure we don't have
        # username in remote URI. Otherwise Mercurial won't use 
        # password from config!        
        uri = URI.parse(self.paths[remote] || remote)
        uri.user = nil
        uri = uri.to_s
        uri_alias = if self.paths.has_key? remote then remote else 'xxx' end
        authconf << "auth.#{uri_alias}.prefix=#{uri}"
        authconf << "auth.#{uri_alias}.username=#{user}"        
        authconf << "auth.#{uri_alias}.password=#{pass}"        
      end      
      hg("push", remote, config: authconf, rev: rev) do | status |
        if status.exitstatus != 0 and status.exitstatus != 1 then
          raise Exception.new("Failed to push to #{remote} (exit code #{status.exitstatus})")
        end
      end            
    end

    # Create a shared clone in given directory, Return a new
    # HG::Repository object on the shared clone
    def share(dst, rev = nil)
      if File.exist? dst then
        raise Exception.new("Destination file exists: #{dst}")
      end
      if rev == nil then
        rev = log('.')[0]
      end
      if not has_revision?(rev) 
        raise Exception.new("Revision #{rev} does not exist")
      end
      mkdir_p File.dirname(dst);
      HG::hg("share", path, dst, config: 'extensions.share=', noupdate: true, bookmarks: false)
      share = Repository.new(dst)
      share.update(rev);
      return share
    end

    # Updates the repository's working copy to given 
    # revision if given. If not, update to most-recent
    # head, as plain
    #
    #   hg update
    #
    # would do. 
    def update(rev = nil)
      if rev 
        if not has_revision? rev then
          raise Exception.new("Revision #{rev} does not exist")
        end
        hg("update", rev: rev)
      else
        hg("update")
      end
    end

    # Merge given revision. Return true, if the merge was
    # successful, false otherwise
    def merge(rev)
      if not has_revision? rev then
        raise Exception.new("Revision #{rev} does not exist")
      end
      hg("merge", rev) do | status |
        return status.success?
      end
    end

    def commit(message)
      user = ''
      if not @config['ui'].has_key? 'username' then
        user = @config['ui']['username']
      end
      hg("commit", message: message, user: user)
    end

    def has_revision?(rev)
    	revs = log(rev)
    	return revs.size > 0      
    end

    # Lookup a repository in given `directory`. If found,
    # return it as instance of HG::Repository. If not,
    # `nil` is returned.
    def self.lookup(directory)
      return nil if not File.exist?(directory)
      repo_dir = directory
      while repo_dir != nil
        if HG::repository? repo_dir
          return Repository.new(repo_dir)
        end
        repo_dir_parent = File.dirname(repo_dir)
        if repo_dir_parent == repo_dir
          repo_dir = nil
        else 
          repo_dir = repo_dir_parent
        end
      end
    end    

    # Initializes and empty Mercurial repository in given `directory`
    def self.init(directory)
      FileUtils.mkdir_p File.dirname(directory)
      HG::hg("init", directory)
      return Repository.new(directory)
    end
  end # class Repository 

  # Return `true` if given `directory` is a root of mercurial
  # repository, `false` otherwise.
  def self.repository?(directory)
    return File.directory? File.join(directory, '.hg')
  end

  # Enumerate all repositories in given `directory`
  def self.forest(directory, &block)      
    if repository? directory  
      yield Repository.new(directory)
    end
    Dir.foreach(directory) do |x|
      path = File.join(directory, x)
      if File.directory? path       
        if x == "." or x == ".." or x == ".svn" or x == '.git'
          next    
        elsif File.directory?(path)
          forest(path, &block)
        end
      end
    end  
  end
end # module HG