comparison lib/tasks/migrate_from_trac.rake @ 1464:261b3d9a4903 redmine-2.4

Update to Redmine 2.4 branch rev 12663
author Chris Cannam
date Tue, 14 Jan 2014 14:37:42 +0000
parents 433d4f72a19b
children e248c7af89ec
comparison
equal deleted inserted replaced
1296:038ba2d95de8 1464:261b3d9a4903
1 # Redmine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 # 3 #
4 # This program is free software; you can redistribute it and/or 4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License 5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2 6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version. 7 # of the License, or (at your option) any later version.
14 # You should have received a copy of the GNU General Public License 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 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. 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 require 'active_record' 18 require 'active_record'
19 require 'iconv' 19 require 'iconv' if RUBY_VERSION < '1.9'
20 require 'pp' 20 require 'pp'
21 21
22 namespace :redmine do 22 namespace :redmine do
23 desc 'Trac migration script' 23 desc 'Trac migration script'
24 task :migrate_from_trac => :environment do 24 task :migrate_from_trac => :environment do
28 28
29 DEFAULT_STATUS = IssueStatus.default 29 DEFAULT_STATUS = IssueStatus.default
30 assigned_status = IssueStatus.find_by_position(2) 30 assigned_status = IssueStatus.find_by_position(2)
31 resolved_status = IssueStatus.find_by_position(3) 31 resolved_status = IssueStatus.find_by_position(3)
32 feedback_status = IssueStatus.find_by_position(4) 32 feedback_status = IssueStatus.find_by_position(4)
33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true } 33 closed_status = IssueStatus.where(:is_closed => true).first
34 STATUS_MAPPING = {'new' => DEFAULT_STATUS, 34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
35 'reopened' => feedback_status, 35 'reopened' => feedback_status,
36 'assigned' => assigned_status, 36 'assigned' => assigned_status,
37 'closed' => closed_status 37 'closed' => closed_status
38 } 38 }
59 'enhancement' => TRACKER_FEATURE, 59 'enhancement' => TRACKER_FEATURE,
60 'task' => TRACKER_FEATURE, 60 'task' => TRACKER_FEATURE,
61 'patch' =>TRACKER_FEATURE 61 'patch' =>TRACKER_FEATURE
62 } 62 }
63 63
64 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC') 64 roles = Role.where(:builtin => 0).order('position ASC').all
65 manager_role = roles[0] 65 manager_role = roles[0]
66 developer_role = roles[1] 66 developer_role = roles[1]
67 DEFAULT_ROLE = roles.last 67 DEFAULT_ROLE = roles.last
68 ROLE_MAPPING = {'admin' => manager_role, 68 ROLE_MAPPING = {'admin' => manager_role,
69 'developer' => developer_role 69 'developer' => developer_role
151 end 151 end
152 152
153 private 153 private
154 def trac_fullpath 154 def trac_fullpath
155 attachment_type = read_attribute(:type) 155 attachment_type = read_attribute(:type)
156 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) } 156 #replace exotic characters with their hex representation to avoid invalid filenames
157 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) do |x|
158 codepoint = RUBY_VERSION < '1.9' ? x[0] : x.codepoints.to_a[0]
159 sprintf('%%%02x', codepoint)
160 end
157 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}" 161 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
158 end 162 end
159 end 163 end
160 164
161 class TracTicket < ActiveRecord::Base 165 class TracTicket < ActiveRecord::Base
243 247
244 name = username 248 name = username
245 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name') 249 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
246 name = name_attr.value 250 name = name_attr.value
247 end 251 end
248 name =~ (/(.*)(\s+\w+)?/) 252 name =~ (/(\w+)(\s+\w+)?/)
249 fn = $1.strip 253 fn = ($1 || "-").strip
250 ln = ($2 || '-').strip 254 ln = ($2 || '-').strip
251 255
252 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'), 256 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
253 :firstname => fn[0, limit_for(User, 'firstname')], 257 :firstname => fn[0, limit_for(User, 'firstname')],
254 :lastname => ln[0, limit_for(User, 'lastname')] 258 :lastname => ln[0, limit_for(User, 'lastname')]
255 259
256 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-') 260 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
257 u.password = 'trac' 261 u.password = 'trac'
258 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin') 262 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
259 # finally, a default user is used if the new user is not valid 263 # finally, a default user is used if the new user is not valid
260 u = User.find(:first) unless u.save 264 u = User.first unless u.save
261 end 265 end
262 # Make sure he is a member of the project 266 # Make sure user is a member of the project
263 if project_member && !u.member_of?(@target_project) 267 if project_member && !u.member_of?(@target_project)
264 role = DEFAULT_ROLE 268 role = DEFAULT_ROLE
265 if u.admin 269 if u.admin
266 role = ROLE_MAPPING['admin'] 270 role = ROLE_MAPPING['admin']
267 elsif TracPermission.find_by_username_and_action(username, 'developer') 271 elsif TracPermission.find_by_username_and_action(username, 'developer')
388 wiki_edit_count = 0 392 wiki_edit_count = 0
389 393
390 # Components 394 # Components
391 print "Migrating components" 395 print "Migrating components"
392 issues_category_map = {} 396 issues_category_map = {}
393 TracComponent.find(:all).each do |component| 397 TracComponent.all.each do |component|
394 print '.' 398 print '.'
395 STDOUT.flush 399 STDOUT.flush
396 c = IssueCategory.new :project => @target_project, 400 c = IssueCategory.new :project => @target_project,
397 :name => encode(component.name[0, limit_for(IssueCategory, 'name')]) 401 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
398 next unless c.save 402 next unless c.save
402 puts 406 puts
403 407
404 # Milestones 408 # Milestones
405 print "Migrating milestones" 409 print "Migrating milestones"
406 version_map = {} 410 version_map = {}
407 TracMilestone.find(:all).each do |milestone| 411 TracMilestone.all.each do |milestone|
408 print '.' 412 print '.'
409 STDOUT.flush 413 STDOUT.flush
410 # First we try to find the wiki page... 414 # First we try to find the wiki page...
411 p = wiki.find_or_new_page(milestone.name.to_s) 415 p = wiki.find_or_new_page(milestone.name.to_s)
412 p.content = WikiContent.new(:page => p) if p.new_record? 416 p.content = WikiContent.new(:page => p) if p.new_record?
441 # Or create a new one 445 # Or create a new one
442 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize, 446 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
443 :field_format => 'string') 447 :field_format => 'string')
444 448
445 next if f.new_record? 449 next if f.new_record?
446 f.trackers = Tracker.find(:all) 450 f.trackers = Tracker.all
447 f.projects << @target_project 451 f.projects << @target_project
448 custom_field_map[field.name] = f 452 custom_field_map[field.name] = f
449 end 453 end
450 puts 454 puts
451 455
452 # Trac 'resolution' field as a Redmine custom field 456 # Trac 'resolution' field as a Redmine custom field
453 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" }) 457 r = IssueCustomField.where(:name => "Resolution").first
454 r = IssueCustomField.new(:name => 'Resolution', 458 r = IssueCustomField.new(:name => 'Resolution',
455 :field_format => 'list', 459 :field_format => 'list',
456 :is_filter => true) if r.nil? 460 :is_filter => true) if r.nil?
457 r.trackers = Tracker.find(:all) 461 r.trackers = Tracker.all
458 r.projects << @target_project 462 r.projects << @target_project
459 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq 463 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
460 r.save! 464 r.save!
461 custom_field_map['resolution'] = r 465 custom_field_map['resolution'] = r
462 466
547 puts 551 puts
548 552
549 # Wiki 553 # Wiki
550 print "Migrating wiki" 554 print "Migrating wiki"
551 if wiki.save 555 if wiki.save
552 TracWikiPage.find(:all, :order => 'name, version').each do |page| 556 TracWikiPage.order('name, version').all.each do |page|
553 # Do not migrate Trac manual wiki pages 557 # Do not migrate Trac manual wiki pages
554 next if TRAC_WIKI_PAGES.include?(page.name) 558 next if TRAC_WIKI_PAGES.include?(page.name)
555 wiki_edit_count += 1 559 wiki_edit_count += 1
556 print '.' 560 print '.'
557 STDOUT.flush 561 STDOUT.flush
601 def self.limit_for(klass, attribute) 605 def self.limit_for(klass, attribute)
602 klass.columns_hash[attribute.to_s].limit 606 klass.columns_hash[attribute.to_s].limit
603 end 607 end
604 608
605 def self.encoding(charset) 609 def self.encoding(charset)
606 @ic = Iconv.new('UTF-8', charset) 610 @charset = charset
607 rescue Iconv::InvalidEncoding
608 puts "Invalid encoding!"
609 return false
610 end 611 end
611 612
612 def self.set_trac_directory(path) 613 def self.set_trac_directory(path)
613 @@trac_directory = path 614 @@trac_directory = path
614 raise "This directory doesn't exist!" unless File.directory?(path) 615 raise "This directory doesn't exist!" unless File.directory?(path)
711 next unless klass.respond_to? 'establish_connection' 712 next unless klass.respond_to? 'establish_connection'
712 klass.establish_connection connection_params 713 klass.establish_connection connection_params
713 end 714 end
714 end 715 end
715 716
716 private
717 def self.encode(text) 717 def self.encode(text)
718 @ic.iconv text 718 if RUBY_VERSION < '1.9'
719 rescue 719 @ic ||= Iconv.new('UTF-8', @charset)
720 text 720 @ic.iconv text
721 else
722 text.to_s.force_encoding(@charset).encode('UTF-8')
723 end
721 end 724 end
722 end 725 end
723 726
724 puts 727 puts
725 if Redmine::DefaultData::Loader.no_data? 728 if Redmine::DefaultData::Loader.no_data?
761 end 764 end
762 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding} 765 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
763 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier} 766 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
764 puts 767 puts
765 768
766 # Turn off email notifications 769 old_notified_events = Setting.notified_events
767 Setting.notified_events = [] 770 old_password_min_length = Setting.password_min_length
768 771 begin
769 TracMigrate.migrate 772 # Turn off email notifications temporarily
773 Setting.notified_events = []
774 Setting.password_min_length = 4
775 # Run the migration
776 TracMigrate.migrate
777 ensure
778 # Restore previous settings
779 Setting.notified_events = old_notified_events
780 Setting.password_min_length = old_password_min_length
781 end
770 end 782 end
771 end 783 end
772 784