Project

General

Profile

Feature #4455 » ya-hg-overhaul-0.9-stable-2010-03-27.patch

Yet another overhaul patch for 0.9-stable - Yuya Nishihara, 2010-03-27 14:29

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?
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:tags
27
    ui.write(':tags: rev node name\n')
28
    for t, n in reversed(repo.tagslist()):
29
        if t in SPECIAL_TAGS:
30
            continue
31
        try:
32
            r = repo.changelog.rev(n)
33
        except error.LookupError:
34
            r = -1
35
        ui.write('%d %s %s\n' % (r, node.short(n), t))
36

  
37
    # see mercurial/commands.py:branches
38
    def iterbranches():
39
        for t, n in repo.branchtags().iteritems():
40
            yield t, n, repo.changelog.rev(n)
41

  
42
    # TODO: closed branch?
43
    ui.write(':branches: rev node name\n')
44
    for t, n, r in sorted(iterbranches(), key=lambda e: e[2], reverse=True):
45
        ui.write('%d %s %s\n' % (r, node.short(n), t))
46

  
47
def rhentries(ui, repo, path='', **opts):
48
    """output the entries of the specified directory"""
49
    rev = opts.get('rev')
50
    pathprefix = (path.rstrip('/') + '/').lstrip('/')
51

  
52
    # TODO: clean up
53
    dirs, files = {}, {}
17 54
    mf = repo[rev].manifest()
18 55
    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
56
        if not f.startswith(pathprefix):
57
            continue
58

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

  
73
    ui.write(':dirs: name\n')
74
    for n, v in sorted(dirs.iteritems(), key=lambda e: e[0]):
75
        ui.write(' '.join(v) + '\n')
76

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

  
26 81

  
27 82
cmdtable = {
28
    'overhaul': (overhaul,commands.templateopts, _('hg overhaul [rev]'))
83
    'rhsummary': (rhsummary, [], 'hg rhsummary'),
84
    'rhentries': (rhentries,
85
                  [('r', 'rev', '', 'show the specified revision')],
86
                  'hg rhentries [path]'),
29 87
}
lib/redmine/scm/adapters/mercurial_adapter.rb
24 24
        
25 25
        # Mercurial executable name
26 26
        HG_BIN = "hg"
27
        HG_ENV = {'HGPLAIN' => '', 'HGENCODING' => 'utf-8',
28
          'LANG' => nil, 'LANGUAGE' => nil, 'LC_MESSAGES' => nil}
27 29
        TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
28 30
        TEMPLATE_NAME = "hg-template"
29 31
        TEMPLATE_EXTENSION = "tmpl"
......
59 61
            end
60 62
            "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
61 63
          end
64

  
65
          def shellout(cmd, &block)
66
            orig = Hash[HG_ENV.keys.zip(ENV.values_at(*HG_ENV.keys))]
67
            ENV.update(HG_ENV)
68
            begin
69
              ret = super
70
            ensure
71
              ENV.update(orig)
72
              ret
73
            end
74
          end
62 75
        end
63 76
        
64 77
        def info
lib/redmine/scm/adapters/mercurial_adapter.rb
26 26
        HG_BIN = "hg"
27 27
        HG_ENV = {'HGPLAIN' => '', 'HGENCODING' => 'utf-8',
28 28
          'LANG' => nil, 'LANGUAGE' => nil, 'LC_MESSAGES' => nil}
29
        HG_HELPER_EXT = "#{RAILS_ROOT}/extra/mercurial/redminehelper.py"
29 30
        TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
30 31
        TEMPLATE_NAME = "hg-template"
31 32
        TEMPLATE_EXTENSION = "tmpl"
......
183 184
        end
184 185
        
185 186
        def cat(path, identifier=nil)
186
          cmd = "#{HG_BIN} -R #{target('')} cat"
187
          cmd << " -r " + (identifier ? identifier.to_s : "tip")
188
          cmd << " #{target(path)}"
189
          cat = nil
190
          shellout(cmd) do |io|
187
          hg 'cat', '-r', hgrev(identifier), without_leading_slash(path) do |io|
191 188
            io.binmode
192
            cat = io.read
189
            io.read
193 190
          end
194
          return nil if $? && $?.exitstatus != 0
195
          cat
196 191
        end
197 192
        
198 193
        def annotate(path, identifier=nil)
......
212 207
          return nil if $? && $?.exitstatus != 0
213 208
          blame
214 209
        end
210

  
211
        # Runs 'hg' command with the given args
212
        def hg(*args, &block)
213
          full_args = [HG_BIN, '--cwd', url]
214
          full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
215
          full_args += args
216
          ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
217
          if $? && $?.exitstatus != 0
218
            raise CommandFailed, "hg exited with non-zero status: #{$?.exitstatus}"
219
          end
220
          ret
221
        end
222
        private :hg
223

  
224
        # Returns correct revision identifier
225
        def hgrev(identifier)
226
          identifier.blank? ? 'tip' : identifier.to_s
227
        end
228
        private :hgrev
215 229
      end
216 230
    end
217 231
  end
lib/redmine/scm/adapters/mercurial_adapter.rb
165 165
        end
166 166
        
167 167
        def diff(path, identifier_from, identifier_to=nil)
168
          path ||= ''
168
          hg_args = ['diff', '--nodates']
169 169
          if identifier_to
170
            identifier_to = identifier_to.to_i 
170
            hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
171 171
          else
172
            identifier_to = identifier_from.to_i - 1
172
            hg_args << '-c' << hgrev(identifier_from)
173 173
          end
174
          cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
175
          cmd << " -I #{target(path)}" unless path.empty?
176
          diff = []
177
          shellout(cmd) do |io|
178
            io.each_line do |line|
179
              diff << line
180
            end
174
          hg_args << without_leading_slash(path) unless path.blank?
175

  
176
          hg *hg_args do |io|
177
            io.collect
181 178
          end
182
          return nil if $? && $?.exitstatus != 0
183
          diff
184 179
        end
185 180
        
186 181
        def cat(path, identifier=nil)
lib/redmine/scm/adapters/mercurial_adapter.rb
186 186
        end
187 187
        
188 188
        def annotate(path, identifier=nil)
189
          path ||= ''
190
          cmd = "#{HG_BIN} -R #{target('')}"
191
          cmd << " annotate -n -u"
192
          cmd << " -r " + (identifier ? identifier.to_s : "tip")
193
          cmd << " -r #{identifier.to_i}" if identifier
194
          cmd << " #{target(path)}"
195 189
          blame = Annotate.new
196
          shellout(cmd) do |io|
197
            io.each_line do |line|
198
              next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
199
              blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
190
          hg 'annotate', '-ncu', '-r', hgrev(identifier), without_leading_slash(path) do |io|
191
            io.each do |line|
192
              next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):(.*)$}
193
              blame.add_line($4.rstrip,
194
                             Revision.new(:identifier => $2.to_i, :author => $1.strip,
195
                                          :revision => $2, :scmid => $3))
200 196
            end
201 197
          end
202
          return nil if $? && $?.exitstatus != 0
203 198
          blame
204 199
        end
205 200

  
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
......
115 116
          entries.sort_by_name
116 117
        end
117 118
        
118
        # Fetch the revisions by using a template file that 
119
        # TODO: is this api necessary?
120
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
121
          revisions = Revisions.new
122
          each_revision { |e| revisions << e }
123
          revisions
124
        end
125

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

  
136
          doc.each_element('log/logentry') do |le|
137
            cpalist = le.get_elements('paths/path-copied').map do |e|
138
              [e.text, e.attributes['copyfrom-path']]
139
            end
140
            cpmap = Hash[*cpalist.flatten]
141

  
142
            paths = le.get_elements('paths/path').map do |e|
143
              {:action => e.attributes['action'], :path => with_leading_slash(e.text),
144
                :from_path => (cpmap.member?(e.text) ? with_leading_slash(cpmap[e.text]) : nil),
145
                :from_revision => (cpmap.member?(e.text) ? le.attributes['revision'] : nil)}
146
            end.sort { |a, b| a[:path] <=> b[:path] }
147

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

  
87
        def summary
88
          @summary ||= fetchg 'rhsummary'
89
        end
90
        private :summary
93 91
        
94 92
        def entries(path=nil, identifier=nil)
95 93
          path ||= ''
......
203 201
        end
204 202
        private :hg
205 203

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

  
206 230
        # Returns correct revision identifier
207 231
        def hgrev(identifier)
208 232
          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
                    :order => "#{Changeset.table_name}.id DESC",
62
                    :limit => limit)
63
  end
64

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

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

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

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

  
57 85
  def fetch_changesets
58 86
    scm_rev = scm.info.lastrev.revision.to_i
59 87
    db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
app/models/repository/mercurial.rb
23 23

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

  
26
  SHORT_NODEID_LEN = 12
27

  
26 28
  def scm_adapter
27 29
    Redmine::Scm::Adapters::MercurialAdapter
28 30
  end
......
82 84
  end
83 85
  private :latest_changesets_cond
84 86

  
87
  # Finds and returns a revision with a number or the beginning of a hash
88
  def find_changeset_by_name(name)
89
    if /[^\d]/ =~ name or name.length >= SHORT_NODEID_LEN
90
      if e = changesets.find(:first, :conditions => ["scmid = ?", name])
91
        e
92
      else
93
        changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"])
94
      end
95
    else
96
      super
97
    end
98
  end
99

  
85 100
  def fetch_changesets
86 101
    scm_rev = scm.info.lastrev.revision.to_i
87 102
    db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
app/models/repository/mercurial.rb
67 67
  def latest_changesets_cond(path, rev)
68 68
    cond, args = [], []
69 69

  
70
    if last = rev ? find_changeset_by_name(rev) : nil
70
    if last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
71 71
      cond << "#{Changeset.table_name}.id <= ?"
72 72
      args << last.id
73 73
    end
lib/redmine/scm/adapters/mercurial_adapter.rb
84 84
                                            :scmid => tip['node']))
85 85
        end
86 86

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

  
91
        # Returns map of {'tag' => 'nodeid', ...}
92
        def tagmap
93
          alist = summary['tags'].map { |e| e.values_at('name', 'node') }
94
          Hash[*alist.flatten]
95
        end
96

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

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

  
107
          fetched_entries['dirs'].each do |e|
108
            entries << Entry.new(:name => e['name'],
109
                                 :path => "#{with_trailling_slash(path)}#{e['name']}",
110
                                 :kind => 'dir')
122 111
          end
123
          return nil if $? && $?.exitstatus != 0
124
          entries.sort_by_name
112

  
113
          fetched_entries['files'].each do |e|
114
            entries << Entry.new(:name => e['name'],
115
                                 :path => "#{with_trailling_slash(path)}#{e['name']}",
116
                                 :kind => 'file',
117
                                 :size => e['size'].to_i,
118
                                 :lastrev => Revision.new(:identifier => e['rev'].to_i,
119
                                                          :time => Time.at(e['time'].to_i)))
120
          end
121

  
122
          entries
125 123
        end
126 124
        
127 125
        # TODO: is this api necessary?
app/models/repository/mercurial.rb
39 39

  
40 40
  # Returns the latest changesets for +path+
41 41
  def latest_changesets(path, rev, limit=10)
42
    # TODO: filter by branch if rev is branch name
42 43
    changesets.find(:all, :include => :user,
43
                    :conditions => latest_changesets_cond(path, rev),
44
                    :conditions => latest_changesets_cond(path, rev, limit),
44 45
                    :order => "#{Changeset.table_name}.id DESC",
45 46
                    :limit => limit)
46 47
  end
47 48

  
48
  def latest_changesets_cond(path, rev)
49
  def latest_changesets_cond(path, rev, limit)
49 50
    cond, args = [], []
50 51

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

  
97
        def branches
98
          summary['branches'].map { |e| e['name'] }
99
        end
100

  
101
        # Returns map of {'branch' => 'nodeid', ...}
102
        def branchmap
103
          alist = summary['branches'].map { |e| e.values_at('name', 'node') }
104
          Hash[*alist.flatten]
105
        end
106

  
107
        # NOTE: DO NOT IMPLEMENT default_branch !!
108
        # It's used as the default revision by RepositoriesController.
109

  
97 110
        def summary
98 111
          @summary ||= fetchg 'rhsummary'
99 112
        end
......
161 174
          end
162 175
          self
163 176
        end
177

  
178
        # Returns list of nodes in the specified branch
179
        def nodes_in_branch(branch, path=nil, identifier_from=nil, identifier_to=nil, options={})
180
          hg_args = ['log', '--template', '{node|short}\n', '-b', branch]
181
          hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
182
          hg_args << '--limit' << options[:limit] if options[:limit]
183
          hg_args << without_leading_slash(path) unless path.blank?
184
          hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
185
        end
164 186
        
165 187
        def diff(path, identifier_from, identifier_to=nil)
166 188
          hg_args = ['diff', '--nodates']
lib/redmine/scm/adapters/mercurial_adapter.rb
48 48
          end
49 49
          
50 50
          def hgversion_from_command_line
51
            %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
51
            shellout("#{HG_BIN} --version") do |io|
52
              io.read.match(/\(version (.*)\)/)[1]
53
            end
52 54
          end
53 55
          
54 56
          def template_path
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",
109
                            :order => "#{Changeset.table_name}.id DESC",
110 110
                            :limit => limit)
111 111
    else
112 112
      changes.find(:all, :include => {:changeset => :user}, 
113 113
                         :conditions => ["path = ?", path.with_leading_slash],
114
                         :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
114
                         :order => "#{Changeset.table_name}.id DESC",
115 115
                         :limit => limit).collect(&:changeset)
116 116
    end
117 117
  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 identifier of the revision.
52
  # e.g. revision number for centralized system; hash id for DVCS
53
  def revision_id
54
    # TODO: should be 'revision' by default, and overriden by sub-class or mixin
55
    scmid || revision
56
  end
57

  
58
  # Returns human-readable string representing the revision
59
  def revision_repr
60
    # TODO: should be 'revision' by default, and overriden by sub-class or mixin
61
    if scmid and revision != scmid
62
      "#{revision}:#{scmid}"  # Mercurial style
63
    else
64
      revision[0, 8]  # TODO: [0, 8] maybe for git
65
    end
66
  end
50 67
  
51 68
  def comments=(comment)
52 69
    write_attribute(:comments, Changeset.normalize_comments(comment))
lib/redmine/scm/adapters/abstract_adapter.rb
285 285
          self.branch = attributes[:branch]
286 286
        end
287 287

  
288
        # FIXME: Changeset class has same methods to revision_id and revision_repr.
289
        # They should be mix-in module or something.
290

  
291
        # FIXME: It seems 'revision_id' should be 'identifier', but Revision.save uses
292
        # 'identifier' for :revision. I need to check usage of Revision class to
293
        # improve class design.
294

  
295
        # Returns identifier of the revision.
296
        # e.g. revision number for centralized system; hash id for DVCS
297
        def revision_id  # TODO: confusing name; there's already 'identifier'
298
          # TODO: should be 'revision' by default, and overriden by sub-class or mixin
299
          scmid || revision || identifier
300
        end
301

  
302
        # Returns human-readable string representing the revision
303
        def revision_repr
304
          # TODO: should be 'revision' by default, and overriden by sub-class or mixin
305
          if scmid and revision != scmid
306
            "#{revision}:#{scmid}"  # Mercurial style
307
          elsif revision
308
            revision[0, 8]  # TODO: [0, 8] maybe for git
309
          else
310
            "#{identifier}"[0, 8]
311
          end
312
        end
313

  
288 314
        def save(repo)
289 315
          Changeset.transaction do
290 316
            changeset = Changeset.new(
app/helpers/application_helper.rb
99 99
  # Generates a link to a SCM revision
100 100
  # Options:
101 101
  # * :text - Link text (default to the formatted revision)
102
  def link_to_revision(revision, project, options={})
103
    text = options.delete(:text) || format_revision(revision)
102
  def link_to_revision(changeset, project, options={})
103
    text = options.delete(:text) || format_revision(changeset)
104
    revision = changeset.respond_to?(:revision_id) ? changeset.revision_id : changeset
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 => revision}, :title => l(:label_revision_id, format_revision(changeset)))
106 107
  end
107 108

  
108 109
  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
  def format_revision(changeset)
22
    if changeset.respond_to? :revision_repr
23
      changeset.revision_repr
24
    else
25
      return changeset.to_s[0,8]
26
    end
23 27
  end
24 28
  
25 29
  def truncate_at_line_break(text, length = 255)
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>
16
<td class="id"><%= link_to_revision(changeset, project) %></td>
17 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 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>
19 19
<td class="committed_on"><%= format_time(changeset.committed_on) %></td>
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>
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.revision_id ? 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>
(11-11/24)