commit d585eeacdb4b0ec5c0a65f0af37d25236b717974
Author: pdf <github@obfusc8.org>
Date:   Mon Dec 21 11:28:20 2009 +1100

    Mercurial overhaul
    Features:
    * Tag/branch support (#1981)
    
    Bugs:
    * Browsing a revision does not display file size (requires the Mercurial Size extension) (#3421)
    * Browsing a revision does not display correct identifier for files at that revision
    * Repository tab takes a long time to display with large/complex repositories (#3449)
    * File contents fail
    
    Changes:
    * Revision numbers are far too brittle in mercurial, and commit IDs should be used instead (#3724)

diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb
index 18cbc94..58009b5 100644
--- a/app/models/repository/mercurial.rb
+++ b/app/models/repository/mercurial.rb
@@ -28,67 +28,62 @@ class Repository::Mercurial < Repository
   def self.scm_name
     'Mercurial'
   end
-  
-  def entries(path=nil, identifier=nil)
-    entries=scm.entries(path, identifier)
-    if entries
-      entries.each do |entry|
-        next unless entry.is_file?
-        # Set the filesize unless browsing a specific revision
-        if identifier.nil?
-          full_path = File.join(root_url, entry.path)
-          entry.size = File.stat(full_path).size if File.file?(full_path)
-        end
-        # Search the DB for the entry's last change
-        change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
-        if change
-          entry.lastrev.identifier = change.changeset.revision
-          entry.lastrev.name = change.changeset.revision
-          entry.lastrev.author = change.changeset.committer
-          entry.lastrev.revision = change.revision
-        end
-      end
-    end
-    entries
+
+  def branches
+    scm.branches
+  end
+
+  def tags
+    scm.tags
   end
 
+  # Sequential changesets are brittle in Mercurial, so we take
+  # a leaf out of Git's book, but run two passes to take 
+  # advantage of the 'lite' log speed to build our sync list
   def fetch_changesets
     scm_info = scm.info
-    if scm_info
-      # latest revision found in database
-      db_revision = latest_changeset ? latest_changeset.revision.to_i : -1
-      # latest revision in the repository
-      latest_revision = scm_info.lastrev
-      return if latest_revision.nil?
-      scm_revision = latest_revision.identifier.to_i
-      if db_revision < scm_revision
-        logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
-        identifier_from = db_revision + 1
-        while (identifier_from <= scm_revision)
-          # loads changesets by batches of 100
-          identifier_to = [identifier_from + 99, scm_revision].min
-          revisions = scm.revisions('', identifier_from, identifier_to, :with_paths => true)
-          transaction do
-            revisions.each do |revision|
-              changeset = Changeset.create(:repository => self,
-                                           :revision => revision.identifier,
-                                           :scmid => revision.scmid,
-                                           :committer => revision.author, 
-                                           :committed_on => revision.time,
-                                           :comments => revision.message)
-              
-              revision.paths.each do |change|
-                Change.create(:changeset => changeset,
-                              :action => change[:action],
-                              :path => change[:path],
-                              :from_path => change[:from_path],
-                              :from_revision => change[:from_revision])
-              end
-            end
-          end unless revisions.nil?
-          identifier_from = identifier_to + 1
-        end
-      end
-    end
+    return unless scm_info or scm_info.lastrev.nil?
+    
+    db_revision = latest_changeset ? latest_changeset.scmid.to_s : 0
+    scm_revision = scm_info.lastrev.scmid.to_s
+    # Save ourselves an expensive operation if we're already up to date
+    scm_revcount = scm.num_revisions
+    db_revcount = changesets.count
+    return if scm.num_revisions == changesets.count and db_revision == scm_revision
+   
+    lite_revisions = scm.revisions(nil, nil, scm_revision, :lite => true)
+    return if lite_revisions.nil? or lite_revisions.empty?
+
+    # Find revisions that redmine knows about already
+    existing_revisions = changesets.find(:all).map!{|c| c.scmid}
+
+    # Clean out revisions that are no longer in Mercurial
+    Changeset.delete_all(["scmid NOT IN (?) AND repository_id = (?)", lite_revisions.map{|r| r.scmid}, self.id])
+
+    # Subtract revisions that redmine already knows about
+    lite_revisions.reject!{|r| existing_revisions.include?(r.scmid)}
+    return if lite_revisions.nil? or lite_revisions.empty?
+   
+    # Retrieve full revisions for the remainder
+    revisions = []
+    lite_revisions.each {|r| revisions += scm.revisions(nil, r.scmid, r.scmid)}
+    return if revisions.nil? or revisions.empty?
+
+    # Save the results to the database
+    revisions.each{|r| r.save(self)} unless revisions.nil?
+  end
+  
+  def latest_changesets(path, rev, limit=10)
+    revisions = scm.revisions(path, rev, 0, :limit => limit, :lite => true)
+    return [] if revisions.nil? or revisions.empty?
+
+    changesets.find(
+      :all, 
+      :conditions => [
+        "scmid IN (?)", 
+        revisions.map!{|c| c.scmid}
+      ],
+      :order => 'committed_on DESC'
+    )
   end
 end
diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5-lite.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5-lite.tmpl
new file mode 100644
index 0000000..9686357
--- /dev/null
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5-lite.tmpl
@@ -0,0 +1,8 @@
+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}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\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-0.9.5.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
index b3029e6..98218a1 100644
--- a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
@@ -1,7 +1,7 @@
 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'
+changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\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'
diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-1.0-lite.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-1.0-lite.tmpl
new file mode 100644
index 0000000..5da81e8
--- /dev/null
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-1.0-lite.tmpl
@@ -0,0 +1,8 @@
+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}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths />\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
+
+tag = '<tag>{tag|escape}</tag>\n'
+header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
+# footer="</log>"
diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
index 3eef850..d90594b 100644
--- a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
@@ -1,7 +1,7 @@
 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'
+changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\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'
diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
index 79e2d13..6108818 100644
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -21,18 +21,18 @@ 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"
-        
+
         class << self
           def client_version
             @@client_version ||= (hgversion || [])
           end
-          
+
           def hgversion  
             # The hg version is expressed either as a
             # release number (eg 0.9.5 or 1.0) or as a revision
@@ -42,25 +42,82 @@ module Redmine
               theversion.split(".").collect(&:to_i)
             end
           end
-          
+
           def hgversion_from_command_line
             %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
           end
-          
+
           def template_path
             @@template_path ||= template_path_for(client_version)
           end
-          
-          def template_path_for(version)
+
+          def lite_template_path
+            @@lite_template_path ||= template_path_for(client_version,'lite')
+          end
+
+          def template_path_for(version,style=nil)
             if ((version <=> [0,9,5]) > 0) || version.empty?
               ver = "1.0"
             else
               ver = "0.9.5"
             end
-            "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
+            if style
+              tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}-#{style}.#{TEMPLATE_EXTENSION}"
+            else
+            	tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
+            end
+            tmpl
+          end
+        end
+
+        def default_branch
+          @default_branch ||= 'tip'
+        end
+
+        def branches
+          @branches ||= get_branches
+        end
+
+        def get_branches
+          branches = []
+          cmd = "#{HG_BIN} -R #{target('')} branches"
+          shellout(cmd) do |io|
+            io.each_line do |line|
+              branches << line.chomp.match('^([^\s]+).*$')[1]
+            end
+          end
+          branches.sort!
+        end
+
+        def tags
+          @tags ||= get_tags
+        end
+
+        def get_tags
+          tags = []
+          cmd = "#{HG_BIN} -R #{target('')} tags"
+          shellout(cmd) do |io|
+            io.each_line do |line|
+              tags << line.chomp.match('^([\w]+).*$')[1]
+            end
           end
+          tags.sort!
+        end
+
+        def tip
+          @tip ||= get_tip
         end
         
+        def get_tip
+          tip = nil
+          cmd = "#{HG_BIN} -R #{target('')} tip"
+          shellout(cmd) do |io|
+            tip = io.gets.chomp.match('^changeset:\s+\d+:(\w+)$')[1]
+          end
+          return nil if $? && $?.exitstatus != 0
+          tip
+        end
+
         def info
           cmd = "#{HG_BIN} -R #{target('')} root"
           root_url = nil
@@ -69,19 +126,49 @@ 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 => lastrev(nil,tip)
                           })
           info
         rescue CommandFailed
           return nil
         end
         
-        def entries(path=nil, identifier=nil)
+        def lastrev(path=nil, identifier=nil)
+          lastrev = revisions(path,identifier,0,:limit => 1, :lite => true)
+          return nil if lastrev.nil? or lastrev.empty?
+          lastrev.last
+        end
+
+        def num_revisions
+          cmd = "#{HG_BIN} -R #{target('')} log -r :tip --template='\n' | wc -l"
+          shellout(cmd) {|io| io.gets.chomp.to_i}
+        end
+        
+        # Returns the entry identified by path and revision identifier
+        # or nil if entry doesn't exist in the repository
+        def entry(path=nil, identifier=nil)
+          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
+          search_path = parts[0..-2].join('/')
+          search_name = parts[-1]
+          if search_path.blank? && search_name.blank?
+            # Root entry
+            Entry.new(:path => '', :kind => 'dir')
+          else
+            # Search for the entry in the parent directory
+            es = entries(search_path, identifier, :search => search_name)
+            es ? es.detect {|e| e.name == search_name} : nil
+          end
+        end
+
+        def entries(path=nil, identifier=nil, options={})  
           path ||= ''
+          identifier ||= 'tip'
           entries = Entries.new
           cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
-          cmd << " -r " + (identifier ? identifier.to_s : "tip")
+          cmd << " -r #{shell_quote(identifier.to_s)}"
+          cmd << " -I" if options[:search] unless path.empty?
           cmd << " " + shell_quote("path:#{path}") unless path.empty?
+          cmd << " " + shell_quote(options[:search]) if options[:search]
           shellout(cmd) do |io|
             io.each_line do |line|
               # HG uses antislashs as separator on Windows
@@ -89,10 +176,24 @@ module Redmine
               if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
                 e ||= line
                 e = e.chomp.split(%r{[\/\\]})
+                k = (e.size > 1 ? 'dir' : 'file')
+                p = (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}")
+                # Always set the file size if we have the 'size' extension for 
+                # Mercurial, otherwise set from the filesystem if we're browsing 
+                # the default 'branch' (tip)
+                s = nil
+                if (k == 'file')
+                  s = size(p,identifier)
+                  if s.nil? and (identifier.to_s == default_branch or identifier.to_s == 'tip')
+                    full_path = info.root_url + '/' + p
+                    s = File.stat(full_path).size if File.file?(full_path)
+                  end
+                end
                 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
+                                       :path => p,
+                                       :kind => k,
+                                       :size => s,
+                                       :lastrev => lastrev(p,identifier)
                                      }) unless e.empty? || entries.detect{|entry| entry.name == e.first}
               end
             end
@@ -100,23 +201,32 @@ module Redmine
           return nil if $? && $?.exitstatus != 0
           entries.sort_by_name
         end
-        
+
         # 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} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
+          cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} --cwd #{target('')} log"
+          if options[:lite]
+            cmd << " --style #{shell_quote self.class.lite_template_path}" 
+          else
+          	cmd << " -C --style #{shell_quote self.class.template_path}"
+          end
           if identifier_from && identifier_to
-            cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
+            cmd << " -r #{shell_quote(identifier_from.to_s)}:#{shell_quote(identifier_to.to_s)}"
           elsif identifier_from
-            cmd << " -r #{identifier_from.to_i}:"
+            cmd << " -r #{shell_quote(identifier_from.to_s)}:"
+          elsif identifier_to
+            cmd << " -r :#{shell_quote(identifier_to.to_s)}"
           end
           cmd << " --limit #{options[:limit].to_i}" if options[:limit]
           cmd << " #{path}" if path
           shellout(cmd) do |io|
             begin
               # HG doesn't close the XML Document...
-              doc = REXML::Document.new(io.read << "</log>")
+              output = io.read
+              return nil if output.empty?
+              doc = REXML::Document.new(output << "</log>")
               doc.elements.each("log/logentry") do |logentry|
                 paths = []
                 copies = logentry.get_elements('paths/path-copied')
@@ -124,7 +234,7 @@ module Redmine
                   # 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']
+                    from_rev = logentry.attributes['shortnode']
                   end
                   paths << {:action => path.attributes['action'],
                     :path => "/#{path.text}",
@@ -132,9 +242,9 @@ module Redmine
                     :from_revision => from_rev ? from_rev : nil
                   }
                 end
-                paths.sort! { |x,y| x[:path] <=> y[:path] }
-                
-                revisions << Revision.new({:identifier => logentry.attributes['revision'],
+                paths.sort! { |x,y| x[:path] <=> y[:path] } unless paths.empty?
+
+                revisions << Revision.new({:identifier => logentry.attributes['shortnode'],
                                             :scmid => logentry.attributes['node'],
                                             :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
                                             :time => Time.parse(logentry.elements['date'].text).localtime,
@@ -149,15 +259,14 @@ module Redmine
           return nil if $? && $?.exitstatus != 0
           revisions
         end
-        
+
         def diff(path, identifier_from, identifier_to=nil)
           path ||= ''
           if identifier_to
-            identifier_to = identifier_to.to_i 
+            cmd = "#{HG_BIN} -R #{target('')} diff -r #{shell_quote(identifier_to.to_s)} -r #{shell_quote(identifier_from.to_s)} --nodates"
           else
-            identifier_to = identifier_from.to_i - 1
+            cmd = "#{HG_BIN} -R #{target('')} diff -c #{identifier_from} --nodates"
           end
-          cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
           cmd << " -I #{target(path)}" unless path.empty?
           diff = []
           shellout(cmd) do |io|
@@ -168,11 +277,11 @@ module Redmine
           return nil if $? && $?.exitstatus != 0
           diff
         end
-        
+
         def cat(path, identifier=nil)
           cmd = "#{HG_BIN} -R #{target('')} cat"
-          cmd << " -r " + (identifier ? identifier.to_s : "tip")
-          cmd << " #{target(path)}"
+          cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip"))
+          cmd << " #{target(path)}" unless path.empty?
           cat = nil
           shellout(cmd) do |io|
             io.binmode
@@ -181,19 +290,30 @@ module Redmine
           return nil if $? && $?.exitstatus != 0
           cat
         end
-        
+
+        def size(path, identifier=nil)
+          cmd = "#{HG_BIN} --cwd #{target('')} size"
+          cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip"))
+          cmd << " #{path}" unless path.empty?
+          size = nil
+          shellout(cmd) do |io|
+            size = io.read
+          end
+          return nil if $? && $?.exitstatus != 0
+          size.to_i
+        end
+
         def annotate(path, identifier=nil)
           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)}"
+          cmd << " annotate -c -u"
+          cmd << " -r #{shell_quote(identifier.to_s)}" if identifier
+          cmd << " #{target(path)}" unless path.empty?
           blame = Annotate.new
           shellout(cmd) do |io|
             io.each_line do |line|
-              next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
-              blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
+              next unless line =~ %r{^([^:]+)\s(\w+):(.*)$}
+              blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_s, :author => $1.strip))
             end
           end
           return nil if $? && $?.exitstatus != 0
