diff -Nrup redmine-1.0.4/lib/tasks/migrate_from_bugzilla.rake redmine/lib/tasks/migrate_from_bugzilla.rake --- redmine-1.0.4/lib/tasks/migrate_from_bugzilla.rake 1969-12-31 17:00:00.000000000 -0700 +++ redmine/lib/tasks/migrate_from_bugzilla.rake 2011-01-12 10:24:12.602338400 -0700 @@ -0,0 +1,572 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# Bugzilla migration by Arjen Roodselaar, Lindix bv +# + +desc 'Bugzilla migration script' + +require 'active_record' +require 'iconv' +require 'pp' + +module ActiveRecord + namespace :redmine do + task :migrate_from_bugzilla => :environment do + + module BugzillaMigrate + + STATUS_DEFAULT = IssueStatus.default + STATUS_MAPPING = { + "UNCONFIRMED" => IssueStatus.find_by_name("New"), + "NEW" => IssueStatus.find_by_name("New"), + "ASSIGNED" => IssueStatus.find_by_name("Assigned"), + "REOPENED" => IssueStatus.find_by_name("Reopened"), + "RESOLVED" => IssueStatus.find_by_name("Closed"), + "VERIFIED" => IssueStatus.find_by_name("Closed"), + "CLOSED" => IssueStatus.find_by_name("Closed") + } + + RESOLUTION_MAPPING = { + "INVALID" => IssueStatus.find_by_name("Rejected"), + "WONTFIX" => IssueStatus.find_by_name("Rejected"), + "LATER" => IssueStatus.find_by_name("Postponed"), + "REMIND" => IssueStatus.find_by_name("Postponed"), + "WORKSFORME" => IssueStatus.find_by_name("Rejected") + } + + PRIORITY_DEFAULT = IssuePriority.default + PRIORITY_MAPPING = { + "P1" => IssuePriority.find_by_name("Low"), + "P2" => IssuePriority.find_by_name("Normal"), + "P3" => IssuePriority.find_by_name("High"), + "P4" => IssuePriority.find_by_name("Urgent"), + "P5" => IssuePriority.find_by_name("Immediate"), + + "Low" => IssuePriority.find_by_name("Low"), + "Medium" => IssuePriority.find_by_name("Normal"), + "High" => IssuePriority.find_by_name("High"), + "Critical" => IssuePriority.find_by_name("Urgent"), + } + + TRACKER_DEFAULT = Tracker.find_by_name("Defect") + TRACKER_MAPPING = { + "enhancement" => Tracker.find_by_name("Improvement") + } + + ROLE_DEFAULT = Role.find_by_name("Observer") + + CUSTOM_FIELD_TYPE_MAPPING = { + 1 => 'string', # Freetext + 2 => 'string', # Single-select + 3 => 'text', # Multi-select + 4 => 'text', # Text-area + 5 => 'date', # Date-time + } + + CUSTOM_FIELD_BUGZILLA_ID_NAME = "Bugzilla-Id" + CUSTOM_FIELD_PLATFORM_OS_NAME = "Configuration" + + USER_DEFAULT = User.find_by_login("admin") + + class BugzillaProfile < ActiveRecord::Base + set_table_name :profiles + set_primary_key :userid + + has_and_belongs_to_many :groups, + :class_name => "BugzillaGroup", + :join_table => :user_group_map, + :foreign_key => :user_id, + :association_foreign_key => :group_id + + def login + login_name[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-') + end + + def email + if login_name.match(/^.*@.*$/i) + login_name + else + "#{login_name}@foo.bar" + end + end + + def firstname + s = read_attribute(:realname) + return s.split(/[,]+/).last.strip if s[','] + return s.split(/[ ]+/).first.strip + end + + def lastname + s = read_attribute(:realname) + return s.split(/[,]+/).first.strip if s[','] + return s.split(/[ ]+/).last.strip + end + end + + class BugzillaGroup < ActiveRecord::Base + set_table_name :groups + + has_and_belongs_to_many :profiles, + :class_name => "BugzillaProfile", + :join_table => :user_group_map, + :foreign_key => :group_id, + :association_foreign_key => :user_id + end + + class BugzillaProduct < ActiveRecord::Base + set_table_name :products + + has_many :components, :class_name => "BugzillaComponent", :foreign_key => :product_id + has_many :versions, :class_name => "BugzillaVersion", :foreign_key => :product_id + has_many :bugs, :class_name => "BugzillaBug", :foreign_key => :product_id + end + + class BugzillaComponent < ActiveRecord::Base + set_table_name :components + end + + class BugzillaVersion < ActiveRecord::Base + set_table_name :versions + end + + class BugzillaBug < ActiveRecord::Base + set_table_name :bugs + set_primary_key :bug_id + + belongs_to :product, :class_name => "BugzillaProduct", :foreign_key => :product_id + has_many :descriptions, :class_name => "BugzillaDescription", :foreign_key => :bug_id + has_many :attachments, :class_name => "BugzillaAttachment", :foreign_key => :bug_id + end + + class BugzillaDependency < ActiveRecord::Base + set_table_name :dependencies + end + + class BugzillaDuplicate < ActiveRecord::Base + set_table_name :duplicates + end + + class BugzillaDescription < ActiveRecord::Base + set_table_name :longdescs + set_inheritance_column :bongo + belongs_to :bug, :class_name => "BugzillaBug", :foreign_key => :bug_id + + def eql(desc) + self.bug_when == desc.bug_when + end + + def === desc + self.eql(desc) + end + + def text + if self.thetext.blank? + return nil + else + self.thetext + end + end + end + + class BugzillaAttachment < ActiveRecord::Base + set_table_name :attachments + set_primary_key :attach_id + + has_one :attach_data, :class_name => 'BugzillaAttachData', :foreign_key => :id + + + def size + return 0 if self.attach_data.nil? + return self.attach_data.thedata.size + end + + def original_filename + return self.filename + end + + def content_type + self.mimetype + end + + def read(*args) + if @read_finished + nil + else + @read_finished = true + return nil if self.attach_data.nil? + return self.attach_data.thedata + end + end + end + + class BugzillaAttachData < ActiveRecord::Base + set_table_name :attach_data + end + + def self.establish_connection(params) + constants.each do |const| + klass = const_get(const) + next unless klass.respond_to? 'establish_connection' + klass.establish_connection params + end + end + + def self.map_user(userid) + return @user_map[userid] || USER_DEFAULT.id + end + + def self.migrate_users + puts + puts "Migrating profiles\n" + + # Use email address as the matching mechanism. If profile + # exists in redmine, leave it untouched, otherwise create + # a new user and copy the profile data from bugzilla + + @user_map = {} + BugzillaProfile.all(:order => :userid).each do |profile| + profile_email = profile.email + profile_email.strip! + login = "#{profile.firstname.downcase}.#{profile.lastname.downcase}" + existing_redmine_user = User.find_by_mail(profile_email) || User.find_by_login(login) + if existing_redmine_user + #puts "Existing Redmine User: \n #{existing_redmine_user.inspect}" + @user_map[profile.userid] = existing_redmine_user.id + else + # create the new user and make an entry in the mapping + user = User.new + user.login = login + user.password = "bugzilla" + user.firstname = profile.firstname + user.lastname = profile.lastname + user.mail = profile.email + user.mail.strip! + user.status = User::STATUS_LOCKED if !profile.disabledtext.empty? + user.admin = @preserve_admin if profile.groups.include?(BugzillaGroup.find_by_name("admin")) + unless user.save then + puts "FAILURE saving user" + puts "user: #{user.inspect}" + puts "bugzilla profile: #{profile.inspect}" + validation_errors = user.errors.collect {|e| e.to_s }.join(", ") + puts "validation errors: #{validation_errors}" + false! + end + @user_map[profile.userid] = user.id + end + print '.' + $stdout.flush + end + end + + def self.migrate_products + puts + puts "Migrating products\n" + + @project_map = {} + @category_map = {} + + BugzillaProduct.find_each do |product| + project = Project.new + if (product.name.length > 30) + puts "Product name '#{product.name}' is too long (max 30)\n" + puts "Please choose a new name for the project: " + product.name = STDIN.gets.strip + end + project.name = product.name + project.description = product.description + puts "Enter a project identifier for '#{project.name}': " + project.identifier = STDIN.gets.strip + project.issue_custom_fields << @custom_field_bugzilla_id + project.issue_custom_fields << @custom_field_platform_os + + project.save! + @project_map[product.id] = project.id + + product.versions.each do |version| + Version.create(:name => version.value, :project => project, :status => 'locked') + end + + # Enable issue tracking + enabled_module = EnabledModule.new( + :project => project, + :name => 'issue_tracking' + ) + enabled_module.save! + + # Components + product.components.each do |component| + # assume all components become a new category + category = IssueCategory.new + if (component.name.length > 30) + puts "Component name '#{component.name}' is too long (max 30)\n" + puts "Please choose a new name for the component: " + component.name = STDIN.gets.strip + end + category.name = component.name + category.project = project + # puts "User mapping is: #{@user_map.inspect}" + # puts "component owner = #{component.initialowner} mapped to user #{map_user(component.initialowner)}" + uid = map_user(component.initialowner) + category.assigned_to = User.first(:conditions => {:id => uid }) + category.save! + @category_map[component.id] = category.id + end + + Tracker.find_each do |tracker| + project.trackers << tracker + end + + User.find_each do |user| + membership = Member.new( + :user => user, + :project => project + ) + membership.roles << ROLE_DEFAULT + membership.save + end + + end + + end + + def self.migrate_issues() + puts + puts "Migrating issues" + + # Issue.destroy_all + @issue_map = {} + + BugzillaBug.find(:all, :order => "bug_id ASC").each do |bug| + #puts "Processing bugzilla bug #{bug.bug_id}" + description = bug.descriptions.first.text.to_s + + issue = Issue.new( + :project_id => @project_map[bug.product_id], + :subject => bug.short_desc, + :description => description || bug.short_desc, + :author_id => map_user(bug.reporter), + :tracker => TRACKER_MAPPING[bug.bug_severity] || TRACKER_DEFAULT, + :priority => PRIORITY_MAPPING[bug.priority] || PRIORITY_DEFAULT, + :status => RESOLUTION_MAPPING[bug.resolution] || STATUS_MAPPING[bug.bug_status] || STATUS_DEFAULT, + :start_date => bug.creation_ts, + :created_on => bug.creation_ts, + :updated_on => bug.delta_ts + ) + + issue.category_id = @category_map[bug.component_id] unless bug.component_id.blank? + issue.assigned_to_id = map_user(bug.assigned_to) unless bug.assigned_to.blank? + issue.found_version = Version.first(:conditions => {:project_id => @project_map[bug.product_id], :name => bug.version }) + issue.custom_field_values = { @custom_field_bugzilla_id.id => "#{bug.bug_id}", + @custom_field_platform_os.id => "#{bug.rep_platform}\n#{bug.op_sys}" } + + unless issue.save then + puts "FAILURE saving issue" + puts "issue: #{issue.inspect}" + puts "bug: #{bug.inspect}" + puts "users: #{@user_map.inspect}" + validation_errors = issue.errors.collect {|e| e.to_s }.join(", ") + puts "validation errors: #{validation_errors}" + false! + end + #puts "Redmine issue number is #{issue.id}" + @issue_map[bug.bug_id] = issue.id + + bug.descriptions.each do |description| + # the first comment is already added to the description field of the bug + next if description === bug.descriptions.first + journal = Journal.new( + :journalized => issue, + :user_id => map_user(description.who), + :notes => description.text, + :created_on => description.bug_when + ) + journal.save! + end + + print '.' + $stdout.flush + end + end + + def self.migrate_attachments() + puts + puts "Migrating attachments" + BugzillaAttachment.find_each() do |attachment| + next if attachment.attach_data.nil? + a = Attachment.new :created_on => attachment.creation_ts + a.file = attachment + a.author = User.find(map_user(attachment.submitter_id)) || User.first + a.container = Issue.find(@issue_map[attachment.bug_id]) + a.save + + print '.' + $stdout.flush + end + end + + def self.migrate_issue_relations() + puts + puts "Migrating issue relations" + BugzillaDependency.find_by_sql("select blocked, dependson from dependencies").each do |dep| + rel = IssueRelation.new + rel.issue_from_id = @issue_map[dep.blocked] + rel.issue_to_id = @issue_map[dep.dependson] + rel.relation_type = "blocks" + rel.save + print '.' + $stdout.flush + end + + BugzillaDuplicate.find_by_sql("select dupe_of, dupe from duplicates").each do |dup| + rel = IssueRelation.new + rel.issue_from_id = @issue_map[dup.dupe] + rel.issue_to_id = @issue_map[dup.dupe_of] + rel.relation_type = "duplicates" + rel.save + print '.' + $stdout.flush + end + end + + def self.create_custom_field_bugzilla_id + @custom_field_bugzilla_id = IssueCustomField.find_by_name(CUSTOM_FIELD_BUGZILLA_ID_NAME) + return if @custom_field_bugzilla_id + @custom_field_bugzilla_id = IssueCustomField.new({ + :regexp => "^\\d+$", + :position => 1, + :name => CUSTOM_FIELD_BUGZILLA_ID_NAME, + :is_required => true, + :min_length => 0, + :default_value => "", + :searchable =>true, + :is_for_all => false, + :max_length => 0, + :is_filter => true, + :editable => true, + :field_format => "string" + }) + @custom_field_bugzilla_id.save! + + Tracker.all.each do |t| + t.custom_fields << @custom_field_bugzilla_id + t.save! + end + end + + + def self.create_custom_field_platform_os + @custom_field_platform_os = IssueCustomField.find_by_name(CUSTOM_FIELD_PLATFORM_OS_NAME) + return if @custom_field_platform_os + @custom_field_platform_os = IssueCustomField.new({ + :regexp => "", + :position => 1, + :name => CUSTOM_FIELD_PLATFORM_OS_NAME, + :is_required => true, + :min_length => 0, + :default_value => "", + :searchable =>true, + :is_for_all => false, + :max_length => 0, + :is_filter => true, + :editable => true, + :field_format => "text" + }) + @custom_field_platform_os.save! + + Tracker.all.each do |t| + t.custom_fields << @custom_field_platform_os + t.save! + end + end + + def self.create_custom_fields + self.create_custom_field_bugzilla_id + self.create_custom_field_platform_os + end + + puts + puts "WARNING: Your Redmine data could be corrupted during this process." + print "Are you sure you want to continue ? [y/N] " + break unless STDIN.gets.match(/^y$/i) + + # Default Bugzilla database settings + db_params = {:adapter => 'mysql', + :database => 'bugs', + :host => 'localhost', + :port => 3306, + :username => 'bugs', + :encoding => 'utf8'} + :password => 'bugs', + + puts + puts "Please enter settings for your Bugzilla database" + [:adapter, :host, :port, :database, :username, :password].each do |param| + print "#{param} [#{db_params[param]}]: " + value = STDIN.gets.chomp! + value = value.to_i if param == :port + db_params[param] = value unless value.blank? + end + + # Make sure bugs can refer bugs in other projects + Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations' + + # Turn off email notifications + Setting.notified_events = [] + + puts + print "Preserve adminsitrator privileges? [y/N] " + @preserve_admin = STDIN.gets.match(/^y$/i) ? true : false + + BugzillaMigrate.establish_connection db_params + BugzillaMigrate.create_custom_fields + BugzillaMigrate.migrate_users + BugzillaMigrate.migrate_products + + puts + puts "Begin migrating issues? [Y/n] " + break if STDIN.gets.match(/^n$/i) + + BugzillaMigrate.migrate_issues + BugzillaMigrate.migrate_attachments + BugzillaMigrate.migrate_issue_relations + + puts + puts "Migration complete" + puts + end + end + end +end