File: lib/redmine/scm/adapters/mercurial_adapter.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: Redmine#21
  module: Scm#22
  module: Adapters#23
  class: MercurialAdapter#24
inherits from
  AbstractAdapter ( Redmine::Scm::Adapters )
has properties
constant: HG_BIN #27
constant: HELPERS_DIR #28
constant: HG_HELPER_EXT #29
constant: TEMPLATE_NAME #30
constant: TEMPLATE_EXTENSION #31
class method: client_command #37
class method: sq_bin #41
class method: client_version #45
class method: client_available #49
class method: hgversion #53
class method: hgversion_from_command_line #66
class method: template_path #70
class method: template_path_for / 1 #74
method: initialize / 5 #79
method: path_encoding #84
method: info #88
method: tags #99
method: tagmap #104
method: branches #111
method: branchmap #123
method: summary #130
method: entries / 3 #145
method: revisions / 4 #182
method: each_revision / 4 #190
method: nodes_in_branch / 2 #236
method: diff / 3 #244
method: cat / 2 #266
method: annotate / 2 #276
method: hg / 2 #302
method: hgrev / 2 #320
method: hgtarget / 1 #327
method: as_ary / 1 #333
  class: HgCommandAborted#34
inherits from
  CommandFailed ( Redmine::Scm::Adapters )
  class: Revision#294
inherits from
  Revision ( Redmine::Scm::Adapters )
has properties
method: format_identifier #296

Code

   1  # Redmine - project management software
   2  # Copyright (C) 2006-2011  Jean-Philippe Lang
   3  #
   4  # This program is free software; you can redistribute it and/or
   5  # modify it under the terms of the GNU General Public License
   6  # as published by the Free Software Foundation; either version 2
   7  # of the License, or (at your option) any later version.
   8  #
   9  # This program is distributed in the hope that it will be useful,
  10  # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  # GNU General Public License for more details.
  13  #
  14  # You should have received a copy of the GNU General Public License
  15  # along with this program; if not, write to the Free Software
  16  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  17 
  18  require 'redmine/scm/adapters/abstract_adapter'
  19  require 'cgi'
  20 
  21  module Redmine
  22    module Scm
  23      module Adapters
  24        class MercurialAdapter < AbstractAdapter
  25 
  26          # Mercurial executable name
  27          HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
  28          HELPERS_DIR = File.dirname(__FILE__) + "/mercurial"
  29          HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py"
  30          TEMPLATE_NAME = "hg-template"
  31          TEMPLATE_EXTENSION = "tmpl"
  32 
  33          # raised if hg command exited with error, e.g. unknown revision.
  34          class HgCommandAborted < CommandFailed; end
  35 
  36          class << self
  37            def client_command
  38              @@bin    ||= HG_BIN
  39            end
  40 
  41            def sq_bin
  42              @@sq_bin ||= shell_quote_command
  43            end
  44 
  45            def client_version
  46              @@client_version ||= (hgversion || [])
  47            end
  48 
  49            def client_available
  50              client_version_above?([1, 2])
  51            end
  52 
  53            def hgversion
  54              # The hg version is expressed either as a
  55              # release number (eg 0.9.5 or 1.0) or as a revision
  56              # id composed of 12 hexa characters.
  57              theversion = hgversion_from_command_line.dup
  58              if theversion.respond_to?(:force_encoding)
  59                theversion.force_encoding('ASCII-8BIT')
  60              end
  61              if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
  62                m[2].scan(%r{\d+}).collect(&:to_i)
  63              end
  64            end
  65 
  66            def hgversion_from_command_line
  67              shellout("#{sq_bin} --version") { |io| io.read }.to_s
  68            end
  69 
  70            def template_path
  71              @@template_path ||= template_path_for(client_version)
  72            end
  73 
  74            def template_path_for(version)
  75              "#{HELPERS_DIR}/#{TEMPLATE_NAME}-1.0.#{TEMPLATE_EXTENSION}"
  76            end
  77          end
  78 
  79          def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
  80            super
  81            @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
  82          end
  83 
  84          def path_encoding
  85            @path_encoding
  86          end
  87 
  88          def info
  89            tip = summary['repository']['tip']
  90            Info.new(:root_url => CGI.unescape(summary['repository']['root']),
  91                     :lastrev => Revision.new(:revision => tip['revision'],
  92                                              :scmid => tip['node']))
  93          # rescue HgCommandAborted
  94          rescue Exception => e
  95            logger.error "hg: error during getting info: #{e.message}"
  96            nil
  97          end
  98 
  99          def tags
 100            as_ary(summary['repository']['tag']).map { |e| e['name'] }
 101          end
 102 
 103          # Returns map of {'tag' => 'nodeid', ...}
 104          def tagmap
 105            alist = as_ary(summary['repository']['tag']).map do |e|
 106              e.values_at('name', 'node')
 107            end
 108            Hash[*alist.flatten]
 109          end
 110 
 111          def branches
 112            brs = []
 113            as_ary(summary['repository']['branch']).each do |e|
 114              br = Branch.new(e['name'])
 115              br.revision =  e['revision']
 116              br.scmid    =  e['node']
 117              brs << br
 118            end
 119            brs
 120          end
 121 
 122          # Returns map of {'branch' => 'nodeid', ...}
 123          def branchmap
 124            alist = as_ary(summary['repository']['branch']).map do |e|
 125              e.values_at('name', 'node')
 126            end
 127            Hash[*alist.flatten]
 128          end
 129 
 130          def summary
 131            return @summary if @summary
 132            hg 'rhsummary' do |io|
 133              output = io.read
 134              if output.respond_to?(:force_encoding)
 135                output.force_encoding('UTF-8')
 136              end
 137              begin
 138                @summary = ActiveSupport::XmlMini.parse(output)['rhsummary']
 139              rescue
 140              end
 141            end
 142          end
 143          private :summary
 144 
 145          def entries(path=nil, identifier=nil, options={})
 146            p1 = scm_iconv(@path_encoding, 'UTF-8', path)
 147            manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)),
 148                          CGI.escape(without_leading_slash(p1.to_s))) do |io|
 149              output = io.read
 150              if output.respond_to?(:force_encoding)
 151                output.force_encoding('UTF-8')
 152              end
 153              begin
 154                ActiveSupport::XmlMini.parse(output)['rhmanifest']['repository']['manifest']
 155              rescue
 156              end
 157            end
 158            path_prefix = path.blank? ? '' : with_trailling_slash(path)
 159 
 160            entries = Entries.new
 161            as_ary(manifest['dir']).each do |e|
 162              n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
 163              p = "#{path_prefix}#{n}"
 164              entries << Entry.new(:name => n, :path => p, :kind => 'dir')
 165            end
 166 
 167            as_ary(manifest['file']).each do |e|
 168              n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
 169              p = "#{path_prefix}#{n}"
 170              lr = Revision.new(:revision => e['revision'], :scmid => e['node'],
 171                                :identifier => e['node'],
 172                                :time => Time.at(e['time'].to_i))
 173              entries << Entry.new(:name => n, :path => p, :kind => 'file',
 174                                   :size => e['size'].to_i, :lastrev => lr)
 175            end
 176 
 177            entries
 178          rescue HgCommandAborted
 179            nil  # means not found
 180          end
 181 
 182          def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
 183            revs = Revisions.new
 184            each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
 185            revs
 186          end
 187 
 188          # Iterates the revisions by using a template file that
 189          # makes Mercurial produce a xml output.
 190          def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
 191            hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
 192            hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
 193            hg_args << '--limit' << options[:limit] if options[:limit]
 194            hg_args << hgtarget(path) unless path.blank?
 195            log = hg(*hg_args) do |io|
 196              output = io.read
 197              if output.respond_to?(:force_encoding)
 198                output.force_encoding('UTF-8')
 199              end
 200              begin
 201                # Mercurial < 1.5 does not support footer template for '</log>'
 202                ActiveSupport::XmlMini.parse("#{output}</log>")['log']
 203              rescue
 204              end
 205            end
 206            as_ary(log['logentry']).each do |le|
 207              cpalist = as_ary(le['paths']['path-copied']).map do |e|
 208                [e['__content__'], e['copyfrom-path']].map do |s|
 209                  scm_iconv('UTF-8', @path_encoding, CGI.unescape(s))
 210                end
 211              end
 212              cpmap = Hash[*cpalist.flatten]
 213              paths = as_ary(le['paths']['path']).map do |e|
 214                p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
 215                {:action        => e['action'],
 216                 :path          => with_leading_slash(p),
 217                 :from_path     => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
 218                 :from_revision => (cpmap.member?(p) ? le['node'] : nil)}
 219              end.sort { |a, b| a[:path] <=> b[:path] }
 220              parents_ary = []
 221              as_ary(le['parents']['parent']).map do |par|
 222                parents_ary << par['__content__'] if par['__content__'] != "000000000000"
 223              end
 224              yield Revision.new(:revision => le['revision'],
 225                                 :scmid    => le['node'],
 226                                 :author   => (le['author']['__content__'] rescue ''),
 227                                 :time     => Time.parse(le['date']['__content__']),
 228                                 :message  => le['msg']['__content__'],
 229                                 :paths    => paths,
 230                                 :parents  => parents_ary)
 231            end
 232            self
 233          end
 234 
 235          # Returns list of nodes in the specified branch
 236          def nodes_in_branch(branch, options={})
 237            hg_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)]
 238            hg_args << '--from' << CGI.escape(branch)
 239            hg_args << '--to'   << '0'
 240            hg_args << '--limit' << options[:limit] if options[:limit]
 241            hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
 242          end
 243 
 244          def diff(path, identifier_from, identifier_to=nil)
 245            hg_args = %w|rhdiff|
 246            if identifier_to
 247              hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
 248            else
 249              hg_args << '-c' << hgrev(identifier_from)
 250            end
 251            unless path.blank?
 252              p = scm_iconv(@path_encoding, 'UTF-8', path)
 253              hg_args << CGI.escape(hgtarget(p))
 254            end
 255            diff = []
 256            hg *hg_args do |io|
 257              io.each_line do |line|
 258                diff << line
 259              end
 260            end
 261            diff
 262          rescue HgCommandAborted
 263            nil  # means not found
 264          end
 265 
 266          def cat(path, identifier=nil)
 267            p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
 268            hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
 269              io.binmode
 270              io.read
 271            end
 272          rescue HgCommandAborted
 273            nil  # means not found
 274          end
 275 
 276          def annotate(path, identifier=nil)
 277            p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
 278            blame = Annotate.new
 279            hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
 280              io.each_line do |line|
 281                line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding)
 282                next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
 283                r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
 284                                 :identifier => $3)
 285                blame.add_line($4.rstrip, r)
 286              end
 287            end
 288            blame
 289          rescue HgCommandAborted
 290            # means not found or cannot be annotated
 291            Annotate.new
 292          end
 293 
 294          class Revision < Redmine::Scm::Adapters::Revision
 295            # Returns the readable identifier
 296            def format_identifier
 297              "#{revision}:#{scmid}"
 298            end
 299          end
 300 
 301          # Runs 'hg' command with the given args
 302          def hg(*args, &block)
 303            repo_path = root_url || url
 304            full_args = ['-R', repo_path, '--encoding', 'utf-8']
 305            full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
 306            full_args << '--config' << 'diff.git=false'
 307            full_args += args
 308            ret = shellout(
 309                     self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
 310                     &block
 311                     )
 312            if $? && $?.exitstatus != 0
 313              raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
 314            end
 315            ret
 316          end
 317          private :hg
 318 
 319          # Returns correct revision identifier
 320          def hgrev(identifier, sq=false)
 321            rev = identifier.blank? ? 'tip' : identifier.to_s
 322            rev = shell_quote(rev) if sq
 323            rev
 324          end
 325          private :hgrev
 326 
 327          def hgtarget(path)
 328            path ||= ''
 329            root_url + '/' + without_leading_slash(path)
 330          end
 331          private :hgtarget
 332 
 333          def as_ary(o)
 334            return [] unless o
 335            o.is_a?(Array) ? o : Array[o]
 336          end
 337          private :as_ary
 338        end
 339      end
 340    end
 341  end