changeset 3:7c48bad7d85d yuya

* Import Mercurial overhaul patches from Yuya Nishihara (see http://www.redmine.org/issues/4455)
author Chris Cannam
date Wed, 28 Jul 2010 12:40:01 +0100
parents b940d200fbd2
children 93b07183b4a9
files app/helpers/application_helper.rb app/helpers/repositories_helper.rb app/models/changeset.rb app/models/issue.rb app/models/repository.rb app/models/repository/git.rb app/models/repository/mercurial.rb app/views/repositories/_dir_list_content.rhtml app/views/repositories/_revisions.rhtml app/views/repositories/annotate.rhtml app/views/repositories/revision.rhtml extra/mercurial/redminehelper.py lib/redmine/scm/adapters/abstract_adapter.rb lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl lib/redmine/scm/adapters/mercurial_adapter.rb
diffstat 16 files changed, 422 insertions(+), 224 deletions(-) [+]
line wrap: on
line diff
--- a/app/helpers/application_helper.rb	Wed Jul 28 12:12:43 2010 +0100
+++ b/app/helpers/application_helper.rb	Wed Jul 28 12:40:01 2010 +0100
@@ -99,8 +99,10 @@
   # * :text - Link text (default to the formatted revision)
   def link_to_revision(revision, project, options={})
     text = options.delete(:text) || format_revision(revision)
+    rev = revision.respond_to?(:identifier) ? revision.identifier : revision
 
-    link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
+    link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
+            :title => l(:label_revision_id, format_revision(revision)))
   end
 
   def toggle_link(name, id, options={})
@@ -612,7 +614,7 @@
             end
           when 'commit'
             if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
-              link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
+              link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
                                            :class => 'changeset',
                                            :title => truncate_single_line(changeset.comments, :length => 100)
             end
--- a/app/helpers/repositories_helper.rb	Wed Jul 28 12:12:43 2010 +0100
+++ b/app/helpers/repositories_helper.rb	Wed Jul 28 12:40:01 2010 +0100
@@ -18,9 +18,24 @@
 require 'iconv'
 
 module RepositoriesHelper
-  def format_revision(txt)
-    txt.to_s[0,8]
+  # truncate rev to 8 chars if it's quite long
+  def truncate_long_revision_name(rev)
+    rev.to_s.size <= 12 ? rev.to_s : rev.to_s[0, 8]
   end
+  private :truncate_long_revision_name
+
+  def format_revision(revision)
+    if [:identifier, :revision, :scmid].all? { |e| revision.respond_to? e }
+      if revision.scmid and revision.revision != revision.scmid and /[^\d]/ !~ revision.revision
+        "#{revision.revision}:#{revision.scmid}"  # number:hashid
+      else
+        truncate_long_revision_name(revision.identifier)
+      end
+    else
+      truncate_long_revision_name(revision)
+    end
+  end
+  module_function :format_revision  # callable as RepositoriesHelper.format_revision
   
   def truncate_at_line_break(text, length = 255)
     if text
@@ -87,7 +102,7 @@
                              :action => 'show',
                              :id => @project,
                              :path => path_param,
-                             :rev => @changeset.revision)
+                             :rev => @changeset.identifier)
         output << "<li class='#{style}'>#{text}</li>"
         output << render_changes_tree(s)
       elsif c = tree[file][:c]
@@ -97,13 +112,13 @@
                              :action => 'entry',
                              :id => @project,
                              :path => path_param,
-                             :rev => @changeset.revision) unless c.action == 'D'
+                             :rev => @changeset.identifier) unless c.action == 'D'
         text << " - #{c.revision}" unless c.revision.blank?
         text << ' (' + link_to('diff', :controller => 'repositories',
                                        :action => 'diff',
                                        :id => @project,
                                        :path => path_param,
-                                       :rev => @changeset.revision) + ') ' if c.action == 'M'
+                                       :rev => @changeset.identifier) + ') ' if c.action == 'M'
         text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
         output << "<li class='#{style}'>#{text}</li>"
       end
--- a/app/models/changeset.rb	Wed Jul 28 12:12:43 2010 +0100
+++ b/app/models/changeset.rb	Wed Jul 28 12:40:01 2010 +0100
@@ -23,10 +23,10 @@
   has_many :changes, :dependent => :delete_all
   has_and_belongs_to_many :issues
 
-  acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
+  acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{RepositoriesHelper.format_revision(o)}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
                 :description => :long_comments,
                 :datetime => :committed_on,
-                :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
+                :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
                 
   acts_as_searchable :columns => 'comments',
                      :include => {:repository => :project},
@@ -47,6 +47,22 @@
   def revision=(r)
     write_attribute :revision, (r.nil? ? nil : r.to_s)
   end
+
+  # Returns the identifier of this changeset.
+  # e.g. revision number for centralized system; hash id for DVCS
+  def identifier
+    scmid || revision
+  end
+
+  # Returns the wiki identifier, "rN" or "commit:ABCDEF"
+  def wiki_identifier
+    if scmid  # hash-like
+      "commit:#{scmid}"
+    else  # numeric
+      "r#{revision}"
+    end
+  end
+  private :wiki_identifier
   
   def comments=(comment)
     write_attribute(:comments, Changeset.normalize_comments(comment))
@@ -109,11 +125,7 @@
           issue.reload
           # don't change the status is the issue is closed
           next if issue.status.is_closed?
-          csettext = "r#{self.revision}"
-          if self.scmid && (! (csettext =~ /^r[0-9]+$/))
-            csettext = "commit:\"#{self.scmid}\""
-          end
-          journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
+          journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, wiki_identifier))
           issue.status = fix_status
           unless Setting.commit_fix_done_ratio.blank?
             issue.done_ratio = Setting.commit_fix_done_ratio.to_i
--- a/app/models/issue.rb	Wed Jul 28 12:12:43 2010 +0100
+++ b/app/models/issue.rb	Wed Jul 28 12:40:01 2010 +0100
@@ -27,7 +27,7 @@
 
   has_many :journals, :as => :journalized, :dependent => :destroy
   has_many :time_entries, :dependent => :delete_all
-  has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
+  has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.id ASC"
   
   has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
   has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
--- a/app/models/repository.rb	Wed Jul 28 12:12:43 2010 +0100
+++ b/app/models/repository.rb	Wed Jul 28 12:40:01 2010 +0100
@@ -17,7 +17,7 @@
 
 class Repository < ActiveRecord::Base
   belongs_to :project
-  has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
+  has_many :changesets, :order => "#{Changeset.table_name}.id DESC"
   has_many :changes, :through => :changesets
   
   # Raw SQL to delete changesets and changes in the database
@@ -94,7 +94,10 @@
   
   # Finds and returns a revision with a number or the beginning of a hash
   def find_changeset_by_name(name)
-    changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
+    # TODO: is this query efficient enough? can we write as single query?
+    e = changesets.find(:first, :conditions => ['revision = ? OR scmid = ?', name.to_s, name.to_s])
+    return e if e
+    changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
   end
   
   def latest_changeset
@@ -106,12 +109,11 @@
   def latest_changesets(path, rev, limit=10)
     if path.blank?
       changesets.find(:all, :include => :user,
-                            :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
                             :limit => limit)
     else
       changes.find(:all, :include => {:changeset => :user}, 
                          :conditions => ["path = ?", path.with_leading_slash],
-                         :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
+                         :order => "#{Changeset.table_name}.id DESC",
                          :limit => limit).collect(&:changeset)
     end
   end
@@ -172,7 +174,11 @@
   def self.fetch_changesets
     Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
       if project.repository
-        project.repository.fetch_changesets
+        begin
+          project.repository.fetch_changesets
+        rescue Redmine::Scm::Adapters::CommandFailed => e
+          logger.error "Repository: error during fetching changesets: #{e.message}"
+        end
       end
     end
   end
--- a/app/models/repository/git.rb	Wed Jul 28 12:12:43 2010 +0100
+++ b/app/models/repository/git.rb	Wed Jul 28 12:40:01 2010 +0100
@@ -49,7 +49,7 @@
     c = changesets.find(:first, :order => 'committed_on DESC')
     since = (c ? c.committed_on - 7.days : nil)
 
-    revisions = scm.revisions('', nil, nil, :all => true, :since => since)
+    revisions = scm.revisions('', nil, nil, :all => true, :since => since, :reverse => true)
     return if revisions.nil? || revisions.empty?
 
     recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since])
@@ -75,7 +75,7 @@
         "scmid IN (?)", 
         revisions.map!{|c| c.scmid}
       ],
-      :order => 'committed_on DESC'
+      :order => 'id DESC'
     )
   end
 end
--- a/app/models/repository/mercurial.rb	Wed Jul 28 12:12:43 2010 +0100
+++ b/app/models/repository/mercurial.rb	Wed Jul 28 12:40:01 2010 +0100
@@ -21,6 +21,8 @@
   attr_protected :root_url
   validates_presence_of :url
 
+  FETCH_AT_ONCE = 100  # number of changesets to fetch at once
+
   def scm_adapter
     Redmine::Scm::Adapters::MercurialAdapter
   end
@@ -30,61 +32,64 @@
   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
+    scm.entries(path, identifier)
+  end
+
+  def branches
+    bras = scm.branches
+    bras.sort unless bras == %w|default|
+  end
+
+  # Returns the latest changesets for +path+
+  def latest_changesets(path, rev, limit=10)
+    changesets.find(:all, :include => :user,
+                    :conditions => latest_changesets_cond(path, rev, limit),
+                    :limit => limit)
+  end
+
+  def latest_changesets_cond(path, rev, limit)
+    cond, args = [], []
+
+    if scm.branchmap.member? rev
+      # dirty hack to filter by branch. branch name should be in database.
+      cond << "#{Changeset.table_name}.scmid IN (?)"
+      args << scm.nodes_in_branch(rev, path, rev, 0, :limit => limit)
+    elsif last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
+      cond << "#{Changeset.table_name}.id <= ?"
+      args << last.id
+    end
+
+    unless path.blank?
+      # TODO: there must be a better way to build sub-query
+      cond << "EXISTS (SELECT * FROM #{Change.table_name}
+                 WHERE #{Change.table_name}.changeset_id = #{Changeset.table_name}.id
+                 AND (#{Change.table_name}.path = ? OR #{Change.table_name}.path LIKE ?))"
+      args << path.with_leading_slash << "#{path.with_leading_slash}/%"
+    end
+
+    [cond.join(' AND '), *args] unless cond.empty?
+  end
+  private :latest_changesets_cond
+
+  def fetch_changesets
+    scm_rev = scm.info.lastrev.revision.to_i
+    db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
+    return unless db_rev < scm_rev  # already up-to-date
+
+    logger.debug "Fetching changesets for repository #{url}" if logger
+    (db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i|
+      transaction do
+        scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re|
+          cs = Changeset.create(:repository => self,
+                                :revision => re.revision,
+                                :scmid => re.scmid,
+                                :committer => re.author,
+                                :committed_on => re.time,
+                                :comments => re.message)
+          re.paths.each { |e| cs.create_change(e) }
         end
       end
     end
-    entries
-  end
-
-  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|
-                changeset.create_change(change)
-              end
-            end
-          end unless revisions.nil?
-          identifier_from = identifier_to + 1
-        end
-      end
-    end
+    self
   end
 end
--- a/app/views/repositories/_dir_list_content.rhtml	Wed Jul 28 12:12:43 2010 +0100
+++ b/app/views/repositories/_dir_list_content.rhtml	Wed Jul 28 12:40:01 2010 +0100
@@ -17,7 +17,7 @@
 </td>
 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
-<td class="revision"><%= link_to_revision(changeset.revision, @project) if changeset %></td>
+<td class="revision"><%= link_to_revision(changeset, @project) if changeset %></td>
 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
 <td class="author"><%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %></td>
 <td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
--- a/app/views/repositories/_revisions.rhtml	Wed Jul 28 12:12:43 2010 +0100
+++ b/app/views/repositories/_revisions.rhtml	Wed Jul 28 12:40:01 2010 +0100
@@ -13,9 +13,9 @@
 <% line_num = 1 %>
 <% revisions.each do |changeset| %>
 <tr class="changeset <%= cycle 'odd', 'even' %>">
-<td class="id"><%= link_to_revision(changeset.revision, project) %></td>
-<td class="checkbox"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
-<td class="checkbox"><%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
+<td class="id"><%= link_to_revision(changeset, project) %></td>
+<td class="checkbox"><%= radio_button_tag('rev', changeset.identifier, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
+<td class="checkbox"><%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
 <td class="author"><%=h changeset.author %></td>
 <td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
--- a/app/views/repositories/annotate.rhtml	Wed Jul 28 12:12:43 2010 +0100
+++ b/app/views/repositories/annotate.rhtml	Wed Jul 28 12:40:01 2010 +0100
@@ -19,7 +19,7 @@
     <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
       <th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th>
       <td class="revision">
-      <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %></td>
+      <%= (revision.identifier ? link_to_revision(revision, @project) : format_revision(revision)) if revision %></td>
       <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
       <td class="line-code"><pre><%= line %></pre></td>
     </tr>
--- a/app/views/repositories/revision.rhtml	Wed Jul 28 12:12:43 2010 +0100
+++ b/app/views/repositories/revision.rhtml	Wed Jul 28 12:40:01 2010 +0100
@@ -1,25 +1,25 @@
 <div class="contextual">
   &#171;
   <% unless @changeset.previous.nil? -%>
-    <%= link_to_revision(@changeset.previous.revision, @project, :text => l(:label_previous)) %>
+    <%= link_to_revision(@changeset.previous, @project, :text => l(:label_previous)) %>
   <% else -%>
     <%= l(:label_previous) %>
   <% end -%>
 |
   <% unless @changeset.next.nil? -%>
-    <%= link_to_revision(@changeset.next.revision, @project, :text => l(:label_next)) %>
+    <%= link_to_revision(@changeset.next, @project, :text => l(:label_next)) %>
   <% else -%>
     <%= l(:label_next) %>
   <% end -%>
   &#187;&nbsp;
 
   <% form_tag({:controller => 'repositories', :action => 'revision', :id => @project, :rev => nil}, :method => :get) do %>
-    <%= text_field_tag 'rev', @rev[0,8], :size => 8 %>
+    <%= text_field_tag 'rev', @rev, :size => 8 %>
     <%= submit_tag 'OK', :name => nil %>
   <% end %>
 </div>
 
-<h2><%= l(:label_revision) %> <%= format_revision(@changeset.revision) %></h2>
+<h2><%= l(:label_revision) %> <%= format_revision(@changeset) %></h2>
 
 <p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
 <span class="author"><%= authoring(@changeset.committed_on, @changeset.author) %></span></p>
@@ -45,7 +45,7 @@
 <li class="change change-D"><%= l(:label_deleted) %></li>
 </ul>
 
-<p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %></p>
+<p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.identifier) if @changeset.changes.any? %></p>
 
 <div class="changeset-changes">
 <%= render_changeset_changes %>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extra/mercurial/redminehelper.py	Wed Jul 28 12:40:01 2010 +0100
@@ -0,0 +1,91 @@
+# redminehelper: Redmine helper extension for Mercurial
+# it's a draft to show a possible way to explore repository by the Redmine overhaul patch
+# see: http://www.redmine.org/issues/4455
+#
+# Copyright 2010 Alessio Franceschelli (alefranz.net)
+# Copyright 2010 Yuya Nishihara <yuya@tcha.org>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+'''command to list revision of each file
+'''
+
+import re, time
+from mercurial import cmdutil, commands, node, error
+
+SPECIAL_TAGS = ('tip',)
+
+def rhsummary(ui, repo, **opts):
+    """output the summary of the repository"""
+    # see mercurial/commands.py:tip
+    ui.write(':tip: rev node\n')
+    tipctx = repo[len(repo) - 1]
+    ui.write('%d %s\n' % (tipctx.rev(), tipctx))
+
+    # see mercurial/commands.py:root
+    ui.write(':root: path\n')
+    ui.write(repo.root + '\n')
+
+    # see mercurial/commands.py:tags
+    ui.write(':tags: rev node name\n')
+    for t, n in reversed(repo.tagslist()):
+        if t in SPECIAL_TAGS:
+            continue
+        try:
+            r = repo.changelog.rev(n)
+        except error.LookupError:
+            r = -1
+        ui.write('%d %s %s\n' % (r, node.short(n), t))
+
+    # see mercurial/commands.py:branches
+    def iterbranches():
+        for t, n in repo.branchtags().iteritems():
+            yield t, n, repo.changelog.rev(n)
+
+    ui.write(':branches: rev node name\n')
+    for t, n, r in sorted(iterbranches(), key=lambda e: e[2], reverse=True):
+        if repo.lookup(r) in repo.branchheads(t, closed=False):
+            ui.write('%d %s %s\n' % (r, node.short(n), t))  # only open branch
+
+def rhentries(ui, repo, path='', **opts):
+    """output the entries of the specified directory"""
+    rev = opts.get('rev')
+    pathprefix = (path.rstrip('/') + '/').lstrip('/')
+
+    # TODO: clean up
+    dirs, files = {}, {}
+    mf = repo[rev].manifest()
+    for f in repo[rev]:
+        if not f.startswith(pathprefix):
+            continue
+
+        name = re.sub(r'/.*', '', f[len(pathprefix):])
+        if '/' in f[len(pathprefix):]:
+            dirs[name] = (name,)
+        else:
+            try:
+                fctx = repo.filectx(f, fileid=mf[f])
+                ctx = fctx.changectx()
+                tm, tzoffset = ctx.date()
+                localtime = int(tm) + tzoffset - time.timezone
+                files[name] = (ctx.rev(), node.short(ctx.node()), localtime,
+                               fctx.size(), name)
+            except LookupError:  # TODO: when this occurs?
+                pass
+
+    ui.write(':dirs: name\n')
+    for n, v in sorted(dirs.iteritems(), key=lambda e: e[0]):
+        ui.write(' '.join(v) + '\n')
+
+    ui.write(':files: rev node time size name\n')
+    for n, v in sorted(files.iteritems(), key=lambda e: e[0]):
+        ui.write(' '.join(str(e) for e in v) + '\n')
+
+
+cmdtable = {
+    'rhsummary': (rhsummary, [], 'hg rhsummary'),
+    'rhentries': (rhentries,
+                  [('r', 'rev', '', 'show the specified revision')],
+                  'hg rhentries [path]'),
+}
--- a/lib/redmine/scm/adapters/abstract_adapter.rb	Wed Jul 28 12:12:43 2010 +0100
+++ b/lib/redmine/scm/adapters/abstract_adapter.rb	Wed Jul 28 12:40:01 2010 +0100
@@ -271,7 +271,8 @@
       end
       
       class Revision
-        attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
+        attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch
+        attr_writer :identifier
 
         def initialize(attributes={})
           self.identifier = attributes[:identifier]
@@ -285,6 +286,12 @@
           self.branch = attributes[:branch]
         end
 
+        # Returns the identifier of this revision.
+        # e.g. revision number for centralized system; hash id for DVCS
+        def identifier
+          @identifier || scmid || revision
+        end
+
         def save(repo)
           Changeset.transaction do
             changeset = Changeset.new(
--- a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl	Wed Jul 28 12:12:43 2010 +0100
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl	Wed Jul 28 12:40:01 2010 +0100
@@ -9,4 +9,4 @@
 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
+footer='</log>'
--- a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl	Wed Jul 28 12:12:43 2010 +0100
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl	Wed Jul 28 12:40:01 2010 +0100
@@ -9,4 +9,4 @@
 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>"
+footer='</log>'
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb	Wed Jul 28 12:12:43 2010 +0100
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb	Wed Jul 28 12:40:01 2010 +0100
@@ -16,6 +16,7 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 require 'redmine/scm/adapters/abstract_adapter'
+require 'rexml/document'
 
 module Redmine
   module Scm
@@ -24,31 +25,34 @@
         
         # Mercurial executable name
         HG_BIN = "hg"
+        HG_HELPER_EXT = "#{RAILS_ROOT}/extra/mercurial/redminehelper.py"
         TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
         TEMPLATE_NAME = "hg-template"
         TEMPLATE_EXTENSION = "tmpl"
         
+        # raised if hg command exited with error, e.g. unknown revision.
+        class HgCommandAborted < CommandFailed; end
+
         class << self
           def client_version
-            @@client_version ||= (hgversion || [])
+            @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
             # id composed of 12 hexa characters.
-            theversion = hgversion_from_command_line
-            if theversion.match(/^\d+(\.\d+)+/)
-              theversion.split(".").collect(&:to_i)
-            end
+            hgversion_str.to_s.split('.').map { |e| e.to_i }
           end
+          private :hgversion
           
-          def hgversion_from_command_line
-            %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
+          def hgversion_str
+            shellout("#{HG_BIN} --version") { |io| io.gets }.to_s[/\d+(\.\d+)+/]
           end
+          private :hgversion_str
           
           def template_path
-            @@template_path ||= template_path_for(client_version)
+            template_path_for(client_version)
           end
           
           def template_path_for(version)
@@ -59,146 +63,202 @@
             end
             "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
           end
+          private :template_path_for
         end
         
         def info
-          cmd = "#{HG_BIN} -R #{target('')} root"
-          root_url = nil
-          shellout(cmd) do |io|
-            root_url = io.read
-          end
-          return nil if $? && $?.exitstatus != 0
-          info = Info.new({:root_url => root_url.chomp,
-                            :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
-                          })
-          info
-        rescue CommandFailed
-          return nil
+          tip = summary['tip'].first
+          Info.new(:root_url => summary['root'].first['path'],
+                   :lastrev => Revision.new(:identifier => tip['rev'].to_i,
+                                            :revision => tip['rev'],
+                                            :scmid => tip['node']))
+        end
+
+        def tags
+          summary['tags'].map { |e| e['name'] }
         end
         
+        # Returns map of {'tag' => 'nodeid', ...}
+        def tagmap
+          alist = summary['tags'].map { |e| e.values_at('name', 'node') }
+          Hash[*alist.flatten]
+        end
+       
+        def branches
+          summary['branches'].map { |e| e['name'] }
+        end
+
+        # Returns map of {'branch' => 'nodeid', ...}
+        def branchmap
+          alist = summary['branches'].map { |e| e.values_at('name', 'node') }
+          Hash[*alist.flatten]
+        end
+
+        # NOTE: DO NOT IMPLEMENT default_branch !!
+        # It's used as the default revision by RepositoriesController.
+
+        def summary
+          @summary ||= fetchg 'rhsummary'
+        end
+        private :summary
+ 
         def entries(path=nil, identifier=nil)
-          path ||= ''
           entries = Entries.new
-          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|
-              # 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 e.empty? || entries.detect{|entry| entry.name == e.first}
+          fetched_entries = fetchg('rhentries', '-r', hgrev(identifier),
+                                   without_leading_slash(path.to_s))
+
+          fetched_entries['dirs'].each do |e|
+            entries << Entry.new(:name => e['name'],
+                                 :path => "#{with_trailling_slash(path)}#{e['name']}",
+                                 :kind => 'dir')
+          end
+
+          fetched_entries['files'].each do |e|
+            entries << Entry.new(:name => e['name'],
+                                 :path => "#{with_trailling_slash(path)}#{e['name']}",
+                                 :kind => 'file',
+                                 :size => e['size'].to_i,
+                                 :lastrev => Revision.new(:identifier => e['rev'].to_i,
+                                                          :time => Time.at(e['time'].to_i)))
+          end
+
+          entries
+        rescue HgCommandAborted
+          nil  # means not found
+        end
+        
+        # TODO: is this api necessary?
+        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+          revisions = Revisions.new
+          each_revision { |e| revisions << e }
+          revisions
+        end
+
+        # Iterates the revisions by using a template file that
+        # makes Mercurial produce a xml output.
+        def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
+          hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
+          hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
+          hg_args << '--limit' << options[:limit] if options[:limit]
+          hg_args << without_leading_slash(path) unless path.blank?
+          doc = hg(*hg_args) { |io| REXML::Document.new(io.read) }
+          # TODO: ??? HG doesn't close the XML Document...
+
+          doc.each_element('log/logentry') do |le|
+            cpalist = le.get_elements('paths/path-copied').map do |e|
+              [e.text, e.attributes['copyfrom-path']]
+            end
+            cpmap = Hash[*cpalist.flatten]
+
+            paths = le.get_elements('paths/path').map do |e|
+              {:action => e.attributes['action'], :path => with_leading_slash(e.text),
+                :from_path => (cpmap.member?(e.text) ? with_leading_slash(cpmap[e.text]) : nil),
+                :from_revision => (cpmap.member?(e.text) ? le.attributes['revision'] : nil)}
+            end.sort { |a, b| a[:path] <=> b[:path] }
+
+            yield Revision.new(:identifier => le.attributes['revision'],
+                               :revision => le.attributes['revision'],
+                               :scmid => le.attributes['node'],
+                               :author => (le.elements['author'].text rescue ''),
+                               :time => Time.parse(le.elements['date'].text).localtime,
+                               :message => le.elements['msg'].text,
+                               :paths => paths)
+          end
+          self
+        end
+
+        # Returns list of nodes in the specified branch
+        def nodes_in_branch(branch, path=nil, identifier_from=nil, identifier_to=nil, options={})
+          hg_args = ['log', '--template', '{node|short}\n', '-b', branch]
+          hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
+          hg_args << '--limit' << options[:limit] if options[:limit]
+          hg_args << without_leading_slash(path) unless path.blank?
+          hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
+        end
+        
+        def diff(path, identifier_from, identifier_to=nil)
+          hg_args = ['diff', '--nodates']
+          if identifier_to
+            hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
+          else
+            hg_args << '-c' << hgrev(identifier_from)
+          end
+          hg_args << without_leading_slash(path) unless path.blank?
+
+          hg *hg_args do |io|
+            io.collect
+          end
+        rescue HgCommandAborted
+          nil  # means not found
+        end
+        
+        def cat(path, identifier=nil)
+          hg 'cat', '-r', hgrev(identifier), without_leading_slash(path) do |io|
+            io.binmode
+            io.read
+          end
+        rescue HgCommandAborted
+          nil  # means not found
+        end
+        
+        def annotate(path, identifier=nil)
+          blame = Annotate.new
+          hg 'annotate', '-ncu', '-r', hgrev(identifier), without_leading_slash(path) do |io|
+            io.each do |line|
+              next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):(.*)$}
+              r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3)
+              blame.add_line($4.rstrip, r)
+            end
+          end
+          blame
+        rescue HgCommandAborted
+          nil  # means not found or cannot be annotated
+        end
+
+        # Runs 'hg' command with the given args
+        def hg(*args, &block)
+          full_args = [HG_BIN, '--cwd', url]
+          full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
+          full_args += args
+          ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
+          if $? && $?.exitstatus != 0
+            raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
+          end
+          ret
+        end
+        private :hg
+
+        # Runs 'hg' helper, then parses output to return
+        def fetchg(*args)
+          # command output example:
+          #   :tip: rev node
+          #   100 abcdef012345
+          #   :tags: rev node name
+          #   100 abcdef012345 tip
+          #   ...
+          data = Hash.new { |h, k| h[k] = [] }
+          hg(*args) do |io|
+            key, attrs = nil, nil
+            io.each do |line|
+              next if line.chomp.empty?
+              if /^:(\w+): ([\w ]+)/ =~ line
+                key = $1
+                attrs = $2.split(/ /)
+              elsif key
+                alist = attrs.zip(line.chomp.split(/ /, attrs.size))
+                data[key] << Hash[*alist.flatten]
               end
             end
           end
-          return nil if $? && $?.exitstatus != 0
-          entries.sort_by_name
+          data
         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}"
-          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|
-            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
-            rescue
-              logger.debug($!)
-            end
-          end
-          return nil if $? && $?.exitstatus != 0
-          revisions
+        private :fetchg
+
+        # Returns correct revision identifier
+        def hgrev(identifier)
+          identifier.blank? ? 'tip' : identifier.to_s
         end
-        
-        def diff(path, identifier_from, identifier_to=nil)
-          path ||= ''
-          if identifier_to
-            identifier_to = identifier_to.to_i 
-          else
-            identifier_to = identifier_from.to_i - 1
-          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|
-            io.each_line do |line|
-              diff << line
-            end
-          end
-          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)}"
-          cat = nil
-          shellout(cmd) do |io|
-            io.binmode
-            cat = io.read
-          end
-          return nil if $? && $?.exitstatus != 0
-          cat
-        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)}"
-          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))
-            end
-          end
-          return nil if $? && $?.exitstatus != 0
-          blame
-        end
+        private :hgrev
       end
     end
   end