From 3937caeb3c8fe87763e19c179d418949fb31dc61 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 Nov 2007 12:11:40 +0000 Subject: [PATCH] Trac importer improvements (by Mat Trudel): * better support for wiki internal links (still not perfect, but much improved) * support for unordered lists * support for most of trac's highlighting tags (underline, bold, etc) * import progress dots now flush to stdout on every dot, so the import doesn't look frozen * support for migration of multiple trac instances into a single Redmine install (as separate projects) git-svn-id: http://redmine.rubyforge.org/svn/trunk@931 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_trac.rake | 77 +++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index ba002cb6f..6467a5430 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -24,6 +24,7 @@ namespace :redmine do task :migrate_from_trac => :environment do module TracMigrate + TICKET_MAP = []; DEFAULT_STATUS = IssueStatus.default assigned_status = IssueStatus.find_by_position(2) @@ -181,11 +182,38 @@ namespace :redmine do # Basic wiki syntax conversion def self.convert_wiki_text(text) # Titles - text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "h#{$1.length}. #{$2}\n"} - # Links + text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"} + # External Links text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"} + # Internal Links + text = text.gsub(/[[BR]]/, "\n") # This has to go before the rules below + text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:([^\s\]]+).*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} # Revisions links text = text.gsub(/\[(\d+)\]/, 'r\1') + # Ticket number re-writing + text = text.gsub(/#(\d+)/) do |s| + TICKET_MAP[$1.to_i] ||= $1 + "\##{TICKET_MAP[$1.to_i]}" + end + # Preformatted blocks + text = text.gsub(/\{\{\{/, '
')
+        text = text.gsub(/\}\}\}/, '
') + # Highlighting + text = text.gsub(/'''''([^\s])/, '_*\1') + text = text.gsub(/([^\s])'''''/, '\1*_') + text = text.gsub(/'''/, '*') + text = text.gsub(/''/, '_') + text = text.gsub(/__/, '+') + text = text.gsub(/~~/, '-') + text = text.gsub(/`/, '@') + text = text.gsub(/,,/, '~') + # Lists + text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "} + + text end @@ -193,16 +221,9 @@ namespace :redmine do establish_connection({:adapter => trac_adapter, :database => trac_db_path}) - # Quick database test before clearing Redmine data + # Quick database test TracComponent.count - - puts "Deleting data" - CustomField.destroy_all - Issue.destroy_all - IssueCategory.destroy_all - Version.destroy_all - User.destroy_all "login <> 'admin'" - + migrated_components = 0 migrated_milestones = 0 migrated_tickets = 0 @@ -215,6 +236,7 @@ namespace :redmine do issues_category_map = {} TracComponent.find(:all).each do |component| print '.' + STDOUT.flush c = IssueCategory.new :project => @target_project, :name => encode(component.name[0, limit_for(IssueCategory, 'name')]) next unless c.save @@ -228,9 +250,10 @@ namespace :redmine do version_map = {} TracMilestone.find(:all).each do |milestone| print '.' + STDOUT.flush v = Version.new :project => @target_project, :name => encode(milestone.name[0, limit_for(Version, 'name')]), - :description => encode(milestone.description[0, limit_for(Version, 'description')]), + :description => encode(milestone.description.to_s[0, limit_for(Version, 'description')]), :effective_date => milestone.due next unless v.save version_map[milestone.name] = v @@ -244,9 +267,16 @@ namespace :redmine do custom_field_map = {} TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field| print '.' - f = IssueCustomField.new :name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize, - :field_format => 'string' - next unless f.save + STDOUT.flush + # Redmine custom field name + field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize + # Find if the custom already exists in Redmine + f = IssueCustomField.find_by_name(field_name) + # Or create a new one + f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize, + :field_format => 'string') + + next if f.new_record? f.trackers = Tracker.find(:all) f.projects << @target_project custom_field_map[field.name] = f @@ -254,9 +284,10 @@ namespace :redmine do puts # Trac 'resolution' field as a Redmine custom field - r = IssueCustomField.new :name => 'Resolution', + r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" }) + r = IssueCustomField.new(:name => 'Resolution', :field_format => 'list', - :is_filter => true + :is_filter => true) if r.nil? r.trackers = Tracker.find(:all) r.projects << @target_project r.possible_values = %w(fixed invalid wontfix duplicate worksforme) @@ -264,8 +295,9 @@ namespace :redmine do # Tickets print "Migrating tickets" - TracTicket.find(:all).each do |ticket| + TracTicket.find(:all, :order => 'id ASC').each do |ticket| print '.' + STDOUT.flush i = Issue.new :project => @target_project, :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]), :description => convert_wiki_text(encode(ticket.description)), @@ -276,9 +308,10 @@ namespace :redmine do i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank? i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER - i.id = ticket.id i.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank? + i.id = ticket.id unless Issue.exists?(ticket.id) next unless i.save + TICKET_MAP[ticket.id] = i.id migrated_tickets += 1 # Owner @@ -327,6 +360,7 @@ namespace :redmine do # Custom fields ticket.customs.each do |custom| + next if custom_field_map[custom.name].nil? v = CustomValue.new :custom_field => custom_field_map[custom.name], :value => custom.value v.customized = i @@ -344,6 +378,7 @@ namespace :redmine do if wiki.save TracWikiPage.find(:all, :order => 'name, version').each do |page| print '.' + STDOUT.flush p = wiki.find_or_new_page(page.name) p.content = WikiContent.new(:page => p) if p.new_record? p.content.text = page.text @@ -415,6 +450,8 @@ namespace :redmine do puts "Unable to create a project with identifier '#{identifier}'!" unless project.save # enable issues and wiki for the created project project.enabled_module_names = ['issue_tracking', 'wiki'] + project.trackers << TRACKER_BUG + project.trackers << TRACKER_FEATURE end @target_project = project.new_record? ? nil : project end @@ -436,7 +473,7 @@ namespace :redmine do end puts - puts "WARNING: Your Redmine data will be deleted during this process." + puts "WARNING: Your Redmine install will have a new project added during this process." print "Are you sure you want to continue ? [y/N] " break unless STDIN.gets.match(/^y$/i) puts