Project

General

Profile

Feature #4455 » hg-overhaul-0.9.3.patch

Toshi MARUYAMA, 2010-03-07 11:19

View differences:

.gitignore
17 17
/tmp/sockets/*
18 18
/tmp/test/*
19 19
/vendor/rails
20

  
21
/.hg/
22
/.hgignore
23
/.hgtags
app/helpers/repositories_helper.rb
158 158
  def darcs_field_tags(form, repository)
159 159
      content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
160 160
  end
161
  
161

  
162 162
  def mercurial_field_tags(form, repository)
163
      content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
163
      content_tag('p', form.text_field(
164
          :url,
165
          :label => 'Root directory',
166
          :size => 60,
167
          :required => true,
168
          ## Mercurial repository is removable.
169
          # :disabled => (repository && !repository.root_url.blank?)
170
          :disabled => false
171
       ))
164 172
  end
165 173

  
166 174
  def git_field_tags(form, repository)
167
      content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
175
      content_tag('p', form.text_field(
176
        :url,
177
        :label => 'Path to .git directory',
178
        :size => 60,
179
        :required => true,
180
        :disabled => (repository && !repository.root_url.blank?)
181
     ))
168 182
  end
169 183

  
170 184
  def cvs_field_tags(form, repository)
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}.scm_order DESC, #{Changeset.table_name}.committed_on DESC, #{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
......
96 96
  def find_changeset_by_name(name)
97 97
    changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
98 98
  end
99
  
99

  
100 100
  def latest_changeset
101 101
    @latest_changeset ||= changesets.find(:first)
102 102
  end
......
105 105
  # Default behaviour is to search in cached changesets
106 106
  def latest_changesets(path, rev, limit=10)
107 107
    if path.blank?
108
       # this is defined at "has_many"
109
       # :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
108 110
      changesets.find(:all, :include => :user,
109
                            :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
110 111
                            :limit => limit)
111 112
    else
112 113
      changes.find(:all, :include => {:changeset => :user}, 
113 114
                         :conditions => ["path = ?", path.with_leading_slash],
114
                         :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
115
                         :order => "#{Changeset.table_name}.scm_order DESC, #{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
115 116
                         :limit => limit).collect(&:changeset)
116 117
    end
117 118
  end
118
    
119

  
119 120
  def scan_changesets_for_issue_ids
120 121
    self.changesets.each(&:scan_comment_for_issue_ids)
121 122
  end
app/models/repository/mercurial.rb
18 18
require 'redmine/scm/adapters/mercurial_adapter'
19 19

  
20 20
class Repository::Mercurial < Repository
21
  attr_protected :root_url
21
  attr_protected        :root_url
22 22
  validates_presence_of :url
23 23

  
24
  @@limit_check_strip       = 100
25
  @@num_convert_redmine_0_9 =  20
26

  
24 27
  def scm_adapter
25 28
    Redmine::Scm::Adapters::MercurialAdapter
26 29
  end
27
  
30

  
28 31
  def self.scm_name
29 32
    'Mercurial'
30 33
  end
31
  
34

  
35
  def branches
36
    scm.branches
37
  end
38

  
39
  def tags
40
    scm.tags
41
  end
42

  
32 43
  def entries(path=nil, identifier=nil)
33
    entries=scm.entries(path, identifier)
44
    entries=scm.entries(path, identifier,:include_file_revs => true )
34 45
    if entries
35 46
      entries.each do |entry|
36
        next unless entry.is_file?
37
        # Set the filesize unless browsing a specific revision
38
        if identifier.nil?
39
          full_path = File.join(root_url, entry.path)
40
          entry.size = File.stat(full_path).size if File.file?(full_path)
41
        end
42
        # Search the DB for the entry's last change
43
        change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
44
        if change
45
          entry.lastrev.identifier = change.changeset.revision
46
          entry.lastrev.name = change.changeset.revision
47
          entry.lastrev.author = change.changeset.committer
48
          entry.lastrev.revision = change.revision
47
        if entry && entry.lastrev && entry.lastrev.identifier
48
          rev = changesets.find(
49
              :first,
50
              :conditions => [ "revision = ?" , entry.lastrev.identifier ]
51
            )
52
          next if rev
53
          rev = changesets.find(
54
              :first,
55
              :conditions => ["scmid LIKE ?", entry.lastrev.identifier + '%']
56
            )
57
          entry.lastrev.identifier = rev.revision if rev
49 58
        end
50 59
      end
51 60
    end
52 61
    entries
53 62
  end
54 63

  
64
  # TODO:
65
  #   This logic fails in following case.
66
  #
67
  # Before
68
  #
69
  #   /-C
70
  # A-------B
71
  #
72
  # After
73
  #   
74
  #   /-D
75
  # A-------B
76
  #
77
  # This is very very rare case.
78
  #
79
  # For this case, we need to store HEAD info on DB?
80
  # http://www.redmine.org/issues/4773#note-11
81
  #
55 82
  def fetch_changesets
56 83
    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
                Change.create(:changeset => changeset,
82
                              :action => change[:action],
83
                              :path => change[:path],
84
                              :from_path => change[:from_path],
85
                              :from_revision => change[:from_revision])
84
    # Backout Redmine 0.9.x
85
    # return unless scm_info or scm_info.lastrev.nil?
86
    return unless scm_info
87

  
88
    transaction do
89
      Changeset.update_all(
90
        "scm_order = -1" ,
91
        ["repository_id = ? AND scm_order is null", id]
92
      )
93
    end
94

  
95
    identifier_from = 0
96

  
97
    transaction do
98
      tip_on_db = changesets.find(:first, :order => 'scm_order DESC')
99
      tip_on_db = convert_changeset(tip_on_db)  if ( tip_on_db && ( tip_on_db.scm_order == -1 ) )
100
      tip_revno_on_db       = -1
101
      hg_revno_dbtip        = -1
102
      if tip_on_db
103
        tip_revno_on_db = tip_on_db.scm_order
104
        revs = scm.revisions( nil, tip_on_db.scmid, tip_on_db.scmid,
105
                            :lite => true)
106
        hg_revno_dbtip = revs.first.scm_order if revs && revs.first
107
      end
108

  
109
      # Redmine cannot check changeset.count == scm.num_revisions
110
      # because Redmine ver.0.9.x has stripped revision on DB
111
      # and 'revision' is uniq.
112
      if ( tip_revno_on_db == hg_revno_dbtip )
113
        identifier_from = tip_revno_on_db   + 1
114
      else
115
        # At Redmine SVN r3394, git check history editing for only one week.
116
        # Mercurial has revision number,
117
        # so Redmine can check from big revision number.
118
        # And strip revision in middle of history on shared repository
119
        # is very rare case.
120
        ver09x_changeset_first =
121
          changesets.find(:first,:conditions =>[ "scm_order = -1" ] )
122
        if ( ver09x_changeset_first )
123
          convert_changesets(@@num_convert_redmine_0_9)
124
        end
125
        converted_cs    = nil
126
        flag_loop_break = false
127
        convert_limit   = @@limit_check_strip
128
        changesets.find(
129
             :all,
130
             :order => 'scm_order DESC',
131
             :limit => convert_limit).each do |cs|
132
          prev_no      = cs.scm_order
133
          converted_cs = convert_changeset(cs)
134
          if ( converted_cs && converted_cs.scm_order )
135
            if ( converted_cs.scm_order == prev_no )
136
                changesets.find(:all,:conditions =>
137
                  [ "scm_order = ? AND scmid != ?" , cs.scm_order , cs.scmid ]
138
                 ).each do |cs1|
139
                convert_changeset(cs1)
140
              end
141
              flag_loop_break = true
142
              break
143
            end
144
          end
145
        end
146
        if ( flag_loop_break )
147
          identifier_from = converted_cs.scm_order + 1
148
        else
149
          identifier_from = [scm.num_revisions - convert_limit , 0].max
150
        end
151
      end
152
    end
153

  
154
    scm_revision    = scm.num_revisions - 1
155
    # Reffered from Subversion logic.
156
    while (identifier_from <= scm_revision)
157
        transaction do
158
          identifier_to = [identifier_from + 19, scm_revision].min
159
          revisions = scm.revisions(
160
              nil, identifier_from, identifier_to )
161
          if revisions
162
            revisions.each do |rev|
163
              dups = changesets.find(
164
                :all,
165
                :conditions =>["scmid = ? or scmid = ? " ,
166
                                 rev.scmid , rev.identifier]
167
              )
168
              unless dups.empty?
169
                dups.each do |cs1|
170
                  ## There is no way to store
171
                  ## Redmine 0.9.x original revision number.
172
                  cs1.scmid     = rev.scmid
173
                  cs1.scm_order = rev.scm_order.to_i
174
                  cs1.save
175
                end
176
                next
86 177
              end
178
              rev.save(self)
87 179
            end
88
          end unless revisions.nil?
180
          end
89 181
          identifier_from = identifier_to + 1
90 182
        end
183
    end
184
  end
185

  
186
  # TODO:
187
  #  1. Mercurial has Named branches.
188
  #     http://mercurial.selenic.com/wiki/NamedBranches
189
  #   a)
190
  #      Mercurial has --only-branch option.
191
  #      But, this is show this branch only.
192
  #      TortoiseHg 0.9 Repository Explorer is different.
193
  #      How does TortoiseHg handle branches?
194
  #   b)
195
  #     If no changeset on this named branch,
196
  #     no revisons show on repository tab.
197
  #   c)
198
  #      Mercurial version 1.2 introduced the ability to close a branch.
199
  #      http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches
200
  #
201
  #  2.
202
  #    If Setting.autofetch_changesets is off,
203
  #    no revisons show on repository tab.
204
  #
205
  def latest_changesets(path, rev, limit=10)
206
    branch = nil
207
    if branches.index(rev)
208
      branch = rev
209
    end
210
    revisions = scm.revisions(
211
                  path, rev, 0, :limit => limit,
212
                  :lite => true , :branch => branch
213
             )
214

  
215
    return [] if revisions.nil? or revisions.empty?
216

  
217
    # Redmine 0.9.x changeset scmid is short id.
218
     changesets.find(
219
       :all, 
220
       :conditions => [
221
        "scmid IN (?) or scmid IN (?)",
222
        # revisions.map!{|c| c.scmid} ,
223
        revisions.map{|c| c.scmid} ,
224
        revisions.map{|c| c.identifier}
225
       ]
226
     )
227
  end
228

  
229
  def diff(path, rev, rev_to)
230
    from_scmid = rev
231
    if ( rev =~ /^\d*$/ )
232
      from_rev = find_changeset_by_name(rev)
233
      from_scmid = from_rev.scmid if from_rev
234
    end
235
    return nil if from_scmid.nil?
236
    to_scmid = rev_to
237
    if ( rev_to =~ /^\d*$/ )
238
      to_rev   = find_changeset_by_name(rev_to)
239
      to_scmid   = to_rev.scmid   if to_rev
240
    end
241
    scm.diff(path, from_scmid, to_scmid)
242
  end
243

  
244
  # Redmine 0.9.x has revision with revision number.
245
  # TODO:
246
  #   In case of strip, revision number is renumberd,
247
  #   this logic fails?
248
  #   We need to test with fixtures?
249
  #   And tag and branch can use '\d'???
250
  def find_changeset_by_name(name)
251
    if name
252
      changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["scmid LIKE ?", name + '%']))
253
    else
254
      nil
255
    end
256
  end
257

  
258
  # This method is not used now.
259
  # But, I plan to use in rake task.
260
  def convert_changesets_all
261
    changesets.find_each(
262
           :conditions => [ "scm_order = -1 or scm_order is null" ],
263
           :batch_size => 100) do |cs|
264
      convert_changeset(cs)
265
    end
266
  end
267

  
268
  def convert_changesets(limit=10)
269
    changesets.find(
270
             :all,
271
             :conditions =>[ "scm_order = -1 or scm_order is null" ],
272
             :limit => limit).each do |cs|
273
      convert_changeset(cs)
274
    end
275
  end
276

  
277
  # Mercurial revision number is sequential from 0.
278
  # And Mercurial has multipile heads.
279
  # If one head in middle of history was stripped,
280
  # revision number was renumbered.
281
  # In this case, Redmine 0.9.x had duplicate scmid on DB.
282
  # 
283
  # Because revision is uniq on table, convert fails.
284
  # And "rNN" is written in Wiki, issue message, etc.
285
  def convert_changeset(cs)
286
        ret  = cs
287
        revs = scm.revisions(nil, cs.scmid, cs.scmid, :lite => true)
288
        rev  = nil
289
        rev  = revs.first if revs
290
        if rev
291
          ret = convert_changeset_with_revision(cs, rev)
292
        else
293
          cs.delete
294
          ret = nil
295
        end
296
        ret
297
  end
298

  
299
  def convert_changeset_with_revision(cs, rev)
300
    cs.scm_order = rev.scm_order.to_i
301
    if false
302
      dup_first1 = changesets.find(
303
        :first,
304
        :conditions =>["revision = ?" , rev.identifier ]
305
      )
306
      if dup_first1.nil?
307
      then
308
        ## There is no way to store
309
        ## Redmine 0.9.x original revision number.
310
        # cs.revision  = rev.identifier  if cs.revision  != rev.identifier
91 311
      end
92 312
    end
313
    dup_first = changesets.find(
314
      :first,
315
      :conditions =>["scmid = ?" , rev.scmid ]
316
    )
317
    if dup_first.nil?
318
    then
319
      cs.scmid     = rev.scmid
320
    end
321

  
322
    cs.save
323
    ret = cs
324
    ret
93 325
  end
94 326
end
app/views/repositories/revision.rhtml
21 21

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

  
24
<p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
24
<p>
25
<% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
26
<% if @changeset.scm_order %>SCM Order: <%= @changeset.scm_order %><br /><% end %>
25 27
<span class="author"><%= authoring(@changeset.committed_on, @changeset.author) %></span></p>
26 28

  
27 29
<%= textilizable @changeset.comments %>
db/migrate/20100222000000_add_scm_order_to_changesets.rb
1

  
2
class AddScmOrderToChangesets < ActiveRecord::Migration
3
  def self.up
4
    add_column :changesets, :scm_order, :integer,  :null => true
5
    add_index  :changesets, :scm_order
6
  end
7

  
8
  def self.down
9
    remove_index  :changesets, :scm_order
10
    remove_column :changesets, :scm_order
11
  end
12
end
lib/redmine/scm/adapters/abstract_adapter.rb
269 269
          }.last
270 270
        end 
271 271
      end
272
      
272

  
273

  
273 274
      class Revision
274
        attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
275
        attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch, :scm_order
275 276

  
276 277
        def initialize(attributes={})
277 278
          self.identifier = attributes[:identifier]
......
283 284
          self.paths = attributes[:paths]
284 285
          self.revision = attributes[:revision]
285 286
          self.branch = attributes[:branch]
287
          self.scm_order = attributes[:scm_order]
286 288
        end
287 289

  
288 290
        def save(repo)
......
293 295
              :scmid => scmid,
294 296
              :committer => author, 
295 297
              :committed_on => time,
296
              :comments => message)
297
            
298
              :comments  => message   ,
299
              :scm_order => scm_order
300
            )
298 301
            if changeset.save
299 302
              paths.each do |file|
300 303
                Change.create(
......
306 309
          end
307 310
        end
308 311
      end
309
        
312

  
310 313
      class Annotate
311 314
        attr_reader :lines, :revisions
312 315
        
lib/redmine/scm/adapters/mercurial/hg-template-0.9.5-lite.tmpl
1
changeset = 'This template must be used with --debug option\n'
2
changeset_quiet =  'This template must be used with --debug option\n'
3
changeset_verbose = 'This template must be used with --debug option\n'
4
changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
5

  
6
tag = '<tag>{tag|escape}</tag>\n'
7
header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
8
# footer="</log>"
lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
1 1
changeset = 'This template must be used with --debug option\n'
2 2
changeset_quiet =  'This template must be used with --debug option\n'
3 3
changeset_verbose = 'This template must be used with --debug option\n'
4
changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
4
changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
5 5

  
6 6
file = '<path action="M">{file|escape}</path>\n'
7 7
file_add = '<path action="A">{file_add|escape}</path>\n'
lib/redmine/scm/adapters/mercurial/hg-template-1.0-lite.tmpl
1
changeset = 'This template must be used with --debug option\n'
2
changeset_quiet =  'This template must be used with --debug option\n'
3
changeset_verbose = 'This template must be used with --debug option\n'
4
changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths />\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
5

  
6
tag = '<tag>{tag|escape}</tag>\n'
7
header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
8
# footer="</log>"
lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
1 1
changeset = 'This template must be used with --debug option\n'
2 2
changeset_quiet =  'This template must be used with --debug option\n'
3 3
changeset_verbose = 'This template must be used with --debug option\n'
4
changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
4
changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
5 5

  
6 6
file_mod = '<path action="M">{file_mod|escape}</path>\n'
7 7
file_add = '<path action="A">{file_add|escape}</path>\n'
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
22
    module Adapters    
23
    module Adapters
23 24
      class MercurialAdapter < AbstractAdapter
24
        
25 25
        # Mercurial executable name
26 26
        HG_BIN = "hg"
27 27
        TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
28 28
        TEMPLATE_NAME = "hg-template"
29 29
        TEMPLATE_EXTENSION = "tmpl"
30
        
30

  
31 31
        class << self
32
          @@limit_include_file_revs = 20
33
          @@has_size_ext            = true
34

  
32 35
          def client_version
33
            @@client_version ||= (hgversion || [])
36
            @@client_version        ||= (hgversion || [])
34 37
          end
35
          
36
          def hgversion  
38

  
39
          # TODO:
40
          #  Mercurial version 1.2 introduced the ability to close a branch.
41
          #  http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches
42
          def hgversion
37 43
            # The hg version is expressed either as a
38 44
            # release number (eg 0.9.5 or 1.0) or as a revision
39 45
            # id composed of 12 hexa characters.
......
42 48
              theversion.split(".").collect(&:to_i)
43 49
            end
44 50
          end
45
          
51

  
46 52
          def hgversion_from_command_line
47 53
            %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
48 54
          end
49
          
55

  
50 56
          def template_path
51 57
            @@template_path ||= template_path_for(client_version)
52 58
          end
53
          
54
          def template_path_for(version)
59

  
60
          def lite_template_path
61
            @@lite_template_path ||= template_path_for(client_version,'lite')
62
          end
63

  
64
          def template_path_for(version,style=nil)
55 65
            if ((version <=> [0,9,5]) > 0) || version.empty?
56 66
              ver = "1.0"
57 67
            else
58 68
              ver = "0.9.5"
59 69
            end
60
            "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
70
            if style
71
              tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}-#{style}.#{TEMPLATE_EXTENSION}"
72
            else
73
              tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
74
            end
75
            tmpl
76
          end
77
        end
78

  
79
        # Mercurial default branch is "default".
80
        # But, Mercurial has multipile heads.
81
        def default_branch
82
          @default_branch ||= 'tip'
83
        end
84

  
85
        def branches
86
          @branches ||= get_branches
87
        end
88

  
89
        # TODO:
90
        #  Mercurial version 1.2 introduced the ability to close a branch.
91
        #  http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches
92
        def get_branches
93
          branches = []
94
          cmd = "#{HG_BIN} -R #{target('')} branches"
95
          shellout(cmd) do |io|
96
            io.each_line do |line|
97
              branches << line.chomp.match('^([^:]+[^\s]+)[\s]+[\d]+:.*$')[1]
98
            end
61 99
          end
100
          branches
101
        end
102

  
103
        def tags
104
          @tags ||= get_tags
62 105
        end
63
        
106

  
107
        def get_tags
108
          tags = []
109
          cmd = "#{HG_BIN} -R #{target('')} tags -v"
110
          shellout(cmd) do |io|
111
            io.each_line do |line|
112
              strs = line.chomp.match('^([^:]+[^\s]+)[\s]+[\d]+:(.*)$')
113
              if strs[2] !~ /[\s]+local/
114
                tags << strs[1]
115
              end
116
            end
117
          end
118
          tags
119
        end
120

  
121
        def tip
122
          @tip ||= get_tip
123
        end
124

  
125
        def get_tip
126
          tip = nil
127
          cmd = "#{HG_BIN} -R #{target('')} id -i -r tip"
128
          shellout(cmd) do |io|
129
            tip = io.gets.chomp
130
          end
131
          return nil if $? && $?.exitstatus != 0
132
          tip
133
        end
134

  
64 135
        def info
65 136
          cmd = "#{HG_BIN} -R #{target('')} root"
66 137
          root_url = nil
67 138
          shellout(cmd) do |io|
68
            root_url = io.gets
139
            root_url = io.gets.chomp
69 140
          end
70 141
          return nil if $? && $?.exitstatus != 0
71
          info = Info.new({:root_url => root_url.chomp,
72
                            :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
73
                          })
142
          info = Info.new(
143
              {
144
                :root_url => root_url.chomp,
145
                # :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
146
              })
74 147
          info
75 148
        rescue CommandFailed
76 149
          return nil
77 150
        end
78
        
79
        def entries(path=nil, identifier=nil)
151

  
152
        def lastrev(path=nil, identifier=nil)
153
          lastrev = revisions(path,identifier,0,:limit => 1, :lite => true)
154
          return nil if lastrev.nil? or lastrev.empty?
155
          lastrev.last
156
        end
157

  
158
        def num_revisions
159
          num = 0
160
          cmd = "#{HG_BIN} -R #{target('')} log -r tip --template=#{shell_quote('{rev}\n')}"
161
          shellout(cmd) do |io|
162
            line = io.gets
163
            if line.nil?
164
              num = 0
165
            else
166
              num = line.chomp.to_i + 1
167
            end
168
            break
169
          end
170
          num
171
        end
172

  
173
        def entries(path=nil, identifier=nil, options={})
80 174
          path ||= ''
81 175
          entries = Entries.new
82 176
          cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
......
89 183
              if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
90 184
                e ||= line
91 185
                e = e.chomp.split(%r{[\/\\]})
92
                entries << Entry.new({:name => e.first,
93
                                       :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
94
                                       :kind => (e.size > 1 ? 'dir' : 'file'),
95
                                       :lastrev => Revision.new
96
                                     }) unless e.empty? || entries.detect{|entry| entry.name == e.first}
186
                unless e.empty? ||
187
                     entries.detect{|entry| entry.name == e.first}
188
                  kind = (e.size > 1 ? 'dir' : 'file')
189
                  ent_path = (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}")
190
                  lastrev = nil
191
                  s = nil
192
                  entries << Entry.new(
193
                                    {
194
                                       :name    => e.first   ,
195
                                       :path    => ent_path  ,
196
                                       :kind    => kind      ,
197
                                       :size    => s         ,
198
                                       :lastrev => lastrev
199
                                     }
200
                                 )
201
                end
97 202
              end
98 203
            end
99 204
          end
100 205
          return nil if $? && $?.exitstatus != 0
206

  
207
          file_cnt = 0
208
          entries.each do |ent|
209
            if ( ent.kind == 'file' )
210
              file_cnt += 1
211
            end
212
          end
213
          entries.each do |ent|
214
                  if ( ent.kind == 'file' )
215
                    if options[:include_file_revs] && file_cnt < @@limit_include_file_revs
216
                      # Following process is very heavy.
217
                      ent.lastrev = lastrev(ent.path,identifier)
218
                      s = nil
219
                      s = size(path,identifier) if @@has_size_ext
220
                      if s.nil? and (identifier.to_s == default_branch or identifier.to_s == 'tip')
221
                        full_path = info.root_url + '/' + ent.path
222
                        ent.size = File.stat(full_path).size if File.file?(full_path)
223
                      end
224
                    else
225
                      ent.lastrev = Revision.new
226
                    end
227
                  else
228
                    # "hg log -l1 DIR" is VERY VERY HEAVY!!
229
                    ent.lastrev = Revision.new
230
                  end
231
          end
232

  
101 233
          entries.sort_by_name
102 234
        end
103
        
235

  
104 236
        # Fetch the revisions by using a template file that 
105 237
        # makes Mercurial produce a xml output.
106
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})  
238
        #
239
        # TODO:
240
        #  Mercurial version 1.2 introduced the ability to close a branch.
241
        #  http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches
242
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
107 243
          revisions = Revisions.new
108
          cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
244
          cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} --cwd #{target('')} log"
245
          if options[:lite]
246
            cmd << " --style #{shell_quote self.class.lite_template_path}" 
247
          else
248
          	cmd << " -C --style #{shell_quote self.class.template_path}"
249
          end
109 250
          if identifier_from && identifier_to
110
            cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
251
            cmd << " -r #{shell_quote(identifier_from.to_s)}:#{shell_quote(identifier_to.to_s)}"
111 252
          elsif identifier_from
112
            cmd << " -r #{identifier_from.to_i}:"
253
            cmd << " -r #{shell_quote(identifier_from.to_s)}:"
254
          elsif identifier_to
255
            cmd << " -r :#{shell_quote(identifier_to.to_s)}"
113 256
          end
114 257
          cmd << " --limit #{options[:limit].to_i}" if options[:limit]
258
          cmd << " --only-branch #{options[:branch]}" if options[:branch]
115 259
          cmd << " #{path}" if path
116 260
          shellout(cmd) do |io|
117 261
            begin
118 262
              # HG doesn't close the XML Document...
119
              doc = REXML::Document.new(io.read << "</log>")
263
              output = io.read
264
              return nil if output.empty?
265
              doc = REXML::Document.new(output << "</log>")
120 266
              doc.elements.each("log/logentry") do |logentry|
121 267
                paths = []
122 268
                copies = logentry.get_elements('paths/path-copied')
......
124 270
                  # Detect if the added file is a copy
125 271
                  if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
126 272
                    from_path = c.attributes['copyfrom-path']
127
                    from_rev = logentry.attributes['revision']
273
                    from_rev = logentry.attributes['shortnode']
128 274
                  end
129 275
                  paths << {:action => path.attributes['action'],
130 276
                    :path => "/#{path.text}",
......
132 278
                    :from_revision => from_rev ? from_rev : nil
133 279
                  }
134 280
                end
135
                paths.sort! { |x,y| x[:path] <=> y[:path] }
136
                
137
                revisions << Revision.new({:identifier => logentry.attributes['revision'],
138
                                            :scmid => logentry.attributes['node'],
139
                                            :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
140
                                            :time => Time.parse(logentry.elements['date'].text).localtime,
141
                                            :message => logentry.elements['msg'].text,
142
                                            :paths => paths
143
                                          })
281
                paths.sort! { |x,y| x[:path] <=> y[:path] } unless paths.empty?
282
                revisions << Revision.new(
283
                      {
284
                        :identifier => logentry.attributes['shortnode'],
285
                        :scmid      => logentry.attributes['node'],
286
                        :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
287
                        :time => Time.parse(logentry.elements['date'].text).localtime,
288
                        :message   => logentry.elements['msg'].text,
289
                        :paths     => paths,
290
                        :scm_order => logentry.attributes['revision'].to_i ,
291
                       }
292
                 )
144 293
              end
145 294
            rescue
146 295
              logger.debug($!)
......
149 298
          return nil if $? && $?.exitstatus != 0
150 299
          revisions
151 300
        end
152
        
301

  
153 302
        def diff(path, identifier_from, identifier_to=nil)
154 303
          path ||= ''
155 304
          if identifier_to
156
            identifier_to = identifier_to.to_i 
305
            cmd = "#{HG_BIN} -R #{target('')} diff -r #{shell_quote(identifier_to.to_s)} -r #{shell_quote(identifier_from.to_s)} --nodates"
157 306
          else
158
            identifier_to = identifier_from.to_i - 1
307
            cmd = "#{HG_BIN} -R #{target('')} diff -c #{identifier_from} --nodates"
159 308
          end
160
          cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
161 309
          cmd << " -I #{target(path)}" unless path.empty?
162 310
          diff = []
163 311
          shellout(cmd) do |io|
......
168 316
          return nil if $? && $?.exitstatus != 0
169 317
          diff
170 318
        end
171
        
319

  
172 320
        def cat(path, identifier=nil)
173 321
          cmd = "#{HG_BIN} -R #{target('')} cat"
174
          cmd << " -r " + (identifier ? identifier.to_s : "tip")
175
          cmd << " #{target(path)}"
322
          cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip"))
323
          cmd << " #{target(path)}" unless path.empty?
176 324
          cat = nil
177 325
          shellout(cmd) do |io|
178 326
            io.binmode
......
181 329
          return nil if $? && $?.exitstatus != 0
182 330
          cat
183 331
        end
184
        
332

  
333
        def size(path, identifier=nil)
334
          # return nil
335

  
336
          cmd = "#{HG_BIN} --cwd #{target('')} size"
337
          cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip"))
338
          cmd << " #{path}" unless path.empty?
339
          size = nil
340
          shellout(cmd) do |io|
341
            size = io.read
342
          end
343
          return nil if $? && $?.exitstatus != 0
344
          size.to_i
345
        end
346

  
347
        # TODO:
348
        #   hg annotate behavior small changes at Ver.1.5.
349
        #    http://mercurial.selenic.com/wiki/UpgradeNotes#A1.5:_Small_behavior_changes
350
        #       hg annotate now follows copies and renames by default,
351
        #       use --no-follow for old behavior.
185 352
        def annotate(path, identifier=nil)
186 353
          path ||= ''
354
          identifier = 'tip' if identifier.blank?
187 355
          cmd = "#{HG_BIN} -R #{target('')}"
188
          cmd << " annotate -n -u"
189
          cmd << " -r " + (identifier ? identifier.to_s : "tip")
190
          cmd << " -r #{identifier.to_i}" if identifier
191
          cmd << " #{target(path)}"
356
          cmd << " annotate -c -u"
357
          cmd << " -r #{shell_quote(identifier.to_s)}"
358
          cmd << " #{target(path)}" unless path.empty?
192 359
          blame = Annotate.new
193 360
          shellout(cmd) do |io|
194 361
            io.each_line do |line|
195
              next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
196
              blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
362
              next unless line =~ %r{^([^:]+)\s(\w+):(.*)$}
363
              blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_s, :author => $1.strip))
197 364
            end
198 365
          end
199 366
          return nil if $? && $?.exitstatus != 0
lib/tasks/testing.rake
26 26
        system "svnadmin create #{repo_path}"
27 27
        system "gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load #{repo_path}"
28 28
      end
29
      
30
      (supported_scms - [:subversion]).each do |scm|
29

  
30
      desc "Creates a test mercurial repository"
31
      task :mercurial => :create_dir do
32
        repo_path = "tmp/test/mercurial_repository"
33
        system "hg init #{repo_path}"
34
        system "cp -f test/fixtures/repositories/mercurial/hgrc #{repo_path}/.hg/hgrc "
35
        system "hg -R #{repo_path} pull test/fixtures/repositories/mercurial/default.r0.bundle"
36
        system "hg -R #{repo_path} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle"
37
        system "hg -R #{repo_path} pull test/fixtures/repositories/mercurial/branch00.r1.bundle"
38

  
39
      end
40

  
41
      (supported_scms - [:subversion, :mercurial]).each do |scm|
31 42
        desc "Creates a test #{scm} repository"
32 43
        task scm => :create_dir do
33
          system "gunzip < test/fixtures/repositories/#{scm}_repository.tar.gz | tar -xv -C tmp/test"
44
          # system "gunzip < test/fixtures/repositories/#{scm}_repository.tar.gz | tar -xv -C tmp/test"
45
          system "tar -xvz -C tmp/test -f test/fixtures/repositories/#{scm}_repository.tar.gz"
34 46
        end
35 47
      end
36 48
      
test/fixtures/repositories/mercurial/hgrc
1

  
2
[ui]
3
username = test00 <test00@example.com>
4

  
5
[extensions]
6
# MQ extention needs for unit test of check history editing.
7
hgext.mq =
8

  
9
# share =
10
# hgext.convert = 
11
# hgext.graphlog = 
12
# extdiff = 
13
# hgext.hgk = 
14
# hgext.bookmarks =
15
# rebase=
16
# hgext.purge=
17

  
18
# hggit = 
19
# svn = 
20

  
test/functional/repositories_mercurial_controller_test.rb
33 33
    @response   = ActionController::TestResponse.new
34 34
    User.current = nil
35 35
    Repository::Mercurial.create(:project => Project.find(3), :url => REPOSITORY_PATH)
36

  
37
    %x{hg -R #{REPOSITORY_PATH} update null}
38
    %x{hg -R #{REPOSITORY_PATH} strip 0}
39
    %x{hg -R #{REPOSITORY_PATH} verify}
40
    %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r0.bundle}
41
    %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch00.r1.bundle}
42
    %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle}
43

  
36 44
  end
37 45
  
38 46
  if File.directory?(REPOSITORY_PATH)
......
92 100
                 :attributes => { :class => /line-num/ },
93 101
                 :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ }
94 102
    end
95
    
103

  
96 104
    def test_entry_download
97 105
      get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
98 106
      assert_response :success
......
100 108
      assert @response.body.include?('WITHOUT ANY WARRANTY')
101 109
    end
102 110

  
111
    # This shows "default branch".
112
    # git shows "master", but Mercurial shows "tip".
103 113
    def test_directory_entry
104 114
      get :entry, :id => 3, :path => ['sources']
105 115
      assert_response :success
......
107 117
      assert_not_nil assigns(:entry)
108 118
      assert_equal 'sources', assigns(:entry).name
109 119
    end
110
    
120

  
121
    def test_browse_branch
122
      get :show, :id => 3, :rev => 'branch00'
123
      assert_response :success
124
      assert_template 'show'
125
      assert_not_nil assigns(:entries)
126
      assert_equal 4, assigns(:entries).size
127
      assert assigns(:entries).detect {|e| e.name == 'branch00-dir' && e.kind == 'dir'}
128
      assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
129
      assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
130
      assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
131
    end
132

  
111 133
    def test_diff
112 134
      # Full diff of changeset 4
113
      get :diff, :id => 3, :rev => 4
135
      get :diff, :id => 3, :rev => 'def6d2f1254a'
114 136
      assert_response :success
115 137
      assert_template 'diff'
116 138
      # Line 22 removed
......
120 142
                               :attributes => { :class => /diff_out/ },
121 143
                               :content => /def remove/ }
122 144
    end
123
    
145

  
124 146
    def test_annotate
125 147
      get :annotate, :id => 3, :path => ['sources', 'watchers_controller.rb']
126 148
      assert_response :success
......
131 153
                 :sibling => { :tag => 'td', :content => /jsmith/ },
132 154
                 :sibling => { :tag => 'td', :content => /watcher =/ }
133 155
    end
156

  
134 157
  else
135 158
    puts "Mercurial test repository NOT FOUND. Skipping functional tests !!!"
136 159
    def test_fake; assert true end
test/unit/repository_mercurial_test.rb
26 26
  def setup
27 27
    @project = Project.find(1)
28 28
    assert @repository = Repository::Mercurial.create(:project => @project, :url => REPOSITORY_PATH)
29

  
30
    %x{hg -R #{REPOSITORY_PATH} update null}
31
    %x{hg -R #{REPOSITORY_PATH} strip 0}
32
    %x{hg -R #{REPOSITORY_PATH} verify}
33

  
34
    %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r0.bundle}
35
    %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch00.r1.bundle}
36
    %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle}
29 37
  end
30
  
31
  if File.directory?(REPOSITORY_PATH)  
38

  
39
  if File.directory?(REPOSITORY_PATH)
40

  
32 41
    def test_fetch_changesets_from_scratch
33 42
      @repository.fetch_changesets
34 43
      @repository.reload
35
      
36
      assert_equal 6, @repository.changesets.count
37
      assert_equal 11, @repository.changes.count
38
      assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision('0').comments
44

  
45
      assert_equal 7, @repository.changesets.count
46
      assert_equal 12, @repository.changes.count
47
      assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision('0885933ad4f6').comments
39 48
    end
40 49
    
41 50
    def test_fetch_changesets_incremental
42 51
      @repository.fetch_changesets
43
      # Remove changesets with revision > 2
44
      @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2}
52
      ## Mercurial can set commit date,
53
      # @repository.changesets.find(:all, :order => 'committed_on DESC', :limit => 3).each(&:destroy)
54
      @repository.changesets.find(:all, :order => 'scm_order DESC', :limit => 3).each(&:destroy)
55

  
45 56
      @repository.reload
46
      assert_equal 3, @repository.changesets.count
47
      
57
      assert_equal 4, @repository.changesets.count
58

  
48 59
      @repository.fetch_changesets
49
      assert_equal 6, @repository.changesets.count
60
      assert_equal 7, @repository.changesets.count
50 61
    end
51 62
    
52 63
    def test_entries
53
      assert_equal 2, @repository.entries("sources", 2).size
54
      assert_equal 1, @repository.entries("sources", 3).size
64
      %x{hg -R #{REPOSITORY_PATH} up null}
65
      assert_equal 2, @repository.entries("sources", '400bb8672109').size
66
      assert_equal 1, @repository.entries("sources", 'b3a615152df8').size
55 67
    end
56 68

  
57 69
    def test_locate_on_outdated_repository
58 70
      # Change the working dir state
59
      %x{hg -R #{REPOSITORY_PATH} up -r 0}
60
      assert_equal 1, @repository.entries("images", 0).size
71
      %x{hg -R #{REPOSITORY_PATH} up null}
72
      assert_equal 1, @repository.entries("images", '0885933ad4f6').size
61 73
      assert_equal 2, @repository.entries("images").size
62
      assert_equal 2, @repository.entries("images", 2).size
74
      assert_equal 2, @repository.entries("images", '400bb8672109').size
63 75
    end
64 76

  
65 77

  
66 78
    def test_cat
67
      assert @repository.scm.cat("sources/welcome_controller.rb", 2)
79
      assert @repository.scm.cat("sources/welcome_controller.rb", '400bb8672109')
68 80
      assert_nil @repository.scm.cat("sources/welcome_controller.rb")
69 81
    end
70 82

  
83
    def test_simple_strip
... This diff was truncated because it exceeds the maximum size that can be displayed.
(7-7/24)