| 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
 |