ChiliProject is not maintained anymore. Please be advised that there will be no more updates.

We do not recommend that you setup new ChiliProject instances and we urge all existing users to migrate their data to a maintained system, e.g. Redmine. We will provide a migration script later. In the meantime, you can use the instructions by Christian Daehn.

migrate_from_trac.rake.patch

Matthias Scholz, 2012-01-26 01:28 pm

Download (54.2 kB)

 
migrate_from_trac12.rake
1
#-- encoding: UTF-8
2
#-- copyright
3
# ChiliProject is a project management system.
4
#
5
# Copyright (C) 2010-2012 the ChiliProject Team
1
# redMine - project management software
2
# Copyright (C) 2006-2007  Jean-Philippe Lang
6 3
#
7 4
# This program is free software; you can redistribute it and/or
8 5
# modify it under the terms of the GNU General Public License
9 6
# as published by the Free Software Foundation; either version 2
10 7
# of the License, or (at your option) any later version.
11 8
#
12
# See doc/COPYRIGHT.rdoc for more details.
13
#++
14

  
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
15 17

  
16 18
require 'active_record'
17 19
require 'iconv'
18 20
require 'pp'
19 21

  
22

  
23
## Default presets
24
$DEFAULT_TRAC_DIRECTORY=ENV['TRAC_ENV']
25
$DEFAULT_PROJECT_IDENTIFIER=ENV['TRAC_ENV']).downcase!
26

  
27
# part of the email after the '@'
28
$DEFAULT_MAIL_SUFFIX='foo.bar'
29

  
30

  
20 31
namespace :redmine do
21 32
  desc 'Trac migration script'
22
  task :migrate_from_trac => :environment do
33
  task :migrate_from_trac12 => :environment do
23 34

  
24 35
    module TracMigrate
25 36
        TICKET_MAP = []
......
52 63

  
53 64
        TRACKER_BUG = Tracker.find_by_position(1)
54 65
        TRACKER_FEATURE = Tracker.find_by_position(2)
66
        # Add a fourth issue type for tasks as we use them heavily
67
        t = Tracker.find_by_name('Task')
68
        if !t
69
          t = Tracker.create(:name => 'Task',     :is_in_chlog => true,  :is_in_roadmap => false, :position => 4)
70
          t.workflows.copy(Tracker.find(1))
71
        end
72
        TRACKER_TASK = t
55 73
        DEFAULT_TRACKER = TRACKER_BUG
56 74
        TRACKER_MAPPING = {'defect' => TRACKER_BUG,
57 75
                           'enhancement' => TRACKER_FEATURE,
58
                           'task' => TRACKER_FEATURE,
76
                           'task' => TRACKER_TASK,
59 77
                           'patch' =>TRACKER_FEATURE
60 78
                           }
61 79

  
......
66 84
        ROLE_MAPPING = {'admin' => manager_role,
67 85
                        'developer' => developer_role
68 86
                        }
87
        # Add an Hash Table for comments' updatable fields
88
        PROP_MAPPING = {'status' => 'status_id',
89
                        'owner' => 'assigned_to_id',
90
                        'component' => 'category_id',
91
                        'milestone' => 'fixed_version_id',
92
                        'priority' => 'priority_id',
93
                        'summary' => 'subject',
94
                        'type' => 'tracker_id'}
95
        
96
        # Hash table to map completion ratio
97
        RATIO_MAPPING = {'' => 0,
98
                        'fixed' => 100,
99
                        'invalid' => 0,
100
                        'wontfix' => 0,
101
                        'duplicate' => 100,
102
                        'worksforme' => 0}
103

  
104
        @migrate_accounts = false
105
        @migrate_accounts_valid_email = true
69 106

  
70 107
      class ::Time
71 108
        class << self
72 109
          alias :real_now :now
110
          alias :old_at_method :at
73 111
          def now
74 112
            real_now - @fake_diff.to_i
75 113
          end
......
78 116
            res = yield
79 117
            @fake_diff = 0
80 118
           res
119
          end
120
          def at(t)
121
            old_at_method(t>1e6? t*1e-6 : t)
81 122
          end
82 123
        end
83 124
      end
......
151 192
      private
152 193
        def trac_fullpath
153 194
          attachment_type = read_attribute(:type)
154
          trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
155
          "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
195
          trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*]/n ) {|x| sprintf('%%%02X', x[0]) }
196
          trac_dir = id.gsub( /[^a-zA-Z0-9\-_\.!~*\\\/]/n ) {|x| sprintf('%%%02X', x[0]) }
197
          "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{trac_dir}/#{trac_file}"
156 198
        end
157 199
      end
158 200

  
......
161 203
        set_inheritance_column :none
162 204

  
163 205
        # ticket changes: only migrate status changes and comments
164
        has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
165
        has_many :attachments, :class_name => "TracAttachment",
206
        has_many :changes, :class_name => "TracMigrate::TracTicketChange", :foreign_key => :ticket
207
        has_many :attachments, :class_name => "TracMigrate::TracAttachment",
166 208
                               :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
167 209
                                              " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
168 210
                                              ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\''
169
        has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
211
        has_many :customs, :class_name => "TracMigrate::TracTicketCustom", :foreign_key => :ticket
170 212

  
171 213
        def ticket_type
172 214
          read_attribute(:type)
......
187 229
      class TracTicketChange < ActiveRecord::Base
188 230
        set_table_name :ticket_change
189 231

  
190
        def time; Time.at(read_attribute(:time)) end
191
      end
192

  
193
      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
232
        def time
233
        	Time.at(read_attribute(:time)) 
234
        end
235
      end
236

  
237
      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup \
238
                           TracBrowser TracCgi TracChangeset TracInstallPlatforms TracMultipleProjects TracModWSGI \
194 239
                           TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
195 240
                           TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
196 241
                           TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
197 242
                           TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
198 243
                           WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
199
                           CamelCase TitleIndex)
200

  
244
                           CamelCase TitleIndex TracNavigation TracFineGrainedPermissions TracWorkflow TimingAndEstimationPluginUserManual \
245
                           PageTemplates BadContent TracRepositoryAdmin TracWikiMacros)
201 246
      class TracWikiPage < ActiveRecord::Base
202 247
        set_table_name :wiki
203 248
        set_primary_key :name
204 249

  
205
        has_many :attachments, :class_name => "TracAttachment",
250
        has_many :attachments, :class_name => "TracMigrate::TracAttachment",
206 251
                               :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
207 252
                                      " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
208 253
                                      ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\''
......
215 260
        def time; Time.at(read_attribute(:time)) end
216 261
      end
217 262

  
263
      class TracAccount < ActiveRecord::Base
264
        set_table_name :session
265
        named_scope :authenticated, {:conditions => {:authenticated => 1}}
266

  
267
      end
268

  
218 269
      class TracPermission < ActiveRecord::Base
219 270
        set_table_name :permission
220 271
      end
......
225 276

  
226 277
      def self.find_or_create_user(username, project_member = false)
227 278
        return User.anonymous if username.blank?
228

  
229
        u = User.find_by_login(username)
230
        if !u
279
        return User.anonymous if [
280
          'admin', 'anonymous', 'asas', 'remote_user', 'somebody', 'trac', 'foo',
281
        ].include? username.downcase
282
        
283
        ## HACK clean username - if username is an email use first part of the address
284
        username = username.split("@")[0] if username.include?("@")
285

  
286
        _u = User.find_by_login(username)
287
        if !_u
231 288
          # Create a new user if not found
232 289
          mail = username[0,limit_for(User, 'mail')]
233 290
          if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
234 291
            mail = mail_attr.value
235
          end
236
          mail = "#{mail}@foo.bar" unless mail.include?("@")
292
          elsif @migrate_accounts_valid_email
293
            return User.find(:first)
294
          end
295
          mail = "#{mail}@#{$DEFAULT_MAIL_SUFFIX}" unless mail.include?("@")
237 296

  
238 297
          name = username
239 298
          if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
240 299
            name = name_attr.value
241 300
          end
242
          name =~ (/(.*)(\s+\w+)?/)
301
          name =~ (/(.+?)(?:[\ \t]+(.+)?|[\ \t]+|)$/)
243 302
          fn = $1.strip
303
          # Add a dash for lastname or the user is not saved (bugfix)
244 304
          ln = ($2 || '-').strip
245 305

  
246
          u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
306
          _u = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option,
307
                       :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
247 308
                       :firstname => fn[0, limit_for(User, 'firstname')],
248
                       :lastname => ln[0, limit_for(User, 'lastname')]
249

  
250
          u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
251
          u.password = 'trac'
252
          u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
309
                       :lastname => ln[0, limit_for(User, 'lastname')])
310

  
311
          _u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
312
          _u.password = 'tractrac'
313
          _u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
253 314
          # finally, a default user is used if the new user is not valid
254
          u = User.find(:first) unless u.save
315
          _u = User.find(:first) unless _u.save
255 316
        end
256 317
        # Make sure he is a member of the project
257
        if project_member && !u.member_of?(@target_project)
318
        if project_member && !_u.member_of?(@target_project)
258 319
          role = DEFAULT_ROLE
259
          if u.admin
320
          if _u.admin
260 321
            role = ROLE_MAPPING['admin']
261 322
          elsif TracPermission.find_by_username_and_action(username, 'developer')
262 323
            role = ROLE_MAPPING['developer']
263 324
          end
264
          Member.create(:user => u, :project => @target_project, :roles => [role])
265
          u.reload
266
        end
267
        u
325
          Member.create(:user => _u, :project => @target_project, :roles => [role])
326
          _u.reload
327
        end
328
        _u
268 329
      end
269 330

  
270 331
      # Basic wiki syntax conversion
271 332
      def self.convert_wiki_text(text)
272
        # Titles
273
        text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
274
        # External Links
275
        text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
276
        # Ticket links:
277
        #      [ticket:234 Text],[ticket:234 This is a test]
278
        text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
279
        #      ticket:1234
280
        #      #1 is working cause Redmine uses the same syntax.
281
        text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
282
        # Milestone links:
283
        #      [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
284
        #      The text "Milestone 0.1.0 (Mercury)" is not converted,
285
        #      cause Redmine's wiki does not support this.
286
        text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
287
        #      [milestone:"0.1.0 Mercury"]
288
        text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
289
        text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
290
        #      milestone:0.1.0
291
        text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
292
        text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
293
        # Internal Links
294
        text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
295
        text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
296
        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
297
        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
298
        text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
299
        text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
300

  
301
  # Links to pages UsingJustWikiCaps
302
  text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
303
  # Normalize things that were supposed to not be links
304
  # like !NotALink
305
  text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
306
        # Revisions links
307
        text = text.gsub(/\[(\d+)\]/, 'r\1')
308
        # Ticket number re-writing
309
        text = text.gsub(/#(\d+)/) do |s|
310
          if $1.length < 10
311
#            TICKET_MAP[$1.to_i] ||= $1
312
            "\##{TICKET_MAP[$1.to_i] || $1}"
313
          else
314
            s
315
          end
316
        end
317
        # We would like to convert the Code highlighting too
318
        # This will go into the next line.
319
        shebang_line = false
320
        # Reguar expression for start of code
321
        pre_re = /\{\{\{/
322
        # Code hightlighing...
323
        shebang_re = /^\#\!([a-z]+)/
324
        # Regular expression for end of code
325
        pre_end_re = /\}\}\}/
326

  
327
        # Go through the whole text..extract it line by line
328
        text = text.gsub(/^(.*)$/) do |line|
329
          m_pre = pre_re.match(line)
330
          if m_pre
331
            line = '<pre>'
332
          else
333
            m_sl = shebang_re.match(line)
334
            if m_sl
335
              shebang_line = true
336
              line = '<code class="' + m_sl[1] + '">'
337
            end
338
            m_pre_end = pre_end_re.match(line)
339
            if m_pre_end
340
              line = '</pre>'
341
              if shebang_line
342
                line = '</code>' + line
343
              end
344
            end
345
          end
346
          line
347
        end
348

  
349
        # Highlighting
350
        text = text.gsub(/'''''([^\s])/, '_*\1')
351
        text = text.gsub(/([^\s])'''''/, '\1*_')
352
        text = text.gsub(/'''/, '*')
353
        text = text.gsub(/''/, '_')
354
        text = text.gsub(/__/, '+')
355
        text = text.gsub(/~~/, '-')
356
        text = text.gsub(/`/, '@')
357
        text = text.gsub(/,,/, '~')
358
        # Lists
359
        text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
360

  
361
        text
333
        convert_wiki_text_mapping(text, TICKET_MAP)
362 334
      end
363 335

  
364 336
      def self.migrate
......
367 339
        # Quick database test
368 340
        TracComponent.count
369 341

  
342
        migrated_accounts = 0
370 343
        migrated_components = 0
371 344
        migrated_milestones = 0
372 345
        migrated_tickets = 0
......
375 348
        migrated_wiki_edits = 0
376 349
        migrated_wiki_attachments = 0
377 350

  
378
        #Wiki system initializing...
351
        # Wiki system initializing...
379 352
        @target_project.wiki.destroy if @target_project.wiki
380 353
        @target_project.reload
381 354
        wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
382 355
        wiki_edit_count = 0
383 356

  
357
        # Accounts
358
        # SELECT sid FROM session WHERE authenticated=1;
359
        # SELECT value FROM session_attribute WHERE sid='s0undt3ch' AND name='email';
360
        if @migrate_accounts
361
          who = "Migrating Accounts"
362
          accounts_total = TracAccount.authenticated.find(:all).count
363
          TracAccount.authenticated.find(:all).each do |account|
364
            if account.authenticated == 1
365
              a = find_or_create_user(account.sid, true)
366
              next unless a.save!
367
              migrated_accounts += 1
368
              simplebar(who, migrated_accounts, accounts_total)
369
            end
370
          end
371
        end
372

  
373
        # After this point set migrate_accounts_valid_email to false so that
374
        # tickets still have a issuer even though they don't have a valid
375
        # email address
376
        TracMigrate.set_migrate_accounts_valid_email(false)
377

  
384 378
        # Components
385
        print "Migrating components"
379
        who = "Migrating components"
386 380
        issues_category_map = {}
381
        components_total = TracComponent.count
387 382
        TracComponent.find(:all).each do |component|
388
        print '.'
389
        STDOUT.flush
390
          c = IssueCategory.new :project => @target_project,
383
          	_c = IssueCategory.new :project => @target_project,
391 384
                                :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
392
        next unless c.save
393
        issues_category_map[component.name] = c
394
        migrated_components += 1
395
        end
396
        puts
385
			# Owner
386
			unless component.owner.blank?
387
			  _c.assigned_to = find_or_create_user(component.owner, true)
388
			end
389
			next unless _c.save
390
			issues_category_map[component.name] = _c
391
			migrated_components += 1
392
			simplebar(who, migrated_components, components_total)
393
        end
394
        puts if migrated_components < components_total
397 395

  
398 396
        # Milestones
399
        print "Migrating milestones"
397
        who = "Migrating milestones"
400 398
        version_map = {}
399
        milestone_wiki = Array.new
400
        milestones_total = TracMilestone.count
401 401
        TracMilestone.find(:all).each do |milestone|
402
          print '.'
403
          STDOUT.flush
404 402
          # First we try to find the wiki page...
405 403
          p = wiki.find_or_new_page(milestone.name.to_s)
406 404
          p.content = WikiContent.new(:page => p) if p.new_record?
407
          p.content.text = milestone.description.to_s
405
          # be sure that milestone text is not empty
406
          p.content.text = milestone.description.nil? || milestone.description.empty? ? milestone.name.to_s : milestone.description.to_s
408 407
          p.content.author = find_or_create_user('trac')
409 408
          p.content.comments = 'Milestone'
410
          p.save
409

  
410
          status_p_save = p.save
411
          puts ".status_p_save: " + status_p_save unless status_p_save
411 412

  
412 413
          v = Version.new :project => @target_project,
413 414
                          :name => encode(milestone.name[0, limit_for(Version, 'name')]),
......
417 418

  
418 419
          next unless v.save
419 420
          version_map[milestone.name] = v
421
          milestone_wiki.push(milestone.name);
420 422
          migrated_milestones += 1
421
        end
422
        puts
423
          simplebar(who, migrated_milestones, milestones_total)
424
        end
425
        puts if migrated_milestones < milestones_total
423 426

  
424 427
        # Custom fields
425 428
        # TODO: read trac.ini instead
426
        print "Migrating custom fields"
429
        who = "Migrating custom fields"
427 430
        custom_field_map = {}
428 431
        TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
429
          print '.'
430
          STDOUT.flush
432
          #print '.' # Maybe not needed this out?
433
          #STDOUT.flush
431 434
          # Redmine custom field name
432 435
          field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
433 436
          # Find if the custom already exists in Redmine
434 437
          f = IssueCustomField.find_by_name(field_name)
438
          # Ugly hack to handle billable checkbox. Would require to read the ini file to be cleaner
439
          if field_name == 'Billable'
440
            format = 'bool'
441
          else
442
            format = 'string'
443
          end
435 444
          # Or create a new one
436 445
          f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
437
                                        :field_format => 'string')
446
                                        :field_format => format, :default_value => '')
438 447

  
439 448
          next if f.new_record?
440 449
          f.trackers = Tracker.find(:all)
441 450
          f.projects << @target_project
442 451
          custom_field_map[field.name] = f
443 452
        end
444
        puts
453
        #puts
445 454

  
446 455
        # Trac 'resolution' field as a Redmine custom field
447 456
        r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
448 457
        r = IssueCustomField.new(:name => 'Resolution',
449 458
                                 :field_format => 'list',
459
                                 :default_value => '',
450 460
                                 :is_filter => true) if r.nil?
451 461
        r.trackers = Tracker.find(:all)
452 462
        r.projects << @target_project
......
454 464
        r.save!
455 465
        custom_field_map['resolution'] = r
456 466

  
467
        # Trac 'keywords' field as a Redmine custom field
468
        k = IssueCustomField.find(:first, :conditions => { :name => "Keywords" })
469
        k = IssueCustomField.new(:name => 'Keywords',
470
                                 :field_format => 'string',
471
                                 :default_value => '',
472
                                 :is_filter => true) if k.nil?
473
        k.trackers = Tracker.find(:all)
474
        k.projects << @target_project
475
        k.save!
476
        custom_field_map['keywords'] = k
477

  
478
        # Trac ticket id as a Redmine custom field
479
        tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
480
        tid = IssueCustomField.new(:name => 'TracID',
481
                                 :field_format => 'string',
482
                                 :default_value => '',
483
                                 :is_filter => true) if tid.nil?
484
        tid.trackers = Tracker.find(:all)
485
        tid.projects << @target_project
486
        tid.save!
487
        custom_field_map['tracid'] = tid
488
  
457 489
        # Tickets
458
        print "Migrating tickets"
459
          TracTicket.find_each(:batch_size => 200) do |ticket|
460
          print '.'
461
          STDOUT.flush
462
          i = Issue.new :project => @target_project,
490
        who = "Migrating tickets"
491
        tickets_total = TracTicket.count
492
        TracTicket.find_each(:batch_size => 200) do |ticket|
493
          #puts ".Ticket.time: " + ticket.time.inspect
494
          begin
495
            ticket_desc = encode(ticket.description)
496
          rescue Exception=>e
497
            ticket_desc = Iconv.conv("UTF8", "LATIN1", ticket.description)
498
          end
499
          _i = Issue.new :project => @target_project,
463 500
                          :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
464
                          :description => convert_wiki_text(encode(ticket.description)),
501
                          :description => ticket_desc,
465 502
                          :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
466 503
                          :created_on => ticket.time
467
          i.author = find_or_create_user(ticket.reporter)
468
          i.category = issues_category_map[ticket.component] unless ticket.component.blank?
469
          i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
470
          i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
471
          i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
472
          i.id = ticket.id unless Issue.exists?(ticket.id)
473
          next unless Time.fake(ticket.changetime) { i.save }
474
          TICKET_MAP[ticket.id] = i.id
504

  
505
          # Add the ticket's author to project's reporter list (bugfix)
506
          _i.author 	= find_or_create_user(ticket.reporter,true)
507
          # Extrapolate done_ratio from ticket's resolution
508
          _i.done_ratio = RATIO_MAPPING[ticket.resolution] || 0
509
          _i.category 	= issues_category_map[ticket.component] unless ticket.component.blank?
510
          _i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
511
          _i.status 	= STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
512
          _i.tracker 	= TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
513
          _i.id 		= ticket.id unless Issue.exists?(ticket.id)
514
          
515
          # Owner
516
          unless ticket.owner.blank?
517
            _i.assigned_to = find_or_create_user(ticket.owner, true)
518
          end
519
          
520
          Time.fake(ticket.changetime) { _i.save }
521
          
522
          TICKET_MAP[ticket.id] = _i.id
475 523
          migrated_tickets += 1
476

  
477
          # Owner
478
            unless ticket.owner.blank?
479
              i.assigned_to = find_or_create_user(ticket.owner, true)
480
              Time.fake(ticket.changetime) { i.save }
481
            end
482

  
483
          # Comments and status/resolution changes
524
          simplebar(who, migrated_tickets, tickets_total)
525
          
526
          
527
          # Handle CC field
528
          ticket.cc.split(',').each do |email|
529
            w = Watcher.new :watchable_type => 'Issue',
530
                            :watchable_id => _i.id,
531
                            :user_id => find_or_create_user(email.strip).id
532
            status_w_save = w.save
533
          end
534

  
535
          # Necessary to handle direct link to note from timelogs and putting the right start time in issue
536
          noteid = 1
537
          # Comments and status/resolution/keywords changes
484 538
          ticket.changes.group_by(&:time).each do |time, changeset|
485
              status_change = changeset.select {|change| change.field == 'status'}.first
539
              status_change 	= changeset.select {|change| change.field == 'status'}.first
486 540
              resolution_change = changeset.select {|change| change.field == 'resolution'}.first
487
              comment_change = changeset.select {|change| change.field == 'comment'}.first
488

  
489
              n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
490
                              :created_on => time
491
              n.user = find_or_create_user(changeset.first.author)
492
              n.journalized = i
541
              keywords_change 	= changeset.select {|change| change.field == 'keywords'}.first
542
              comment_change 	= changeset.select {|change| change.field == 'comment'}.first
543
              # Handle more ticket changes (owner, component, milestone, priority, summary, type, done_ratio and hours)
544
              assigned_change 	= changeset.select {|change| change.field == 'owner'}.first
545
              category_change 	= changeset.select {|change| change.field == 'component'}.first
546
              version_change 	= changeset.select {|change| change.field == 'milestone'}.first
547
              priority_change 	= changeset.select {|change| change.field == 'priority'}.first
548
              subject_change 	= changeset.select {|change| change.field == 'summary'}.first
549
              tracker_change 	= changeset.select {|change| change.field == 'type'}.first
550
              time_change 		= changeset.select {|change| change.field == 'hours'}.first
551

  
552
              # If it's the first note then we set the start working time to handle calendar and gantts
553
              if noteid == 1
554
                _i.start_date = time
555
              end
556

  
557
              n = IssueJournal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
558
                              :created_at => time,
559
                              :version => _i.last_journal.version + 1
560
			  
561
              n.user = find_or_create_user(changeset.first.author, true)
562
              n.journalized = _i
563
			  
493 564
              if status_change &&
494 565
                   STATUS_MAPPING[status_change.oldvalue] &&
495 566
                   STATUS_MAPPING[status_change.newvalue] &&
496 567
                   (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
497
                n.details << JournalDetail.new(:property => 'attr',
498
                                               :prop_key => 'status_id',
499
                                               :old_value => STATUS_MAPPING[status_change.oldvalue].id,
500
                                               :value => STATUS_MAPPING[status_change.newvalue].id)
568
                n.changes = n.changes.merge({
569
                               'status_id' => [
570
                                               STATUS_MAPPING[status_change.oldvalue].id,
571
                                               STATUS_MAPPING[status_change.newvalue].id
572
                                               ]
573
                })
501 574
              end
502
              if resolution_change
503
                n.details << JournalDetail.new(:property => 'cf',
504
                                               :prop_key => custom_field_map['resolution'].id,
505
                                               :old_value => resolution_change.oldvalue,
506
                                               :value => resolution_change.newvalue)
575
              
576
              # Handle resolution changes
577
              if resolution_change              	
578
				## BUG not working
579
                # n.changes = n.changes.merge({
580
                               # custom_field_map['resolution'].id => [
581
                                               # resolution_change.oldvalue,
582
                                               # resolution_change.newvalue
583
                               # ]
584
                # })
585

  
586
                # Add a change for the done_ratio
587
				n.changes = n.changes.merge({
588
								'done_ratio' => [
589
												RATIO_MAPPING[resolution_change.oldvalue],
590
												RATIO_MAPPING[resolution_change.newvalue]
591
												]
592
				})
593
                
594
				# Arbitrary set the due time to the day the ticket was resolved for calendar and gantts
595
                case RATIO_MAPPING[resolution_change.newvalue]
596
                when 0
597
                  _i.due_date = nil
598
                when 100
599
                  _i.due_date = time
600
                end
507 601
              end
508
              n.save unless n.details.empty? && n.notes.blank?
509
          end
602
              
603
              # Handle keyword changes
604
              if keywords_change
605
                n.changes = n.changes.merge({
606
                				custom_field_map['keywords'].id => [
607
                								keywords_change.oldvalue,
608
                								keywords_change.newvalue
609
                				]
610
                })
611
              end
612
              
613
              # Handle assignement/owner changes
614
              if assigned_change
615
                n.changes = n.changes.merge({
616
                 				PROP_MAPPING['owner'] => [
617
                							find_or_create_user(assigned_change.oldvalue, true).id,
618
                							find_or_create_user(assigned_change.newvalue, true).id
619
                				]
620
                })
621
              end
622
              
623
              # Handle component/category changes
624
              if category_change
625
                n.changes = n.changes.merge({
626
                				PROP_MAPPING['component'] => [
627
                							issues_category_map[category_change.oldvalue].nil? ? nil : issues_category_map[category_change.oldvalue].id,
628
                							issues_category_map[category_change.newvalue].nil? ? nil : issues_category_map[category_change.newvalue].id
629
                				]
630
                })
631
              end
632
              
633
              # Handle version/milestone changes
634
              if version_change
635
                n.changes = n.changes.merge({
636
                				PROP_MAPPING['milestone'] => [
637
                							version_map[version_change.oldvalue].nil? ? nil : version_map[version_change.oldvalue].id,
638
                							version_map[version_change.newvalue].nil? ? nil : version_map[version_change.newvalue].id
639
                				]
640
				})
641
              end
642
              
643
              # Handle priority changes
644
              if priority_change
645
                n.changes = n.changes.merge({
646
                				PROP_MAPPING['priority'] => [
647
                							PRIORITY_MAPPING[priority_change.oldvalue].id,
648
                							PRIORITY_MAPPING[priority_change.newvalue].id
649
                				]
650
                })
651
              end
652
              
653
              # Handle subject/summary changes
654
              if subject_change
655
                n.changes = n.changes.merge({
656
                				PROP_MAPPING['summary'] => [
657
                							encode(subject_change.oldvalue[0, limit_for(Issue, 'subject')]),
658
                							encode(subject_change.newvalue[0, limit_for(Issue, 'subject')])
659
                				]
660
                })
661
              end
662
              
663
              # Handle tracker/type (bug, feature) changes
664
              if tracker_change
665
                n.changes = n.changes.merge({
666
                				PROP_MAPPING['type'] => [
667
                							TRACKER_MAPPING[tracker_change.oldvalue].id || DEFAULT_TRACKER.id,
668
                							TRACKER_MAPPING[tracker_change.newvalue].id || DEFAULT_TRACKER.id
669
                				]
670
                })
671
              end
672

  
673
              # Add timelog entries for each time changes (from timeandestimation plugin)
674
              if time_change && time_change.newvalue != '0' && time_change.newvalue != ''
675
                t = TimeEntry.new(:project => @target_project, 
676
                                  :issue => _i, 
677
                                  :user => n.user,
678
                                  :spent_on => time,
679
                                  :hours => time_change.newvalue.to_i.abs,		## HACK, hours has to be positiv
680
                                  :created_on => time,
681
                                  :updated_on => time,
682
                                  :activity_id => TimeEntryActivity.find_by_position(2).id,
683
                                  :comments => "#{convert_wiki_text(n.notes.each_line.first.chomp)[0,100] unless !n.notes.each_line.first}... \"more\":/issues/#{_i.id}#note-#{noteid}")
684
                t.save
685
                t.errors.each_full{|msg| puts msg }
686
              end
687
			  
688
			  # saving changes and registering them for the issue
689
              n.save unless n.changes.empty? && n.notes.blank?
690
			  _i.journals << n unless n.changes.empty? && n.notes.blank?
691
			  
692
			  # Set correct changetime of the issue
693
              ## HACK Unnecessary next unless Time.fake(ticket.changetime) { _i.save }
694
              
695
              noteid += 1
696
          end
697
          
698
          # saving issue changes to database
699
          Time.fake(ticket.changetime) { _i.save }
700
		  
701
		  #end
510 702

  
511 703
          # Attachments
512 704
          ticket.attachments.each do |attachment|
......
515 707
                a = Attachment.new :created_on => attachment.time
516 708
                a.file = attachment
517 709
                a.author = find_or_create_user(attachment.author)
518
                a.container = i
710
                a.container = _i
519 711
                a.description = attachment.description
712
                
713
                ## TODO DUMMY HACK be sure, that the attachment exists on the disk - copy real files afterwards
714
                ## TODO get attachment file directory from project settings
715
                result = system( "touch files/" + a.disk_filename )
716
                
520 717
                migrated_ticket_attachments += 1 if a.save
521 718
              }
522 719
          end
523 720

  
524
          # Custom fields
721
          # Custom fields          
525 722
          custom_values = ticket.customs.inject({}) do |h, custom|
526 723
            if custom_field = custom_field_map[custom.name]
527 724
              h[custom_field.id] = custom.value
......
532 729
          if custom_field_map['resolution'] && !ticket.resolution.blank?
533 730
            custom_values[custom_field_map['resolution'].id] = ticket.resolution
534 731
          end
535
          i.custom_field_values = custom_values
536
          i.save_custom_field_values
732
          if custom_field_map['keywords'] && !ticket.keywords.blank?
733
            custom_values[custom_field_map['keywords'].id] = ticket.keywords
734
          end
735
          if custom_field_map['tracid'] 
736
            custom_values[custom_field_map['tracid'].id] = ticket.id
737
          end
738
          _i.custom_field_values = custom_values
739
          _i.save_custom_field_values
740
          
537 741
        end
538 742

  
539 743
        # update issue id sequence if needed (postgresql)
540 744
        Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
541
        puts
745
        puts if migrated_tickets < tickets_total
542 746

  
543 747
        # Wiki
544
        print "Migrating wiki"
748
        who = "Migrating wiki"
545 749
        if wiki.save
750
          wiki_edits_total = TracWikiPage.count
546 751
          TracWikiPage.find(:all, :order => 'name, version').each do |page|
547 752
            # Do not migrate Trac manual wiki pages
548
            next if TRAC_WIKI_PAGES.include?(page.name)
549
            wiki_edit_count += 1
550
            print '.'
551
            STDOUT.flush
753
            if TRAC_WIKI_PAGES.include?(page.name) then
754
              wiki_edits_total -= 1
755
              next
756
            end
552 757
            p = wiki.find_or_new_page(page.name)
553 758
            p.content = WikiContent.new(:page => p) if p.new_record?
554 759
            p.content.text = page.text
555 760
            p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
556 761
            p.content.comments = page.comment
557 762
            Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
763
            migrated_wiki_edits += 1
764
            simplebar(who, migrated_wiki_edits, wiki_edits_total)
558 765

  
559 766
            next if p.content.new_record?
560
            migrated_wiki_edits += 1
561 767

  
562 768
            # Attachments
563 769
            page.attachments.each do |attachment|
......
574 780
            end
575 781
          end
576 782

  
577
          wiki.reload
578
          wiki.pages.each do |page|
579
            page.content.text = convert_wiki_text(page.content.text)
580
            Time.fake(page.content.updated_on) { page.content.save }
581
          end
582
        end
783
        end
784
        puts if migrated_wiki_edits < wiki_edits_total
785

  
786
        # Now load each wiki page and transform its content into textile format
787
        puts "\nTransform texts to textile format:"
788
        
789
        wiki_pages_count = 0
790
        issues_count = 0
791
        milestone_wiki_count = 0
792

  
793
        who = "   in Wiki pages"
794
        wiki.reload
795
        wiki_pages_total = wiki.pages.count
796
        wiki.pages.each do |page|
797
          page.content.text = convert_wiki_text(page.content.text)
798
          Time.fake(page.content.updated_on) { page.content.save }
799
          wiki_pages_count += 1
800
          simplebar(who, wiki_pages_count, wiki_pages_total)
801
        end
802
        puts if wiki_pages_count < wiki_pages_total
803
        
804
	
805
        who = "   in Issues"
806
        issues_total = TICKET_MAP.count
807
        TICKET_MAP.each do |newId|
808
          issues_count += 1
809
          simplebar(who, issues_count, issues_total)
810
          next if newId.nil?
811
          issue = findIssue(newId)
812
          next if issue.nil?
813
          
814
          # convert issue description
815
          issue.description = convert_wiki_text(issue.description)
816
          
817
          # Converted issue comments had their last updated time set to the day of the migration (bugfix)
818
          next unless Time.fake(issue.updated_on) { issue.save }
819
          
820
          ## TODO CHECKEN
821
          # convert issue journals
822
          #issue.journals.find(:all).each do |journal|
823
          #  journal.notes = convert_wiki_text(journal.notes)
824
          #  journal.save
825
          #end
826
        end
827
        puts if issues_count < issues_total
828

  
829
        who = "   in Milestone descriptions"
830
        milestone_wiki_total = milestone_wiki.count
831
        milestone_wiki.each do |name|
832
          milestone_wiki_count += 1
833
          simplebar(who, milestone_wiki_count, milestone_wiki_total)
834
          p = wiki.find_page(name)            
835
          next if p.nil?
836
          p.content.text = convert_wiki_text(p.content.text)
837
          p.content.save
838
        end
839
        puts if milestone_wiki_count < milestone_wiki_total
840

  
583 841
        puts
584

  
585
        puts
586
        puts "Components:      #{migrated_components}/#{TracComponent.count}"
587
        puts "Milestones:      #{migrated_milestones}/#{TracMilestone.count}"
588
        puts "Tickets:         #{migrated_tickets}/#{TracTicket.count}"
842
        puts "Components:      #{migrated_components}/#{components_total}"
843
        puts "Milestones:      #{migrated_milestones}/#{milestones_total}"
844
        puts "Tickets:         #{migrated_tickets}/#{tickets_total}"
589 845
        puts "Ticket files:    #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
590 846
        puts "Custom values:   #{migrated_custom_values}/#{TracTicketCustom.count}"
591
        puts "Wiki edits:      #{migrated_wiki_edits}/#{wiki_edit_count}"
847
        puts "Wiki edits:      #{migrated_wiki_edits}/#{wiki_edits_total}"
592 848
        puts "Wiki files:      #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
593 849
      end
594

  
850
      
851
      def self.findIssue(id)
852
        return Issue.find(id)
853
      rescue ActiveRecord::RecordNotFound
854
        puts "[#{id}] not found"
855
        nil
856
      end
857
      
595 858
      def self.limit_for(klass, attribute)
596 859
        klass.columns_hash[attribute.to_s].limit
597 860
      end
......
601 864
      rescue Iconv::InvalidEncoding
602 865
        puts "Invalid encoding!"
603 866
        return false
867
      end
868

  
869
      def self.set_migrate_accounts(migrate)
870
        @migrate_accounts = migrate
871
        @migrate_accounts
872
      end
873

  
874
      def self.set_migrate_accounts_valid_email(valid_email)
875
        @migrate_accounts_valid_email = valid_email
876
        @migrate_accounts_valid_email
604 877
      end
605 878

  
606 879
      def self.set_trac_directory(path)
......
669 942
          project.identifier = identifier
670 943
          puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
671 944
          # enable issues and wiki for the created project
672
          project.enabled_module_names = ['issue_tracking', 'wiki']
945
          # Enable all project modules by default
946
          #project.enabled_module_names = ['issue_tracking', 'wiki', 'time_tracking', 'news', 'documents', 'files', 'repository', 'boards', 'calendar', 'gantt']
947
          # Only enable modules which might have content from the migrated project
948
          project.enabled_module_names = ['issue_tracking', 'wiki', 'repository']
673 949
        else
674 950
          puts
675 951
          puts "This project already exists in your Redmine database."
......
679 955
        end
680 956
        project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
681 957
        project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
958
        # Add Task type to the project
959
        project.trackers << TRACKER_TASK unless project.trackers.include?(TRACKER_TASK)
682 960
        @target_project = project.new_record? ? nil : project
683 961
        @target_project.reload
684 962
      end
......
715 993
      end
716 994
    end
717 995

  
996
    
718 997
    puts
719 998
    if Redmine::DefaultData::Loader.no_data?
720 999
      puts "Redmine configuration need to be loaded before importing data."
......
730 1009
    break unless STDIN.gets.match(/^y$/i)
731 1010
    puts
732 1011

  
733
    def prompt(text, options = {}, &block)
734
      default = options[:default] || ''
735
      while true
736
        print "#{text} [#{default}]: "
737
        STDOUT.flush
738
        value = STDIN.gets.chomp!
739
        value = default if value.blank?
740
        break if yield value
741
      end
742
    end
743

  
744 1012
    DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
745 1013

  
746
    prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
747
    prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
1014
	# Get Trac configuration and migration settings
1015
	prompt('Trac directory', :default => $DEFAULT_TRAC_DIRECTORY) {|directory| TracMigrate.set_trac_directory directory.strip}
1016
    prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
748 1017
    unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
749 1018
      prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
750 1019
      prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
......
754 1023
      prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
755 1024
    end
756 1025
    prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
757
    prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
1026
	prompt('Target project identifier', :default => $DEFAULT_PROJECT_IDENTIFIER) {|identifier| TracMigrate.target_project_identifier identifier.downcase}
1027
    
1028
	# Ask for user account migration
1029
    print "Migrate ALL authenticated accounts [y/N]? "
1030
    STDOUT.flush
1031
    migrate_accounts = STDIN.gets.match(/^y$/i)
1032
    TracMigrate.set_migrate_accounts(migrate_accounts)
1033

  
1034
    if migrate_accounts
1035
      print "Require authenticated accounts to have an email address set [Y/n]? "
1036
      STDOUT.flush
1037
      TracMigrate.set_migrate_accounts_valid_email(STDIN.gets.match(/^y$/i))
1038
    end
758 1039
    puts
759

  
1040
    
760 1041
    # Turn off email notifications
761 1042
    Setting.notified_events = []
762

  
1043
    
763 1044
    TracMigrate.migrate
764 1045
  end
1046

  
1047

  
1048
  # Prompt
1049
  def prompt(text, options = {}, &block)
1050
    default = options[:default] || ''
1051
    while true
1052
      print "#{text} [#{default}]: "
1053
      STDOUT.flush
1054
      value = STDIN.gets.chomp!
1055
      value = default if value.blank?
1056
      break if yield value
1057
    end
1058
  end
1059

  
1060
  # Basic wiki syntax conversion
1061
  def convert_wiki_text_mapping(text, ticket_map = [])
1062
        # Hide links
1063
        def wiki_links_hide(src)
1064
          @wiki_links = []
1065
          @wiki_links_hash = "####WIKILINKS#{src.hash.to_s}####"
1066
          src.gsub(/(\[\[.+?\|.+?\]\])/) do
1067
            @wiki_links << $1
1068
            @wiki_links_hash
1069
          end
1070
        end
1071
        # Restore links
1072
        def wiki_links_restore(src)
1073
          @wiki_links.each do |s|
1074
            src = src.sub("#{@wiki_links_hash}", s.to_s)
1075
          end
1076
          src
1077
        end
1078
        # Hidding code blocks
1079
        def code_hide(src)
1080
          @code = []
1081
          @code_hash = "####CODEBLOCK#{src.hash.to_s}####"
1082
          src.gsub(/(\{\{\{.+?\}\}\}|`.+?`)/m) do
1083
            @code << $1
1084
            @code_hash
1085
          end
1086
        end
1087
        # Convert code blocks
1088
        def code_convert(src)
1089
          @code.each do |s|
1090
            s = s.to_s
1091
            if s =~ (/`(.+?)`/m) || s =~ (/\{\{\{(.+?)\}\}\}/) then
1092
              # inline code
1093
              s = s.replace("@#{$1}@")
1094
            else
1095
              # We would like to convert the Code highlighting too
1096
              # This will go into the next line.
1097
              shebang_line = false
1098
              # Reguar expression for start of code
1099
              pre_re = /\{\{\{/
1100
              # Code hightlighing...
1101
              shebang_re = /^\#\!([a-z]+)/
1102
              # Regular expression for end of code
1103
              pre_end_re = /\}\}\}/
1104
      
1105
              # Go through the whole text..extract it line by line
1106
              s = s.gsub(/^(.*)$/) do |line|
1107
                m_pre = pre_re.match(line)
1108
                if m_pre
1109
                  line = '<pre>'
1110
                else
1111
                  m_sl = shebang_re.match(line)
1112
                  if m_sl
1113
                    shebang_line = true
1114
                    line = '<code class="' + m_sl[1] + '">'
1115
                  end
1116
                  m_pre_end = pre_end_re.match(line)
1117
                  if m_pre_end
1118
                    line = '</pre>'
1119
                    if shebang_line
1120
                      line = '</code>' + line
1121
                    end
1122
                  end
1123
                end
1124
                line
1125
              end
1126
            end
1127
            src = src.sub("#{@code_hash}", s)
1128
          end
1129
          src
1130
        end
1131

  
1132
        # Hide code blocks
1133
        text = code_hide(text)
1134
        # New line
1135
        text = text.gsub(/\[\[[Bb][Rr]\]\]/, "\n") # This has to go before the rules below
1136
        # Titles (only h1. to h6., and remove #...)
1137
        text = text.gsub(/(?:^|^\ +)(\={1,6})\ (.+)\ (?:\1)(?:\ *(\ \#.*))?/) {|s| "\nh#{$1.length}. #{$2}#{$3}\n"}
1138
        
1139
        # External Links:
1140
        #      [http://example.com/]
1141
        text = text.gsub(/\[((?:https?|s?ftp)\:\S+)\]/, '\1')
1142
        #      [http://example.com/ Example],[http://example.com/ "Example"]
1143
        #      [http://example.com/ "Example for "Example""] -> "Example for 'Example'":http://example.com/
1144
        text = text.gsub(/\[((?:https?|s?ftp)\:\S+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":#{$1}"}
1145
        #      [mailto:some@example.com],[mailto:"some@example.com"]
1146
        text = text.gsub(/\[mailto\:([\"']?)(.+?)\1\]/, '\2')
1147
        
1148
        # Ticket links:
1149
        #      [ticket:234 Text],[ticket:234 This is a test],[ticket:234 "This is a test"]
1150
        #      [ticket:234 "Test "with quotes""] -> "Test 'with quotes'":issues/show/234
1151
        text = text.gsub(/\[ticket\:(\d+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":/issues/show/#{$1}"}
1152
        #      ticket:1234
1153
        #      excluding ticket:1234:file.txt (used in macros)
1154
        #      #1 - working cause Redmine uses the same syntax.
1155
        text = text.gsub(/ticket\:(\d+?)([^\:])/, '#\1\2')
1156

  
1157
        # Source & attachments links:
1158
        #      [source:/trunk/readme.txt Readme File],[source:"/trunk/readme.txt" Readme File],
1159
        #      [source:/trunk/readme.txt],[source:"/trunk/readme.txt"]
1160
        #       The text "Readme File" is not converted,
1161
        #       cause Redmine's wiki does not support this.
1162
        #      Attachments use same syntax.
1163
        text = text.gsub(/\[(source|attachment)\:([\"']?)([^\"']+?)\2(?:\ +(.+?))?\]/, '\1:"\3"')
1164
        #      source:"/trunk/readme.txt"
1165
        #      source:/trunk/readme.txt - working cause Redmine uses the same syntax.
1166
        text = text.gsub(/(source|attachment)\:([\"'])([^\"']+?)\2/, '\1:"\3"')
1167

  
1168
        # Milestone links:
1169
        #      [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)],
1170
        #      [milestone:"0.1.0 Mercury"],milestone:"0.1.0 Mercury"
1171
        #       The text "Milestone 0.1.0 (Mercury)" is not converted,
1172
        #       cause Redmine's wiki does not support this.
1173
        text = text.gsub(/\[milestone\:([\"'])([^\"']+?)\1(?:\ +(.+?))?\]/, 'version:"\2"')
1174
        text = text.gsub(/milestone\:([\"'])([^\"']+?)\1/, 'version:"\2"')
1175
        #      [milestone:0.1.0],milestone:0.1.0
1176
        text = text.gsub(/\[milestone\:([^\ ]+?)\]/, 'version:\1')
1177
        text = text.gsub(/milestone\:([^\ ]+?)/, 'version:\1')
1178

  
1179
        # Internal Links:
1180
        #      ["Some Link"]
1181
        text = text.gsub(/\[([\"'])(.+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
1182
        #      [wiki:"Some Link" "Link description"],[wiki:"Some Link" Link description]
1183
        text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1[\ \t]+([\"']?)(.+?)\3\]/) {|s| "[[#{$2.delete(',./?;|:')}|#{$4}]]"}
1184
        #      [wiki:"Some Link"]
1185
        text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
1186
        #      [wiki:SomeLink]
1187
        text = text.gsub(/\[wiki\:([^\s\]]+?)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
1188
        #      [wiki:SomeLink Link description],[wiki:SomeLink "Link description"]
1189
        text = text.gsub(/\[wiki\:([^\s\]\"']+?)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$3}]]"}
1190

  
1191
        # Before convert CamelCase links, must hide wiki links with description.
1192
        # Like this: [[http://www.freebsd.org|Hello FreeBSD World]]
1193
        text = wiki_links_hide(text)
1194
        # Links to CamelCase pages (not work for unicode)
1195
        #      UsingJustWikiCaps,UsingJustWikiCaps/Subpage
1196
        text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+(?:\/[^\s[:punct:]]+)*)/) {|s| "#{$1}#{$2}[[#{$3.delete('/')}]]"}
1197
        # Normalize things that were supposed to not be links
1198
        # like !NotALink
1199
        text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
1200
        # Now restore hidden links
1201
        text = wiki_links_restore(text)
1202
        
1203
        # Revisions links
1204
        text = text.gsub(/\[(\d+)\]/, 'r\1')
1205
        # Ticket number re-writing
1206
        text = text.gsub(/#(\d+)/) do |s|
1207
          if $1.length < 10
1208
            #ticket_map[$1.to_i] ||= $1
1209
            "\##{ticket_map[$1.to_i] || $1}"
1210
          else
1211
            s
1212
          end
1213
        end
1214
        
1215
        # Highlighting
1216
        text = text.gsub(/'''''([^\s])/, '_*\1')
1217
        text = text.gsub(/([^\s])'''''/, '\1*_')
1218
        text = text.gsub(/'''/, '*')
1219
        text = text.gsub(/''/, '_')
1220
        text = text.gsub(/__/, '+')
1221
        text = text.gsub(/~~/, '-')
1222
        text = text.gsub(/,,/, '~')
1223
        # Tables
1224
        text = text.gsub(/\|\|/, '|')
1225
        # Lists:
1226
        #      bullet
1227
        text = text.gsub(/^(\ +)[\*-] /) {|s| '*' * $1.length + " "}
1228
        #      numbered
1229
        text = text.gsub(/^(\ +)\d+\. /) {|s| '#' * $1.length + " "}
1230
        # Images (work for only attached in current page [[Image(picture.gif)]])
1231
        # need rules for:  * [[Image(wiki:WikiFormatting:picture.gif)]] (referring to attachment on another page)
1232
        #                  * [[Image(ticket:1:picture.gif)]] (file attached to a ticket)
1233
        #                  * [[Image(htdocs:picture.gif)]] (referring to a file inside project htdocs)
1234
        #                  * [[Image(source:/trunk/trac/htdocs/trac_logo_mini.png)]] (a file in repository) 
1235
        text = text.gsub(/\[\[image\((.+?)(?:,.+?)?\)\]\]/i, '!\1!')
1236
        # TOC (is right-aligned, because that in Trac)
1237
        text = text.gsub(/\[\[TOC(?:\((.*?)\))?\]\]/m) {|s| "{{>toc}}\n"}
1238

  
1239
        # Restore and convert code blocks
1240
        text = code_convert(text)
1241

  
1242
        text
1243
  end
1244
  
1245
  # Simple progress bar
1246
  def simplebar(title, current, total, out = STDOUT)
1247
    def get_width
1248
      default_width = 80
1249
      begin
1250
        tiocgwinsz = 0x5413
1251
        data = [0, 0, 0, 0].pack("SSSS")
1252
        if out.ioctl(tiocgwinsz, data) >= 0 then
1253
          rows, cols, xpixels, ypixels = data.unpack("SSSS")
1254
          if cols >= 0 then cols else default_width end
1255
        else
1256
          default_width
1257
        end
1258
      rescue Exception
1259
        default_width
1260
      end
1261
    end
1262
    mark = "*"
1263
    title_width = 40
1264
    max = get_width - title_width - 10
1265
    format = "%-#{title_width}s [%-#{max}s] %3d%%  [%d/%d]%s"
1266
    bar = current * max / total
1267
    percentage = bar * 100 / max
1268
    current == total ? eol = "\n" : eol ="\r"
1269
    printf(format, title, mark * bar, percentage, current, total, eol)
1270
    out.flush
1271
  end
765 1272
end
766