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
This commit is contained in:
parent
f2a12d9eb5
commit
3937caeb3c
|
@ -24,6 +24,7 @@ namespace :redmine do
|
||||||
task :migrate_from_trac => :environment do
|
task :migrate_from_trac => :environment do
|
||||||
|
|
||||||
module TracMigrate
|
module TracMigrate
|
||||||
|
TICKET_MAP = [];
|
||||||
|
|
||||||
DEFAULT_STATUS = IssueStatus.default
|
DEFAULT_STATUS = IssueStatus.default
|
||||||
assigned_status = IssueStatus.find_by_position(2)
|
assigned_status = IssueStatus.find_by_position(2)
|
||||||
|
@ -181,11 +182,38 @@ namespace :redmine do
|
||||||
# Basic wiki syntax conversion
|
# Basic wiki syntax conversion
|
||||||
def self.convert_wiki_text(text)
|
def self.convert_wiki_text(text)
|
||||||
# Titles
|
# Titles
|
||||||
text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "h#{$1.length}. #{$2}\n"}
|
text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
|
||||||
# Links
|
# External Links
|
||||||
text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
|
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
|
# Revisions links
|
||||||
text = text.gsub(/\[(\d+)\]/, 'r\1')
|
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(/\{\{\{/, '<pre>')
|
||||||
|
text = text.gsub(/\}\}\}/, '</pre>')
|
||||||
|
# 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
|
text
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -193,16 +221,9 @@ namespace :redmine do
|
||||||
establish_connection({:adapter => trac_adapter,
|
establish_connection({:adapter => trac_adapter,
|
||||||
:database => trac_db_path})
|
:database => trac_db_path})
|
||||||
|
|
||||||
# Quick database test before clearing Redmine data
|
# Quick database test
|
||||||
TracComponent.count
|
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_components = 0
|
||||||
migrated_milestones = 0
|
migrated_milestones = 0
|
||||||
migrated_tickets = 0
|
migrated_tickets = 0
|
||||||
|
@ -215,6 +236,7 @@ namespace :redmine do
|
||||||
issues_category_map = {}
|
issues_category_map = {}
|
||||||
TracComponent.find(:all).each do |component|
|
TracComponent.find(:all).each do |component|
|
||||||
print '.'
|
print '.'
|
||||||
|
STDOUT.flush
|
||||||
c = IssueCategory.new :project => @target_project,
|
c = IssueCategory.new :project => @target_project,
|
||||||
:name => encode(component.name[0, limit_for(IssueCategory, 'name')])
|
:name => encode(component.name[0, limit_for(IssueCategory, 'name')])
|
||||||
next unless c.save
|
next unless c.save
|
||||||
|
@ -228,9 +250,10 @@ namespace :redmine do
|
||||||
version_map = {}
|
version_map = {}
|
||||||
TracMilestone.find(:all).each do |milestone|
|
TracMilestone.find(:all).each do |milestone|
|
||||||
print '.'
|
print '.'
|
||||||
|
STDOUT.flush
|
||||||
v = Version.new :project => @target_project,
|
v = Version.new :project => @target_project,
|
||||||
:name => encode(milestone.name[0, limit_for(Version, 'name')]),
|
: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
|
:effective_date => milestone.due
|
||||||
next unless v.save
|
next unless v.save
|
||||||
version_map[milestone.name] = v
|
version_map[milestone.name] = v
|
||||||
|
@ -244,9 +267,16 @@ namespace :redmine do
|
||||||
custom_field_map = {}
|
custom_field_map = {}
|
||||||
TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
|
TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
|
||||||
print '.'
|
print '.'
|
||||||
f = IssueCustomField.new :name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
|
STDOUT.flush
|
||||||
:field_format => 'string'
|
# Redmine custom field name
|
||||||
next unless f.save
|
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.trackers = Tracker.find(:all)
|
||||||
f.projects << @target_project
|
f.projects << @target_project
|
||||||
custom_field_map[field.name] = f
|
custom_field_map[field.name] = f
|
||||||
|
@ -254,9 +284,10 @@ namespace :redmine do
|
||||||
puts
|
puts
|
||||||
|
|
||||||
# Trac 'resolution' field as a Redmine custom field
|
# 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',
|
:field_format => 'list',
|
||||||
:is_filter => true
|
:is_filter => true) if r.nil?
|
||||||
r.trackers = Tracker.find(:all)
|
r.trackers = Tracker.find(:all)
|
||||||
r.projects << @target_project
|
r.projects << @target_project
|
||||||
r.possible_values = %w(fixed invalid wontfix duplicate worksforme)
|
r.possible_values = %w(fixed invalid wontfix duplicate worksforme)
|
||||||
|
@ -264,8 +295,9 @@ namespace :redmine do
|
||||||
|
|
||||||
# Tickets
|
# Tickets
|
||||||
print "Migrating tickets"
|
print "Migrating tickets"
|
||||||
TracTicket.find(:all).each do |ticket|
|
TracTicket.find(:all, :order => 'id ASC').each do |ticket|
|
||||||
print '.'
|
print '.'
|
||||||
|
STDOUT.flush
|
||||||
i = Issue.new :project => @target_project,
|
i = Issue.new :project => @target_project,
|
||||||
:subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
|
:subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
|
||||||
:description => convert_wiki_text(encode(ticket.description)),
|
: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.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
|
||||||
i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
|
i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
|
||||||
i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
|
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.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
|
next unless i.save
|
||||||
|
TICKET_MAP[ticket.id] = i.id
|
||||||
migrated_tickets += 1
|
migrated_tickets += 1
|
||||||
|
|
||||||
# Owner
|
# Owner
|
||||||
|
@ -327,6 +360,7 @@ namespace :redmine do
|
||||||
|
|
||||||
# Custom fields
|
# Custom fields
|
||||||
ticket.customs.each do |custom|
|
ticket.customs.each do |custom|
|
||||||
|
next if custom_field_map[custom.name].nil?
|
||||||
v = CustomValue.new :custom_field => custom_field_map[custom.name],
|
v = CustomValue.new :custom_field => custom_field_map[custom.name],
|
||||||
:value => custom.value
|
:value => custom.value
|
||||||
v.customized = i
|
v.customized = i
|
||||||
|
@ -344,6 +378,7 @@ namespace :redmine do
|
||||||
if wiki.save
|
if wiki.save
|
||||||
TracWikiPage.find(:all, :order => 'name, version').each do |page|
|
TracWikiPage.find(:all, :order => 'name, version').each do |page|
|
||||||
print '.'
|
print '.'
|
||||||
|
STDOUT.flush
|
||||||
p = wiki.find_or_new_page(page.name)
|
p = wiki.find_or_new_page(page.name)
|
||||||
p.content = WikiContent.new(:page => p) if p.new_record?
|
p.content = WikiContent.new(:page => p) if p.new_record?
|
||||||
p.content.text = page.text
|
p.content.text = page.text
|
||||||
|
@ -415,6 +450,8 @@ namespace :redmine do
|
||||||
puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
|
puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
|
||||||
# enable issues and wiki for the created project
|
# enable issues and wiki for the created project
|
||||||
project.enabled_module_names = ['issue_tracking', 'wiki']
|
project.enabled_module_names = ['issue_tracking', 'wiki']
|
||||||
|
project.trackers << TRACKER_BUG
|
||||||
|
project.trackers << TRACKER_FEATURE
|
||||||
end
|
end
|
||||||
@target_project = project.new_record? ? nil : project
|
@target_project = project.new_record? ? nil : project
|
||||||
end
|
end
|
||||||
|
@ -436,7 +473,7 @@ namespace :redmine do
|
||||||
end
|
end
|
||||||
|
|
||||||
puts
|
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] "
|
print "Are you sure you want to continue ? [y/N] "
|
||||||
break unless STDIN.gets.match(/^y$/i)
|
break unless STDIN.gets.match(/^y$/i)
|
||||||
puts
|
puts
|
||||||
|
|
Loading…
Reference in New Issue