Project

General

Profile

Trac migration script failing ยป migrate_from_trac.rake

rake file being used - Over Kill, 2020-03-13 15:58

 
1
# Redmine - project management software
2
# Copyright (C) 2006-2019  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
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.
17

    
18
require 'active_record'
19
require 'pp'
20

    
21
namespace :redmine do
22
  desc 'Trac migration script'
23
  task :migrate_from_trac => :environment do
24

    
25
    module TracMigrate
26
        TICKET_MAP = []
27

    
28
        new_status = IssueStatus.find_by_position(1)
29
        assigned_status = IssueStatus.find_by_position(2)
30
        resolved_status = IssueStatus.find_by_position(3)
31
        feedback_status = IssueStatus.find_by_position(4)
32
        closed_status = IssueStatus.where(:is_closed => true).first
33
        STATUS_MAPPING = {'new' => new_status,
34
                          'reopened' => feedback_status,
35
                          'assigned' => assigned_status,
36
                          'closed' => closed_status
37
                          }
38

    
39
        priorities = IssuePriority.all
40
        DEFAULT_PRIORITY = priorities[0]
41
        PRIORITY_MAPPING = {'lowest' => priorities[0],
42
                            'low' => priorities[0],
43
                            'normal' => priorities[1],
44
                            'high' => priorities[2],
45
                            'highest' => priorities[3],
46
                            # ---
47
                            'trivial' => priorities[0],
48
                            'minor' => priorities[1],
49
                            'major' => priorities[2],
50
                            'critical' => priorities[3],
51
                            'blocker' => priorities[4]
52
                            }
53

    
54
        TRACKER_BUG = Tracker.find_by_position(1)
55
        TRACKER_FEATURE = Tracker.find_by_position(2)
56
        DEFAULT_TRACKER = TRACKER_BUG
57
        TRACKER_MAPPING = {'defect' => TRACKER_BUG,
58
                           'enhancement' => TRACKER_FEATURE,
59
                           'task' => TRACKER_FEATURE,
60
                           'patch' =>TRACKER_FEATURE
61
                           }
62

    
63
        roles = Role.where(:builtin => 0).order('position ASC').all
64
        manager_role = roles[0]
65
        developer_role = roles[1]
66
        DEFAULT_ROLE = roles.last
67
        ROLE_MAPPING = {'admin' => manager_role,
68
                        'developer' => developer_role
69
                        }
70

    
71
      class ::Time
72
        class << self
73
          def at2(time)
74
            if TracMigrate.database_version > 22
75
              Time.at(time / 1000000)
76
            else
77
              Time.at(time)
78
            end
79
          end
80
        end
81
      end
82

    
83

    
84
      class TracSystem < ActiveRecord::Base
85
        self.table_name = :system
86
      end
87

    
88
      class TracComponent < ActiveRecord::Base
89
        self.table_name = :component
90
      end
91

    
92
      class TracMilestone < ActiveRecord::Base
93
        self.table_name = :milestone
94
        # If this attribute is set a milestone has a defined target timepoint
95
        def due
96
          if read_attribute(:due) && read_attribute(:due) > 0
97
            Time.at2(read_attribute(:due)).to_date
98
          else
99
            nil
100
          end
101
        end
102
        # This is the real timepoint at which the milestone has finished.
103
        def completed
104
          if read_attribute(:completed) && read_attribute(:completed) > 0
105
            Time.at2(read_attribute(:completed)).to_date
106
          else
107
            nil
108
          end
109
        end
110

    
111
        def description
112
          # Attribute is named descr in Trac v0.8.x
113
          has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
114
        end
115
      end
116

    
117
      class TracTicketCustom < ActiveRecord::Base
118
        self.table_name = :ticket_custom
119
      end
120

    
121
      class TracAttachment < ActiveRecord::Base
122
        self.table_name = :attachment
123
        self.inheritance_column = :none
124

    
125
        def time; Time.at2(read_attribute(:time)) end
126

    
127
        def original_filename
128
          filename
129
        end
130

    
131
        def content_type
132
          ''
133
        end
134

    
135
        def exist?
136
          File.file? trac_fullpath
137
        end
138

    
139
        def open
140
          File.open("#{trac_fullpath}", 'rb') {|f|
141
            @file = f
142
            yield self
143
          }
144
        end
145

    
146
        def read(*args)
147
          @file.read(*args)
148
        end
149

    
150
        def description
151
          read_attribute(:description).to_s.slice(0,255)
152
        end
153

    
154
      private
155
        def trac_fullpath
156
          attachment_type = read_attribute(:type)
157
          #replace exotic characters with their hex representation to avoid invalid filenames
158
          trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) do |x|
159
            codepoint = x.codepoints.to_a[0]
160
            sprintf('%%%02x', codepoint)
161
          end
162
          "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
163
        end
164
      end
165

    
166
      class TracTicket < ActiveRecord::Base
167
        self.table_name = :ticket
168
        self.inheritance_column = :none
169

    
170
        # ticket changes: only migrate status changes and comments
171
        has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
172
        has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
173

    
174
        def attachments
175
          TracMigrate::TracAttachment.where(:type => 'ticket', :id => self.id.to_s)
176
        end
177

    
178
        def ticket_type
179
          read_attribute(:type)
180
        end
181

    
182
        def summary
183
          read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
184
        end
185

    
186
        def description
187
          read_attribute(:description).blank? ? summary : read_attribute(:description)
188
        end
189

    
190
        def time; Time.at2(read_attribute(:time)) end
191
        def changetime; Time.at2(read_attribute(:changetime)) end
192
      end
193

    
194
      class TracTicketChange < ActiveRecord::Base
195
        self.table_name = :ticket_change
196

    
197
        def self.columns
198
          # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
199
          super.select {|column| column.name.to_s != 'field'}
200
        end
201

    
202
        def time; Time.at2(read_attribute(:time)) end
203
      end
204

    
205
      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
206
                           TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
207
                           TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
208
                           TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
209
                           TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
210
                           WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
211
                           CamelCase TitleIndex)
212

    
213
      class TracWikiPage < ActiveRecord::Base
214
        self.table_name = :wiki
215
        self.primary_key = :name
216

    
217
        def self.columns
218
           # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
219
          super.select {|column| column.name.to_s != 'readonly'}
220
        end
221

    
222
        def attachments
223
          TracMigrate::TracAttachment.where(:type => 'wiki', :id => self.id.to_s)
224
        end
225

    
226
        def time; Time.at2(read_attribute(:time)) end
227
      end
228

    
229
      class TracPermission < ActiveRecord::Base
230
        self.table_name = :permission
231
      end
232

    
233
      class TracSessionAttribute < ActiveRecord::Base
234
        self.table_name = :session_attribute
235
      end
236

    
237
      def self.find_or_create_user(username, project_member = false)
238
        return User.anonymous if username.blank?
239

    
240
        u = User.find_by_login(username)
241
        if !u
242
          # Create a new user if not found
243
          mail = username[0, User::MAIL_LENGTH_LIMIT]
244
          if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
245
            mail = mail_attr.value
246
          end
247
          mail = "#{mail}@foo.bar" unless mail.include?("@")
248

    
249
          name = username
250
          if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
251
            name = name_attr.value
252
          end
253
          name =~ (/(\w+)(\s+\w+)?/)
254
          fn = ($1 || "-").strip
255
          ln = ($2 || '-').strip
256

    
257
          u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
258
                       :firstname => fn[0, limit_for(User, 'firstname')],
259
                       :lastname => ln[0, limit_for(User, 'lastname')]
260

    
261
          u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
262
          u.password = 'trac'
263
          u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
264
          # finally, a default user is used if the new user is not valid
265
          u = User.first unless u.save
266
        end
267
        # Make sure user is a member of the project
268
        if project_member && !u.member_of?(@target_project)
269
          role = DEFAULT_ROLE
270
          if u.admin
271
            role = ROLE_MAPPING['admin']
272
          elsif TracPermission.find_by_username_and_action(username, 'developer')
273
            role = ROLE_MAPPING['developer']
274
          end
275
          Member.create(:user => u, :project => @target_project, :roles => [role])
276
          u.reload
277
        end
278
        u
279
      end
280

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

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

    
338
        # Go through the whole text..extract it line by line
339
        text = text.gsub(/^(.*)$/) do |line|
340
          m_pre = pre_re.match(line)
341
          if m_pre
342
            line = '<pre>'
343
          else
344
            m_sl = shebang_re.match(line)
345
            if m_sl
346
              shebang_line = true
347
              line = '<code class="' + m_sl[1] + '">'
348
            end
349
            m_pre_end = pre_end_re.match(line)
350
            if m_pre_end
351
              line = '</pre>'
352
              if shebang_line
353
                line = '</code>' + line
354
              end
355
            end
356
          end
357
          line
358
        end
359

    
360
        # Highlighting
361
        text = text.gsub(/'''''([^\s])/, '_*\1')
362
        text = text.gsub(/([^\s])'''''/, '\1*_')
363
        text = text.gsub(/'''/, '*')
364
        text = text.gsub(/''/, '_')
365
        text = text.gsub(/__/, '+')
366
        text = text.gsub(/~~/, '-')
367
        text = text.gsub(/`/, '@')
368
        text = text.gsub(/,,/, '~')
369
        # Lists
370
        text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
371

    
372
        text
373
      end
374

    
375
      def self.migrate
376
        establish_connection
377

    
378
        # Quick database test
379
        TracComponent.count
380
        lookup_database_version
381
        print "Trac database version is: ", database_version, "\n"
382
        migrated_components = 0
383
        migrated_milestones = 0
384
        migrated_tickets = 0
385
        migrated_custom_values = 0
386
        migrated_ticket_attachments = 0
387
        migrated_wiki_edits = 0
388
        migrated_wiki_attachments = 0
389

    
390
        #Wiki system initializing...
391
        @target_project.wiki.destroy if @target_project.wiki
392
        @target_project.reload
393
        wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
394
        wiki_edit_count = 0
395

    
396
        # Components
397
        print "Migrating components"
398
        issues_category_map = {}
399
        TracComponent.all.each do |component|
400
        print '.'
401
        STDOUT.flush
402
          c = IssueCategory.new :project => @target_project,
403
                                :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
404
        next unless c.save
405
        issues_category_map[component.name] = c
406
        migrated_components += 1
407
        end
408
        puts
409

    
410
        # Milestones
411
        print "Migrating milestones"
412
        version_map = {}
413
        TracMilestone.all.each do |milestone|
414
          print '.'
415
          STDOUT.flush
416
          # First we try to find the wiki page...
417
          p = wiki.find_or_new_page(milestone.name.to_s)
418
          p.content = WikiContent.new(:page => p) if p.new_record?
419
          p.content.text = milestone.description.to_s
420
          p.content.author = find_or_create_user('trac')
421
          p.content.comments = 'Milestone'
422
          p.save
423

    
424
          v = Version.new :project => @target_project,
425
                          :name => encode(milestone.name[0, limit_for(Version, 'name')]),
426
                          :description => nil,
427
                          :wiki_page_title => milestone.name.to_s,
428
                          :effective_date => milestone.completed
429

    
430
          next unless v.save
431
          version_map[milestone.name] = v
432
          migrated_milestones += 1
433
        end
434
        puts
435

    
436
        # Custom fields
437
        # TODO: read trac.ini instead
438
        print "Migrating custom fields"
439
        custom_field_map = {}
440
        TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
441
          print '.'
442
          STDOUT.flush
443
          # Redmine custom field name
444
          field_name = encode(field.name).humanize
445
          # Find if the custom already exists in Redmine
446
          f = IssueCustomField.find_by_name(field_name)
447
          # Or create a new one
448
          f ||= IssueCustomField.create(:name => encode(field.name).humanize,
449
                                        :field_format => 'string')
450

    
451
          next if f.new_record?
452
          f.trackers = Tracker.all
453
          f.projects << @target_project
454
          custom_field_map[field.name] = f
455
        end
456
        puts
457

    
458
        # Trac 'resolution' field as a Redmine custom field
459
        r = IssueCustomField.find_by(:name => "Resolution")
460
        r = IssueCustomField.new(:name => 'Resolution',
461
                                 :field_format => 'list',
462
                                 :is_filter => true) if r.nil?
463
        r.trackers = Tracker.all
464
        r.projects << @target_project
465
        r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
466
        r.save!
467
        custom_field_map['resolution'] = r
468

    
469
        # Tickets
470
        print "Migrating tickets"
471
          TracTicket.find_each(:batch_size => 200) do |ticket|
472
          print '.'
473
          STDOUT.flush
474
          i = Issue.new :project => @target_project,
475
                          :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
476
                          :description => convert_wiki_text(encode(ticket.description)),
477
                          :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
478
                          :created_on => ticket.time
479
          i.author = find_or_create_user(ticket.reporter)
480
          i.category = issues_category_map[ticket.component] unless ticket.component.blank?
481
          i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
482
          i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
483
          i.status = STATUS_MAPPING[ticket.status] || i.default_status
484
          i.id = ticket.id unless Issue.exists?(ticket.id)
485
          next unless i.save
486
          TICKET_MAP[ticket.id] = i.id
487
          migrated_tickets += 1
488

    
489
          # Owner
490
            unless ticket.owner.blank?
491
              i.assigned_to = find_or_create_user(ticket.owner, true)
492
              i.save
493
            end
494

    
495
          # Comments and status/resolution changes
496
          ticket.ticket_changes.group_by(&:time).each do |time, changeset|
497
              status_change = changeset.select {|change| change.field == 'status'}.first
498
              resolution_change = changeset.select {|change| change.field == 'resolution'}.first
499
              comment_change = changeset.select {|change| change.field == 'comment'}.first
500

    
501
              n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
502
                              :created_on => time
503
              n.user = find_or_create_user(changeset.first.author)
504
              n.journalized = i
505
              if status_change &&
506
                   STATUS_MAPPING[status_change.oldvalue] &&
507
                   STATUS_MAPPING[status_change.newvalue] &&
508
                   (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
509
                n.details << JournalDetail.new(:property => 'attr',
510
                                               :prop_key => 'status_id',
511
                                               :old_value => STATUS_MAPPING[status_change.oldvalue].id,
512
                                               :value => STATUS_MAPPING[status_change.newvalue].id)
513
              end
514
              if resolution_change
515
                n.details << JournalDetail.new(:property => 'cf',
516
                                               :prop_key => custom_field_map['resolution'].id,
517
                                               :old_value => resolution_change.oldvalue,
518
                                               :value => resolution_change.newvalue)
519
              end
520
              n.save unless n.details.empty? && n.notes.blank?
521
          end
522

    
523
          # Attachments
524
          ticket.attachments.each do |attachment|
525
            next unless attachment.exist?
526
              attachment.open {
527
                a = Attachment.new :created_on => attachment.time
528
                a.file = attachment
529
                a.author = find_or_create_user(attachment.author)
530
                a.container = i
531
                a.description = attachment.description
532
                migrated_ticket_attachments += 1 if a.save
533
              }
534
          end
535

    
536
          # Custom fields
537
          custom_values = ticket.customs.inject({}) do |h, custom|
538
            if custom_field = custom_field_map[custom.name]
539
              h[custom_field.id] = custom.value
540
              migrated_custom_values += 1
541
            end
542
            h
543
          end
544
          if custom_field_map['resolution'] && !ticket.resolution.blank?
545
            custom_values[custom_field_map['resolution'].id] = ticket.resolution
546
          end
547
          i.custom_field_values = custom_values
548
          i.save_custom_field_values
549
        end
550

    
551
        # update issue id sequence if needed (postgresql)
552
        Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
553
        puts
554

    
555
        # Wiki
556
        print "Migrating wiki"
557
        if wiki.save
558
          TracWikiPage.order('name, version').all.each do |page|
559
            # Do not migrate Trac manual wiki pages
560
            next if TRAC_WIKI_PAGES.include?(page.name)
561
            wiki_edit_count += 1
562
            print '.'
563
            STDOUT.flush
564
            p = wiki.find_or_new_page(page.name)
565
            p.content = WikiContent.new(:page => p) if p.new_record?
566
            p.content.text = page.text
567
            p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
568
            p.content.comments = page.comment
569
            p.new_record? ? p.save : p.content.save
570

    
571
            next if p.content.new_record?
572
            migrated_wiki_edits += 1
573

    
574
            # Attachments
575
            page.attachments.each do |attachment|
576
              next unless attachment.exist?
577
              next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
578
              attachment.open {
579
                a = Attachment.new :created_on => attachment.time
580
                a.file = attachment
581
                a.author = find_or_create_user(attachment.author)
582
                a.description = attachment.description
583
                a.container = p
584
                migrated_wiki_attachments += 1 if a.save
585
              }
586
            end
587
          end
588

    
589
          wiki.reload
590
          wiki.pages.each do |page|
591
            page.content.text = convert_wiki_text(page.content.text)
592
            page.content.save
593
          end
594
        end
595
        puts
596

    
597
        puts
598
        puts "Components:      #{migrated_components}/#{TracComponent.count}"
599
        puts "Milestones:      #{migrated_milestones}/#{TracMilestone.count}"
600
        puts "Tickets:         #{migrated_tickets}/#{TracTicket.count}"
601
        puts "Ticket files:    #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
602
        puts "Custom values:   #{migrated_custom_values}/#{TracTicketCustom.count}"
603
        puts "Wiki edits:      #{migrated_wiki_edits}/#{wiki_edit_count}"
604
        puts "Wiki files:      #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
605
      end
606

    
607
      def self.limit_for(klass, attribute)
608
        klass.columns_hash[attribute.to_s].limit
609
      end
610

    
611
      def self.encoding(charset)
612
        @charset = charset
613
      end
614

    
615
      def self.lookup_database_version
616
        f = TracSystem.find_by_name("database_version")
617
        @@database_version = f.value.to_i
618
      end
619

    
620
      def self.database_version
621
        @@database_version
622
      end
623

    
624
      def self.set_trac_directory(path)
625
        @@trac_directory = path
626
        raise "This directory doesn't exist!" unless File.directory?(path)
627
        raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
628
        @@trac_directory
629
      rescue => e
630
        puts e
631
        return false
632
      end
633

    
634
      def self.trac_directory
635
        @@trac_directory
636
      end
637

    
638
      def self.set_trac_adapter(adapter)
639
        return false if adapter.blank?
640
        raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
641
        # If adapter is sqlite or sqlite3, make sure that trac.db exists
642
        raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
643
        @@trac_adapter = adapter
644
      rescue => e
645
        puts e
646
        return false
647
      end
648

    
649
      def self.set_trac_db_host(host)
650
        return nil if host.blank?
651
        @@trac_db_host = host
652
      end
653

    
654
      def self.set_trac_db_port(port)
655
        return nil if port.to_i == 0
656
        @@trac_db_port = port.to_i
657
      end
658

    
659
      def self.set_trac_db_name(name)
660
        return nil if name.blank?
661
        @@trac_db_name = name
662
      end
663

    
664
      def self.set_trac_db_username(username)
665
        @@trac_db_username = username
666
      end
667

    
668
      def self.set_trac_db_password(password)
669
        @@trac_db_password = password
670
      end
671

    
672
      def self.set_trac_db_schema(schema)
673
        @@trac_db_schema = schema
674
      end
675

    
676
      mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
677

    
678
      def self.trac_db_path; "#{trac_directory}/db/trac.db" end
679
      def self.trac_attachments_directory; "#{trac_directory}/attachments" end
680

    
681
      def self.target_project_identifier(identifier)
682
        project = Project.find_by_identifier(identifier)
683
        if !project
684
          # create the target project
685
          project = Project.new :name => identifier.humanize,
686
                                :description => ''
687
          project.identifier = identifier
688
          puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
689
          # enable issues and wiki for the created project
690
          project.enabled_module_names = ['issue_tracking', 'wiki']
691
        else
692
          puts
693
          puts "This project already exists in your Redmine database."
694
          print "Are you sure you want to append data to this project ? [Y/n] "
695
          STDOUT.flush
696
          exit if STDIN.gets.match(/^n$/i)
697
        end
698
        project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
699
        project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
700
        @target_project = project.new_record? ? nil : project
701
        @target_project.reload
702
      end
703

    
704
      def self.connection_params
705
        if trac_adapter == 'sqlite3'
706
          {:adapter => 'sqlite3',
707
           :database => trac_db_path}
708
        else
709
          {:adapter => trac_adapter,
710
           :database => trac_db_name,
711
           :host => trac_db_host,
712
           :port => trac_db_port,
713
           :username => trac_db_username,
714
           :password => trac_db_password,
715
           :schema_search_path => trac_db_schema
716
          }
717
        end
718
      end
719

    
720
      def self.establish_connection
721
        constants.each do |const|
722
          klass = const_get(const)
723
          next unless klass.respond_to? 'establish_connection'
724
          klass.establish_connection connection_params
725
        end
726
      end
727

    
728
      def self.encode(text)
729
        text.to_s.force_encoding(@charset).encode('UTF-8')
730
      end
731
    end
732

    
733
    puts
734
    if Redmine::DefaultData::Loader.no_data?
735
      puts "Redmine configuration need to be loaded before importing data."
736
      puts "Please, run this first:"
737
      puts
738
      puts "  rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
739
      exit
740
    end
741

    
742
    puts "WARNING: a new project will be added to Redmine during this process."
743
    print "Are you sure you want to continue ? [y/N] "
744
    STDOUT.flush
745
    break unless STDIN.gets.match(/^y$/i)
746
    puts
747

    
748
    def prompt(text, options = {}, &block)
749
      default = options[:default] || ''
750
      while true
751
        print "#{text} [#{default}]: "
752
        STDOUT.flush
753
        value = STDIN.gets.chomp!
754
        value = default if value.blank?
755
        break if yield value
756
      end
757
    end
758

    
759
    DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
760

    
761
    prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
762
    prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
763
    unless %w(sqlite3).include?(TracMigrate.trac_adapter)
764
      prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
765
      prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
766
      prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
767
      prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
768
      prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
769
      prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
770
    end
771
    prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
772
    prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
773
    puts
774

    
775
    old_notified_events = Setting.notified_events
776
    old_password_min_length = Setting.password_min_length
777
    begin
778
      # Turn off email notifications temporarily
779
      Setting.notified_events = []
780
      Setting.password_min_length = 4
781
      # Run the migration
782
      TracMigrate.migrate
783
    ensure
784
      # Restore previous settings
785
      Setting.notified_events = old_notified_events
786
      Setting.password_min_length = old_password_min_length
787
    end
788
  end
789
end
    (1-1/1)