task :migrate_from_trac => :environment do
module TracMigrate
+ TICKET_MAP = [];
DEFAULT_STATUS = IssueStatus.default
assigned_status = IssueStatus.find_by_position(2)
# 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(/\{\{\{/, '<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
end
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
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
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
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
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)
# 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)),
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
# 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
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
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
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