]> source.dussan.org Git - redmine.git/commitdiff
Mercurial adapter improvements (patch #1199 by Pierre Paysant-Le Roux).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 7 Jun 2008 09:19:50 +0000 (09:19 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 7 Jun 2008 09:19:50 +0000 (09:19 +0000)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1499 e93f8b46-1217-0410-a6f0-8f06a7374b81

lib/redmine/scm/adapters/abstract_adapter.rb
lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl [new file with mode: 0644]
lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl [new file with mode: 0644]
lib/redmine/scm/adapters/mercurial_adapter.rb
test/unit/mercurial_adapter_test.rb [new file with mode: 0644]
test/unit/repository_mercurial_test.rb

index 9563ed800894f7ebc4112ebf4a2044891a9dde8a..80058a2bf11af43cb524cc94d1237d4104e051cf 100644 (file)
@@ -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 (file)
index 0000000..b3029e6
--- /dev/null
@@ -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 = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
+
+file = '<path action="M">{file|escape}</path>\n'
+file_add = '<path action="A">{file_add|escape}</path>\n'
+file_del = '<path action="D">{file_del|escape}</path>\n'
+file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
+tag = '<tag>{tag|escape}</tag>\n'
+header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
+# footer="</log>"
\ 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 (file)
index 0000000..3eef850
--- /dev/null
@@ -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 = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
+
+file_mod = '<path action="M">{file_mod|escape}</path>\n'
+file_add = '<path action="A">{file_add|escape}</path>\n'
+file_del = '<path action="D">{file_del|escape}</path>\n'
+file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
+tag = '<tag>{tag|escape}</tag>\n'
+header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
+# footer="</log>"
index 6f42dda06f88ca9e7e5612c7ba80b61f0e9b6349..be01b7bbcddf425bef62cf288b30b91a448d6e09 100644 (file)
@@ -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 << "</log>")
+              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
+        # 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
         
-        # 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
-                       })
+        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
         
-        # 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
+        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 (file)
index 0000000..b4aaaec
--- /dev/null
@@ -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
index 21ddf1e3a27f6848d341bba1292d08439cf9d959..0f993ac163aa99d2042775af6eefebb8aba720f3 100644 (file)
@@ -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