changeset 15:9c6c72729d91 yuya

* Merge from default (for SVN trunk update)
author Chris Cannam
date Wed, 25 Aug 2010 16:33:28 +0100
parents 80433603a2cd (diff) 1d32c0a0efbf (current diff)
children 020926a36823
files app/helpers/application_helper.rb app/models/changeset.rb app/views/issues/.svn/prop-base/auto_complete.html.erb.svn-base app/views/issues/.svn/prop-base/changes.rxml.svn-base app/views/issues/.svn/prop-base/context_menu.rhtml.svn-base app/views/issues/.svn/prop-base/move.rhtml.svn-base app/views/issues/.svn/prop-base/preview.html.erb.svn-base app/views/issues/.svn/text-base/auto_complete.html.erb.svn-base app/views/issues/.svn/text-base/changes.rxml.svn-base app/views/issues/.svn/text-base/context_menu.rhtml.svn-base app/views/issues/.svn/text-base/move.rhtml.svn-base app/views/issues/.svn/text-base/preview.html.erb.svn-base app/views/issues/auto_complete.html.erb app/views/issues/changes.rxml app/views/issues/context_menu.rhtml app/views/issues/move.rhtml app/views/issues/preview.html.erb config/locales/.svn/text-base/sr-CY.yml.svn-base config/locales/sr-CY.yml public/javascripts/calendar/lang/.svn/text-base/calendar-sr-CY.js.svn-base public/javascripts/calendar/lang/calendar-sr-CY.js
diffstat 20 files changed, 868 insertions(+), 225 deletions(-) [+]
line wrap: on
line diff
--- a/app/helpers/application_helper.rb	Wed Aug 25 16:30:24 2010 +0100
+++ b/app/helpers/application_helper.rb	Wed Aug 25 16:33:28 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
 
   # Generates a link to a project if active
@@ -641,7 +643,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 Aug 25 16:30:24 2010 +0100
+++ b/app/helpers/repositories_helper.rb	Wed Aug 25 16:33:28 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 Aug 25 16:30:24 2010 +0100
+++ b/app/models/changeset.rb	Wed Aug 25 16:33:28 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))
@@ -108,11 +124,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 Aug 25 16:30:24 2010 +0100
+++ b/app/models/issue.rb	Wed Aug 25 16:33:28 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 Aug 25 16:30:24 2010 +0100
+++ b/app/models/repository.rb	Wed Aug 25 16:33:28 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 Aug 25 16:30:24 2010 +0100
+++ b/app/models/repository/git.rb	Wed Aug 25 16:33:28 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 Aug 25 16:30:24 2010 +0100
+++ b/app/models/repository/mercurial.rb	Wed Aug 25 16:33:28 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 Aug 25 16:30:24 2010 +0100
+++ b/app/views/repositories/_dir_list_content.rhtml	Wed Aug 25 16:33:28 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 Aug 25 16:30:24 2010 +0100
+++ b/app/views/repositories/_revisions.rhtml	Wed Aug 25 16:33:28 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 Aug 25 16:30:24 2010 +0100
+++ b/app/views/repositories/annotate.rhtml	Wed Aug 25 16:33:28 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 Aug 25 16:30:24 2010 +0100
+++ b/app/views/repositories/revision.rhtml	Wed Aug 25 16:33:28 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 Aug 25 16:33:28 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]'),
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extra/svn/SoundSoftware.pm	Wed Aug 25 16:33:28 2010 +0100
@@ -0,0 +1,422 @@
+package Apache::Authn::SoundSoftware;
+
+=head1 Apache::Authn::SoundSoftware
+
+SoundSoftware - a mod_perl module for Apache authentication against a
+Redmine database and optional LDAP implementing the access control
+rules required for the SoundSoftware.ac.uk repository site.
+
+=head1 SYNOPSIS
+
+This module is closely based on the Redmine.pm authentication module
+provided with Redmine.  It is intended to be used for authentication
+in front of a repository service such as hgwebdir.
+
+Requirements:
+
+1. Clone/pull from repo for public project: Any user, no
+authentication required
+
+2. Clone/pull from repo for private project: Project members only
+
+3. Push to repo for public project: "Permitted" users only (this
+probably means project members who are also identified in the hgrc web
+section for the repository and so will be approved by hgwebdir?)
+
+4. Push to repo for private project: "Permitted" users only (as above)
+
+=head1 INSTALLATION
+
+Debian/ubuntu:
+
+  apt-get install libapache-dbi-perl libapache2-mod-perl2 \
+    libdbd-mysql-perl libauthen-simple-ldap-perl libio-socket-ssl-perl
+
+Note that LDAP support is hardcoded "on" in this script (it is
+optional in the original Redmine.pm).
+
+=head1 CONFIGURATION
+
+   ## This module has to be in your perl path
+   ## eg:  /usr/local/lib/site_perl/Apache/Authn/SoundSoftware.pm
+   PerlLoadModule Apache::Authn::SoundSoftware
+
+   # Example when using hgwebdir
+   ScriptAlias / "/var/hg/hgwebdir.cgi/"
+
+   <Location />
+       AuthName "Mercurial"
+       AuthType Basic
+       Require valid-user
+       PerlAccessHandler Apache::Authn::SoundSoftware::access_handler
+       PerlAuthenHandler Apache::Authn::SoundSoftware::authen_handler
+       SoundSoftwareDSN "DBI:mysql:database=redmine;host=localhost"
+       SoundSoftwareDbUser "redmine"
+       SoundSoftwareDbPass "password"
+       Options +ExecCGI
+       AddHandler cgi-script .cgi
+       ## Optional where clause (fulltext search would be slow and
+       ## database dependant).
+       # SoundSoftwareDbWhereClause "and members.role_id IN (1,2)"
+       ## Optional prefix for local repository URLs
+       # SoundSoftwareRepoPrefix "/var/hg/"
+  </Location>
+
+See the original Redmine.pm for further configuration notes.
+
+=cut
+
+use strict;
+use warnings FATAL => 'all', NONFATAL => 'redefine';
+
+use DBI;
+use Digest::SHA1;
+use Authen::Simple::LDAP;
+use Apache2::Module;
+use Apache2::Access;
+use Apache2::ServerRec qw();
+use Apache2::RequestRec qw();
+use Apache2::RequestUtil qw();
+use Apache2::Const qw(:common :override :cmd_how);
+use APR::Pool ();
+use APR::Table ();
+
+my @directives = (
+  {
+    name => 'SoundSoftwareDSN',
+    req_override => OR_AUTHCFG,
+    args_how => TAKE1,
+    errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
+  },
+  {
+    name => 'SoundSoftwareDbUser',
+    req_override => OR_AUTHCFG,
+    args_how => TAKE1,
+  },
+  {
+    name => 'SoundSoftwareDbPass',
+    req_override => OR_AUTHCFG,
+    args_how => TAKE1,
+  },
+  {
+    name => 'SoundSoftwareDbWhereClause',
+    req_override => OR_AUTHCFG,
+    args_how => TAKE1,
+  },
+  {
+    name => 'SoundSoftwareRepoPrefix',
+    req_override => OR_AUTHCFG,
+    args_how => TAKE1,
+  },
+);
+
+sub SoundSoftwareDSN { 
+    my ($self, $parms, $arg) = @_;
+    $self->{SoundSoftwareDSN} = $arg;
+    my $query = "SELECT 
+                 hashed_password, auth_source_id, permissions
+              FROM members, projects, users, roles, member_roles
+              WHERE 
+                projects.id=members.project_id
+                AND member_roles.member_id=members.id
+                AND users.id=members.user_id 
+                AND roles.id=member_roles.role_id
+                AND users.status=1 
+                AND login=? 
+                AND identifier=? ";
+    $self->{SoundSoftwareQuery} = trim($query);
+}
+
+sub SoundSoftwareDbUser { set_val('SoundSoftwareDbUser', @_); }
+sub SoundSoftwareDbPass { set_val('SoundSoftwareDbPass', @_); }
+sub SoundSoftwareDbWhereClause { 
+    my ($self, $parms, $arg) = @_;
+    $self->{SoundSoftwareQuery} = trim($self->{SoundSoftwareQuery}.($arg ? $arg : "")." ");
+}
+
+sub SoundSoftwareRepoPrefix { 
+    my ($self, $parms, $arg) = @_;
+    if ($arg) {
+	$self->{SoundSoftwareRepoPrefix} = $arg;
+    }
+}
+
+sub trim {
+    my $string = shift;
+    $string =~ s/\s{2,}/ /g;
+    return $string;
+}
+
+sub set_val {
+    my ($key, $self, $parms, $arg) = @_;
+    $self->{$key} = $arg;
+}
+
+Apache2::Module::add(__PACKAGE__, \@directives);
+
+
+my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
+
+sub access_handler {
+    my $r = shift;
+
+    print STDERR "SoundSoftware.pm: In access handler\n";
+
+    unless ($r->some_auth_required) {
+	$r->log_reason("No authentication has been configured");
+	return FORBIDDEN;
+    }
+
+    my $method = $r->method;
+
+    print STDERR "SoundSoftware.pm: Method: $method, uri " . $r->uri . ", location " . $r->location . "\n";
+    print STDERR "SoundSoftware.pm: Accept: " . $r->headers_in->{Accept} . "\n";
+
+    if (!defined $read_only_methods{$method}) {
+	print STDERR "SoundSoftware.pm: Method is not read-only, authentication handler required\n";
+	return OK;
+    }
+
+    my $dbh = connect_database($r);
+
+    my $project_id = get_project_identifier($dbh, $r);
+    my $status = get_project_status($dbh, $project_id, $r);
+
+    $dbh->disconnect();
+    undef $dbh;
+
+    if ($status == 0) { # nonexistent
+	print STDERR "SoundSoftware.pm: Project does not exist, refusing access\n";
+	return FORBIDDEN;
+    } elsif ($status == 1) { # public
+	print STDERR "SoundSoftware.pm: Project is public, no restriction here\n";
+	$r->set_handlers(PerlAuthenHandler => [\&OK])
+    } else { # private
+	print STDERR "SoundSoftware.pm: Project is private, authentication handler required\n";
+    }
+
+    return OK
+}
+
+sub authen_handler {
+    my $r = shift;
+    
+    print STDERR "SoundSoftware.pm: In authentication handler\n";
+
+    my $dbh = connect_database($r);
+    
+    my $project_id = get_project_identifier($dbh, $r);
+    my $realm = get_realm($dbh, $project_id, $r);
+    $r->auth_name($realm);
+
+    my ($res, $redmine_pass) =  $r->get_basic_auth_pw();
+    unless ($res == OK) {
+	$dbh->disconnect();
+	undef $dbh;
+	return $res;
+    }
+    
+    print STDERR "SoundSoftware.pm: User is " . $r->user . ", got password\n";
+
+    my $permitted = is_permitted($dbh, $project_id, $r->user, $redmine_pass, $r);
+    
+    $dbh->disconnect();
+    undef $dbh;
+
+    if ($permitted) {
+	return OK;
+    } else {
+	print STDERR "SoundSoftware.pm: Not permitted\n";
+	$r->note_auth_failure();
+	return AUTH_REQUIRED;
+    }
+}
+
+sub get_project_status {
+    my $dbh = shift;
+    my $project_id = shift;
+    my $r = shift;
+
+    if (!defined $project_id or $project_id eq '') {
+	return 0; # nonexistent
+    }
+    
+    my $sth = $dbh->prepare(
+        "SELECT is_public FROM projects WHERE projects.identifier = ?;"
+    );
+
+    $sth->execute($project_id);
+    my $ret = 0; # nonexistent
+    if (my @row = $sth->fetchrow_array) {
+    	if ($row[0] eq "1" || $row[0] eq "t") {
+	    $ret = 1; # public
+    	} else {
+	    $ret = 2; # private
+	}
+    }
+    $sth->finish();
+    undef $sth;
+
+    $ret;
+}
+
+sub is_permitted {
+    my $dbh = shift;
+    my $project_id = shift;
+    my $redmine_user = shift;
+    my $redmine_pass = shift;
+    my $r = shift;
+
+    my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
+
+    my $cfg = Apache2::Module::get_config
+	(__PACKAGE__, $r->server, $r->per_dir_config);
+
+    my $query = $cfg->{SoundSoftwareQuery};
+    my $sth = $dbh->prepare($query);
+    $sth->execute($redmine_user, $project_id);
+
+    my $ret;
+    while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) {
+
+	# Test permissions for this user before we verify credentials
+	# -- if the user is not permitted this action anyway, there's
+	# not much point in e.g. contacting the LDAP
+
+	my $method = $r->method;
+
+	if ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/)
+	    || $permissions =~ /:commit_access/) {
+
+	    # User would be permitted this action, if their
+	    # credentials checked out -- test those now
+
+	    print STDERR "SoundSoftware.pm: User $redmine_user has required role, checking credentials\n";
+
+	    unless ($auth_source_id) {
+		if ($hashed_password eq $pass_digest) {
+		    print STDERR "SoundSoftware.pm: User $redmine_user authenticated via password\n";
+		    $ret = 1;
+		    last;
+		}
+	    } else {
+		my $sthldap = $dbh->prepare(
+		    "SELECT host,port,tls,account,account_password,base_dn,attr_login FROM auth_sources WHERE id = ?;"
+		    );
+		$sthldap->execute($auth_source_id);
+		while (my @rowldap = $sthldap->fetchrow_array) {
+		    my $ldap = Authen::Simple::LDAP->new(
+			host    => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
+			port    => $rowldap[1],
+			basedn  => $rowldap[5],
+			binddn  => $rowldap[3] ? $rowldap[3] : "",
+			bindpw  => $rowldap[4] ? $rowldap[4] : "",
+			filter  => "(".$rowldap[6]."=%s)"
+			);
+		    if ($ldap->authenticate($redmine_user, $redmine_pass)) {
+			print STDERR "SoundSoftware.pm: User $redmine_user authenticated via LDAP\n";
+			$ret = 1;
+		    }
+		}
+		$sthldap->finish();
+		undef $sthldap;
+	    }
+	} else {
+	    print STDERR "SoundSoftware.pm: User $redmine_user lacks required role for this project\n";
+	}
+    }
+
+    $sth->finish();
+    undef $sth;
+
+    $ret;
+}
+
+sub get_project_identifier {
+    my $dbh = shift;
+    my $r = shift;
+
+    my $location = $r->location;
+    my ($repo) = $r->uri =~ m{$location/*([^/]+)};
+
+    return $repo if (!$repo);
+
+    $repo =~ s/[^a-zA-Z0-9\._-]//g;
+
+    # The original Redmine.pm returns the string just calculated as
+    # the project identifier.  That won't do for us -- we may have
+    # (and in fact already do have, in our test instance) projects
+    # whose repository names differ from the project identifiers.
+
+    # This is a rather fundamental change because it means that almost
+    # every request needs more than one database query -- which
+    # prompts us to start passing around $dbh instead of connecting
+    # locally within each function as is done in Redmine.pm.
+
+    my $sth = $dbh->prepare(
+        "SELECT projects.identifier FROM projects, repositories WHERE repositories.project_id = projects.id AND repositories.url LIKE ?;"
+    );
+
+    my $cfg = Apache2::Module::get_config
+	(__PACKAGE__, $r->server, $r->per_dir_config);
+
+    my $prefix = $cfg->{SoundSoftwareRepoPrefix};
+    if (!defined $prefix) { $prefix = '%/'; }
+
+    my $identifier = '';
+
+    $sth->execute($prefix . $repo);
+    my $ret = 0;
+    if (my @row = $sth->fetchrow_array) {
+	$identifier = $row[0];
+    }
+    $sth->finish();
+    undef $sth;
+
+    print STDERR "SoundSoftware.pm: Repository '$repo' belongs to project '$identifier'\n";
+
+    $identifier;
+}
+
+sub get_realm {
+    my $dbh = shift;
+    my $project_id = shift;
+    my $r = shift;
+
+    my $sth = $dbh->prepare(
+        "SELECT projects.name FROM projects WHERE projects.identifier = ?;"
+    );
+
+    my $name = $project_id;
+
+    $sth->execute($project_id);
+    my $ret = 0;
+    if (my @row = $sth->fetchrow_array) {
+	$name = $row[0];
+    }
+    $sth->finish();
+    undef $sth;
+
+    # be timid about characters not permitted in auth realm and revert
+    # to project identifier if any are found
+    if ($name =~ m/[^\w\d\s\._-]/) {
+	$name = $project_id;
+    }
+
+    my $realm = '"Mercurial repository for ' . "'$name'" . '"';
+
+    $realm;
+}
+
+sub connect_database {
+    my $r = shift;
+    
+    my $cfg = Apache2::Module::get_config
+	(__PACKAGE__, $r->server, $r->per_dir_config);
+
+    return DBI->connect($cfg->{SoundSoftwareDSN},
+			$cfg->{SoundSoftwareDbUser},
+			$cfg->{SoundSoftwareDbPass});
+}
+
+1;
--- a/extra/svn/reposman.rb	Wed Aug 25 16:30:24 2010 +0100
+++ b/extra/svn/reposman.rb	Wed Aug 25 16:33:28 2010 +0100
@@ -48,6 +48,8 @@
 #                             kind.
 #                             This command override the default creation for git
 #                             and subversion.
+#   --http-user=USER          User for HTTP Basic authentication with Redmine WS
+#   --http-pass=PASSWORD      Password for Basic authentication with Redmine WS
 #   -f, --force               force repository creation even if the project
 #                             repository is already declared in Redmine
 #   -t, --test                only show what should be done
@@ -78,6 +80,8 @@
                       ['--url',          '-u', GetoptLong::REQUIRED_ARGUMENT],
                       ['--command' ,     '-c', GetoptLong::REQUIRED_ARGUMENT],
                       ['--scm',                GetoptLong::REQUIRED_ARGUMENT],
+                      ['--http-user',          GetoptLong::REQUIRED_ARGUMENT],
+                      ['--http-pass',          GetoptLong::REQUIRED_ARGUMENT],
                       ['--test',         '-t', GetoptLong::NO_ARGUMENT],
                       ['--force',        '-f', GetoptLong::NO_ARGUMENT],
                       ['--verbose',      '-v', GetoptLong::NO_ARGUMENT],
@@ -90,6 +94,8 @@
 $quiet        = false
 $redmine_host = ''
 $repos_base   = ''
+$http_user    = ''
+$http_pass    = ''
 $svn_owner    = 'root'
 $svn_group    = 'root'
 $use_groupid  = true
@@ -138,6 +144,8 @@
     when '--group';          $svn_group    = arg.dup; $use_groupid = false;
     when '--url';            $svn_url      = arg.dup
     when '--scm';            $scm          = arg.dup.capitalize; log("Invalid SCM: #{$scm}", :exit => true) unless SUPPORTED_SCM.include?($scm)
+    when '--http-user';      $http_user    = arg.dup
+    when '--http-pass';      $http_pass    = arg.dup
     when '--command';        $command =      arg.dup
     when '--verbose';        $verbose += 1
     when '--test';           $test = true
@@ -188,6 +196,8 @@
 $redmine_host.gsub!(/\/$/, '')
 
 Project.site = "#{$redmine_host}/sys";
+Project.user = $http_user;
+Project.password = $http_pass;
 
 begin
   # Get all active projects that have the Repository module enabled
@@ -307,4 +317,4 @@
   end
 
 end
-  
\ No newline at end of file
+  
--- a/lib/redmine/scm/adapters/abstract_adapter.rb	Wed Aug 25 16:30:24 2010 +0100
+++ b/lib/redmine/scm/adapters/abstract_adapter.rb	Wed Aug 25 16:33:28 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 Aug 25 16:30:24 2010 +0100
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl	Wed Aug 25 16:33:28 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 Aug 25 16:30:24 2010 +0100
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl	Wed Aug 25 16:33:28 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 Aug 25 16:30:24 2010 +0100
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb	Wed Aug 25 16:33:28 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
Binary file public/favicon.ico has changed
--- a/public/themes/ssamr/stylesheets/application.css	Wed Aug 25 16:30:24 2010 +0100
+++ b/public/themes/ssamr/stylesheets/application.css	Wed Aug 25 16:33:28 2010 +0100
@@ -20,6 +20,19 @@
   src: url('DroidSans-Bold.ttf');
 }
 
+@font-face
+{
+  font-family: Gillius;
+  src: url('GilliusADFNo2-Regular.otf');
+}
+
+@font-face
+{
+  font-family: Gillius; 
+  font-weight: bold;
+  src: url('GilliusADFNo2-Bold.otf');
+}
+
 body {
   background: #ffffff;
   color: #404040;