From aa9d04a4a7ec1232477f0823a584da49aae11d58 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 7 Jun 2008 09:19:50 +0000 Subject: [PATCH] Mercurial adapter improvements (patch #1199 by Pierre Paysant-Le Roux). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1499 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/abstract_adapter.rb | 7 +- .../adapters/mercurial/hg-template-0.9.5.tmpl | 12 ++ .../adapters/mercurial/hg-template-1.0.tmpl | 12 ++ lib/redmine/scm/adapters/mercurial_adapter.rb | 160 +++++++++--------- test/unit/mercurial_adapter_test.rb | 49 ++++++ test/unit/repository_mercurial_test.rb | 20 +++ 6 files changed, 181 insertions(+), 79 deletions(-) create mode 100644 lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl create mode 100644 lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl create mode 100644 test/unit/mercurial_adapter_test.rb diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index 9563ed80..80058a2b 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -94,6 +94,11 @@ module Redmine path ||= '' (path[0,1]!="/") ? "/#{path}" : path end + + def with_trailling_slash(path) + path ||= '' + (path[-1,1] == "/") ? path : "#{path}/" + end def shell_quote(str) if RUBY_PLATFORM =~ /mswin/ @@ -102,7 +107,7 @@ module Redmine "'" + str.gsub(/'/, "'\"'\"'") + "'" end end - + private def retrieve_root_url info = self.info diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl new file mode 100644 index 00000000..b3029e6f --- /dev/null +++ b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl @@ -0,0 +1,12 @@ +changeset = 'This template must be used with --debug option\n' +changeset_quiet = 'This template must be used with --debug option\n' +changeset_verbose = 'This template must be used with --debug option\n' +changeset_debug = '\n{author|escape}\n{date|isodate}\n\n{files}{file_adds}{file_dels}{file_copies}\n{desc|escape}\n{tags}\n\n' + +file = '{file|escape}\n' +file_add = '{file_add|escape}\n' +file_del = '{file_del|escape}\n' +file_copy = '{name|urlescape}\n' +tag = '{tag|escape}\n' +header='\n\n\n' +# footer="" \ No newline at end of file diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl new file mode 100644 index 00000000..3eef8501 --- /dev/null +++ b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl @@ -0,0 +1,12 @@ +changeset = 'This template must be used with --debug option\n' +changeset_quiet = 'This template must be used with --debug option\n' +changeset_verbose = 'This template must be used with --debug option\n' +changeset_debug = '\n{author|escape}\n{date|isodate}\n\n{file_mods}{file_adds}{file_dels}{file_copies}\n{desc|escape}\n{tags}\n\n' + +file_mod = '{file_mod|escape}\n' +file_add = '{file_add|escape}\n' +file_del = '{file_del|escape}\n' +file_copy = '{name|urlescape}\n' +tag = '{tag|escape}\n' +header='\n\n\n' +# footer="" diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index 6f42dda0..be01b7bb 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -21,9 +21,12 @@ module Redmine module Scm module Adapters class MercurialAdapter < AbstractAdapter - + # Mercurial executable name HG_BIN = "hg" + TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial" + TEMPLATE_NAME = "hg-template" + TEMPLATE_EXTENSION = "tmpl" def info cmd = "#{HG_BIN} -R #{target('')} root" @@ -33,8 +36,8 @@ module Redmine end return nil if $? && $?.exitstatus != 0 info = Info.new({:root_url => root_url.chomp, - :lastrev => revisions(nil,nil,nil,{:limit => 1}).last - }) + :lastrev => revisions(nil,nil,nil,{:limit => 1}).last + }) info rescue CommandFailed return nil @@ -43,62 +46,72 @@ module Redmine def entries(path=nil, identifier=nil) path ||= '' entries = Entries.new - cmd = "#{HG_BIN} -R #{target('')} --cwd #{target(path)} locate" - cmd << " -r #{identifier.to_i}" if identifier - cmd << " " + shell_quote('glob:**') + cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate" + cmd << " -r " + (identifier ? identifier.to_s : "tip") + cmd << " " + shell_quote("path:#{path}") unless path.empty? shellout(cmd) do |io| io.each_line do |line| - e = line.chomp.split(%r{[\/\\]}) - entries << Entry.new({:name => e.first, - :path => (path.empty? ? e.first : "#{path}/#{e.first}"), - :kind => (e.size > 1 ? 'dir' : 'file'), - :lastrev => Revision.new - }) unless entries.detect{|entry| entry.name == e.first} + # HG uses antislashs as separator on Windows + line = line.gsub(/\\/, "/") + if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'') + e ||= line + e = e.chomp.split(%r{[\/\\]}) + entries << Entry.new({:name => e.first, + :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"), + :kind => (e.size > 1 ? 'dir' : 'file'), + :lastrev => Revision.new + }) unless entries.detect{|entry| entry.name == e.first} + end end end return nil if $? && $?.exitstatus != 0 entries.sort_by_name end - - def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + + # Fetch the revisions by using a template file that + # makes Mercurial produce a xml output. + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) revisions = Revisions.new - cmd = "#{HG_BIN} -v --encoding utf8 -R #{target('')} log" + cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{self.template_path}" if identifier_from && identifier_to cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}" elsif identifier_from cmd << " -r #{identifier_from.to_i}:" end cmd << " --limit #{options[:limit].to_i}" if options[:limit] + cmd << " #{path}" if path shellout(cmd) do |io| - changeset = {} - parsing_descr = false - line_feeds = 0 - - io.each_line do |line| - if line =~ /^(\w+):\s*(.*)$/ - key = $1 - value = $2 - if parsing_descr && line_feeds > 1 - parsing_descr = false - revisions << build_revision_from_changeset(changeset) - changeset = {} - end - if !parsing_descr - changeset.store key.to_sym, value - if $1 == "description" - parsing_descr = true - line_feeds = 0 - next + begin + # HG doesn't close the XML Document... + doc = REXML::Document.new(io.read << "") + doc.elements.each("log/logentry") do |logentry| + paths = [] + copies = logentry.get_elements('paths/path-copied') + logentry.elements.each("paths/path") do |path| + # Detect if the added file is a copy + if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text } + from_path = c.attributes['copyfrom-path'] + from_rev = logentry.attributes['revision'] end + paths << {:action => path.attributes['action'], + :path => "/#{path.text}", + :from_path => from_path ? "/#{from_path}" : nil, + :from_revision => from_rev ? from_rev : nil + } end + paths.sort! { |x,y| x[:path] <=> y[:path] } + + revisions << Revision.new({:identifier => logentry.attributes['revision'], + :scmid => logentry.attributes['node'], + :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""), + :time => Time.parse(logentry.elements['date'].text).localtime, + :message => logentry.elements['msg'].text, + :paths => paths + }) end - if parsing_descr - changeset[:description] << line - line_feeds += 1 if line.chomp.empty? - end + rescue + logger.debug($!) end - # Add the last changeset if there is one left - revisions << build_revision_from_changeset(changeset) if changeset[:date] end return nil if $? && $?.exitstatus != 0 revisions @@ -125,7 +138,7 @@ module Redmine def cat(path, identifier=nil) cmd = "#{HG_BIN} -R #{target('')} cat" - cmd << " -r #{identifier.to_i}" if identifier + cmd << " -r " + (identifier ? identifier.to_s : "tip") cmd << " #{target(path)}" cat = nil shellout(cmd) do |io| @@ -140,6 +153,7 @@ module Redmine path ||= '' cmd = "#{HG_BIN} -R #{target('')}" cmd << " annotate -n -u" + cmd << " -r " + (identifier ? identifier.to_s : "tip") cmd << " -r #{identifier.to_i}" if identifier cmd << " #{target(path)}" blame = Annotate.new @@ -153,45 +167,35 @@ module Redmine blame end - private - - # Builds a revision objet from the changeset returned by hg command - def build_revision_from_changeset(changeset) - rev_id = changeset[:changeset].to_s.split(':').first.to_i - - # Changes - paths = (rev_id == 0) ? - # Can't get changes for revision 0 with hg status - changeset[:files].to_s.split.collect{|path| {:action => 'A', :path => "/#{path}"}} : - status(rev_id) - - Revision.new({:identifier => rev_id, - :scmid => changeset[:changeset].to_s.split(':').last, - :author => changeset[:user], - :time => Time.parse(changeset[:date]), - :message => changeset[:description], - :paths => paths - }) + # The hg version version is expressed either as a + # release number (eg 0.9.5 or 1.0) or as a revision + # id composed of 12 hexa characters. + def hgversion + theversion = hgversion_from_command_line + if theversion.match(/^\d+(\.\d+)+/) + theversion.split(".").collect(&:to_i) + # elsif match = theversion.match(/[[:xdigit:]]{12}/) + # match[0] + else + "Unknown version" + end end - # Returns the file changes for a given revision - def status(rev_id) - cmd = "#{HG_BIN} -R #{target('')} status --rev #{rev_id.to_i - 1}:#{rev_id.to_i}" - result = [] - shellout(cmd) do |io| - io.each_line do |line| - action, file = line.chomp.split - next unless action && file - file.gsub!("\\", "/") - case action - when 'R' - result << { :action => 'D' , :path => "/#{file}" } - else - result << { :action => action, :path => "/#{file}" } - end - end - end - result + def template_path + @template ||= begin + if hgversion.is_a?(String) or ((hgversion <=> [0,9,5]) > 0) + ver = "1.0" + else + ver = "0.9.5" + end + "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" + end + end + + private + + def hgversion_from_command_line + @hgversion ||= %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1] end end end diff --git a/test/unit/mercurial_adapter_test.rb b/test/unit/mercurial_adapter_test.rb new file mode 100644 index 00000000..b4aaaec6 --- /dev/null +++ b/test/unit/mercurial_adapter_test.rb @@ -0,0 +1,49 @@ +require File.dirname(__FILE__) + '/../test_helper' +begin + require 'mocha' + + class MercurialAdapterTest < Test::Unit::TestCase + + TEMPLATES_DIR = "#{RAILS_ROOT}/extra/mercurial" + TEMPLATE_NAME = "hg-template" + TEMPLATE_EXTENSION = "tmpl" + + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository' + + + def test_version_template_0_9_5 + # 0.9.5 + test_version_template_for("0.9.5", [0,9,5], "0.9.5") + end + + def test_version_template_1_0 + # 1.0 + test_version_template_for("1.0", [1,0], "1.0") + end + + def test_version_template_1_0_win + test_version_template_for("1e4ddc9ac9f7+20080325", "Unknown version", "1.0") + end + + def test_version_template_1_0_1_win + test_version_template_for("1.0.1+20080525", [1,0,1], "1.0") + end + + def test_version_template_changeset_id + test_version_template_for("1916e629a29d", "Unknown version", "1.0") + end + + private + + def test_version_template_for(hgversion, version, templateversion) + Redmine::Scm::Adapters::MercurialAdapter.any_instance.stubs(:hgversion_from_command_line).returns(hgversion) + adapter = Redmine::Scm::Adapters::MercurialAdapter.new(REPOSITORY_PATH) + assert_equal version, adapter.hgversion + assert_equal "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{templateversion}.#{TEMPLATE_EXTENSION}", adapter.template_path + assert File.exist?(adapter.template_path) + end + end + +rescue LoadError + def test_fake; assert(false, "Requires mocha to run those tests") end +end diff --git a/test/unit/repository_mercurial_test.rb b/test/unit/repository_mercurial_test.rb index 21ddf1e3..0f993ac1 100644 --- a/test/unit/repository_mercurial_test.rb +++ b/test/unit/repository_mercurial_test.rb @@ -48,6 +48,26 @@ class RepositoryMercurialTest < Test::Unit::TestCase @repository.fetch_changesets assert_equal 6, @repository.changesets.count end + + def test_entries + assert_equal 2, @repository.entries("sources", 2).size + assert_equal 1, @repository.entries("sources", 3).size + end + + def test_locate_on_outdated_repository + # Change the working dir state + %x{hg -R #{REPOSITORY_PATH} up -r 0} + assert_equal 1, @repository.entries("images", 0).size + assert_equal 2, @repository.entries("images").size + assert_equal 2, @repository.entries("images", 2).size + end + + + def test_cat + assert @repository.scm.cat("sources/welcome_controller.rb", 2) + assert_nil @repository.scm.cat("sources/welcome_controller.rb") + end + else puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!" def test_fake; assert true end