Refactored SCM support to use `hglib.rb` for performing Mercurial related tasks
authorJan Vrany <jan.vrany@fit.cvut.cz>
Sat, 29 Oct 2016 23:54:12 +0000
changeset 66 8d2d5dfe94d0
parent 65 070c2f837f59
child 67 75b6eb7b781c
Refactored SCM support to use `hglib.rb` for performing Mercurial related tasks ...rather than issuing hg commands by hand. `hglib.rb` provides higher-level API for repositories and thus would allow more complex logic (such as using mirrors to fetch base or smarter handling of bookmarks) being written in a more concise way.
rakelib/hglib.rb
rakelib/inifile.rb
rakelib/scm.rb
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rakelib/hglib.rb	Sat Oct 29 23:54:12 2016 +0000
@@ -0,0 +1,350 @@
+# 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                
+        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      
+    $LOGGER.debug("executing: #{cmd.shelljoin}")
+    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
+
+    # 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
+
+    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 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)
+      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(remote)
+        uri.user = nil
+        remote = uri.to_s        
+        authconf << "auth.bb.prefix=#{remote}"
+        authconf << "auth.bb.username=#{user}"        
+        authconf << "auth.bb.password=#{pass}"        
+      end
+      hg("pull", remote, config: authconf) do | status |
+        if not status.success? then
+          raise Exception.new("Failed to pull from #{remote}")
+        end
+      end
+    end
+
+    def push(remote = 'default', user: nil, pass: 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(remote)
+        uri.user = nil
+        remote = uri.to_s                
+        authconf << "bb.prefix = #{remote}"
+        authconf << "bb.username = #{user}"        
+        authconf << "bb.password = #{pass}"        
+      end
+      hg("pull", remote, config: authconf) do | status |
+        if status.exitstatus != 0 and status.exitstatus != 1 then
+          raise Exception.new("Failed to pull from #{remote}")
+        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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rakelib/inifile.rb	Sat Oct 29 23:54:12 2016 +0000
@@ -0,0 +1,628 @@
+#encoding: UTF-8
+
+# This class represents the INI file and can be used to parse, modify,
+# and write INI files.
+class IniFile
+  include Enumerable
+
+  class Error < StandardError; end
+  VERSION = '3.0.0'
+
+  # Public: Open an INI file and load the contents.
+  #
+  # filename - The name of the file as a String
+  # opts     - The Hash of options (default: {})
+  #            :comment   - String containing the comment character(s)
+  #            :parameter - String used to separate parameter and value
+  #            :encoding  - Encoding String for reading / writing
+  #            :default   - The String name of the default global section
+  #
+  # Examples
+  #
+  #   IniFile.load('file.ini')
+  #   #=> IniFile instance
+  #
+  #   IniFile.load('does/not/exist.ini')
+  #   #=> nil
+  #
+  # Returns an IniFile instance or nil if the file could not be opened.
+  def self.load( filename, opts = {} )
+    return unless File.file? filename
+    new(opts.merge(:filename => filename))
+  end
+
+  # Get and set the filename
+  attr_accessor :filename
+
+  # Get and set the encoding
+  attr_accessor :encoding
+
+  # Public: Create a new INI file from the given set of options. If :content
+  # is provided then it will be used to populate the INI file. If a :filename
+  # is provided then the contents of the file will be parsed and stored in the
+  # INI file. If neither the :content or :filename is provided then an empty
+  # INI file is created.
+  #
+  # opts - The Hash of options (default: {})
+  #   :content   - The String/Hash containing the INI contents
+  #   :comment   - String containing the comment character(s)
+  #   :parameter - String used to separate parameter and value
+  #   :encoding  - Encoding String for reading / writing
+  #   :default   - The String name of the default global section
+  #   :filename  - The filename as a String
+  #
+  # Examples
+  #
+  #   IniFile.new
+  #   #=> an empty IniFile instance
+  #
+  #   IniFile.new( :content => "[global]\nfoo=bar" )
+  #   #=> an IniFile instance
+  #
+  #   IniFile.new( :filename => 'file.ini', :encoding => 'UTF-8' )
+  #   #=> an IniFile instance
+  #
+  #   IniFile.new( :content => "[global]\nfoo=bar", :comment => '#' )
+  #   #=> an IniFile instance
+  #
+  def initialize( opts = {} )
+    @comment  = opts.fetch(:comment, ';#')
+    @param    = opts.fetch(:parameter, '=')
+    @encoding = opts.fetch(:encoding, nil)
+    @default  = opts.fetch(:default, 'global')
+    @filename = opts.fetch(:filename, nil)
+    content   = opts.fetch(:content, nil)
+
+    @ini = Hash.new {|h,k| h[k] = Hash.new}
+
+    if    content.is_a?(Hash) then merge!(content)
+    elsif content             then parse(content)
+    elsif @filename           then read
+    end
+  end
+
+  # Public: Write the contents of this IniFile to the file system. If left
+  # unspecified, the currently configured filename and encoding will be used.
+  # Otherwise the filename and encoding can be specified in the options hash.
+  #
+  # opts - The default options Hash
+  #        :filename - The filename as a String
+  #        :encoding - The encoding as a String
+  #
+  # Returns this IniFile instance.
+  def write( opts = {} )
+    filename = opts.fetch(:filename, @filename)
+    encoding = opts.fetch(:encoding, @encoding)
+    mode = encoding ? "w:#{encoding}" : "w"
+
+    File.open(filename, mode) do |f|
+      @ini.each do |section,hash|
+        f.puts "[#{section}]"
+        hash.each {|param,val| f.puts "#{param} #{@param} #{escape_value val}"}
+        f.puts
+      end
+    end
+
+    self
+  end
+  alias :save :write
+
+  # Public: Read the contents of the INI file from the file system and replace
+  # and set the state of this IniFile instance. If left unspecified the
+  # currently configured filename and encoding will be used when reading from
+  # the file system. Otherwise the filename and encoding can be specified in
+  # the options hash.
+  #
+  # opts - The default options Hash
+  #        :filename - The filename as a String
+  #        :encoding - The encoding as a String
+  #
+  # Returns this IniFile instance if the read was successful; nil is returned
+  # if the file could not be read.
+  def read( opts = {} )
+    filename = opts.fetch(:filename, @filename)
+    encoding = opts.fetch(:encoding, @encoding)
+    return unless File.file? filename
+
+    mode = encoding ? "r:#{encoding}" : "r"
+    File.open(filename, mode) { |fd| parse fd }
+    self
+  end
+  alias :restore :read
+
+  # Returns this IniFile converted to a String.
+  def to_s
+    s = []
+    @ini.each do |section,hash|
+      s << "[#{section}]"
+      hash.each {|param,val| s << "#{param} #{@param} #{escape_value val}"}
+      s << ""
+    end
+    s.join("\n")
+  end
+
+  # Returns this IniFile converted to a Hash.
+  def to_h
+    @ini.dup
+  end
+
+  # Public: Creates a copy of this inifile with the entries from the
+  # other_inifile merged into the copy.
+  #
+  # other - The other IniFile.
+  #
+  # Returns a new IniFile.
+  def merge( other )
+    self.dup.merge!(other)
+  end
+
+  # Public: Merges other_inifile into this inifile, overwriting existing
+  # entries. Useful for having a system inifile with user overridable settings
+  # elsewhere.
+  #
+  # other - The other IniFile.
+  #
+  # Returns this IniFile.
+  def merge!( other )
+    return self if other.nil?
+
+    my_keys = @ini.keys
+    other_keys = case other
+      when IniFile
+        other.instance_variable_get(:@ini).keys
+      when Hash
+        other.keys
+      else
+        raise Error, "cannot merge contents from '#{other.class.name}'"
+      end
+
+    (my_keys & other_keys).each do |key|
+      case other[key]
+      when Hash
+        @ini[key].merge!(other[key])
+      when nil
+        nil
+      else
+        raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}"
+      end
+    end
+
+    (other_keys - my_keys).each do |key|
+      @ini[key] = case other[key]
+        when Hash
+          other[key].dup
+        when nil
+          {}
+        else
+          raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}"
+        end
+    end
+
+    self
+  end
+
+  # Public: Yield each INI file section, parameter, and value in turn to the
+  # given block.
+  #
+  # block - The block that will be iterated by the each method. The block will
+  #         be passed the current section and the parameter/value pair.
+  #
+  # Examples
+  #
+  #   inifile.each do |section, parameter, value|
+  #     puts "#{parameter} = #{value} [in section - #{section}]"
+  #   end
+  #
+  # Returns this IniFile.
+  def each
+    return unless block_given?
+    @ini.each do |section,hash|
+      hash.each do |param,val|
+        yield section, param, val
+      end
+    end
+    self
+  end
+
+  # Public: Yield each section in turn to the given block.
+  #
+  # block - The block that will be iterated by the each method. The block will
+  #         be passed the current section as a Hash.
+  #
+  # Examples
+  #
+  #   inifile.each_section do |section|
+  #     puts section.inspect
+  #   end
+  #
+  # Returns this IniFile.
+  def each_section
+    return unless block_given?
+    @ini.each_key {|section| yield section}
+    self
+  end
+
+  # Public: Remove a section identified by name from the IniFile.
+  #
+  # section - The section name as a String.
+  #
+  # Returns the deleted section Hash.
+  def delete_section( section )
+    @ini.delete section.to_s
+  end
+
+  # Public: Get the section Hash by name. If the section does not exist, then
+  # it will be created.
+  #
+  # section - The section name as a String.
+  #
+  # Examples
+  #
+  #   inifile['global']
+  #   #=> global section Hash
+  #
+  # Returns the Hash of parameter/value pairs for this section.
+  def []( section )
+    return nil if section.nil?
+    @ini[section.to_s]
+  end
+
+  # Public: Set the section to a hash of parameter/value pairs.
+  #
+  # section - The section name as a String.
+  # value   - The Hash of parameter/value pairs.
+  #
+  # Examples
+  #
+  #   inifile['tenderloin'] = { 'gritty' => 'yes' }
+  #   #=> { 'gritty' => 'yes' }
+  #
+  # Returns the value Hash.
+  def []=( section, value )
+    @ini[section.to_s] = value
+  end
+
+  # Public: Create a Hash containing only those INI file sections whose names
+  # match the given regular expression.
+  #
+  # regex - The Regexp used to match section names.
+  #
+  # Examples
+  #
+  #   inifile.match(/^tree_/)
+  #   #=> Hash of matching sections
+  #
+  # Return a Hash containing only those sections that match the given regular
+  # expression.
+  def match( regex )
+    @ini.dup.delete_if { |section, _| section !~ regex }
+  end
+
+  # Public: Check to see if the IniFile contains the section.
+  #
+  # section - The section name as a String.
+  #
+  # Returns true if the section exists in the IniFile.
+  def has_section?( section )
+    @ini.has_key? section.to_s
+  end
+
+  # Returns an Array of section names contained in this IniFile.
+  def sections
+    @ini.keys
+  end
+
+  # Public: Freeze the state of this IniFile object. Any attempts to change
+  # the object will raise an error.
+  #
+  # Returns this IniFile.
+  def freeze
+    super
+    @ini.each_value {|h| h.freeze}
+    @ini.freeze
+    self
+  end
+
+  # Public: Mark this IniFile as tainted -- this will traverse each section
+  # marking each as tainted.
+  #
+  # Returns this IniFile.
+  def taint
+    super
+    @ini.each_value {|h| h.taint}
+    @ini.taint
+    self
+  end
+
+  # Public: Produces a duplicate of this IniFile. The duplicate is independent
+  # of the original -- i.e. the duplicate can be modified without changing the
+  # original. The tainted state of the original is copied to the duplicate.
+  #
+  # Returns a new IniFile.
+  def dup
+    other = super
+    other.instance_variable_set(:@ini, Hash.new {|h,k| h[k] = Hash.new})
+    @ini.each_pair {|s,h| other[s].merge! h}
+    other.taint if self.tainted?
+    other
+  end
+
+  # Public: Produces a duplicate of this IniFile. The duplicate is independent
+  # of the original -- i.e. the duplicate can be modified without changing the
+  # original. The tainted state and the frozen state of the original is copied
+  # to the duplicate.
+  #
+  # Returns a new IniFile.
+  def clone
+    other = dup
+    other.freeze if self.frozen?
+    other
+  end
+
+  # Public: Compare this IniFile to some other IniFile. For two INI files to
+  # be equivalent, they must have the same sections with the same parameter /
+  # value pairs in each section.
+  #
+  # other - The other IniFile.
+  #
+  # Returns true if the INI files are equivalent and false if they differ.
+  def eql?( other )
+    return true if equal? other
+    return false unless other.instance_of? self.class
+    @ini == other.instance_variable_get(:@ini)
+  end
+  alias :== :eql?
+
+  # Escape special characters.
+  #
+  # value - The String value to escape.
+  #
+  # Returns the escaped value.
+  def escape_value( value )
+    value = value.to_s.dup
+    value.gsub!(%r/\\([0nrt])/, '\\\\\1')
+    value.gsub!(%r/\n/, '\n')
+    value.gsub!(%r/\r/, '\r')
+    value.gsub!(%r/\t/, '\t')
+    value.gsub!(%r/\0/, '\0')
+    value
+  end
+
+  # Parse the given content and store the information in this IniFile
+  # instance. All data will be cleared out and replaced with the information
+  # read from the content.
+  #
+  # content - A String or a file descriptor (must respond to `each_line`)
+  #
+  # Returns this IniFile.
+  def parse( content )
+    parser = Parser.new(@ini, @param, @comment, @default)
+    parser.parse(content)
+    self
+  end
+
+  # The IniFile::Parser has the responsibility of reading the contents of an
+  # .ini file and storing that information into a ruby Hash. The object being
+  # parsed must respond to `each_line` - this includes Strings and any IO
+  # object.
+  class Parser
+
+    attr_writer :section
+    attr_accessor :property
+    attr_accessor :value
+
+    # Create a new IniFile::Parser that can be used to parse the contents of
+    # an .ini file.
+    #
+    # hash    - The Hash where parsed information will be stored
+    # param   - String used to separate parameter and value
+    # comment - String containing the comment character(s)
+    # default - The String name of the default global section
+    #
+    def initialize( hash, param, comment, default )
+      @hash = hash
+      @default = default
+
+      comment = comment.to_s.empty? ? "\\z" : "\\s*(?:[#{comment}].*)?\\z"
+
+      @section_regexp  = %r/\A\s*\[([^\]]+)\]#{comment}/
+      @ignore_regexp   = %r/\A#{comment}/
+      @property_regexp = %r/\A(.*?)(?<!\\)#{param}(.*)\z/
+
+      @open_quote      = %r/\A\s*(".*)\z/
+      @close_quote     = %r/\A(.*(?<!\\)")#{comment}/
+      @full_quote      = %r/\A\s*(".*(?<!\\)")#{comment}/
+      @trailing_slash  = %r/\A(.*)(?<!\\)\\#{comment}/
+      @normal_value    = %r/\A(.*?)#{comment}/
+    end
+
+    # Returns `true` if the current value starts with a leading double quote.
+    # Otherwise returns false.
+    def leading_quote?
+      value && value =~ %r/\A"/
+    end
+
+    # Given a string, attempt to parse out a value from that string. This
+    # value might be continued on the following line. So this method returns
+    # `true` if it is expecting more data.
+    #
+    # string - String to parse
+    #
+    # Returns `true` if the next line is also part of the current value.
+    # Returns `fase` if the string contained a complete value.
+    def parse_value( string )
+      continuation = false
+
+      # if our value starts with a double quote, then we are in a
+      # line continuation situation
+      if leading_quote?
+        # check for a closing quote at the end of the string
+        if string =~ @close_quote
+          value << $1
+
+        # otherwise just append the string to the value
+        else
+          value << string
+          continuation = true
+        end
+
+      # not currently processing a continuation line
+      else
+        case string
+        when @full_quote
+          self.value = $1
+
+        when @open_quote
+          self.value = $1
+          continuation = true
+
+        when @trailing_slash
+          self.value ?  self.value << $1 : self.value = $1
+          continuation = true
+
+        when @normal_value
+          self.value ?  self.value << $1 : self.value = $1
+
+        else
+          error
+        end
+      end
+
+      if continuation
+        self.value << $/ if leading_quote?
+      else
+        process_property
+      end
+
+      continuation
+    end
+
+    # Parse the ini file contents. This will clear any values currently stored
+    # in the ini hash.
+    #
+    # content - Any object that responds to `each_line`
+    #
+    # Returns nil.
+    def parse( content )
+      return unless content
+
+      continuation = false
+
+      @hash.clear
+      @line = nil
+      self.section = nil
+
+      content.each_line do |line|
+        @line = line.chomp
+
+        if continuation
+          continuation = parse_value @line
+        else
+          case @line
+          when @ignore_regexp
+            nil
+          when @section_regexp
+            self.section = @hash[$1]
+          when @property_regexp
+            self.property = $1.strip
+            error if property.empty?
+
+            continuation = parse_value $2
+          else
+            error
+          end
+        end
+      end
+
+      # check here if we have a dangling value ... usually means we have an
+      # unmatched open quote
+      if leading_quote?
+        error "Unmatched open quote"
+      elsif property && value
+        process_property
+      elsif value
+        error
+      end
+
+      nil
+    end
+
+    # Store the property/value pair in the currently active section. This
+    # method checks for continuation of the value to the next line.
+    #
+    # Returns nil.
+    def process_property
+      property.strip!
+      value.strip!
+
+      self.value = $1 if value =~ %r/\A"(.*)(?<!\\)"\z/m
+
+      section[property] = typecast(value)
+
+      self.property = nil
+      self.value = nil
+    end
+
+    # Returns the current section Hash.
+    def section
+      @section ||= @hash[@default]
+    end
+
+    # Raise a parse error using the given message and appending the current line
+    # being parsed.
+    #
+    # msg - The message String to use.
+    #
+    # Raises IniFile::Error
+    def error( msg = 'Could not parse line' )
+      raise Error, "#{msg}: #{@line.inspect}"
+    end
+
+    # Attempt to typecast the value string. We are looking for boolean values,
+    # integers, floats, and empty strings. Below is how each gets cast, but it
+    # is pretty logical and straightforward.
+    #
+    #  "true"  -->  true
+    #  "false" -->  false
+    #  ""      -->  nil
+    #  "42"    -->  42
+    #  "3.14"  -->  3.14
+    #  "foo"   -->  "foo"
+    #
+    # Returns the typecast value.
+    def typecast( value )
+      case value
+      when %r/\Atrue\z/i;  true
+      when %r/\Afalse\z/i; false
+      when %r/\A\s*\z/i;   nil
+      else
+        Integer(value) rescue \
+        Float(value)   rescue \
+        unescape_value(value)
+      end
+    end
+
+    # Unescape special characters found in the value string. This will convert
+    # escaped null, tab, carriage return, newline, and backslash into their
+    # literal equivalents.
+    #
+    # value - The String value to unescape.
+    #
+    # Returns the unescaped value.
+    def unescape_value( value )
+      value = value.to_s
+      value.gsub!(%r/\\[0nrt\\]/) { |char|
+        case char
+        when '\0';   "\0"
+        when '\n';   "\n"
+        when '\r';   "\r"
+        when '\t';   "\t"
+        when '\\\\'; "\\"
+        end
+      }
+      value
+    end
+  end
+
+end  # IniFile
+
--- a/rakelib/scm.rb	Thu Oct 27 09:01:50 2016 +0000
+++ b/rakelib/scm.rb	Sat Oct 29 23:54:12 2016 +0000
@@ -1,3 +1,5 @@
+require 'rakelib/hglib'
+
 module Rake
 end
 
@@ -59,27 +61,12 @@
       separator = p[:separator] || '.'
       url = "#{repository}/#{directory.gsub('/', separator)}"
     end
-    if not sh %W{hg --cwd #{wc} pull #{url}} then
-      raise CheckoutException.new("HG: Cannot pull #{wc}")
-    end
-
-    # Get bookmark if any...
-    bookmark = nil
-    Rake::FileUtilsExt::chdir(wc) do
-      Rake::FileUtilsExt::when_writing("hg log --template #...") do
-    	bookmarks = `hg log --template "{bookmarks}" -r "p1()"`
-    	puts "bookmarks: >>#{bookmarks}<<"
-    	bookmark = bookmarks.split(' ')[0]
-      end
-    end
-
-    cmd = %W{hg --cwd #{wc} update}
-    puts "bookmark: #{bookmark}"
-    if bookmark && bookmark != '' then
-    	cmd << '-r' << "#{bookmark}"
-    end
-    if not sh cmd
-        raise CheckoutException.new("HG: Cannot update #{wc}")
+    repo = HG::Repository.new(wc)
+    begin
+      repo.pull(url)
+      repo.update()
+    rescue Exception => ex 
+      raise CheckoutException.new("HG: Cannot update #{wc}: #{ex.message}")
     end
   end
 
@@ -148,16 +135,6 @@
     end
   end
 
-  def self._checkout_hg_impl(repository, package, directory, branch, root, *params)
-    url = "#{repository}/#{package}"
-    if not sh %W{hg clone #{url} #{root / directory}}
-      raise CheckoutException.new("HG: Cannot clone from #{url}")
-    end    
-    if not sh %W{hg --cwd #{root / directory}/ update #{branch}}
-      raise CheckoutException.new("HG: Cannot switch to branch #{branch}")
-    end
-  end
-
   def self._checkout_hg(repository, directory, branch, root, *params)
     if params.size() > 0
       p = params.last
@@ -166,7 +143,13 @@
     end
     separator = p[:separator] || '.'
 
-    _checkout_hg_impl(repository, directory.gsub('/', separator), directory, branch, root)
+    url = "#{repository}/#{directory.gsub('/', separator)}"
+    begin
+      repo = HG::Repository::clone(url, root / directory, noupdate: true)
+      repo.update(branch)
+    #rescue Exception => e
+    #  raise CheckoutException.new("HG: Cannot clone from #{url}: #{e.message}")
+    end
   end
 
   def self._checkout_cvs(repository, directory, branch, root, *params)