ya-hg-overhaul-0.9-stable-2010-05-17.patch

Yet another overhaul patch for 0.9-stable - Yuya Nishihara, 2010-05-17 15:14

Download (64.1 KB)

View differences:

app/models/changeset.rb
152 152
  def self.normalize_comments(str)
153 153
    to_utf8(str.to_s.strip)
154 154
  end
155

  
156
  # Creates a new Change from it's common parameters
157
  def create_change(change)
158
    Change.create(:changeset => self,
159
                  :action => change[:action],
160
                  :path => change[:path],
161
                  :from_path => change[:from_path],
162
                  :from_revision => change[:from_revision])
163
  end
155 164
  
156 165
  private
157 166

  
app/models/repository/darcs.rb
85 85
                                         :comments => revision.message)
86 86
                                         
87 87
            revision.paths.each do |change|
88
              Change.create(:changeset => changeset,
89
                            :action => change[:action],
90
                            :path => change[:path],
91
                            :from_path => change[:from_path],
92
                            :from_revision => change[:from_revision])
88
              changeset.create_change(change)
93 89
            end
94 90
            next_rev += 1
95 91
          end if revisions
app/models/repository/mercurial.rb
78 78
                                           :comments => revision.message)
79 79
              
80 80
              revision.paths.each do |change|
81
                Change.create(:changeset => changeset,
82
                              :action => change[:action],
83
                              :path => change[:path],
84
                              :from_path => change[:from_path],
85
                              :from_revision => change[:from_revision])
81
                changeset.create_change(change)
86 82
              end
87 83
            end
88 84
          end unless revisions.nil?
app/models/repository/subversion.rb
63 63
                                           :comments => revision.message)
64 64
              
65 65
              revision.paths.each do |change|
66
                Change.create(:changeset => changeset,
67
                              :action => change[:action],
68
                              :path => change[:path],
69
                              :from_path => change[:from_path],
70
                              :from_revision => change[:from_revision])
66
                changeset.create_change(change)
71 67
              end unless changeset.new_record?
72 68
            end
73 69
          end unless revisions.nil?
app/helpers/repositories_helper.rb
52 52
      else
53 53
        change
54 54
      end
55
    end.compact
55
   end.compact
56 56
    
57 57
    tree = { }
58 58
    changes.each do |change|
59 59
      p = tree
60 60
      dirs = change.path.to_s.split('/').select {|d| !d.blank?}
61
      path = ''
61 62
      dirs.each do |dir|
63
        path += '/' + dir
62 64
        p[:s] ||= {}
63 65
        p = p[:s]
64
        p[dir] ||= {}
65
        p = p[dir]
66
        p[path] ||= {}
67
        p = p[path]
66 68
      end
67 69
      p[:c] = change
68 70
    end
......
76 78
    output = ''
77 79
    output << '<ul>'
78 80
    tree.keys.sort.each do |file|
79
      s = !tree[file][:s].nil?
80
      c = tree[file][:c]
81
      
82 81
      style = 'change'
83
      style << ' folder' if s
84
      style << " change-#{c.action}" if c
85
      
86
      text = h(file)
87
      unless c.nil?
82
      text = File.basename(h(file))
83
      if s = tree[file][:s]
84
        style << ' folder'
85
        path_param = to_path_param(@repository.relative_path(file))
86
        text = link_to(text, :controller => 'repositories',
87
                             :action => 'show',
88
                             :id => @project,
89
                             :path => path_param,
90
                             :rev => @changeset.revision)
91
        output << "<li class='#{style}'>#{text}</li>"
92
        output << render_changes_tree(s)
93
      elsif c = tree[file][:c]
94
        style << " change-#{c.action}"
88 95
        path_param = to_path_param(@repository.relative_path(c.path))
89 96
        text = link_to(text, :controller => 'repositories',
90 97
                             :action => 'entry',
91 98
                             :id => @project,
92 99
                             :path => path_param,
93
                             :rev => @changeset.revision) unless s || c.action == 'D'
100
                             :rev => @changeset.revision) unless c.action == 'D'
94 101
        text << " - #{c.revision}" unless c.revision.blank?
95 102
        text << ' (' + link_to('diff', :controller => 'repositories',
96 103
                                       :action => 'diff',
......
98 105
                                       :path => path_param,
99 106
                                       :rev => @changeset.revision) + ') ' if c.action == 'M'
100 107
        text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
108
        output << "<li class='#{style}'>#{text}</li>"
101 109
      end
102
      output << "<li class='#{style}'>#{text}</li>"
103
      output << render_changes_tree(tree[file][:s]) if s
104 110
    end
105 111
    output << '</ul>'
106 112
    output
app/models/repository/git.rb
49 49
    c = changesets.find(:first, :order => 'committed_on DESC')
50 50
    since = (c ? c.committed_on - 7.days : nil)
51 51

  
52
    revisions = scm.revisions('', nil, nil, :all => true, :since => since)
52
    revisions = scm.revisions('', nil, nil, :all => true, :since => since, :reverse => true)
53 53
    return if revisions.nil? || revisions.empty?
54 54

  
55 55
    recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since])
......
75 75
        "scmid IN (?)", 
76 76
        revisions.map!{|c| c.scmid}
77 77
      ],
78
      :order => 'committed_on DESC'
78
      :order => 'id DESC'
79 79
    )
80 80
  end
81 81
end
app/models/issue.rb
27 27

  
28 28
  has_many :journals, :as => :journalized, :dependent => :destroy
29 29
  has_many :time_entries, :dependent => :delete_all
30
  has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30
  has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.id ASC"
31 31
  
32 32
  has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 33
  has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
app/models/repository.rb
17 17

  
18 18
class Repository < ActiveRecord::Base
19 19
  belongs_to :project
20
  has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
20
  has_many :changesets, :order => "#{Changeset.table_name}.id DESC"
21 21
  has_many :changes, :through => :changesets
22 22
  
23 23
  # Raw SQL to delete changesets and changes in the database
......
106 106
  def latest_changesets(path, rev, limit=10)
107 107
    if path.blank?
108 108
      changesets.find(:all, :include => :user,
109
                            :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
110 109
                            :limit => limit)
111 110
    else
112 111
      changes.find(:all, :include => {:changeset => :user}, 
113 112
                         :conditions => ["path = ?", path.with_leading_slash],
114
                         :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
113
                         :order => "#{Changeset.table_name}.id DESC",
115 114
                         :limit => limit).collect(&:changeset)
116 115
    end
117 116
  end
app/models/changeset.rb
47 47
  def revision=(r)
48 48
    write_attribute :revision, (r.nil? ? nil : r.to_s)
49 49
  end
50

  
51
  # Returns the identifier of this changeset.
52
  # e.g. revision number for centralized system; hash id for DVCS
53
  def identifier
54
    scmid || revision
55
  end
50 56
  
51 57
  def comments=(comment)
52 58
    write_attribute(:comments, Changeset.normalize_comments(comment))
lib/redmine/scm/adapters/abstract_adapter.rb
271 271
      end
272 272
      
273 273
      class Revision
274
        attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
274
        attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch
275
        attr_writer :identifier
275 276

  
276 277
        def initialize(attributes={})
277 278
          self.identifier = attributes[:identifier]
......
285 286
          self.branch = attributes[:branch]
286 287
        end
287 288

  
289
        # Returns the identifier of this revision.
290
        # e.g. revision number for centralized system; hash id for DVCS
291
        def identifier
292
          @identifier || scmid || revision
293
        end
294

  
288 295
        def save(repo)
289 296
          Changeset.transaction do
290 297
            changeset = Changeset.new(
app/models/repository.rb
94 94
  
95 95
  # Finds and returns a revision with a number or the beginning of a hash
96 96
  def find_changeset_by_name(name)
97
    changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
97
    # TODO: is this query efficient enough? can we write as single query?
98
    e = changesets.find(:first, :conditions => ['revision = ? OR scmid = ?', name.to_s, name.to_s])
99
    return e if e
100
    changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
98 101
  end
99 102
  
100 103
  def latest_changeset
app/helpers/application_helper.rb
101 101
  # * :text - Link text (default to the formatted revision)
102 102
  def link_to_revision(revision, project, options={})
103 103
    text = options.delete(:text) || format_revision(revision)
104
    rev = revision.respond_to?(:identifier) ? revision.identifier : revision
104 105

  
105
    link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
106
    link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
107
            :title => l(:label_revision_id, format_revision(revision)))
106 108
  end
107 109

  
108 110
  def toggle_link(name, id, options={})
app/helpers/repositories_helper.rb
18 18
require 'iconv'
19 19

  
20 20
module RepositoriesHelper
21
  def format_revision(txt)
22
    txt.to_s[0,8]
21
  # truncate rev to 8 chars if it's quite long
22
  def truncate_long_revision_name(rev)
23
    rev.to_s.size <= 12 ? rev.to_s : rev.to_s[0, 8]
24
  end
25
  private :truncate_long_revision_name
26

  
27
  def format_revision(revision)
28
    if [:identifier, :revision, :scmid].all? { |e| revision.respond_to? e }
29
      if revision.scmid and revision.revision != revision.scmid and /[^\d]/ !~ revision.revision
30
        "#{revision.revision}:#{revision.scmid}"  # number:hashid
31
      else
32
        truncate_long_revision_name(revision.identifier)
33
      end
34
    else
35
      truncate_long_revision_name(revision)
36
    end
23 37
  end
24 38
  
25 39
  def truncate_at_line_break(text, length = 255)
app/helpers/application_helper.rb
563 563
            end
564 564
          when 'commit'
565 565
            if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
566
              link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
566
              link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
567 567
                                           :class => 'changeset',
568 568
                                           :title => truncate_single_line(changeset.comments, :length => 100)
569 569
            end
app/models/changeset.rb
53 53
  def identifier
54 54
    scmid || revision
55 55
  end
56

  
57
  # Returns the wiki identifier, "rN" or "commit:ABCDEF"
58
  def wiki_identifier
59
    if scmid  # hash-like
60
      "commit:#{scmid}"
61
    else  # numeric
62
      "r#{revision}"
63
    end
64
  end
65
  private :wiki_identifier
56 66
  
57 67
  def comments=(comment)
58 68
    write_attribute(:comments, Changeset.normalize_comments(comment))
......
115 125
          issue.reload
116 126
          # don't change the status is the issue is closed
117 127
          next if issue.status.is_closed?
118
          csettext = "r#{self.revision}"
119
          if self.scmid && (! (csettext =~ /^r[0-9]+$/))
120
            csettext = "commit:\"#{self.scmid}\""
121
          end
122
          journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
128
          journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, wiki_identifier))
123 129
          issue.status = fix_status
124 130
          unless Setting.commit_fix_done_ratio.blank?
125 131
            issue.done_ratio = Setting.commit_fix_done_ratio.to_i
app/helpers/repositories_helper.rb
35 35
      truncate_long_revision_name(revision)
36 36
    end
37 37
  end
38
  module_function :format_revision  # callable as RepositoriesHelper.format_revision
38 39
  
39 40
  def truncate_at_line_break(text, length = 255)
40 41
    if text
app/models/changeset.rb
23 23
  has_many :changes, :dependent => :delete_all
24 24
  has_and_belongs_to_many :issues
25 25

  
26
  acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
26
  acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{RepositoriesHelper.format_revision(o)}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
27 27
                :description => :long_comments,
28 28
                :datetime => :committed_on,
29
                :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
29
                :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
30 30
                
31 31
  acts_as_searchable :columns => 'comments',
32 32
                     :include => {:repository => :project},
app/helpers/repositories_helper.rb
102 102
                             :action => 'show',
103 103
                             :id => @project,
104 104
                             :path => path_param,
105
                             :rev => @changeset.revision)
105
                             :rev => @changeset.identifier)
106 106
        output << "<li class='#{style}'>#{text}</li>"
107 107
        output << render_changes_tree(s)
108 108
      elsif c = tree[file][:c]
......
112 112
                             :action => 'entry',
113 113
                             :id => @project,
114 114
                             :path => path_param,
115
                             :rev => @changeset.revision) unless c.action == 'D'
115
                             :rev => @changeset.identifier) unless c.action == 'D'
116 116
        text << " - #{c.revision}" unless c.revision.blank?
117 117
        text << ' (' + link_to('diff', :controller => 'repositories',
118 118
                                       :action => 'diff',
119 119
                                       :id => @project,
120 120
                                       :path => path_param,
121
                                       :rev => @changeset.revision) + ') ' if c.action == 'M'
121
                                       :rev => @changeset.identifier) + ') ' if c.action == 'M'
122 122
        text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
123 123
        output << "<li class='#{style}'>#{text}</li>"
124 124
      end
app/views/repositories/_dir_list_content.rhtml
17 17
</td>
18 18
<td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
19 19
<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
20
<td class="revision"><%= link_to_revision(changeset.revision, @project) if changeset %></td>
20
<td class="revision"><%= link_to_revision(changeset, @project) if changeset %></td>
21 21
<td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
22 22
<td class="author"><%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %></td>
23 23
<td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
app/views/repositories/_revisions.rhtml
13 13
<% line_num = 1 %>
14 14
<% revisions.each do |changeset| %>
15 15
<tr class="changeset <%= cycle 'odd', 'even' %>">
16
<td class="id"><%= link_to_revision(changeset.revision, project) %></td>
17
<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>
18
<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>
16
<td class="id"><%= link_to_revision(changeset, project) %></td>
17
<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>
18
<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>
19 19
<td class="committed_on"><%= format_time(changeset.committed_on) %></td>
20 20
<td class="author"><%=h changeset.author %></td>
21 21
<td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
app/views/repositories/annotate.rhtml
19 19
    <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
20 20
      <th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th>
21 21
      <td class="revision">
22
      <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %></td>
22
      <%= (revision.identifier ? link_to_revision(revision, @project) : format_revision(revision)) if revision %></td>
23 23
      <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
24 24
      <td class="line-code"><pre><%= line %></pre></td>
25 25
    </tr>
app/views/repositories/revision.rhtml
1 1
<div class="contextual">
2 2
  &#171;
3 3
  <% unless @changeset.previous.nil? -%>
4
    <%= link_to_revision(@changeset.previous.revision, @project, :text => l(:label_previous)) %>
4
    <%= link_to_revision(@changeset.previous, @project, :text => l(:label_previous)) %>
5 5
  <% else -%>
6 6
    <%= l(:label_previous) %>
7 7
  <% end -%>
8 8
|
9 9
  <% unless @changeset.next.nil? -%>
10
    <%= link_to_revision(@changeset.next.revision, @project, :text => l(:label_next)) %>
10
    <%= link_to_revision(@changeset.next, @project, :text => l(:label_next)) %>
11 11
  <% else -%>
12 12
    <%= l(:label_next) %>
13 13
  <% end -%>
......
19 19
  <% end %>
20 20
</div>
21 21

  
22
<h2><%= l(:label_revision) %> <%= format_revision(@changeset.revision) %></h2>
22
<h2><%= l(:label_revision) %> <%= format_revision(@changeset) %></h2>
23 23

  
24 24
<p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
25 25
<span class="author"><%= authoring(@changeset.committed_on, @changeset.author) %></span></p>
......
45 45
<li class="change change-D"><%= l(:label_deleted) %></li>
46 46
</ul>
47 47

  
48
<p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %></p>
48
<p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.identifier) if @changeset.changes.any? %></p>
49 49

  
50 50
<div class="changeset-changes">
51 51
<%= render_changeset_changes %>
app/views/repositories/revision.rhtml
14 14
  &#187;&nbsp;
15 15

  
16 16
  <% form_tag({:controller => 'repositories', :action => 'revision', :id => @project, :rev => nil}, :method => :get) do %>
17
    <%= text_field_tag 'rev', @rev[0,8], :size => 8 %>
17
    <%= text_field_tag 'rev', @rev, :size => 8 %>
18 18
    <%= submit_tag 'OK', :name => nil %>
19 19
  <% end %>
20 20
</div>
app/models/repository.rb
174 174
  def self.fetch_changesets
175 175
    Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
176 176
      if project.repository
177
        project.repository.fetch_changesets
177
        begin
178
          project.repository.fetch_changesets
179
        rescue Redmine::Scm::Adapters::CommandFailed => e
180
          logger.error "Repository: error during fetching changesets: #{e.message}"
181
        end
178 182
      end
179 183
    end
180 184
  end
extra/mercurial/redminehelper.py
1
# redminehelper: draft extension for Mercurial
2
# it's a draft to show a possible way to explore repository by the Redmine overhaul patch
3
# see: http://www.redmine.org/issues/4455
4
#
5
# Copyright 2010 Alessio Franceschelli (alefranz.net)
6
#
7
# This software may be used and distributed according to the terms of the
8
# GNU General Public License version 2 or any later version.
9

  
10
'''command to list revision of each file
11
'''
12

  
13
from mercurial import cmdutil, commands
14
from mercurial.i18n import _
15

  
16
def overhaul(ui, repo, rev=None, **opts):
17
    mf = repo[rev].manifest()
18
    for f in repo[rev]:
19
        try:
20
            fctx = repo.filectx(f, fileid=mf[f])
21
            ctx = fctx.changectx()
22
            ui.write('%s\t%d\t%s\n' %
23
                     (ctx,fctx.size(),f))
24
        except LookupError:
25
            pass
26

  
27
cmdtable = {
28
    'overhaul': (overhaul,commands.templateopts, _('hg overhaul [rev]'))
29
}
extra/mercurial/redminehelper.py
1
# redminehelper: draft extension for Mercurial
1
# redminehelper: Redmine helper extension for Mercurial
2 2
# it's a draft to show a possible way to explore repository by the Redmine overhaul patch
3 3
# see: http://www.redmine.org/issues/4455
4 4
#
5 5
# Copyright 2010 Alessio Franceschelli (alefranz.net)
6
# Copyright 2010 Yuya Nishihara <yuya@tcha.org>
6 7
#
7 8
# This software may be used and distributed according to the terms of the
8 9
# GNU General Public License version 2 or any later version.
......
10 11
'''command to list revision of each file
11 12
'''
12 13

  
13
from mercurial import cmdutil, commands
14
from mercurial.i18n import _
14
import re, time
15
from mercurial import cmdutil, commands, node, error
15 16

  
16
def overhaul(ui, repo, rev=None, **opts):
17
SPECIAL_TAGS = ('tip',)
18

  
19
def rhsummary(ui, repo, **opts):
20
    """output the summary of the repository"""
21
    # see mercurial/commands.py:tip
22
    ui.write(':tip: rev node\n')
23
    tipctx = repo[len(repo) - 1]
24
    ui.write('%d %s\n' % (tipctx.rev(), tipctx))
25

  
26
    # see mercurial/commands.py:root
27
    ui.write(':root: path\n')
28
    ui.write(repo.root + '\n')
29

  
30
    # see mercurial/commands.py:tags
31
    ui.write(':tags: rev node name\n')
32
    for t, n in reversed(repo.tagslist()):
33
        if t in SPECIAL_TAGS:
34
            continue
35
        try:
36
            r = repo.changelog.rev(n)
37
        except error.LookupError:
38
            r = -1
39
        ui.write('%d %s %s\n' % (r, node.short(n), t))
40

  
41
    # see mercurial/commands.py:branches
42
    def iterbranches():
43
        for t, n in repo.branchtags().iteritems():
44
            yield t, n, repo.changelog.rev(n)
45

  
46
    ui.write(':branches: rev node name\n')
47
    for t, n, r in sorted(iterbranches(), key=lambda e: e[2], reverse=True):
48
        if repo.lookup(r) in repo.branchheads(t, closed=False):
49
            ui.write('%d %s %s\n' % (r, node.short(n), t))  # only open branch
50

  
51
def rhentries(ui, repo, path='', **opts):
52
    """output the entries of the specified directory"""
53
    rev = opts.get('rev')
54
    pathprefix = (path.rstrip('/') + '/').lstrip('/')
55

  
56
    # TODO: clean up
57
    dirs, files = {}, {}
17 58
    mf = repo[rev].manifest()
18 59
    for f in repo[rev]:
19
        try:
20
            fctx = repo.filectx(f, fileid=mf[f])
21
            ctx = fctx.changectx()
22
            ui.write('%s\t%d\t%s\n' %
23
                     (ctx,fctx.size(),f))
24
        except LookupError:
25
            pass
60
        if not f.startswith(pathprefix):
61
            continue
62

  
63
        name = re.sub(r'/.*', '', f[len(pathprefix):])
64
        if '/' in f[len(pathprefix):]:
65
            dirs[name] = (name,)
66
        else:
67
            try:
68
                fctx = repo.filectx(f, fileid=mf[f])
69
                ctx = fctx.changectx()
70
                tm, tzoffset = ctx.date()
71
                localtime = int(tm) + tzoffset - time.timezone
72
                files[name] = (ctx.rev(), node.short(ctx.node()), localtime,
73
                               fctx.size(), name)
74
            except LookupError:  # TODO: when this occurs?
75
                pass
76

  
77
    ui.write(':dirs: name\n')
78
    for n, v in sorted(dirs.iteritems(), key=lambda e: e[0]):
79
        ui.write(' '.join(v) + '\n')
80

  
81
    ui.write(':files: rev node time size name\n')
82
    for n, v in sorted(files.iteritems(), key=lambda e: e[0]):
83
        ui.write(' '.join(str(e) for e in v) + '\n')
84

  
26 85

  
27 86
cmdtable = {
28
    'overhaul': (overhaul,commands.templateopts, _('hg overhaul [rev]'))
87
    'rhsummary': (rhsummary, [], 'hg rhsummary'),
88
    'rhentries': (rhentries,
89
                  [('r', 'rev', '', 'show the specified revision')],
90
                  'hg rhentries [path]'),
29 91
}
lib/redmine/scm/adapters/mercurial_adapter.rb
24 24
        
25 25
        # Mercurial executable name
26 26
        HG_BIN = "hg"
27
        HG_HELPER_EXT = "#{RAILS_ROOT}/extra/mercurial/redminehelper.py"
27 28
        TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
28 29
        TEMPLATE_NAME = "hg-template"
29 30
        TEMPLATE_EXTENSION = "tmpl"
30 31
        
32
        # raised if hg command exited with error, e.g. unknown revision.
33
        class HgCommandAborted < CommandFailed; end
34

  
31 35
        class << self
32 36
          def client_version
33 37
            @@client_version ||= (hgversion || [])
......
170 174
        end
171 175
        
172 176
        def cat(path, identifier=nil)
173
          cmd = "#{HG_BIN} -R #{target('')} cat"
174
          cmd << " -r " + (identifier ? identifier.to_s : "tip")
175
          cmd << " #{target(path)}"
176
          cat = nil
177
          shellout(cmd) do |io|
177
          hg 'cat', '-r', hgrev(identifier), without_leading_slash(path) do |io|
178 178
            io.binmode
179
            cat = io.read
179
            io.read
180 180
          end
181
          return nil if $? && $?.exitstatus != 0
182
          cat
181
        rescue HgCommandAborted
182
          nil  # means not found
183 183
        end
184 184
        
185 185
        def annotate(path, identifier=nil)
......
199 199
          return nil if $? && $?.exitstatus != 0
200 200
          blame
201 201
        end
202

  
203
        # Runs 'hg' command with the given args
204
        def hg(*args, &block)
205
          full_args = [HG_BIN, '--cwd', url]
206
          full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
207
          full_args += args
208
          ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
209
          if $? && $?.exitstatus != 0
210
            raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
211
          end
212
          ret
213
        end
214
        private :hg
215

  
216
        # Returns correct revision identifier
217
        def hgrev(identifier)
218
          identifier.blank? ? 'tip' : identifier.to_s
219
        end
220
        private :hgrev
202 221
      end
203 222
    end
204 223
  end
lib/redmine/scm/adapters/mercurial_adapter.rb
155 155
        end
156 156
        
157 157
        def diff(path, identifier_from, identifier_to=nil)
158
          path ||= ''
158
          hg_args = ['diff', '--nodates']
159 159
          if identifier_to
160
            identifier_to = identifier_to.to_i 
160
            hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
161 161
          else
162
            identifier_to = identifier_from.to_i - 1
162
            hg_args << '-c' << hgrev(identifier_from)
163 163
          end
164
          cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
165
          cmd << " -I #{target(path)}" unless path.empty?
166
          diff = []
167
          shellout(cmd) do |io|
168
            io.each_line do |line|
169
              diff << line
170
            end
164
          hg_args << without_leading_slash(path) unless path.blank?
165

  
166
          hg *hg_args do |io|
167
            io.collect
171 168
          end
172
          return nil if $? && $?.exitstatus != 0
173
          diff
169
        rescue HgCommandAborted
170
          nil  # means not found
174 171
        end
175 172
        
176 173
        def cat(path, identifier=nil)
lib/redmine/scm/adapters/mercurial_adapter.rb
180 180
        end
181 181
        
182 182
        def annotate(path, identifier=nil)
183
          path ||= ''
184
          cmd = "#{HG_BIN} -R #{target('')}"
185
          cmd << " annotate -n -u"
186
          cmd << " -r " + (identifier ? identifier.to_s : "tip")
187
          cmd << " -r #{identifier.to_i}" if identifier
188
          cmd << " #{target(path)}"
189 183
          blame = Annotate.new
190
          shellout(cmd) do |io|
191
            io.each_line do |line|
192
              next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
193
              blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
184
          hg 'annotate', '-ncu', '-r', hgrev(identifier), without_leading_slash(path) do |io|
185
            io.each do |line|
186
              next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):(.*)$}
187
              r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3)
188
              blame.add_line($4.rstrip, r)
194 189
            end
195 190
          end
196
          return nil if $? && $?.exitstatus != 0
197 191
          blame
192
        rescue HgCommandAborted
193
          nil  # means not found or cannot be annotated
198 194
        end
199 195

  
200 196
        # Runs 'hg' command with the given args
lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
9 9
file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
10 10
tag = '<tag>{tag|escape}</tag>\n'
11 11
header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
12
# footer="</log>"
12
footer='</log>'
lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
9 9
file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
10 10
tag = '<tag>{tag|escape}</tag>\n'
11 11
header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
12
# footer="</log>"
12
footer='</log>'
lib/redmine/scm/adapters/mercurial_adapter.rb
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18 18
require 'redmine/scm/adapters/abstract_adapter'
19
require 'rexml/document'
19 20

  
20 21
module Redmine
21 22
  module Scm
......
105 106
          entries.sort_by_name
106 107
        end
107 108
        
108
        # Fetch the revisions by using a template file that 
109
        # TODO: is this api necessary?
110
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
111
          revisions = Revisions.new
112
          each_revision { |e| revisions << e }
113
          revisions
114
        end
115

  
116
        # Iterates the revisions by using a template file that
109 117
        # makes Mercurial produce a xml output.
110
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})  
111
          revisions = Revisions.new
112
          cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
113
          if identifier_from && identifier_to
114
            cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
115
          elsif identifier_from
116
            cmd << " -r #{identifier_from.to_i}:"
118
        def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
119
          hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
120
          hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
121
          hg_args << '--limit' << options[:limit] if options[:limit]
122
          hg_args << without_leading_slash(path) unless path.blank?
123
          doc = hg(*hg_args) { |io| REXML::Document.new(io.read) }
124
          # TODO: ??? HG doesn't close the XML Document...
125

  
126
          doc.each_element('log/logentry') do |le|
127
            cpalist = le.get_elements('paths/path-copied').map do |e|
128
              [e.text, e.attributes['copyfrom-path']]
129
            end
130
            cpmap = Hash[*cpalist.flatten]
131

  
132
            paths = le.get_elements('paths/path').map do |e|
133
              {:action => e.attributes['action'], :path => with_leading_slash(e.text),
134
                :from_path => (cpmap.member?(e.text) ? with_leading_slash(cpmap[e.text]) : nil),
135
                :from_revision => (cpmap.member?(e.text) ? le.attributes['revision'] : nil)}
136
            end.sort { |a, b| a[:path] <=> b[:path] }
137

  
138
            yield Revision.new(:identifier => le.attributes['revision'],
139
                               :revision => le.attributes['revision'],
140
                               :scmid => le.attributes['node'],
141
                               :author => (le.elements['author'].text rescue ''),
142
                               :time => Time.parse(le.elements['date'].text).localtime,
143
                               :message => le.elements['msg'].text,
144
                               :paths => paths)
117 145
          end
118
          cmd << " --limit #{options[:limit].to_i}" if options[:limit]
119
          cmd << " #{path}" if path
120
          shellout(cmd) do |io|
121
            begin
122
              # HG doesn't close the XML Document...
123
              doc = REXML::Document.new(io.read << "</log>")
124
              doc.elements.each("log/logentry") do |logentry|
125
                paths = []
126
                copies = logentry.get_elements('paths/path-copied')
127
                logentry.elements.each("paths/path") do |path|
128
                  # Detect if the added file is a copy
129
                  if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
130
                    from_path = c.attributes['copyfrom-path']
131
                    from_rev = logentry.attributes['revision']
132
                  end
133
                  paths << {:action => path.attributes['action'],
134
                    :path => "/#{path.text}",
135
                    :from_path => from_path ? "/#{from_path}" : nil,
136
                    :from_revision => from_rev ? from_rev : nil
137
                  }
138
                end
139
                paths.sort! { |x,y| x[:path] <=> y[:path] }
140
                
141
                revisions << Revision.new({:identifier => logentry.attributes['revision'],
142
                                            :scmid => logentry.attributes['node'],
143
                                            :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
144
                                            :time => Time.parse(logentry.elements['date'].text).localtime,
145
                                            :message => logentry.elements['msg'].text,
146
                                            :paths => paths
147
                                          })
148
              end
149
            rescue
150
              logger.debug($!)
151
            end
152
          end
153
          return nil if $? && $?.exitstatus != 0
154
          revisions
146
          self
155 147
        end
156 148
        
157 149
        def diff(path, identifier_from, identifier_to=nil)
lib/redmine/scm/adapters/mercurial_adapter.rb
67 67
        end
68 68
        
69 69
        def info
70
          cmd = "#{HG_BIN} -R #{target('')} root"
71
          root_url = nil
72
          shellout(cmd) do |io|
73
            root_url = io.gets
74
          end
75
          return nil if $? && $?.exitstatus != 0
76
          info = Info.new({:root_url => root_url.chomp,
77
                            :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
78
                          })
79
          info
80
        rescue CommandFailed
81
          return nil
70
          tip = summary['tip'].first
71
          Info.new(:root_url => summary['root'].first['path'],
72
                   :lastrev => Revision.new(:identifier => tip['rev'].to_i,
73
                                            :revision => tip['rev'],
74
                                            :scmid => tip['node']))
82 75
        end
76

  
77
        def summary
78
          @summary ||= fetchg 'rhsummary'
79
        end
80
        private :summary
83 81
        
84 82
        def entries(path=nil, identifier=nil)
85 83
          path ||= ''
......
198 196
        end
199 197
        private :hg
200 198

  
199
        # Runs 'hg' helper, then parses output to return
200
        def fetchg(*args)
201
          # command output example:
202
          #   :tip: rev node
203
          #   100 abcdef012345
204
          #   :tags: rev node name
205
          #   100 abcdef012345 tip
206
          #   ...
207
          data = Hash.new { |h, k| h[k] = [] }
208
          hg(*args) do |io|
209
            key, attrs = nil, nil
210
            io.each do |line|
211
              next if line.chomp.empty?
212
              if /^:(\w+): ([\w ]+)/ =~ line
213
                key = $1
214
                attrs = $2.split(/ /)
215
              elsif key
216
                alist = attrs.zip(line.chomp.split(/ /, attrs.size))
217
                data[key] << Hash[*alist.flatten]
218
              end
219
            end
220
          end
221
          data
222
        end
223
        private :fetchg
224

  
201 225
        # Returns correct revision identifier
202 226
        def hgrev(identifier)
203 227
          identifier.blank? ? 'tip' : identifier.to_s
app/models/repository/mercurial.rb
21 21
  attr_protected :root_url
22 22
  validates_presence_of :url
23 23

  
24
  FETCH_AT_ONCE = 100  # number of changesets to fetch at once
25

  
24 26
  def scm_adapter
25 27
    Redmine::Scm::Adapters::MercurialAdapter
26 28
  end
......
53 55
  end
54 56

  
55 57
  def fetch_changesets
56
    scm_info = scm.info
57
    if scm_info
58
      # latest revision found in database
59
      db_revision = latest_changeset ? latest_changeset.revision.to_i : -1
60
      # latest revision in the repository
61
      latest_revision = scm_info.lastrev
62
      return if latest_revision.nil?
63
      scm_revision = latest_revision.identifier.to_i
64
      if db_revision < scm_revision
65
        logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
66
        identifier_from = db_revision + 1
67
        while (identifier_from <= scm_revision)
68
          # loads changesets by batches of 100
69
          identifier_to = [identifier_from + 99, scm_revision].min
70
          revisions = scm.revisions('', identifier_from, identifier_to, :with_paths => true)
71
          transaction do
72
            revisions.each do |revision|
73
              changeset = Changeset.create(:repository => self,
74
                                           :revision => revision.identifier,
75
                                           :scmid => revision.scmid,
76
                                           :committer => revision.author, 
77
                                           :committed_on => revision.time,
78
                                           :comments => revision.message)
79
              
80
              revision.paths.each do |change|
81
                changeset.create_change(change)
82
              end
83
            end
84
          end unless revisions.nil?
85
          identifier_from = identifier_to + 1
58
    scm_rev = scm.info.lastrev.revision.to_i
59
    db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
60
    return unless db_rev < scm_rev  # already up-to-date
61

  
62
    logger.debug "Fetching changesets for repository #{url}" if logger
63
    (db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i|
64
      transaction do
65
        scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re|
66
          cs = Changeset.create(:repository => self,
67
                                :revision => re.revision,
68
                                :scmid => re.scmid,
69
                                :committer => re.author,
70
                                :committed_on => re.time,
71
                                :comments => re.message)
72
          re.paths.each { |e| cs.create_change(e) }
86 73
        end
87 74
      end
88 75
    end
76
    self
89 77
  end
90 78
end
app/models/repository/mercurial.rb
54 54
    entries
55 55
  end
56 56

  
57
  # Returns the latest changesets for +path+
58
  def latest_changesets(path, rev, limit=10)
59
    changesets.find(:all, :include => :user,
60
                    :conditions => latest_changesets_cond(path, rev),
61
                    :limit => limit)
62
  end
63

  
64
  def latest_changesets_cond(path, rev)
65
    cond, args = [], []
66

  
67
    if last = rev ? find_changeset_by_name(rev) : nil
68
      cond << "#{Changeset.table_name}.id <= ?"
69
      args << last.id
70
    end
71

  
72
    unless path.blank?
73
      # TODO: there must be a better way to build sub-query
74
      cond << "EXISTS (SELECT * FROM #{Change.table_name}
75
                 WHERE #{Change.table_name}.changeset_id = #{Changeset.table_name}.id
76
                 AND (#{Change.table_name}.path = ? OR #{Change.table_name}.path LIKE ?))"
77
      args << path.with_leading_slash << "#{path.with_leading_slash}/%"
78
    end
79

  
80
    [cond.join(' AND '), *args] unless cond.empty?
81
  end
82
  private :latest_changesets_cond
83

  
57 84
  def fetch_changesets
58 85
    scm_rev = scm.info.lastrev.revision.to_i
59 86
    db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
app/models/repository/mercurial.rb
64 64
  def latest_changesets_cond(path, rev)
65 65
    cond, args = [], []
66 66

  
67
    if last = rev ? find_changeset_by_name(rev) : nil
67
    if last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
68 68
      cond << "#{Changeset.table_name}.id <= ?"
69 69
      args << last.id
70 70
    end
lib/redmine/scm/adapters/mercurial_adapter.rb
74 74
                                            :scmid => tip['node']))
75 75
        end
76 76

  
77
        def tags
78
          summary['tags'].map { |e| e['name'] }
79
        end
80

  
81
        # Returns map of {'tag' => 'nodeid', ...}
82
        def tagmap
83
          alist = summary['tags'].map { |e| e.values_at('name', 'node') }
84
          Hash[*alist.flatten]
85
        end
86

  
77 87
        def summary
78 88
          @summary ||= fetchg 'rhsummary'
79 89
        end
app/models/repository/mercurial.rb
32 32
  end
33 33
  
34 34
  def entries(path=nil, identifier=nil)
35
    entries=scm.entries(path, identifier)
36
    if entries
37
      entries.each do |entry|
38
        next unless entry.is_file?
39
        # Set the filesize unless browsing a specific revision
40
        if identifier.nil?
41
          full_path = File.join(root_url, entry.path)
42
          entry.size = File.stat(full_path).size if File.file?(full_path)
43
        end
44
        # Search the DB for the entry's last change
45
        change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
46
        if change
47
          entry.lastrev.identifier = change.changeset.revision
48
          entry.lastrev.name = change.changeset.revision
49
          entry.lastrev.author = change.changeset.committer
50
          entry.lastrev.revision = change.revision
51
        end
52
      end
53
    end
54
    entries
35
    scm.entries(path, identifier)
55 36
  end
56 37

  
57 38
  # Returns the latest changesets for +path+
lib/redmine/scm/adapters/mercurial_adapter.rb
90 90
        private :summary
91 91
        
92 92
        def entries(path=nil, identifier=nil)
93
          path ||= ''
94 93
          entries = Entries.new
95
          cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
96
          cmd << " -r " + (identifier ? identifier.to_s : "tip")
97
          cmd << " " + shell_quote("path:#{path}") unless path.empty?
98
          shellout(cmd) do |io|
99
            io.each_line do |line|
100
              # HG uses antislashs as separator on Windows
101
              line = line.gsub(/\\/, "/")
102
              if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
103
                e ||= line
104
                e = e.chomp.split(%r{[\/\\]})
105
                entries << Entry.new({:name => e.first,
106
                                       :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
107
                                       :kind => (e.size > 1 ? 'dir' : 'file'),
108
                                       :lastrev => Revision.new
109
                                     }) unless e.empty? || entries.detect{|entry| entry.name == e.first}
110
              end
111
            end
94
          fetched_entries = fetchg('rhentries', '-r', hgrev(identifier),
95
                                   without_leading_slash(path.to_s))
96

  
97
          fetched_entries['dirs'].each do |e|
98
            entries << Entry.new(:name => e['name'],
99
                                 :path => "#{with_trailling_slash(path)}#{e['name']}",
100
                                 :kind => 'dir')
112 101
          end
113
          return nil if $? && $?.exitstatus != 0
114
          entries.sort_by_name
102

  
103
          fetched_entries['files'].each do |e|
104
            entries << Entry.new(:name => e['name'],
105
                                 :path => "#{with_trailling_slash(path)}#{e['name']}",
106
                                 :kind => 'file',
107
                                 :size => e['size'].to_i,
108
                                 :lastrev => Revision.new(:identifier => e['rev'].to_i,
109
                                                          :time => Time.at(e['time'].to_i)))
110
          end
111

  
112
          entries
113
        rescue HgCommandAborted
114
          nil  # means not found
115 115
        end
116 116
        
117 117
        # TODO: is this api necessary?
app/models/repository/mercurial.rb
35 35
    scm.entries(path, identifier)
36 36
  end
37 37

  
38
  def branches
39
    bras = scm.branches
40
    bras.sort unless bras == %w|default|
41
  end
42

  
38 43
  # Returns the latest changesets for +path+
39 44
  def latest_changesets(path, rev, limit=10)
40 45
    changesets.find(:all, :include => :user,
41
                    :conditions => latest_changesets_cond(path, rev),
46
                    :conditions => latest_changesets_cond(path, rev, limit),
42 47
                    :limit => limit)
43 48
  end
44 49

  
45
  def latest_changesets_cond(path, rev)
50
  def latest_changesets_cond(path, rev, limit)
46 51
    cond, args = [], []
47 52

  
48
    if last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
53
    if scm.branchmap.member? rev
54
      # dirty hack to filter by branch. branch name should be in database.
55
      cond << "#{Changeset.table_name}.scmid IN (?)"
56
      args << scm.nodes_in_branch(rev, path, rev, 0, :limit => limit)
57
    elsif last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
49 58
      cond << "#{Changeset.table_name}.id <= ?"
50 59
      args << last.id
51 60
    end
lib/redmine/scm/adapters/mercurial_adapter.rb
84 84
          Hash[*alist.flatten]
85 85
        end
86 86

  
87
        def branches
88
          summary['branches'].map { |e| e['name'] }
89
        end
90

  
91
        # Returns map of {'branch' => 'nodeid', ...}
92
        def branchmap
... This diff was truncated because it exceeds the maximum size that can be displayed.