Project

General

Profile

Patch #989 » bugzilla-1.0.4.patch

Brian Lindahl, 2011-01-25 20:05

View differences:

redmine/lib/tasks/migrate_from_bugzilla.rake 2011-01-12 10:24:12.602338400 -0700
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
# Bugzilla migration by Arjen Roodselaar, Lindix bv
19
#
20

  
21
desc 'Bugzilla migration script'
22

  
23
require 'active_record'
24
require 'iconv'
25
require 'pp'
26

  
27
module ActiveRecord
28
    namespace :redmine do
29
        task :migrate_from_bugzilla => :environment do
30

  
31
            module BugzillaMigrate 
32
            
33
                STATUS_DEFAULT = IssueStatus.default
34
                STATUS_MAPPING = {
35
                    "UNCONFIRMED" => IssueStatus.find_by_name("New"),
36
                    "NEW"         => IssueStatus.find_by_name("New"),
37
                    "ASSIGNED"    => IssueStatus.find_by_name("Assigned"),
38
                    "REOPENED"    => IssueStatus.find_by_name("Reopened"),
39
                    "RESOLVED"    => IssueStatus.find_by_name("Closed"),
40
                    "VERIFIED"    => IssueStatus.find_by_name("Closed"),
41
                    "CLOSED"      => IssueStatus.find_by_name("Closed")
42
                }
43
                
44
                RESOLUTION_MAPPING = {
45
                    "INVALID"    => IssueStatus.find_by_name("Rejected"),
46
                    "WONTFIX"    => IssueStatus.find_by_name("Rejected"),
47
                    "LATER"      => IssueStatus.find_by_name("Postponed"),
48
                    "REMIND"     => IssueStatus.find_by_name("Postponed"),
49
                    "WORKSFORME" => IssueStatus.find_by_name("Rejected")
50
                }
51

  
52
                PRIORITY_DEFAULT = IssuePriority.default
53
                PRIORITY_MAPPING = {
54
                    "P1" => IssuePriority.find_by_name("Low"),
55
                    "P2" => IssuePriority.find_by_name("Normal"),
56
                    "P3" => IssuePriority.find_by_name("High"),
57
                    "P4" => IssuePriority.find_by_name("Urgent"),
58
                    "P5" => IssuePriority.find_by_name("Immediate"),
59
                    
60
                    "Low"      => IssuePriority.find_by_name("Low"),
61
                    "Medium"   => IssuePriority.find_by_name("Normal"),
62
                    "High"     => IssuePriority.find_by_name("High"),
63
                    "Critical" => IssuePriority.find_by_name("Urgent"),
64
                }
65
                
66
                TRACKER_DEFAULT = Tracker.find_by_name("Defect")
67
                TRACKER_MAPPING = {
68
                    "enhancement" => Tracker.find_by_name("Improvement")
69
                }
70

  
71
                ROLE_DEFAULT = Role.find_by_name("Observer")
72
            
73
                CUSTOM_FIELD_TYPE_MAPPING = {
74
                    1 => 'string', # Freetext
75
                    2 => 'string', # Single-select
76
                    3 => 'text',   # Multi-select
77
                    4 => 'text',   # Text-area
78
                    5 => 'date',   # Date-time
79
                }
80

  
81
                CUSTOM_FIELD_BUGZILLA_ID_NAME = "Bugzilla-Id"
82
                CUSTOM_FIELD_PLATFORM_OS_NAME = "Configuration"
83
                
84
                USER_DEFAULT = User.find_by_login("admin")
85

  
86
                class BugzillaProfile < ActiveRecord::Base
87
                    set_table_name :profiles
88
                    set_primary_key :userid
89
                
90
                    has_and_belongs_to_many :groups,
91
                        :class_name => "BugzillaGroup",
92
                        :join_table => :user_group_map,
93
                        :foreign_key => :user_id,
94
                        :association_foreign_key => :group_id
95
                
96
                    def login
97
                        login_name[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
98
                    end
99
                
100
                    def email
101
                        if login_name.match(/^.*@.*$/i)
102
                            login_name
103
                        else
104
                            "#{login_name}@foo.bar"
105
                        end
106
                    end
107

  
108
                    def firstname
109
                        s = read_attribute(:realname)
110
                        return s.split(/[,]+/).last.strip if s[',']
111
                        return s.split(/[ ]+/).first.strip
112
                    end
113

  
114
                    def lastname
115
                        s = read_attribute(:realname)
116
                        return s.split(/[,]+/).first.strip if s[',']
117
                        return s.split(/[ ]+/).last.strip
118
                    end
119
                end
120
            
121
                class BugzillaGroup < ActiveRecord::Base
122
                    set_table_name :groups
123
                
124
                    has_and_belongs_to_many :profiles,
125
                        :class_name => "BugzillaProfile",
126
                        :join_table => :user_group_map,
127
                        :foreign_key => :group_id,
128
                        :association_foreign_key => :user_id
129
                end
130
            
131
                class BugzillaProduct < ActiveRecord::Base
132
                    set_table_name :products
133
                
134
                    has_many :components, :class_name => "BugzillaComponent", :foreign_key => :product_id
135
                    has_many :versions, :class_name => "BugzillaVersion", :foreign_key => :product_id
136
                    has_many :bugs, :class_name => "BugzillaBug", :foreign_key => :product_id
137
                end
138
            
139
                class BugzillaComponent < ActiveRecord::Base
140
                    set_table_name :components
141
                end
142
            
143
                class BugzillaVersion < ActiveRecord::Base
144
                    set_table_name :versions
145
                end
146
            
147
                class BugzillaBug < ActiveRecord::Base
148
                    set_table_name :bugs
149
                    set_primary_key :bug_id
150
                
151
                    belongs_to :product, :class_name => "BugzillaProduct", :foreign_key => :product_id
152
                    has_many :descriptions, :class_name => "BugzillaDescription", :foreign_key => :bug_id
153
                    has_many :attachments, :class_name => "BugzillaAttachment", :foreign_key => :bug_id
154
                end
155

  
156
                class BugzillaDependency < ActiveRecord::Base
157
                    set_table_name :dependencies
158
                end
159
                
160
                class BugzillaDuplicate < ActiveRecord::Base
161
                    set_table_name :duplicates
162
                end
163

  
164
                class BugzillaDescription < ActiveRecord::Base
165
                    set_table_name :longdescs
166
                    set_inheritance_column :bongo
167
                    belongs_to :bug, :class_name => "BugzillaBug", :foreign_key => :bug_id
168
                
169
                    def eql(desc)
170
                        self.bug_when == desc.bug_when
171
                    end
172
                
173
                    def === desc
174
                        self.eql(desc)
175
                    end
176
                
177
                    def text
178
                        if self.thetext.blank?
179
                            return nil
180
                        else
181
                            self.thetext
182
                        end
183
                    end
184
                end
185

  
186
                class BugzillaAttachment < ActiveRecord::Base
187
                    set_table_name :attachments
188
                    set_primary_key :attach_id
189

  
190
                    has_one :attach_data, :class_name => 'BugzillaAttachData', :foreign_key => :id
191

  
192

  
193
                    def size
194
                        return 0 if self.attach_data.nil?
195
                        return self.attach_data.thedata.size
196
                    end
197

  
198
                    def original_filename
199
                        return self.filename
200
                    end
201

  
202
                    def content_type
203
                        self.mimetype
204
                    end
205

  
206
                    def read(*args)
207
                        if @read_finished
208
                            nil
209
                        else
210
                            @read_finished = true
211
                            return nil if self.attach_data.nil?
212
                            return self.attach_data.thedata
213
                        end
214
                    end
215
                end
216

  
217
                class BugzillaAttachData < ActiveRecord::Base
218
                    set_table_name :attach_data
219
                end
220
            
221
                def self.establish_connection(params)
222
                    constants.each do |const|
223
                        klass = const_get(const)
224
                        next unless klass.respond_to? 'establish_connection'
225
                        klass.establish_connection params
226
                    end
227
                end
228
                
229
                def self.map_user(userid)
230
                    return @user_map[userid] || USER_DEFAULT.id
231
                end
232

  
233
                def self.migrate_users
234
                    puts
235
                    puts "Migrating profiles\n"
236
                    
237
                    # Use email address as the matching mechanism.  If profile 
238
                    # exists in redmine, leave it untouched, otherwise create 
239
                    # a new user and copy the profile data from bugzilla
240
                    
241
                    @user_map = {}
242
                    BugzillaProfile.all(:order => :userid).each do |profile|
243
                        profile_email = profile.email
244
                        profile_email.strip!
245
                        login = "#{profile.firstname.downcase}.#{profile.lastname.downcase}"
246
                        existing_redmine_user = User.find_by_mail(profile_email) || User.find_by_login(login)
247
                        if existing_redmine_user
248
                            #puts "Existing Redmine User: \n #{existing_redmine_user.inspect}"
249
                            @user_map[profile.userid] = existing_redmine_user.id
250
                        else
251
                            # create the new user and make an entry in the mapping
252
                            user = User.new
253
                            user.login = login
254
                            user.password = "bugzilla"
255
                            user.firstname = profile.firstname
256
                            user.lastname = profile.lastname
257
                            user.mail = profile.email
258
                            user.mail.strip!
259
                            user.status = User::STATUS_LOCKED if !profile.disabledtext.empty?
260
                            user.admin = @preserve_admin if profile.groups.include?(BugzillaGroup.find_by_name("admin"))
261
                            unless user.save then
262
                                puts "FAILURE saving user"
263
                                puts "user: #{user.inspect}"
264
                                puts "bugzilla profile: #{profile.inspect}"
265
                                validation_errors = user.errors.collect {|e| e.to_s }.join(", ")
266
                                puts "validation errors: #{validation_errors}"
267
                                false!
268
                            end
269
                            @user_map[profile.userid] = user.id
270
                        end
271
                        print '.'
272
                        $stdout.flush
273
                    end
274
                end
275
                
276
                def self.migrate_products
277
                    puts
278
                    puts "Migrating products\n"
279
                    
280
                    @project_map = {}
281
                    @category_map = {}
282
                    
283
                    BugzillaProduct.find_each do |product|
284
                        project = Project.new
285
                        if (product.name.length > 30)
286
                            puts "Product name '#{product.name}' is too long (max 30)\n"
287
                            puts "Please choose a new name for the project: "
288
                            product.name = STDIN.gets.strip
289
                        end
290
                        project.name = product.name
291
                        project.description = product.description
292
                        puts "Enter a project identifier for '#{project.name}': "
293
                        project.identifier = STDIN.gets.strip
294
                        project.issue_custom_fields << @custom_field_bugzilla_id
295
                        project.issue_custom_fields << @custom_field_platform_os
296

  
297
                        project.save!
298
                        @project_map[product.id] = project.id
299

  
300
                        product.versions.each do |version|
301
                            Version.create(:name => version.value, :project => project, :status => 'locked')
302
                        end
303
                        
304
                        # Enable issue tracking
305
                        enabled_module = EnabledModule.new(
306
                            :project => project,
307
                            :name => 'issue_tracking'
308
                        )
309
                        enabled_module.save!
310

  
311
                        # Components
312
                        product.components.each do |component|
313
                            # assume all components become a new category
314
                            category = IssueCategory.new
315
                            if (component.name.length > 30)
316
                                puts "Component name '#{component.name}' is too long (max 30)\n"
317
                                puts "Please choose a new name for the component: "
318
                                component.name = STDIN.gets.strip
319
                            end
320
                            category.name = component.name
321
                            category.project = project
322
                            # puts "User mapping is: #{@user_map.inspect}"
323
                            # puts "component owner = #{component.initialowner} mapped to user #{map_user(component.initialowner)}"
324
                            uid = map_user(component.initialowner)
325
                            category.assigned_to = User.first(:conditions => {:id => uid })
326
                            category.save!
327
                            @category_map[component.id] = category.id
328
                        end
329
                    
330
                        Tracker.find_each do |tracker|
331
                            project.trackers << tracker
332
                        end
333

  
334
                        User.find_each do |user|
335
                            membership = Member.new(
336
                                :user => user,
337
                                :project => project                
338
                            )
339
                            membership.roles << ROLE_DEFAULT
340
                            membership.save
341
                        end
342
                    
343
                    end
344

  
345
                end
346

  
347
                def self.migrate_issues()
348
                    puts
349
                    puts "Migrating issues"
350
                    
351
                    # Issue.destroy_all
352
                    @issue_map = {}
353

  
354
                    BugzillaBug.find(:all, :order => "bug_id ASC").each  do |bug|
355
                        #puts "Processing bugzilla bug #{bug.bug_id}"
356
                        description = bug.descriptions.first.text.to_s
357

  
358
                        issue = Issue.new(
359
                            :project_id => @project_map[bug.product_id],
360
                            :subject => bug.short_desc,
361
                            :description => description || bug.short_desc,
362
                            :author_id => map_user(bug.reporter),
363
                            :tracker => TRACKER_MAPPING[bug.bug_severity] || TRACKER_DEFAULT,
364
                            :priority => PRIORITY_MAPPING[bug.priority] || PRIORITY_DEFAULT,
365
                            :status => RESOLUTION_MAPPING[bug.resolution] || STATUS_MAPPING[bug.bug_status] || STATUS_DEFAULT,
366
                            :start_date => bug.creation_ts,
367
                            :created_on => bug.creation_ts,
368
                            :updated_on => bug.delta_ts
369
                        )
370
                        
371
                        issue.category_id =  @category_map[bug.component_id] unless bug.component_id.blank?
372
                        issue.assigned_to_id = map_user(bug.assigned_to) unless bug.assigned_to.blank?
373
                        issue.found_version = Version.first(:conditions => {:project_id => @project_map[bug.product_id], :name => bug.version })
374
                        issue.custom_field_values = { @custom_field_bugzilla_id.id => "#{bug.bug_id}",
375
                                                      @custom_field_platform_os.id => "#{bug.rep_platform}\n#{bug.op_sys}" }
376

  
377
                        unless issue.save then
378
                            puts "FAILURE saving issue"
379
                            puts "issue: #{issue.inspect}"
380
                            puts "bug: #{bug.inspect}"
381
                            puts "users: #{@user_map.inspect}"
382
                            validation_errors = issue.errors.collect {|e| e.to_s }.join(", ")
383
                            puts "validation errors: #{validation_errors}"
384
                            false!
385
                        end
386
                        #puts "Redmine issue number is #{issue.id}"
387
                        @issue_map[bug.bug_id] = issue.id                       
388
                        
389
                        bug.descriptions.each do |description|
390
                            # the first comment is already added to the description field of the bug
391
                            next if description === bug.descriptions.first
392
                            journal = Journal.new(
393
                                :journalized => issue,
394
                                :user_id => map_user(description.who),
395
                                :notes => description.text,
396
                                :created_on => description.bug_when
397
                            )
398
                            journal.save!
399
                        end
400

  
401
                        print '.'
402
                        $stdout.flush
403
                    end
404
                end
405
                
406
                def self.migrate_attachments()
407
                    puts 
408
                    puts "Migrating attachments"
409
                    BugzillaAttachment.find_each() do |attachment|
410
                        next if attachment.attach_data.nil?
411
                        a = Attachment.new :created_on => attachment.creation_ts
412
                        a.file = attachment
413
                        a.author = User.find(map_user(attachment.submitter_id)) || User.first
414
                        a.container = Issue.find(@issue_map[attachment.bug_id])
415
                        a.save
416

  
417
                        print '.'
418
                        $stdout.flush
419
                    end
420
                end
421

  
422
                def self.migrate_issue_relations()
423
                    puts
424
                    puts "Migrating issue relations"
425
                    BugzillaDependency.find_by_sql("select blocked, dependson from dependencies").each do |dep|
426
                        rel = IssueRelation.new
427
                        rel.issue_from_id = @issue_map[dep.blocked]
428
                        rel.issue_to_id = @issue_map[dep.dependson]
429
                        rel.relation_type = "blocks"
430
                        rel.save
431
                        print '.'
432
                        $stdout.flush
433
                    end
434

  
435
                    BugzillaDuplicate.find_by_sql("select dupe_of, dupe from duplicates").each do |dup|
436
                        rel = IssueRelation.new
437
                        rel.issue_from_id = @issue_map[dup.dupe]
438
                        rel.issue_to_id = @issue_map[dup.dupe_of]
439
                        rel.relation_type = "duplicates"
440
                        rel.save
441
                        print '.'
442
                        $stdout.flush
443
                    end
444
                end
445

  
446
                def self.create_custom_field_bugzilla_id
447
                    @custom_field_bugzilla_id = IssueCustomField.find_by_name(CUSTOM_FIELD_BUGZILLA_ID_NAME)
448
                    return if @custom_field_bugzilla_id
449
                    @custom_field_bugzilla_id = IssueCustomField.new({
450
                        :regexp => "^\\d+$",
451
                        :position => 1,
452
                        :name => CUSTOM_FIELD_BUGZILLA_ID_NAME,
453
                        :is_required => true,
454
                        :min_length => 0,
455
                        :default_value => "",
456
                        :searchable =>true,
457
                        :is_for_all => false,
458
                        :max_length => 0, 
459
                        :is_filter => true, 
460
                        :editable => true, 
461
                        :field_format => "string" 
462
                    })
463
                    @custom_field_bugzilla_id.save!
464

  
465
                    Tracker.all.each do |t|
466
                        t.custom_fields << @custom_field_bugzilla_id
467
                        t.save!
468
                    end
469
                end
470
                
471
                
472
                def self.create_custom_field_platform_os
473
                    @custom_field_platform_os = IssueCustomField.find_by_name(CUSTOM_FIELD_PLATFORM_OS_NAME)
474
                    return if @custom_field_platform_os
475
                    @custom_field_platform_os = IssueCustomField.new({
476
                        :regexp => "",
477
                        :position => 1,
478
                        :name => CUSTOM_FIELD_PLATFORM_OS_NAME,
479
                        :is_required => true,
480
                        :min_length => 0,
481
                        :default_value => "",
482
                        :searchable =>true,
483
                        :is_for_all => false,
484
                        :max_length => 0, 
485
                        :is_filter => true, 
486
                        :editable => true, 
487
                        :field_format => "text"
488
                    })
489
                    @custom_field_platform_os.save!
490

  
491
                    Tracker.all.each do |t|
492
                        t.custom_fields << @custom_field_platform_os
493
                        t.save!
494
                    end
495
                end
496
                
497
                def self.create_custom_fields
498
                    self.create_custom_field_bugzilla_id
499
                    self.create_custom_field_platform_os
500
                end
501

  
502
                puts
503
                puts "WARNING: Your Redmine data could be corrupted during this process."
504
                print "Are you sure you want to continue ? [y/N] "
505
                break unless STDIN.gets.match(/^y$/i)
506
                               
507
                # Default Bugzilla database settings
508
                db_params = {:adapter => 'mysql',
509
                    :database => 'bugs',
510
                    :host => 'localhost',
511
                    :port => 3306,
512
                    :username => 'bugs',
513
                    :encoding => 'utf8'}
514
                    :password => 'bugs',
515

  
516
                puts
517
                puts "Please enter settings for your Bugzilla database"
518
                [:adapter, :host, :port, :database, :username, :password].each do |param|
519
                        print "#{param} [#{db_params[param]}]: "
520
                        value = STDIN.gets.chomp!
521
                        value = value.to_i if param == :port
522
                        db_params[param] = value unless value.blank?
523
                end
524

  
525
                # Make sure bugs can refer bugs in other projects
526
                Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
527

  
528
                # Turn off email notifications
529
                Setting.notified_events = []
530
                
531
                puts
532
                print "Preserve adminsitrator privileges? [y/N] "
533
                @preserve_admin = STDIN.gets.match(/^y$/i) ? true : false
534
        
535
                BugzillaMigrate.establish_connection db_params
536
                BugzillaMigrate.create_custom_fields
537
                BugzillaMigrate.migrate_users
538
                BugzillaMigrate.migrate_products
539
                
540
                puts
541
                puts "Begin migrating issues? [Y/n] "
542
                break if STDIN.gets.match(/^n$/i)
543
                
544
                BugzillaMigrate.migrate_issues
545
                BugzillaMigrate.migrate_attachments
546
                BugzillaMigrate.migrate_issue_relations
547

  
548
                puts
549
                puts "Migration complete"
550
                puts
551
            end
552
        end
553
    end
554
end
(4-4/4)