Project

General

Profile

Patch #989 » migrate_from_bugzilla.rake

Migration task from Bugzilla - Arjen Roodselaar, 2008-04-03 23:00

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

    
115
        def lastname
116
          read_attribute(:realname).blank? ? login_name : read_attribute(:realname).split[1..-1].join(' ')[0..29]
117
        end
118
      end
119
      
120
      class BugzillaGroup < ActiveRecord::Base
121
        set_table_name :groups
122
        
123
        has_and_belongs_to_many :profiles,
124
          :class_name => "BugzillaProfile",
125
          :join_table => :user_group_map,
126
          :foreign_key => :group_id,
127
          :association_foreign_key => :user_id
128
      end
129
      
130
      class BugzillaProduct < ActiveRecord::Base
131
        set_table_name :products
132
        
133
        has_many :components, :class_name => "BugzillaComponent", :foreign_key => :product_id
134
        has_many :versions, :class_name => "BugzillaVersion", :foreign_key => :product_id
135
        has_many :bugs, :class_name => "BugzillaBug", :foreign_key => :product_id
136
      end
137
      
138
      class BugzillaComponent < ActiveRecord::Base
139
        set_table_name :components
140
      end
141
      
142
      class BugzillaVersion < ActiveRecord::Base
143
        set_table_name :versions
144
      end
145
      
146
      class BugzillaBug < ActiveRecord::Base
147
        set_table_name :bugs
148
        set_primary_key :bug_id
149
        
150
        belongs_to :product, :class_name => "BugzillaProduct", :foreign_key => :product_id
151
        has_many :descriptions, :class_name => "BugzillaDescription", :foreign_key => :bug_id
152
      end
153
      
154
      class BugzillaDescription < ActiveRecord::Base
155
        set_table_name :longdescs
156
        
157
        belongs_to :bug, :class_name => "BugzillaBug", :foreign_key => :bug_id
158
        
159
        def eql(desc)
160
          self.bug_when == desc.bug_when
161
        end
162
        
163
        def === desc
164
          self.eql(desc)
165
        end
166
        
167
        def text
168
          if self.thetext.blank?
169
            return nil
170
          else
171
            self.thetext
172
          end 
173
        end
174
      end
175
      
176
      def self.establish_connection(params)
177
        constants.each do |const|
178
          klass = const_get(const)
179
          next unless klass.respond_to? 'establish_connection'
180
          klass.establish_connection params
181
        end
182
      end
183
      
184
      def self.migrate
185
        
186
        # Profiles
187
        puts
188
        print "Migrating profiles"
189
        $stdout.flush
190
        
191
        User.delete_all "login <> 'admin'"
192
        users_map = {}
193
        users_migrated = 0
194
        BugzillaProfile.find(:all).each do |profile|
195
          user = User.new
196
          user.login = profile.login
197
          user.password = "bugzilla"
198
          user.firstname = profile.firstname
199
          user.lastname = profile.lastname
200
          user.mail = profile.email
201
          user.status = User::STATUS_LOCKED if !profile.disabledtext.empty?
202
          user.admin = true if profile.groups.include?(BugzillaGroup.find_by_name("admin"))
203
          
204
          next unless user.save
205
        	users_migrated += 1
206
        	users_map[profile.userid] = user
207
        	print '.'
208
        	$stdout.flush
209
        end
210
        
211
        # Products
212
        puts
213
        print "Migrating products"
214
        $stdout.flush
215
        
216
        Project.destroy_all
217
        projects_map = {}
218
        versions_map = {}
219
        categories_map = {}
220
        BugzillaProduct.find(:all).each do |product|
221
          project = Project.new
222
          project.name = product.name
223
          project.description = product.description
224
          project.identifier = product.name.downcase.gsub(/\s/, '-')
225
          
226
          next unless project.save
227
          projects_map[product.id] = project
228
        	print '.'
229
        	$stdout.flush
230
        	
231
        	# Enable issue tracking
232
        	enabled_module = EnabledModule.new(
233
        	  :project => project,
234
        	  :name => 'issue_tracking'
235
        	)
236
        	enabled_module.save
237
        	
238
          # Components
239
          product.components.each do |component|
240
            category = IssueCategory.new(:name => component.name[0,30])
241
            category.project = project
242
            category.assigned_to = users_map[component.initialowner]
243
            category.save
244
            categories_map[component.id] = category
245
          end
246
          
247
          # Add default user roles
248
        	1.upto(users_map.length) do |i|
249
            membership = Member.new(
250
              :user => users_map[i],
251
              :project => project,
252
              :role => DEFAULT_ROLE
253
            )
254
            membership.save
255
        	end
256
        end
257
        
258
        # Bugs
259
        puts
260
        print "Migrating bugs"
261
        Issue.destroy_all
262
        issues_map = {}
263
        skipped_bugs = []
264
        BugzillaBug.find(:all).each do |bug|
265
          issue = Issue.new(
266
            :project_id => projects_map[bug.product_id],
267
            :tracker => TRACKER_BUG,
268
            :subject => bug.short_desc,
269
            :description => bug.descriptions.first.text || bug.short_desc,
270
            :author => users_map[bug.reporter],
271
            :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
272
            :status => STATUS_MAPPING[bug.bug_status] || DEFAULT_STATUS,
273
            :start_date => bug.creation_ts,
274
            :created_on => bug.creation_ts,
275
            :updated_on => bug.delta_ts
276
          )
277
          
278
          issue.category = categories_map[bug.component_id] unless bug.component_id.blank?                  
279
          issue.assigned_to = users_map[bug.assigned_to] unless bug.assigned_to.blank?
280
          
281
          if issue.save
282
            print '.'
283
        	else
284
        	  issue.id = bug.bug_id
285
        	  skipped_bugs << issue
286
        	  print '!'
287
        	  next
288
        	end
289
        	$stdout.flush
290
        	
291
          # notes
292
          bug.descriptions.each do |description|
293
            # the first comment is already added to the description field of the bug
294
            next if description === bug.descriptions.first
295
            journal = Journal.new(
296
              :journalized => issue,
297
              :user => users_map[description.who],
298
              :notes => description.text,
299
              :created_on => description.bug_when
300
            )
301
            next unless journal.save
302
          end
303
        end
304
        puts
305
        
306
        puts
307
        puts "Profiles:       #{users_migrated}/#{BugzillaProfile.count}"
308
        puts "Products:       #{Project.count}/#{BugzillaProduct.count}"
309
        puts "Components:     #{IssueCategory.count}/#{BugzillaComponent.count}"
310
        puts "Bugs            #{Issue.count}/#{BugzillaBug.count}"
311
        puts
312
        
313
        if !skipped_bugs.empty?
314
          puts "The following bugs failed to import: "
315
          skipped_bugs.each do |issue|
316
            print "#{issue.id}, reason: "
317
            issue.errors.each{|error| print "#{error}"}
318
            puts
319
          end
320
        end
321
      end
322

    
323
      puts
324
      puts "WARNING: Your Redmine data will be deleted during this process."
325
      print "Are you sure you want to continue ? [y/N] "
326
      break unless STDIN.gets.match(/^y$/i)
327
      
328
      # Default Bugzilla database settings
329
      db_params = {:adapter => 'mysql', 
330
                   :database => 'bugs', 
331
                   :host => 'localhost',
332
                   :socket => '/var/run/mysqld/mysqld.sock',
333
                   :username => 'root', 
334
                   :password => '' }
335

    
336
      puts
337
      puts "Please enter settings for your Bugzilla database"  
338
      [:adapter, :host, :database, :username, :password].each do |param|
339
        print "#{param} [#{db_params[param]}]: "
340
        value = STDIN.gets.chomp!
341
        db_params[param] = value unless value.blank?
342
      end
343
      
344
      BugzillaMigrate.establish_connection db_params
345
      BugzillaMigrate.migrate
346
    end
347
    
348
end
349
end
(1-1/4)