Chris@0: # redMine - project management software Chris@0: # Copyright (C) 2006-2007 Jean-Philippe Lang Chris@0: # Chris@0: # This program is free software; you can redistribute it and/or Chris@0: # modify it under the terms of the GNU General Public License Chris@0: # as published by the Free Software Foundation; either version 2 Chris@0: # of the License, or (at your option) any later version. Chris@0: # Chris@0: # This program is distributed in the hope that it will be useful, Chris@0: # but WITHOUT ANY WARRANTY; without even the implied warranty of Chris@0: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Chris@0: # GNU General Public License for more details. Chris@0: # Chris@0: # You should have received a copy of the GNU General Public License Chris@0: # along with this program; if not, write to the Free Software Chris@0: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Chris@0: Chris@0: require 'redmine/scm/adapters/abstract_adapter' Chris@3: require 'rexml/document' Chris@0: Chris@0: module Redmine Chris@0: module Scm Chris@0: module Adapters Chris@0: class MercurialAdapter < AbstractAdapter Chris@0: Chris@0: # Mercurial executable name Chris@0: HG_BIN = "hg" Chris@3: HG_HELPER_EXT = "#{RAILS_ROOT}/extra/mercurial/redminehelper.py" Chris@0: TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial" Chris@0: TEMPLATE_NAME = "hg-template" Chris@0: TEMPLATE_EXTENSION = "tmpl" Chris@0: Chris@3: # raised if hg command exited with error, e.g. unknown revision. Chris@3: class HgCommandAborted < CommandFailed; end Chris@3: Chris@0: class << self Chris@0: def client_version Chris@3: @client_version ||= hgversion Chris@0: end Chris@0: Chris@0: def hgversion Chris@0: # The hg version is expressed either as a Chris@0: # release number (eg 0.9.5 or 1.0) or as a revision Chris@0: # id composed of 12 hexa characters. Chris@3: hgversion_str.to_s.split('.').map { |e| e.to_i } Chris@0: end Chris@3: private :hgversion Chris@0: Chris@3: def hgversion_str Chris@3: shellout("#{HG_BIN} --version") { |io| io.gets }.to_s[/\d+(\.\d+)+/] Chris@0: end Chris@3: private :hgversion_str Chris@0: Chris@0: def template_path Chris@3: template_path_for(client_version) Chris@0: end Chris@0: Chris@0: def template_path_for(version) Chris@0: if ((version <=> [0,9,5]) > 0) || version.empty? Chris@0: ver = "1.0" Chris@0: else Chris@0: ver = "0.9.5" Chris@0: end Chris@0: "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" Chris@0: end Chris@3: private :template_path_for Chris@0: end Chris@0: Chris@0: def info Chris@3: tip = summary['tip'].first Chris@3: Info.new(:root_url => summary['root'].first['path'], Chris@3: :lastrev => Revision.new(:identifier => tip['rev'].to_i, Chris@3: :revision => tip['rev'], Chris@3: :scmid => tip['node'])) Chris@3: end Chris@3: Chris@3: def tags Chris@3: summary['tags'].map { |e| e['name'] } Chris@0: end Chris@0: Chris@3: # Returns map of {'tag' => 'nodeid', ...} Chris@3: def tagmap Chris@3: alist = summary['tags'].map { |e| e.values_at('name', 'node') } Chris@3: Hash[*alist.flatten] Chris@3: end Chris@3: Chris@3: def branches Chris@3: summary['branches'].map { |e| e['name'] } Chris@3: end Chris@3: Chris@3: # Returns map of {'branch' => 'nodeid', ...} Chris@3: def branchmap Chris@3: alist = summary['branches'].map { |e| e.values_at('name', 'node') } Chris@3: Hash[*alist.flatten] Chris@3: end Chris@3: Chris@3: # NOTE: DO NOT IMPLEMENT default_branch !! Chris@3: # It's used as the default revision by RepositoriesController. Chris@3: Chris@3: def summary Chris@3: @summary ||= fetchg 'rhsummary' Chris@3: end Chris@3: private :summary Chris@3: Chris@0: def entries(path=nil, identifier=nil) Chris@0: entries = Entries.new Chris@3: fetched_entries = fetchg('rhentries', '-r', hgrev(identifier), Chris@3: without_leading_slash(path.to_s)) Chris@3: Chris@3: fetched_entries['dirs'].each do |e| Chris@3: entries << Entry.new(:name => e['name'], Chris@3: :path => "#{with_trailling_slash(path)}#{e['name']}", Chris@3: :kind => 'dir') Chris@3: end Chris@3: Chris@3: fetched_entries['files'].each do |e| Chris@3: entries << Entry.new(:name => e['name'], Chris@3: :path => "#{with_trailling_slash(path)}#{e['name']}", Chris@3: :kind => 'file', Chris@3: :size => e['size'].to_i, Chris@3: :lastrev => Revision.new(:identifier => e['rev'].to_i, Chris@3: :time => Time.at(e['time'].to_i))) Chris@3: end Chris@3: Chris@3: entries Chris@3: rescue HgCommandAborted Chris@3: nil # means not found Chris@3: end Chris@3: Chris@3: # TODO: is this api necessary? Chris@3: def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) Chris@3: revisions = Revisions.new Chris@3: each_revision { |e| revisions << e } Chris@3: revisions Chris@3: end Chris@3: Chris@3: # Iterates the revisions by using a template file that Chris@3: # makes Mercurial produce a xml output. Chris@3: def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={}) Chris@3: hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] Chris@3: hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" Chris@3: hg_args << '--limit' << options[:limit] if options[:limit] Chris@3: hg_args << without_leading_slash(path) unless path.blank? Chris@11: doc = hg(*hg_args) { |io| REXML::Document.new(io.read) } Chris@3: # TODO: ??? HG doesn't close the XML Document... Chris@3: Chris@3: doc.each_element('log/logentry') do |le| Chris@3: cpalist = le.get_elements('paths/path-copied').map do |e| Chris@3: [e.text, e.attributes['copyfrom-path']] Chris@3: end Chris@3: cpmap = Hash[*cpalist.flatten] Chris@3: Chris@3: paths = le.get_elements('paths/path').map do |e| Chris@3: {:action => e.attributes['action'], :path => with_leading_slash(e.text), Chris@3: :from_path => (cpmap.member?(e.text) ? with_leading_slash(cpmap[e.text]) : nil), Chris@3: :from_revision => (cpmap.member?(e.text) ? le.attributes['revision'] : nil)} Chris@3: end.sort { |a, b| a[:path] <=> b[:path] } Chris@3: Chris@3: yield Revision.new(:identifier => le.attributes['revision'], Chris@3: :revision => le.attributes['revision'], Chris@3: :scmid => le.attributes['node'], Chris@3: :author => (le.elements['author'].text rescue ''), Chris@3: :time => Time.parse(le.elements['date'].text).localtime, Chris@3: :message => le.elements['msg'].text, Chris@3: :paths => paths) Chris@3: end Chris@3: self Chris@3: end Chris@3: Chris@3: # Returns list of nodes in the specified branch Chris@3: def nodes_in_branch(branch, path=nil, identifier_from=nil, identifier_to=nil, options={}) Chris@3: hg_args = ['log', '--template', '{node|short}\n', '-b', branch] Chris@3: hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" Chris@3: hg_args << '--limit' << options[:limit] if options[:limit] Chris@3: hg_args << without_leading_slash(path) unless path.blank? Chris@3: hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } } Chris@3: end Chris@3: Chris@3: def diff(path, identifier_from, identifier_to=nil) Chris@3: hg_args = ['diff', '--nodates'] Chris@3: if identifier_to Chris@3: hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) Chris@3: else Chris@3: hg_args << '-c' << hgrev(identifier_from) Chris@3: end Chris@3: hg_args << without_leading_slash(path) unless path.blank? Chris@3: Chris@3: hg *hg_args do |io| Chris@3: io.collect Chris@3: end Chris@3: rescue HgCommandAborted Chris@3: nil # means not found Chris@3: end Chris@3: Chris@3: def cat(path, identifier=nil) Chris@3: hg 'cat', '-r', hgrev(identifier), without_leading_slash(path) do |io| Chris@3: io.binmode Chris@3: io.read Chris@3: end Chris@3: rescue HgCommandAborted Chris@3: nil # means not found Chris@3: end Chris@3: Chris@3: def annotate(path, identifier=nil) Chris@3: blame = Annotate.new Chris@3: hg 'annotate', '-ncu', '-r', hgrev(identifier), without_leading_slash(path) do |io| Chris@3: io.each do |line| Chris@3: next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):(.*)$} Chris@3: r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3) Chris@3: blame.add_line($4.rstrip, r) Chris@3: end Chris@3: end Chris@3: blame Chris@3: rescue HgCommandAborted Chris@3: nil # means not found or cannot be annotated Chris@3: end Chris@3: Chris@3: # Runs 'hg' command with the given args Chris@3: def hg(*args, &block) Chris@3: full_args = [HG_BIN, '--cwd', url] Chris@3: full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}" Chris@3: full_args += args Chris@3: ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block) Chris@3: if $? && $?.exitstatus != 0 Chris@3: raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}" Chris@3: end Chris@3: ret Chris@3: end Chris@3: private :hg Chris@3: Chris@3: # Runs 'hg' helper, then parses output to return Chris@3: def fetchg(*args) Chris@3: # command output example: Chris@3: # :tip: rev node Chris@3: # 100 abcdef012345 Chris@3: # :tags: rev node name Chris@3: # 100 abcdef012345 tip Chris@3: # ... Chris@3: data = Hash.new { |h, k| h[k] = [] } Chris@3: hg(*args) do |io| Chris@3: key, attrs = nil, nil Chris@3: io.each do |line| Chris@3: next if line.chomp.empty? Chris@3: if /^:(\w+): ([\w ]+)/ =~ line Chris@3: key = $1 Chris@3: attrs = $2.split(/ /) Chris@3: elsif key Chris@3: alist = attrs.zip(line.chomp.split(/ /, attrs.size)) Chris@3: data[key] << Hash[*alist.flatten] Chris@0: end Chris@0: end Chris@0: end Chris@3: data Chris@0: end Chris@3: private :fetchg Chris@3: Chris@3: # Returns correct revision identifier Chris@3: def hgrev(identifier) Chris@3: identifier.blank? ? 'tip' : identifier.to_s Chris@0: end Chris@3: private :hgrev Chris@0: end Chris@0: end Chris@0: end Chris@0: end