migrate_from_bugzilla.rake

Andrew Gniadek, 2008-08-25 22:43

Download (12.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
# Bugzilla migration by Arjen Roodselaar, Lindix bv edited by Oliver Sigge
19
#
20
# Successfully tested with Bugzilla 2.20, Redmine devBuild, Rails 2.1
21
#
22
# Please note that the users in the Bugzilla database must have a forename and surename
23

    
24
desc 'Bugzilla migration script'
25

    
26
require 'active_record'
27
require 'iconv'
28
require 'pp'
29

    
30
namespace :redmine do
31
task :migrate_from_bugzilla => :environment do
32
  
33
    module BugzillaMigrate
34
   
35
      DEFAULT_STATUS = IssueStatus.default
36
      CLOSED_STATUS = IssueStatus.find :first, :conditions => { :is_closed => true }
37
      assigned_status = IssueStatus.find_by_position(2)
38
      resolved_status = IssueStatus.find_by_position(3)
39
      feedback_status = IssueStatus.find_by_position(4)
40
      
41
      STATUS_MAPPING = {
42
        "UNCONFIRMED" => DEFAULT_STATUS,
43
        "NEW" => DEFAULT_STATUS,
44
        "VERIFIED" => DEFAULT_STATUS,
45
        "ASSIGNED" => assigned_status,
46
        "REOPENED" => assigned_status,
47
        "RESOLVED" => resolved_status,
48
        "CLOSED" => CLOSED_STATUS
49
      }
50
      # actually close resolved issues
51
      resolved_status.is_closed = true
52
      resolved_status.save
53
                        
54
      priorities = Enumeration.get_values('IPRI')
55
      PRIORITY_MAPPING = {
56
        "P1" => priorities[1], # low
57
        "P2" => priorities[2], # normal
58
        "P3" => priorities[3], # high
59
        "P4" => priorities[4], # urgent
60
        "P5" => priorities[5]  # immediate
61
      }
62
      DEFAULT_PRIORITY = PRIORITY_MAPPING["P2"]
63
    
64
      TRACKER_BUG = Tracker.find_by_position(1)
65
      TRACKER_FEATURE = Tracker.find_by_position(2)
66
      
67
      reporter_role = Role.find_by_position(5)
68
      developer_role = Role.find_by_position(4)
69
      manager_role = Role.find_by_position(3)
70
      DEFAULT_ROLE = reporter_role
71
      
72
      CUSTOM_FIELD_TYPE_MAPPING = {
73
        0 => 'string', # String
74
        1 => 'int',    # Numeric
75
        2 => 'int',    # Float
76
        3 => 'list',   # Enumeration
77
        4 => 'string', # Email
78
        5 => 'bool',   # Checkbox
79
        6 => 'list',   # List
80
        7 => 'list',   # Multiselection list
81
        8 => 'date',   # Date
82
      }
83
                                   
84
      RELATION_TYPE_MAPPING = {
85
        0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
86
        1 => IssueRelation::TYPE_RELATES,    # related to
87
        2 => IssueRelation::TYPE_RELATES,    # parent of
88
        3 => IssueRelation::TYPE_RELATES,    # child of
89
        4 => IssueRelation::TYPE_DUPLICATES  # has duplicate
90
      }
91
                               
92
      class BugzillaProfile < ActiveRecord::Base
93
        set_table_name :profiles
94
        set_primary_key :userid
95
        
96
        has_and_belongs_to_many :groups,
97
          :class_name => "BugzillaGroup",
98
          :join_table => :user_group_map,
99
          :foreign_key => :user_id,
100
          :association_foreign_key => :group_id
101
        
102
        def login
103
          login_name[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '')
104
        end
105
        
106
        def email
107
          if login_name.match(/^.*@.*$/i)
108
            login_name
109
          else
110
            "#{login_name}@foo.bar"
111
          end
112
        end
113
        
114
        def firstname
115
          read_attribute(:realname).blank? ? login_name : read_attribute(:realname).split.first[0..29]
116
        end
117

    
118
        def lastname
119
          read_attribute(:realname).blank? ? login_name : read_attribute(:realname).split[1..-1].join(' ')[0..29]
120
        end
121
      end
122
      
123
      class BugzillaGroup < ActiveRecord::Base
124
        set_table_name :groups
125
        
126
        has_and_belongs_to_many :profiles,
127
          :class_name => "BugzillaProfile",
128
          :join_table => :user_group_map,
129
          :foreign_key => :group_id,
130
          :association_foreign_key => :user_id
131
      end
132
      
133
      class BugzillaProduct < ActiveRecord::Base
134
        set_table_name :products
135
        
136
        has_many :components, :class_name => "BugzillaComponent", :foreign_key => :product_id
137
        has_many :versions, :class_name => "BugzillaVersion", :foreign_key => :product_id
138
        has_many :bugs, :class_name => "BugzillaBug", :foreign_key => :product_id
139
      end
140
      
141
      class BugzillaComponent < ActiveRecord::Base
142
        set_table_name :components
143
      end
144
      
145
      class BugzillaVersion < ActiveRecord::Base
146
        set_table_name :versions
147
      end
148
      
149
      class BugzillaBug < ActiveRecord::Base
150
        set_table_name :bugs
151
        set_primary_key :bug_id
152
        
153
        belongs_to :product, :class_name => "BugzillaProduct", :foreign_key => :product_id
154
        has_many :descriptions, :class_name => "BugzillaDescription", :foreign_key => :bug_id
155
      end
156
      
157
      class BugzillaDescription < ActiveRecord::Base
158
        set_table_name :longdescs
159
        
160
        belongs_to :bug, :class_name => "BugzillaBug", :foreign_key => :bug_id
161
        
162
        def eql(desc)
163
          self.bug_when == desc.bug_when
164
        end
165
        
166
        def === desc
167
          self.eql(desc)
168
        end
169
        
170
        def self.inheritance_column
171
          "inh_type"
172
        end
173
        
174
        def text
175
          if self.thetext.blank?
176
            return nil
177
          else
178
            self.thetext
179
          end 
180
        end
181
      end
182
      
183
      def self.establish_connection(params)
184
        constants.each do |const|
185
          klass = const_get(const)
186
          next unless klass.respond_to? 'establish_connection'
187
          klass.establish_connection params
188
        end
189
      end
190
      
191
      def self.migrate
192
        
193
        # Profiles
194
        print "Migrating profiles\n"
195
        $stdout.flush
196
        User.delete_all "login <> 'admin'"
197
        users_map = {}
198
        users_migrated = 0
199
        BugzillaProfile.find(:all).each do |profile|
200
          user = User.new
201
          user.login = profile.login
202
          user.password = "bugzilla"
203
          user.firstname = profile.firstname
204
          user.lastname = profile.lastname
205
          user.mail = profile.email
206
          user.status = User::STATUS_LOCKED if !profile.disabledtext.empty?
207
          user.admin = true if profile.groups.include?(BugzillaGroup.find_by_name("admin"))
208
                
209
         next unless user.save
210
                users_migrated += 1
211
                users_map[profile.userid] = user
212
                print '.'
213
                $stdout.flush
214
        end
215
        
216
        
217
        # Products
218
        puts
219
        print "Migrating products"
220
        $stdout.flush
221
        
222
        Project.destroy_all
223
        projects_map = {}
224
        versions_map = {}
225
        categories_map = {}
226
        BugzillaProduct.find(:all).each do |product|
227
          project = Project.new
228
          project.name = product.name
229
          project.description = product.description
230
          project.identifier = product.name.downcase.gsub(/[0-9_\s\.]*/, '')[0..15]
231
          project.trackers[0] = TRACKER_BUG
232
          #puts "Name: #{product.name}; Identifier: #{project.identifier}\n"
233
          next unless project.save
234
          projects_map[product.id] = project
235
                print "."
236
                $stdout.flush
237

    
238
                # Enable issue tracking
239
                enabled_module = EnabledModule.new(
240
                  :project => project,
241
                  :name => 'issue_tracking'
242
                )
243
                enabled_module.save
244
          # Components
245
          product.components.each do |component|
246
            category = IssueCategory.new(:name => component.name[0,30])
247
            category.project = project
248
            category.assigned_to = users_map[component.initialowner]
249
            category.save
250
            categories_map[component.id] = category
251
          end
252
          # Add default user roles
253
                1.upto(users_map.length) do |i|
254
            membership = Member.new(
255
              :user => users_map[i],
256
              :project => project,
257
              :role => DEFAULT_ROLE
258
            )
259
            membership.save
260
                end
261
        end
262
        
263
        # Bugs
264
        puts "\n"
265
        print "Migrating bugs"
266
        Issue.destroy_all
267
        issues_map = {}
268
        skipped_bugs = []
269
        BugzillaBug.find(:all).each do |bug|
270
          issue = Issue.new(
271
            :project => projects_map[bug.product_id],
272
            :tracker => TRACKER_BUG,
273
            :subject => bug.short_desc,
274
            :description => bug.descriptions.first.text || bug.short_desc,
275
            :author => users_map[bug.reporter],
276
            :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
277
            :status => STATUS_MAPPING[bug.bug_status] || DEFAULT_STATUS,
278
            :start_date => bug.creation_ts,
279
            :created_on => bug.creation_ts,
280
            :updated_on => bug.delta_ts
281
          )
282

    
283
          issue.category = categories_map[bug.component_id] unless bug.component_id.blank?                  
284
          issue.assigned_to = users_map[bug.assigned_to] unless bug.assigned_to.blank?          
285
          if issue.save
286
            print '.'
287
                else
288
                  issue.id = bug.bug_id
289
                  skipped_bugs << issue
290
                  print "!"
291
                  next
292
                end
293
                $stdout.flush
294
                
295
          # notes
296
          bug.descriptions.each do |description|
297
            # the first comment is already added to the description field of the bug
298
            next if description === bug.descriptions.first
299
            journal = Journal.new(
300
              :journalized => issue,
301
              :user => users_map[description.who],
302
              :notes => description.text,
303
              :created_on => description.bug_when
304
            )
305
            if (journal.user.nil?)
306
             journal.user = User.find(:first)
307
            end
308
            next unless journal.save
309
          end
310
        end
311
        puts
312
        
313
        puts
314
        puts "Profiles:       #{users_migrated}/#{BugzillaProfile.count}"
315
        puts "Products:       #{Project.count}/#{BugzillaProduct.count}"
316
        puts "Components:     #{IssueCategory.count}/#{BugzillaComponent.count}"
317
        puts "Bugs            #{Issue.count}/#{BugzillaBug.count}"
318
        puts
319
        
320
        if !skipped_bugs.empty?
321
          puts "The following bugs failed to import: "
322
          skipped_bugs.each do |issue|
323
            print "#{issue.id}, reason: "
324
            issue.errors.each{|error| print "#{error}"}
325
            puts
326
          end
327
        end
328
      end
329

    
330
      puts
331
      puts "WARNING: Your Redmine data will be deleted during this process."
332
      print "Are you sure you want to continue ? [y/N] "
333
      break unless STDIN.gets.match(/^y$/i)
334
      
335
      # Default Bugzilla database settings
336
      db_params = {:adapter => 'mysql', 
337
                   :database => 'bugzilla', 
338
                   :host => 'localhost',
339
                   #:port => 3308,
340
                   #:socket => '/home/mysql2/mysql2.sock',
341
                   :username => 'bugzilla', 
342
                   :password => 'bugzilla',
343
                   :encoding => 'utf8' }
344

    
345
      puts
346
      puts "Please enter settings for your Bugzilla database"  
347
      [:adapter, :host, :database, :username, :password].each do |param|
348
        print "#{param} [#{db_params[param]}]: "
349
        value = STDIN.gets.chomp!
350
        db_params[param] = value unless value.blank?
351
      end
352
      
353
      BugzillaMigrate.establish_connection db_params
354
      BugzillaMigrate.migrate
355
    end
356
    
357
end
358
end