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_mantis.rake

Reworked mantis migration script - Sven Fischer, 2012-04-04 07:54 am

Download (21.1 kB)

 
1
# redMine - project management software
2
# Copyright (C) 2006-2007  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
desc 'Mantis migration script'
19
20
require 'active_record'
21
require 'iconv'
22
require 'pp'
23
24
namespace :redmine do
25
task :migrate_from_mantis => :environment do
26
  
27
  module ActiveSupport
28
    module Dependencies
29
      extend self
30
      
31
      #def load_missing_constant(from_mod, const_name)
32
      
33
      def forgiving_load_missing_constant( from_mod, const_name )
34
        begin
35
          old_load_missing_constant(from_mod, const_name)
36
        rescue ArgumentError => arg_err
37
          if arg_err.message == "#{from_mod} is not missing constant #{const_name}!"
38
            return from_mod.const_get(const_name)
39
          else
40
            raise
41
          end
42
        end
43
      end
44
      alias :old_load_missing_constant :load_missing_constant
45
      alias :load_missing_constant :forgiving_load_missing_constant
46
    end
47
  end
48
49
  module MantisMigrate
50
   
51
      DEFAULT_STATUS = IssueStatus.default
52
      assigned_status = IssueStatus.find_by_position(2)
53
      resolved_status = IssueStatus.find_by_position(3)
54
      feedback_status = IssueStatus.find_by_position(4)
55
      closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
56
      STATUS_MAPPING = {10 => DEFAULT_STATUS,  # new
57
                        20 => feedback_status, # feedback
58
                        30 => DEFAULT_STATUS,  # acknowledged
59
                        40 => DEFAULT_STATUS,  # confirmed
60
                        50 => assigned_status, # assigned
61
                        80 => resolved_status, # resolved
62
                        90 => closed_status    # closed
63
                        }
64
                        
65
      priorities = IssuePriority.all
66
      DEFAULT_PRIORITY = priorities[2]
67
      PRIORITY_MAPPING = {10 => priorities[1], # none
68
                          20 => priorities[1], # low
69
                          30 => priorities[2], # normal
70
                          40 => priorities[3], # high
71
                          50 => priorities[4], # urgent
72
                          60 => priorities[5]  # immediate
73
                          }
74
    
75
      TRACKER_BUG = Tracker.find_by_position(1)
76
      TRACKER_FEATURE = Tracker.find_by_position(2)
77
      
78
      roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
79
      manager_role = roles[0]
80
      developer_role = roles[1]
81
      DEFAULT_ROLE = roles.last
82
      ROLE_MAPPING = {10 => DEFAULT_ROLE,   # viewer
83
                      25 => DEFAULT_ROLE,   # reporter
84
                      40 => DEFAULT_ROLE,   # updater
85
                      55 => developer_role, # developer
86
                      70 => manager_role,   # manager
87
                      90 => manager_role    # administrator
88
                      }
89
      
90
      CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String
91
                                   1 => 'int',    # Numeric
92
                                   2 => 'int',    # Float
93
                                   3 => 'list',   # Enumeration
94
                                   4 => 'string', # Email
95
                                   5 => 'bool',   # Checkbox
96
                                   6 => 'list',   # List
97
                                   7 => 'list',   # Multiselection list
98
                                   8 => 'date',   # Date
99
                                   }
100
                                   
101
      RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES,    # related to
102
                               2 => IssueRelation::TYPE_RELATES,    # parent of
103
                               3 => IssueRelation::TYPE_RELATES,    # child of
104
                               0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
105
                               4 => IssueRelation::TYPE_DUPLICATES  # has duplicate
106
                               }
107
108
      HISTORY_MAPPING = {'handler_id' => ['assigned_to_id'],
109
                         'status' => ['status_id', STATUS_MAPPING],
110
                         'priority' => ['priority_id', PRIORITY_MAPPING]
111
                        }
112
                                                                   
113
    class MantisUser < ActiveRecord::Base
114
      set_table_name :mantis_user_table
115
      
116
      def firstname
117
        @firstname = realname.blank? ? username : realname.split.first[0..29]
118
        @firstname.gsub!(/[^\w\s\'\-]/i, '')
119
        @firstname
120
      end
121
      
122
      def lastname
123
        @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
124
        @lastname.gsub!(/[^\w\s\'\-]/i, '')
125
        @lastname = '-' if @lastname.blank?
126
        @lastname
127
      end
128
      
129
      def email
130
        if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
131
             !User.find_by_mail(read_attribute(:email))
132
          @email = read_attribute(:email)
133
        else
134
          @email = "#{username}@foo.bar"
135
        end
136
      end
137
      
138
      def username
139
        read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
140
      end
141
    end
142
    
143
    class MantisProject < ActiveRecord::Base
144
      set_table_name :mantis_project_table
145
      has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
146
      has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
147
      has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
148
      has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
149
      
150
      def name
151
        read_attribute(:name)[0..29]
152
      end
153
      
154
      def identifier
155
        read_attribute(:name).underscore[0..19].gsub(/[^a-z0-9\-]/, '-')
156
      end
157
    end
158
    
159
    class MantisVersion < ActiveRecord::Base
160
      set_table_name :mantis_project_version_table
161
      
162
      def version
163
        read_attribute(:version)[0..29]
164
      end
165
      
166
      def description
167
        read_attribute(:description)[0..254]
168
      end
169
    end
170
    
171
    class MantisCategory < ActiveRecord::Base
172
      set_table_name :mantis_category_table
173
    end
174
    
175
    class MantisProjectUser < ActiveRecord::Base
176
      set_table_name :mantis_project_user_list_table
177
    end
178
    
179
    class MantisBug < ActiveRecord::Base
180
      set_table_name :mantis_bug_table
181
      belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
182
      has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
183
      has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
184
      has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
185
      has_many :bug_history, :class_name => "MantisBugHistory", :foreign_key => :bug_id
186
    end
187
    
188
    class MantisBugText < ActiveRecord::Base
189
      set_table_name :mantis_bug_text_table
190
      
191
      # Adds Mantis steps_to_reproduce and additional_information fields
192
      # to description if any
193
      def full_description
194
        full_description = description
195
        full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
196
        full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
197
        full_description
198
      end
199
    end
200
    
201
    class MantisBugNote < ActiveRecord::Base
202
      set_table_name :mantis_bugnote_table
203
      belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
204
      belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
205
    end
206
207
    class MantisBugNoteText < ActiveRecord::Base
208
      set_table_name :mantis_bugnote_text_table
209
    end
210
    
211
    class MantisBugHistory < ActiveRecord::Base
212
      set_table_name :mantis_bug_history_table
213
      set_inheritance_column :none  
214
      belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
215
    end
216
    
217
    class MantisBugFile < ActiveRecord::Base
218
      set_table_name :mantis_bug_file_table
219
      
220
      def size
221
        filesize
222
      end
223
      
224
      def original_filename
225
        MantisMigrate.encode(filename)
226
      end
227
      
228
      def content_type
229
        file_type
230
      end
231
      
232
      def read(*args)
233
              if @read_finished
234
                      nil
235
              else
236
                      @read_finished = true
237
                      content
238
              end
239
      end
240
    end
241
    
242
    class MantisBugRelationship < ActiveRecord::Base
243
      set_table_name :mantis_bug_relationship_table
244
    end
245
    
246
    class MantisBugMonitor < ActiveRecord::Base
247
      set_table_name :mantis_bug_monitor_table
248
    end
249
    
250
    class MantisNews < ActiveRecord::Base
251
      set_table_name :mantis_news_table
252
    end
253
    
254
    class MantisCustomField < ActiveRecord::Base
255
      set_table_name :mantis_custom_field_table
256
      set_inheritance_column :none  
257
      has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
258
      has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
259
      
260
      def format
261
        read_attribute :type
262
      end
263
      
264
      def name
265
        read_attribute(:name)[0..29].gsub(/[^\w\s\'\-]/, '-')
266
      end
267
    end
268
    
269
    class MantisCustomFieldProject < ActiveRecord::Base
270
      set_table_name :mantis_custom_field_project_table  
271
    end
272
    
273
    class MantisCustomFieldString < ActiveRecord::Base
274
      set_table_name :mantis_custom_field_string_table  
275
    end
276
    
277
    def self.mantis_date_convert(time_stamp)
278
      DateTime.strptime(time_stamp.to_s, "%s")
279
    end
280
281
    def self.migrate
282
          
283
      # Users
284
      print "Migrating users"
285
      User.delete_all "login <> 'admin'"
286
      users_map = {}
287
      users_migrated = 0
288
      MantisUser.find(:all).each do |user|
289
            u = User.new :firstname => encode(user.firstname), 
290
                                     :lastname => encode(user.lastname),
291
                                     :mail => user.email,
292
                                     :last_login_on => user.last_visit
293
            u.login = user.username
294
            u.password = 'mantis'
295
            u.status = User::STATUS_LOCKED if user.enabled != 1
296
            u.admin = true if user.access_level == 90
297
            next unless u.save!
298
            users_migrated += 1
299
            users_map[user.id] = u.id
300
            print '.'
301
        STDOUT.flush
302
      end
303
      puts
304
    
305
      # Projects
306
      print "Migrating projects"
307
      Project.destroy_all
308
      projects_map = {}
309
      versions_map = {}
310
      categories_map = {}
311
      MantisProject.find(:all).each do |project|
312
            p = Project.new :name => encode(project.name), 
313
                        :description => encode(project.description),
314
                        :trackers => [TRACKER_BUG, TRACKER_FEATURE] 
315
            p.identifier = project.identifier
316
            next unless p.save
317
            projects_map[project.id] = p.id
318
            p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
319
        p.save
320
            print '.'
321
        STDOUT.flush
322
            
323
            # Project members
324
            project.members.each do |member|
325
          m = Member.new :user => User.find_by_id(users_map[member.user_id]),
326
                               :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
327
              m.project = p
328
              m.save
329
            end        
330
            
331
            # Project versions
332
            project.versions.each do |version|
333
          v = Version.new :name => encode(version.version),
334
                          :description => encode(version.description),
335
                          :effective_date => mantis_date_convert(version.date_order).to_date
336
          v.project = p
337
          v.save
338
          versions_map[version.id] = v.id
339
            end
340
            
341
            # Project categories
342
            project.categories.each do |category|
343
         g = IssueCategory.new :name => category.name[0,30]
344
         g.project = p
345
         g.save
346
         categories_map[category.name] = g.id
347
            end
348
      end        
349
      puts        
350
    
351
      # Bugs
352
      print "Migrating bugs"
353
      Issue.destroy_all
354
      issues_map = {}
355
      keep_bug_ids = (Issue.count == 0)
356
      MantisBug.find_each(:batch_size => 200) do |bug|
357
        next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
358
            i = Issue.new :project_id => projects_map[bug.project_id], 
359
                      :subject => encode(bug.summary),
360
                      :description => encode(bug.bug_text.full_description),
361
                      :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
362
                      :created_on => mantis_date_convert(bug.date_submitted),
363
                      :updated_on => mantis_date_convert(bug.last_updated)
364
            i.author = User.find_by_id(users_map[bug.reporter_id])
365
            i.category = IssueCategory.find_by_project_id_and_id(i.project_id, bug.category_id) unless bug.category_id.blank?
366
            i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
367
            i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
368
            i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
369
            i.id = bug.id if keep_bug_ids
370
        i.updated_on = mantis_date_convert(bug.last_updated)
371
        # Assignee
372
        # Redmine checks that the assignee is a project member
373
        if (bug.handler_id && users_map[bug.handler_id])
374
          i.assigned_to = User.find_by_id(users_map[bug.handler_id])
375
        end        
376
            
377
            next unless i.save_with_validation(false)
378
            issues_map[bug.id] = i.id
379
            print '.'
380
        STDOUT.flush
381
382
        Journal.class_exec {
383
          def touch_journaled_after_creation
384
          end
385
        }
386
            # Bug notes
387
            bug.bug_notes.each do |note|
388
              next unless users_map[note.reporter_id]
389
          n = Journal.new(:user => User.find_by_id(users_map[note.reporter_id]),
390
                          :notes => encode(note.bug_note_text.note))
391
          n.created_at = mantis_date_convert(note.date_submitted)
392
          n.version = i.journals.last.version + 1
393
          n.activity_type = 'issues'
394
          n.journaled_id = i.id
395
          n.changes = '--- {}'
396
          n.type = 'IssueJournal'
397
          n.journalized = i
398
          n.save unless n.notes.blank?
399
            end
400
401
            # Bug history
402
            bug.bug_history.each do |hist|
403
              next unless HISTORY_MAPPING.has_key? hist.field_name
404
          field = HISTORY_MAPPING[hist.field_name][0]
405
          if HISTORY_MAPPING[hist.field_name][1]
406
            m = HISTORY_MAPPING[hist.field_name][1]
407
            old = m[hist.old_value.to_i].id
408
            new = m[hist.new_value.to_i].id
409
          else
410
            if hist.field_name = 'handler_id'
411
              m = users_map
412
              old = m[hist.old_value.to_i]
413
              new = m[hist.new_value.to_i]
414
            else
415
              old = hist.old_value
416
              new = hist.new_value
417
            end
418
          end
419
          n = Journal.new(:user => User.find_by_id(users_map[hist.user_id]))
420
          n.created_at = mantis_date_convert(hist.date_modified)
421
          n.version = i.journals.last.version + 1
422
          n.notes = ""
423
          n.activity_type = 'issues'
424
          n.journaled_id = i.id
425
          n.changes = "--- \n#{field}:\n- #{old}\n- #{new}"
426
          n.type = 'IssueJournal'
427
          n.journalized = i
428
          n.save
429
            end
430
431
        # Bug files
432
        bug.bug_files.each do |file|
433
          a = Attachment.new :created_on => mantis_date_convert(file.date_added)
434
          a.file = file
435
          a.author = User.find :first
436
          a.container = i
437
          a.save
438
        end
439
        
440
        # Bug monitors
441
        bug.bug_monitors.each do |monitor|
442
          next unless users_map[monitor.user_id]
443
          i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
444
        end
445
      end
446
      
447
      # update issue id sequence if needed (postgresql)
448
      Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
449
      puts
450
      
451
      # Bug relationships
452
      print "Migrating bug relations"
453
      MantisBugRelationship.find(:all).each do |relation|
454
        next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
455
        r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
456
        r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
457
        r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
458
        pp r unless r.save
459
        print '.'
460
        STDOUT.flush
461
      end
462
      puts
463
      
464
      # News
465
      print "Migrating news"
466
      News.destroy_all
467
      MantisNews.find(:all, :conditions => 'project_id > 0').each do |news|
468
        next unless projects_map[news.project_id]
469
        n = News.new :project_id => projects_map[news.project_id],
470
                     :title => encode(news.headline[0..59]),
471
                     :description => encode(news.body),
472
                     :created_on => mantis_date_convert(news.date_posted)
473
        n.author = User.find_by_id(users_map[news.poster_id])
474
        n.save
475
        print '.'
476
        STDOUT.flush
477
      end
478
      puts
479
      
480
      # Custom fields
481
      print "Migrating custom fields"
482
      IssueCustomField.destroy_all
483
      MantisCustomField.find(:all).each do |field|
484
        f = IssueCustomField.new :name => field.name[0..29],
485
                                 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
486
                                 :min_length => field.length_min,
487
                                 :max_length => field.length_max,
488
                                 :regexp => field.valid_regexp,
489
                                 :possible_values => field.possible_values.split('|'),
490
                                 :is_required => field.require_report?
491
        next unless f.save
492
        print '.'
493
        STDOUT.flush
494
        
495
        # Trackers association
496
        f.trackers = Tracker.find :all
497
        
498
        # Projects association
499
        field.projects.each do |project|
500
          f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
501
        end
502
        
503
        # Values
504
        field.values.each do |value|
505
          v = CustomValue.new :custom_field_id => f.id,
506
                              :value => value.value
507
          v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
508
          v.save
509
        end unless f.new_record?
510
grep       end
511
      puts
512
    
513
      puts
514
      puts "Users:           #{users_migrated}/#{MantisUser.count}"
515
      puts "Projects:        #{Project.count}/#{MantisProject.count}"
516
      puts "Memberships:     #{Member.count}/#{MantisProjectUser.count}"
517
      puts "Versions:        #{Version.count}/#{MantisVersion.count}"
518
      puts "Categories:      #{IssueCategory.count}/#{MantisCategory.count}"
519
      puts "Bugs:            #{Issue.count}/#{MantisBug.count}"
520
      puts "Bug notes:       #{Journal.count}/#{MantisBugNote.count}"
521
      puts "Bug files:       #{Attachment.count}/#{MantisBugFile.count}"
522
      puts "Bug relations:   #{IssueRelation.count}/#{MantisBugRelationship.count}"
523
      puts "Bug monitors:    #{Watcher.count}/#{MantisBugMonitor.count}"
524
      puts "News:            #{News.count}/#{MantisNews.count}"
525
      puts "Custom fields:   #{IssueCustomField.count}/#{MantisCustomField.count}"
526
    end
527
  
528
    def self.encoding(charset)
529
      @ic = Iconv.new('UTF-8', charset)
530
    rescue Iconv::InvalidEncoding
531
      return false      
532
    end
533
    
534
    def self.establish_connection(params)
535
      constants.each do |const|
536
        klass = const_get(const)
537
        next unless klass.respond_to? 'establish_connection'
538
        klass.establish_connection params
539
      end
540
    end
541
    
542
    def self.encode(text)
543
      @ic.iconv text
544
    rescue
545
      text
546
    end
547
  end
548
  
549
  puts
550
  if Redmine::DefaultData::Loader.no_data?
551
    puts "Redmine configuration need to be loaded before importing data."
552
    puts "Please, run this first:"
553
    puts
554
    puts "  rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
555
    exit
556
  end
557
  
558
  puts "WARNING: Your Redmine data will be deleted during this process."
559
  print "Are you sure you want to continue ? [y/N] "
560
  break unless STDIN.gets.match(/^y$/i)
561
  
562
  # Default Mantis database settings
563
   db_params = {:adapter => 'mysql', 
564
               :database => 'bugtracker', 
565
               :host => 'localhost', 
566
               :username => 'root', 
567
               :password => '' }
568
569
  puts                                
570
  puts "Please enter settings for your Mantis database"  
571
  [:adapter, :host, :database, :username, :password].each do |param|
572
    print "#{param} [#{db_params[param]}]: "
573
    value = STDIN.gets.chomp!
574
    db_params[param] = value unless value.blank?
575
  end
576
    
577
  while true
578
    print "encoding [UTF-8]: "
579
    encoding = STDIN.gets.chomp!
580
    encoding = 'UTF-8' if encoding.blank?
581
    break if MantisMigrate.encoding encoding
582
    puts "Invalid encoding!"
583
  end
584
  puts
585
  
586
  # Make sure bugs can refer bugs in other projects
587
  Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
588
  
589
  # Turn off email notifications
590
  Setting.notified_events = []
591
  
592
  MantisMigrate.establish_connection db_params
593
  MantisMigrate.migrate
594
end
595
end