# HG changeset patch
# User edavis10@e93f8b46-1217-0410-a6f0-8f06a7374b81
# Date 1265130152 0
# Branch 0.9-stable
# Node ID 1528a8a09ab7a8f70f6c8561d2473414c2cd1791
# Parent  50156ae18af6fa8716a3c42adb714ef7a0746b0d
Refactor: Extract method to create a Change from a Changeset.

diff --git a/app/models/changeset.rb b/app/models/changeset.rb
--- a/app/models/changeset.rb
+++ b/app/models/changeset.rb
@@ -152,6 +152,15 @@ class Changeset < ActiveRecord::Base
   def self.normalize_comments(str)
     to_utf8(str.to_s.strip)
   end
+
+  # Creates a new Change from it's common parameters
+  def create_change(change)
+    Change.create(:changeset => self,
+                  :action => change[:action],
+                  :path => change[:path],
+                  :from_path => change[:from_path],
+                  :from_revision => change[:from_revision])
+  end
   
   private
 
diff --git a/app/models/repository/darcs.rb b/app/models/repository/darcs.rb
--- a/app/models/repository/darcs.rb
+++ b/app/models/repository/darcs.rb
@@ -85,11 +85,7 @@ class Repository::Darcs < Repository
                                          :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])
+              changeset.create_change(change)
             end
             next_rev += 1
           end if revisions
diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb
--- a/app/models/repository/mercurial.rb
+++ b/app/models/repository/mercurial.rb
@@ -78,11 +78,7 @@ class Repository::Mercurial < Repository
                                            :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])
+                changeset.create_change(change)
               end
             end
           end unless revisions.nil?
diff --git a/app/models/repository/subversion.rb b/app/models/repository/subversion.rb
--- a/app/models/repository/subversion.rb
+++ b/app/models/repository/subversion.rb
@@ -63,11 +63,7 @@ class Repository::Subversion < Repositor
                                            :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])
+                changeset.create_change(change)
               end unless changeset.new_record?
             end
           end unless revisions.nil?
# HG changeset patch
# User jplang@e93f8b46-1217-0410-a6f0-8f06a7374b81
# Date 1270999129 0
# Branch 0.9-stable
# Node ID bf0b58f0aa2b84653e13cc2973b43053998123e1
# Parent  1528a8a09ab7a8f70f6c8561d2473414c2cd1791
Linkify folder names on revision view (#5164).

diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb
--- a/app/helpers/repositories_helper.rb
+++ b/app/helpers/repositories_helper.rb
@@ -52,17 +52,19 @@ module RepositoriesHelper
       else
         change
       end
-    end.compact
+   end.compact
     
     tree = { }
     changes.each do |change|
       p = tree
       dirs = change.path.to_s.split('/').select {|d| !d.blank?}
+      path = ''
       dirs.each do |dir|
+        path += '/' + dir
         p[:s] ||= {}
         p = p[:s]
-        p[dir] ||= {}
-        p = p[dir]
+        p[path] ||= {}
+        p = p[path]
       end
       p[:c] = change
     end
@@ -76,21 +78,26 @@ module RepositoriesHelper
     output = ''
     output << '<ul>'
     tree.keys.sort.each do |file|
-      s = !tree[file][:s].nil?
-      c = tree[file][:c]
-      
       style = 'change'
-      style << ' folder' if s
-      style << " change-#{c.action}" if c
-      
-      text = h(file)
-      unless c.nil?
+      text = File.basename(h(file))
+      if s = tree[file][:s]
+        style << ' folder'
+        path_param = to_path_param(@repository.relative_path(file))
+        text = link_to(text, :controller => 'repositories',
+                             :action => 'show',
+                             :id => @project,
+                             :path => path_param,
+                             :rev => @changeset.revision)
+        output << "<li class='#{style}'>#{text}</li>"
+        output << render_changes_tree(s)
+      elsif c = tree[file][:c]
+        style << " change-#{c.action}"
         path_param = to_path_param(@repository.relative_path(c.path))
         text = link_to(text, :controller => 'repositories',
                              :action => 'entry',
                              :id => @project,
                              :path => path_param,
-                             :rev => @changeset.revision) unless s || c.action == 'D'
+                             :rev => @changeset.revision) unless c.action == 'D'
         text << " - #{c.revision}" unless c.revision.blank?
         text << ' (' + link_to('diff', :controller => 'repositories',
                                        :action => 'diff',
@@ -98,9 +105,8 @@ module RepositoriesHelper
                                        :path => path_param,
                                        :rev => @changeset.revision) + ') ' 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
-      output << "<li class='#{style}'>#{text}</li>"
-      output << render_changes_tree(tree[file][:s]) if s
     end
     output << '</ul>'
     output
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1274100103 -32400
# Branch 0.9-stable
# Node ID afe8f2ff3ebcef2e2283ae66ebdf4fa31d8392ee
# Parent  bf0b58f0aa2b84653e13cc2973b43053998123e1
git: preserve commit order as possible when inserting to database

Most scm backend can be sorted by changeset.id, but git isn't.
This changes git backend to act in the same manner as them.

This requires database regeneration.

refs #5357

diff --git a/app/models/repository/git.rb b/app/models/repository/git.rb
--- a/app/models/repository/git.rb
+++ b/app/models/repository/git.rb
@@ -49,7 +49,7 @@ class Repository::Git < Repository
     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 @@ class Repository::Git < Repository
         "scmid IN (?)", 
         revisions.map!{|c| c.scmid}
       ],
-      :order => 'committed_on DESC'
+      :order => 'id DESC'
     )
   end
 end
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1260712247 -32400
# Branch 0.9-stable
# Node ID 5d16a3923d07923ead8bd04b0c84eb09e2faaafd
# Parent  afe8f2ff3ebcef2e2283ae66ebdf4fa31d8392ee
repository: sort changesets only by id, not by commited date

In DVCS world, changesets are not in date order.
Because it seems that most of repository backends insert changesets from eariest,
this should not affect them.
BUT THIS BREAKS GIT BACKEND WITHOUT THE PREVIOUS PATCH!! (see #5357)

Also removed duplicated :order specifier of latest_changesets.

fixes #3449, #3567
refs #4455

diff --git a/app/models/issue.rb b/app/models/issue.rb
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -27,7 +27,7 @@ class Issue < ActiveRecord::Base
 
   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
diff --git a/app/models/repository.rb b/app/models/repository.rb
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -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
@@ -106,12 +106,11 @@ class Repository < ActiveRecord::Base
   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
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269353648 -32400
# Branch 0.9-stable
# Node ID 0a723cb5d4d188109b882f9962c62102c834abd3
# Parent  5d16a3923d07923ead8bd04b0c84eb09e2faaafd
changeset/revision: add attrs to identify the changeset

It returns scmid if available; otherwise falls back to revision.

refs #3724

diff --git a/app/models/changeset.rb b/app/models/changeset.rb
--- a/app/models/changeset.rb
+++ b/app/models/changeset.rb
@@ -47,6 +47,12 @@ class Changeset < ActiveRecord::Base
   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
   
   def comments=(comment)
     write_attribute(:comments, Changeset.normalize_comments(comment))
diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb
--- a/lib/redmine/scm/adapters/abstract_adapter.rb
+++ b/lib/redmine/scm/adapters/abstract_adapter.rb
@@ -271,7 +271,8 @@ module Redmine
       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 @@ module Redmine
           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(
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269349610 -32400
# Branch 0.9-stable
# Node ID 9140dda77a2c0bfa9170659e91d9dae52a095129
# Parent  0a723cb5d4d188109b882f9962c62102c834abd3
repository: also query against scmid

 * Mercurial stores node id (hash value) to scmid.
 * Git stores it to both revision and scmid.

TODO: how about darcs?

diff --git a/app/models/repository.rb b/app/models/repository.rb
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -94,7 +94,10 @@ class Repository < ActiveRecord::Base
   
   # 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
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269353648 -32400
# Branch 0.9-stable
# Node ID cbc6cfb13749408249c695d2f971d6a25bc80752
# Parent  9140dda77a2c0bfa9170659e91d9dae52a095129
repository: now format_revision and link_to_revision can accept object

 * link_to_revision uses .identifier to generate URL.
 * format_revision shows .revision and .scmid if they are different and
   .revision is an integer.
 * format_revision truncates revision string to 8 chars if it's more than
   12 chars (= length of Mercurial's short nodeid.)

refs #3724

diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -101,8 +101,10 @@ module ApplicationHelper
   # * :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={})
diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb
--- a/app/helpers/repositories_helper.rb
+++ b/app/helpers/repositories_helper.rb
@@ -18,8 +18,22 @@
 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
   
   def truncate_at_line_break(text, length = 255)
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1274100103 -32400
# Branch 0.9-stable
# Node ID a90d54890c425e708d0af615dcaf0a16766a9f78
# Parent  cbc6cfb13749408249c695d2f971d6a25bc80752
helper: use scmid for "commit:xxx" link if available

scmid is more solid than revision number.

diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -563,7 +563,7 @@ module ApplicationHelper
             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
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1274100103 -32400
# Branch 0.9-stable
# Node ID 5e5b3d7ef920f2ea6665bc035ef69f64ea3506c3
# Parent  a90d54890c425e708d0af615dcaf0a16766a9f78
changeset: prefer hash id than revision number for auto-close issue text

All DVCS backend use scmid as hash id, and it's preferable than
revision number.

diff --git a/app/models/changeset.rb b/app/models/changeset.rb
--- a/app/models/changeset.rb
+++ b/app/models/changeset.rb
@@ -53,6 +53,16 @@ class Changeset < ActiveRecord::Base
   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))
@@ -115,11 +125,7 @@ class Changeset < ActiveRecord::Base
           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
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1274100103 -32400
# Branch 0.9-stable
# Node ID 8ca6b8e07113e124d058c2f61e98abfc0f46a7b0
# Parent  5e5b3d7ef920f2ea6665bc035ef69f64ea3506c3
activity: use scmid and format_revision if possible

TODO: don't call RepositoriesHelper.format_revision() from Changesets

diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb
--- a/app/helpers/repositories_helper.rb
+++ b/app/helpers/repositories_helper.rb
@@ -35,6 +35,7 @@ module RepositoriesHelper
       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
diff --git a/app/models/changeset.rb b/app/models/changeset.rb
--- a/app/models/changeset.rb
+++ b/app/models/changeset.rb
@@ -23,10 +23,10 @@ class Changeset < ActiveRecord::Base
   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},
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269353648 -32400
# Branch 0.9-stable
# Node ID 719aa1a1a0b2845e49007efbfd2615d4666ce079
# Parent  8ca6b8e07113e124d058c2f61e98abfc0f46a7b0
repository: pass object to format_revision and link_to_revision, use changeset.identifier

TODO: not completed yet

refs #3724

diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb
--- a/app/helpers/repositories_helper.rb
+++ b/app/helpers/repositories_helper.rb
@@ -102,7 +102,7 @@ module RepositoriesHelper
                              :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]
@@ -112,13 +112,13 @@ module RepositoriesHelper
                              :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
diff --git a/app/views/repositories/_dir_list_content.rhtml b/app/views/repositories/_dir_list_content.rhtml
--- a/app/views/repositories/_dir_list_content.rhtml
+++ b/app/views/repositories/_dir_list_content.rhtml
@@ -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>
diff --git a/app/views/repositories/_revisions.rhtml b/app/views/repositories/_revisions.rhtml
--- a/app/views/repositories/_revisions.rhtml
+++ b/app/views/repositories/_revisions.rhtml
@@ -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>
diff --git a/app/views/repositories/annotate.rhtml b/app/views/repositories/annotate.rhtml
--- a/app/views/repositories/annotate.rhtml
+++ b/app/views/repositories/annotate.rhtml
@@ -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>
diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml
--- a/app/views/repositories/revision.rhtml
+++ b/app/views/repositories/revision.rhtml
@@ -1,13 +1,13 @@
 <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 -%>
@@ -19,7 +19,7 @@
   <% 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 %>
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1270134336 -32400
# Branch 0.9-stable
# Node ID 1c904e482ff94df35ec077fdde181fa6c17598e0
# Parent  719aa1a1a0b2845e49007efbfd2615d4666ce079
repository: don't truncate revision string of input box

repository/show does not truncate it. repository/revision should be the same.

diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml
--- a/app/views/repositories/revision.rhtml
+++ b/app/views/repositories/revision.rhtml
@@ -14,7 +14,7 @@
   &#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>
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1274100104 -32400
# Branch 0.9-stable
# Node ID c46467b385246b541dac98ef4d2a782492739840
# Parent  1c904e482ff94df35ec077fdde181fa6c17598e0
repository: catch CommandFailed during bulk fetch_changesets

diff --git a/app/models/repository.rb b/app/models/repository.rb
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -174,7 +174,11 @@ class Repository < ActiveRecord::Base
   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
# HG changeset patch
# User Alessio Franceschelli
# Date 1269771309 -32400
# Branch 0.9-stable
# Node ID 19f353ef66526ba7e3ad158c367c3f243877cfa7
# Parent  c46467b385246b541dac98ef4d2a782492739840
mercurial/redminehelper: helper extension to fetch info from hg repo [DRAFT]

Downloaded http://www.redmine.org/attachments/3395/overhaul.py
as redminehelper.py

Then, untabified and removed trailing white spaces.

diff --git a/extra/mercurial/redminehelper.py b/extra/mercurial/redminehelper.py
new file mode 100644
--- /dev/null
+++ b/extra/mercurial/redminehelper.py
@@ -0,0 +1,29 @@
+# redminehelper: draft 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)
+#
+# 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
+'''
+
+from mercurial import cmdutil, commands
+from mercurial.i18n import _
+
+def overhaul(ui, repo, rev=None, **opts):
+    mf = repo[rev].manifest()
+    for f in repo[rev]:
+        try:
+            fctx = repo.filectx(f, fileid=mf[f])
+            ctx = fctx.changectx()
+            ui.write('%s\t%d\t%s\n' %
+                     (ctx,fctx.size(),f))
+        except LookupError:
+            pass
+
+cmdtable = {
+    'overhaul': (overhaul,commands.templateopts, _('hg overhaul [rev]'))
+}
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269771309 -32400
# Branch 0.9-stable
# Node ID 1bcc862fc5176dcbae8336668e366a568a079f21
# Parent  19f353ef66526ba7e3ad158c367c3f243877cfa7
mercurial/redminehelper: helper extension to reduce the number of hg calls

diff --git a/extra/mercurial/redminehelper.py b/extra/mercurial/redminehelper.py
--- a/extra/mercurial/redminehelper.py
+++ b/extra/mercurial/redminehelper.py
@@ -1,8 +1,9 @@
-# redminehelper: draft extension for Mercurial
+# 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.
@@ -10,20 +11,81 @@
 '''command to list revision of each file
 '''
 
-from mercurial import cmdutil, commands
-from mercurial.i18n import _
+import re, time
+from mercurial import cmdutil, commands, node, error
 
-def overhaul(ui, repo, rev=None, **opts):
+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]:
-        try:
-            fctx = repo.filectx(f, fileid=mf[f])
-            ctx = fctx.changectx()
-            ui.write('%s\t%d\t%s\n' %
-                     (ctx,fctx.size(),f))
-        except LookupError:
-            pass
+        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 = {
-    'overhaul': (overhaul,commands.templateopts, _('hg overhaul [rev]'))
+    'rhsummary': (rhsummary, [], 'hg rhsummary'),
+    'rhentries': (rhentries,
+                  [('r', 'rev', '', 'show the specified revision')],
+                  'hg rhentries [path]'),
 }
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269771309 -32400
# Branch 0.9-stable
# Node ID c609836354be54cfebcfce096367e1f6d7e1943d
# Parent  1bcc862fc5176dcbae8336668e366a568a079f21
mercurial: introduced helper method to run hg command

Then, the following methods use it: cat.
I intend to redesign the others.

diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -24,10 +24,14 @@ module Redmine
         
         # 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 || [])
@@ -170,16 +174,12 @@ module Redmine
         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|
+          hg 'cat', '-r', hgrev(identifier), without_leading_slash(path) do |io|
             io.binmode
-            cat = io.read
+            io.read
           end
-          return nil if $? && $?.exitstatus != 0
-          cat
+        rescue HgCommandAborted
+          nil  # means not found
         end
         
         def annotate(path, identifier=nil)
@@ -199,6 +199,25 @@ module Redmine
           return nil if $? && $?.exitstatus != 0
           blame
         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
+
+        # Returns correct revision identifier
+        def hgrev(identifier)
+          identifier.blank? ? 'tip' : identifier.to_s
+        end
+        private :hgrev
       end
     end
   end
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269771309 -32400
# Branch 0.9-stable
# Node ID e1762dd73d4caf1d8ad72bfa5a324a80602d2f85
# Parent  c609836354be54cfebcfce096367e1f6d7e1943d
mercurial: rewrite scm.diff to generate correct changeset diff

diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -155,22 +155,19 @@ module Redmine
         end
         
         def diff(path, identifier_from, identifier_to=nil)
-          path ||= ''
+          hg_args = ['diff', '--nodates']
           if identifier_to
-            identifier_to = identifier_to.to_i 
+            hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
           else
-            identifier_to = identifier_from.to_i - 1
+            hg_args << '-c' << hgrev(identifier_from)
           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
+          hg_args << without_leading_slash(path) unless path.blank?
+
+          hg *hg_args do |io|
+            io.collect
           end
-          return nil if $? && $?.exitstatus != 0
-          diff
+        rescue HgCommandAborted
+          nil  # means not found
         end
         
         def cat(path, identifier=nil)
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269353640 -32400
# Branch 0.9-stable
# Node ID c8a9b6b5995814da85299386cf43ede70d0d36ca
# Parent  e1762dd73d4caf1d8ad72bfa5a324a80602d2f85
mercurial: rewrite scm.annotate

 * Now fetches nodeid.
 * Revision.identifier is omitted. This means to use scmid instead.

diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -180,21 +180,17 @@ module Redmine
         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))
+          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
-          return nil if $? && $?.exitstatus != 0
           blame
+        rescue HgCommandAborted
+          nil  # means not found or cannot be annotated
         end
 
         # Runs 'hg' command with the given args
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269771309 -32400
# Branch 0.9-stable
# Node ID b1435e4221c0796c0ca2ec5e7fa2baecf49f7776
# Parent  c8a9b6b5995814da85299386cf43ede70d0d36ca
mercurial: rewrite MercurialAdapter.revisions as iterator

Reimplemented .revisions as .each_revision.

Also added "require 'rexml/document'" to suppress "uninitialized constant"
error when running as rake task.

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
--- a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
@@ -9,4 +9,4 @@ file_del = '<path action="D">{file_del|e
 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>'
diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
--- a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
@@ -9,4 +9,4 @@ file_del = '<path action="D">{file_del|e
 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>'
diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -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
@@ -105,53 +106,44 @@ module Redmine
           entries.sort_by_name
         end
         
-        # Fetch the revisions by using a template file that 
+        # 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 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}:"
+        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
-          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
+          self
         end
         
         def diff(path, identifier_from, identifier_to=nil)
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269771309 -32400
# Branch 0.9-stable
# Node ID 0c7f612298e89e2ca947efe164d50fa4da652925
# Parent  b1435e4221c0796c0ca2ec5e7fa2baecf49f7776
mercurial: rewrite scm.info by using helper ext

diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -67,19 +67,17 @@ module Redmine
         end
         
         def info
-          cmd = "#{HG_BIN} -R #{target('')} root"
-          root_url = nil
-          shellout(cmd) do |io|
-            root_url = io.gets
-          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 summary
+          @summary ||= fetchg 'rhsummary'
+        end
+        private :summary
         
         def entries(path=nil, identifier=nil)
           path ||= ''
@@ -198,6 +196,32 @@ module Redmine
         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
+          data
+        end
+        private :fetchg
+
         # Returns correct revision identifier
         def hgrev(identifier)
           identifier.blank? ? 'tip' : identifier.to_s
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269771309 -32400
# Branch 0.9-stable
# Node ID ccad2ab43726a09e17c1f9d341fb28b99b09ccfc
# Parent  0c7f612298e89e2ca947efe164d50fa4da652925
mercurial: cleaned up fetch_changesets

diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb
--- a/app/models/repository/mercurial.rb
+++ b/app/models/repository/mercurial.rb
@@ -21,6 +21,8 @@ class Repository::Mercurial < Repository
   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
@@ -53,38 +55,24 @@ class Repository::Mercurial < Repository
   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
+    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
+    self
   end
 end
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269442376 -32400
# Branch 0.9-stable
# Node ID 4c7f66ee40ad95f8b4392cadc0dff7e84b8bd8bc
# Parent  ccad2ab43726a09e17c1f9d341fb28b99b09ccfc
mercurial: override latest_changesets to support rev and path filter

Mercurial doesn't have directory object.

TODO: better way to build sub-query cond

diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb
--- a/app/models/repository/mercurial.rb
+++ b/app/models/repository/mercurial.rb
@@ -54,6 +54,33 @@ class Repository::Mercurial < Repository
     entries
   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)
+  end
+
+  def latest_changesets_cond(path, rev)
+    cond, args = [], []
+
+    if last = rev ? find_changeset_by_name(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
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269353727 -32400
# Branch 0.9-stable
# Node ID 34558768b4d6aa00e90070b2ac830d758f280679
# Parent  4c7f66ee40ad95f8b4392cadc0dff7e84b8bd8bc
mercurial: support tags on repository browser

diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb
--- a/app/models/repository/mercurial.rb
+++ b/app/models/repository/mercurial.rb
@@ -64,7 +64,7 @@ class Repository::Mercurial < Repository
   def latest_changesets_cond(path, rev)
     cond, args = [], []
 
-    if last = rev ? find_changeset_by_name(rev) : nil
+    if last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
       cond << "#{Changeset.table_name}.id <= ?"
       args << last.id
     end
diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -74,6 +74,16 @@ module Redmine
                                             :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 summary
           @summary ||= fetchg 'rhsummary'
         end
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269771310 -32400
# Branch 0.9-stable
# Node ID 9986539011f768683218a7c628772e5ec5abd3e7
# Parent  34558768b4d6aa00e90070b2ac830d758f280679
mercurial: rewrite .entries from scratch

Now it uses redminehelper extension.

TODO: we shouldn't refetch changeset from db, done by template

diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb
--- a/app/models/repository/mercurial.rb
+++ b/app/models/repository/mercurial.rb
@@ -32,26 +32,7 @@ class Repository::Mercurial < Repository
   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
+    scm.entries(path, identifier)
   end
 
   # Returns the latest changesets for +path+
diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -90,28 +90,28 @@ module Redmine
         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}
-              end
-            end
+          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
-          return nil if $? && $?.exitstatus != 0
-          entries.sort_by_name
+
+          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?
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269771310 -32400
# Branch 0.9-stable
# Node ID d17f8f37bacadabaa24099ecb8cdc3c732134290
# Parent  9986539011f768683218a7c628772e5ec5abd3e7
mercurial: now supports named branches

Branches are sorted by name, not by head revision, to follow TortoiseHg.

diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb
--- a/app/models/repository/mercurial.rb
+++ b/app/models/repository/mercurial.rb
@@ -35,17 +35,26 @@ class Repository::Mercurial < Repository
     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),
+                    :conditions => latest_changesets_cond(path, rev, limit),
                     :limit => limit)
   end
 
-  def latest_changesets_cond(path, rev)
+  def latest_changesets_cond(path, rev, limit)
     cond, args = [], []
 
-    if last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
+    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
diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -84,6 +84,19 @@ module Redmine
           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
@@ -153,6 +166,15 @@ module Redmine
           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']
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1269243033 -32400
# Branch 0.9-stable
# Node ID 02a301d18019f2436735fe68d5bd44c0b83968d9
# Parent  d17f8f37bacadabaa24099ecb8cdc3c732134290
mercurial: get client_version correctly even if LANG is set

 * Assumes the version number is of the first line of the output.
 * Uses shellout
 * Makes helper methods to private
 * Uses class' instance variables instead of wide-scope class variables,
   because it's only referenced by class method.
 * Changed not to cache template_path because it isn't a heavy operation.

closes #5117

diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -35,25 +35,24 @@ module Redmine
 
         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)
@@ -64,6 +63,7 @@ module Redmine
             end
             "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
           end
+          private :template_path_for
         end
         
         def info
