Fixed that Trac importer was creating duplicate custom values (#2506).

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2280 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang 2009-01-18 11:54:56 +00:00
parent 08304afe54
commit a4882467cb
1 changed files with 130 additions and 127 deletions

View File

@ -5,12 +5,12 @@
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2 # as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version. # of the License, or (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@ -22,10 +22,10 @@ require 'pp'
namespace :redmine do namespace :redmine do
desc 'Trac migration script' desc 'Trac migration script'
task :migrate_from_trac => :environment do task :migrate_from_trac => :environment do
module TracMigrate module TracMigrate
TICKET_MAP = [] TICKET_MAP = []
DEFAULT_STATUS = IssueStatus.default DEFAULT_STATUS = IssueStatus.default
assigned_status = IssueStatus.find_by_position(2) assigned_status = IssueStatus.find_by_position(2)
resolved_status = IssueStatus.find_by_position(3) resolved_status = IssueStatus.find_by_position(3)
@ -36,7 +36,7 @@ namespace :redmine do
'assigned' => assigned_status, 'assigned' => assigned_status,
'closed' => closed_status 'closed' => closed_status
} }
priorities = Enumeration.get_values('IPRI') priorities = Enumeration.get_values('IPRI')
DEFAULT_PRIORITY = priorities[0] DEFAULT_PRIORITY = priorities[0]
PRIORITY_MAPPING = {'lowest' => priorities[0], PRIORITY_MAPPING = {'lowest' => priorities[0],
@ -51,7 +51,7 @@ namespace :redmine do
'critical' => priorities[3], 'critical' => priorities[3],
'blocker' => priorities[4] 'blocker' => priorities[4]
} }
TRACKER_BUG = Tracker.find_by_position(1) TRACKER_BUG = Tracker.find_by_position(1)
TRACKER_FEATURE = Tracker.find_by_position(2) TRACKER_FEATURE = Tracker.find_by_position(2)
DEFAULT_TRACKER = TRACKER_BUG DEFAULT_TRACKER = TRACKER_BUG
@ -60,7 +60,7 @@ namespace :redmine do
'task' => TRACKER_FEATURE, 'task' => TRACKER_FEATURE,
'patch' =>TRACKER_FEATURE 'patch' =>TRACKER_FEATURE
} }
roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC') roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
manager_role = roles[0] manager_role = roles[0]
developer_role = roles[1] developer_role = roles[1]
@ -68,7 +68,7 @@ namespace :redmine do
ROLE_MAPPING = {'admin' => manager_role, ROLE_MAPPING = {'admin' => manager_role,
'developer' => developer_role 'developer' => developer_role
} }
class ::Time class ::Time
class << self class << self
alias :real_now :now alias :real_now :now
@ -87,10 +87,10 @@ namespace :redmine do
class TracComponent < ActiveRecord::Base class TracComponent < ActiveRecord::Base
set_table_name :component set_table_name :component
end end
class TracMilestone < ActiveRecord::Base class TracMilestone < ActiveRecord::Base
set_table_name :milestone set_table_name :milestone
# If this attribute is set a milestone has a defined target timepoint # If this attribute is set a milestone has a defined target timepoint
def due def due
if read_attribute(:due) && read_attribute(:due) > 0 if read_attribute(:due) && read_attribute(:due) > 0
Time.at(read_attribute(:due)).to_date Time.at(read_attribute(:due)).to_date
@ -112,37 +112,37 @@ namespace :redmine do
has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description) has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
end end
end end
class TracTicketCustom < ActiveRecord::Base class TracTicketCustom < ActiveRecord::Base
set_table_name :ticket_custom set_table_name :ticket_custom
end end
class TracAttachment < ActiveRecord::Base class TracAttachment < ActiveRecord::Base
set_table_name :attachment set_table_name :attachment
set_inheritance_column :none set_inheritance_column :none
def time; Time.at(read_attribute(:time)) end def time; Time.at(read_attribute(:time)) end
def original_filename def original_filename
filename filename
end end
def content_type def content_type
Redmine::MimeType.of(filename) || '' Redmine::MimeType.of(filename) || ''
end end
def exist? def exist?
File.file? trac_fullpath File.file? trac_fullpath
end end
def read def read
File.open("#{trac_fullpath}", 'rb').read File.open("#{trac_fullpath}", 'rb').read
end end
def description def description
read_attribute(:description).to_s.slice(0,255) read_attribute(:description).to_s.slice(0,255)
end end
private private
def trac_fullpath def trac_fullpath
attachment_type = read_attribute(:type) attachment_type = read_attribute(:type)
@ -150,11 +150,11 @@ namespace :redmine do
"#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}" "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
end end
end end
class TracTicket < ActiveRecord::Base class TracTicket < ActiveRecord::Base
set_table_name :ticket set_table_name :ticket
set_inheritance_column :none set_inheritance_column :none
# ticket changes: only migrate status changes and comments # ticket changes: only migrate status changes and comments
has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
has_many :attachments, :class_name => "TracAttachment", has_many :attachments, :class_name => "TracAttachment",
@ -162,29 +162,29 @@ namespace :redmine do
" WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" + " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\'' ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
def ticket_type def ticket_type
read_attribute(:type) read_attribute(:type)
end end
def summary def summary
read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary) read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
end end
def description def description
read_attribute(:description).blank? ? summary : read_attribute(:description) read_attribute(:description).blank? ? summary : read_attribute(:description)
end end
def time; Time.at(read_attribute(:time)) end def time; Time.at(read_attribute(:time)) end
def changetime; Time.at(read_attribute(:changetime)) end def changetime; Time.at(read_attribute(:changetime)) end
end end
class TracTicketChange < ActiveRecord::Base class TracTicketChange < ActiveRecord::Base
set_table_name :ticket_change set_table_name :ticket_change
def time; Time.at(read_attribute(:time)) end def time; Time.at(read_attribute(:time)) end
end end
TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \ TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \ TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \ TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
@ -192,35 +192,35 @@ namespace :redmine do
TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \ TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \ WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
CamelCase TitleIndex) CamelCase TitleIndex)
class TracWikiPage < ActiveRecord::Base class TracWikiPage < ActiveRecord::Base
set_table_name :wiki set_table_name :wiki
set_primary_key :name set_primary_key :name
has_many :attachments, :class_name => "TracAttachment", has_many :attachments, :class_name => "TracAttachment",
:finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" + :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
" WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" + " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\'' ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
def self.columns def self.columns
# Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0) # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
super.select {|column| column.name.to_s != 'readonly'} super.select {|column| column.name.to_s != 'readonly'}
end end
def time; Time.at(read_attribute(:time)) end def time; Time.at(read_attribute(:time)) end
end end
class TracPermission < ActiveRecord::Base class TracPermission < ActiveRecord::Base
set_table_name :permission set_table_name :permission
end end
class TracSessionAttribute < ActiveRecord::Base class TracSessionAttribute < ActiveRecord::Base
set_table_name :session_attribute set_table_name :session_attribute
end end
def self.find_or_create_user(username, project_member = false) def self.find_or_create_user(username, project_member = false)
return User.anonymous if username.blank? return User.anonymous if username.blank?
u = User.find_by_login(username) u = User.find_by_login(username)
if !u if !u
# Create a new user if not found # Create a new user if not found
@ -229,7 +229,7 @@ namespace :redmine do
mail = mail_attr.value mail = mail_attr.value
end end
mail = "#{mail}@foo.bar" unless mail.include?("@") mail = "#{mail}@foo.bar" unless mail.include?("@")
name = username name = username
if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name') if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
name = name_attr.value name = name_attr.value
@ -237,7 +237,7 @@ namespace :redmine do
name =~ (/(.*)(\s+\w+)?/) name =~ (/(.*)(\s+\w+)?/)
fn = $1.strip fn = $1.strip
ln = ($2 || '-').strip ln = ($2 || '-').strip
u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'), u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
:firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'), :firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
:lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-') :lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-')
@ -261,7 +261,7 @@ namespace :redmine do
end end
u u
end end
# Basic wiki syntax conversion # Basic wiki syntax conversion
def self.convert_wiki_text(text) def self.convert_wiki_text(text)
# Titles # Titles
@ -282,7 +282,7 @@ namespace :redmine do
# [milestone:"0.1.0 Mercury"] # [milestone:"0.1.0 Mercury"]
text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"') text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"') text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
# milestone:0.1.0 # milestone:0.1.0
text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1') text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1') text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
# Internal Links # Internal Links
@ -293,11 +293,11 @@ namespace :redmine do
text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"} text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
# Links to pages UsingJustWikiCaps # Links to pages UsingJustWikiCaps
text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]') text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
# Normalize things that were supposed to not be links # Normalize things that were supposed to not be links
# like !NotALink # like !NotALink
text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2') text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
# Revisions links # Revisions links
text = text.gsub(/\[(\d+)\]/, 'r\1') text = text.gsub(/\[(\d+)\]/, 'r\1')
# Ticket number re-writing # Ticket number re-writing
@ -318,7 +318,7 @@ namespace :redmine do
shebang_re = /^\#\!([a-z]+)/ shebang_re = /^\#\!([a-z]+)/
# Regular expression for end of code # Regular expression for end of code
pre_end_re = /\}\}\}/ pre_end_re = /\}\}\}/
# Go through the whole text..extract it line by line # Go through the whole text..extract it line by line
text = text.gsub(/^(.*)$/) do |line| text = text.gsub(/^(.*)$/) do |line|
m_pre = pre_re.match(line) m_pre = pre_re.match(line)
@ -338,7 +338,7 @@ namespace :redmine do
end end
end end
end end
line line
end end
# Highlighting # Highlighting
@ -349,25 +349,25 @@ namespace :redmine do
text = text.gsub(/__/, '+') text = text.gsub(/__/, '+')
text = text.gsub(/~~/, '-') text = text.gsub(/~~/, '-')
text = text.gsub(/`/, '@') text = text.gsub(/`/, '@')
text = text.gsub(/,,/, '~') text = text.gsub(/,,/, '~')
# Lists # Lists
text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "} text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
text text
end end
def self.migrate def self.migrate
establish_connection establish_connection
# Quick database test # Quick database test
TracComponent.count TracComponent.count
migrated_components = 0 migrated_components = 0
migrated_milestones = 0 migrated_milestones = 0
migrated_tickets = 0 migrated_tickets = 0
migrated_custom_values = 0 migrated_custom_values = 0
migrated_ticket_attachments = 0 migrated_ticket_attachments = 0
migrated_wiki_edits = 0 migrated_wiki_edits = 0
migrated_wiki_attachments = 0 migrated_wiki_attachments = 0
#Wiki system initializing... #Wiki system initializing...
@ -375,21 +375,21 @@ namespace :redmine do
@target_project.reload @target_project.reload
wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart') wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
wiki_edit_count = 0 wiki_edit_count = 0
# Components # Components
print "Migrating components" print "Migrating components"
issues_category_map = {} issues_category_map = {}
TracComponent.find(:all).each do |component| TracComponent.find(:all).each do |component|
print '.' print '.'
STDOUT.flush 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
issues_category_map[component.name] = c issues_category_map[component.name] = c
migrated_components += 1 migrated_components += 1
end end
puts puts
# Milestones # Milestones
print "Migrating milestones" print "Migrating milestones"
version_map = {} version_map = {}
@ -415,7 +415,7 @@ namespace :redmine do
migrated_milestones += 1 migrated_milestones += 1
end end
puts puts
# Custom fields # Custom fields
# TODO: read trac.ini instead # TODO: read trac.ini instead
print "Migrating custom fields" print "Migrating custom fields"
@ -430,14 +430,14 @@ namespace :redmine do
# Or create a new one # Or create a new one
f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize, f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
:field_format => 'string') :field_format => 'string')
next if f.new_record? 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
end end
puts puts
# Trac 'resolution' field as a Redmine custom field # Trac 'resolution' field as a Redmine custom field
r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" }) r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
r = IssueCustomField.new(:name => 'Resolution', r = IssueCustomField.new(:name => 'Resolution',
@ -448,45 +448,44 @@ namespace :redmine do
r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
r.save! r.save!
custom_field_map['resolution'] = r custom_field_map['resolution'] = r
# Tickets # Tickets
print "Migrating tickets" print "Migrating tickets"
TracTicket.find(:all, :order => 'id ASC').each do |ticket| TracTicket.find(:all, :order => 'id ASC').each do |ticket|
print '.' print '.'
STDOUT.flush 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)),
:priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY, :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
:created_on => ticket.time :created_on => ticket.time
i.author = find_or_create_user(ticket.reporter) i.author = find_or_create_user(ticket.reporter)
i.category = issues_category_map[ticket.component] unless ticket.component.blank? i.category = issues_category_map[ticket.component] unless ticket.component.blank?
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.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)
i.id = ticket.id unless Issue.exists?(ticket.id) next unless Time.fake(ticket.changetime) { i.save }
next unless Time.fake(ticket.changetime) { i.save } TICKET_MAP[ticket.id] = i.id
TICKET_MAP[ticket.id] = i.id migrated_tickets += 1
migrated_tickets += 1
# Owner
# Owner
unless ticket.owner.blank? unless ticket.owner.blank?
i.assigned_to = find_or_create_user(ticket.owner, true) i.assigned_to = find_or_create_user(ticket.owner, true)
Time.fake(ticket.changetime) { i.save } Time.fake(ticket.changetime) { i.save }
end end
# Comments and status/resolution changes # Comments and status/resolution changes
ticket.changes.group_by(&:time).each do |time, changeset| ticket.changes.group_by(&:time).each do |time, changeset|
status_change = changeset.select {|change| change.field == 'status'}.first status_change = changeset.select {|change| change.field == 'status'}.first
resolution_change = changeset.select {|change| change.field == 'resolution'}.first resolution_change = changeset.select {|change| change.field == 'resolution'}.first
comment_change = changeset.select {|change| change.field == 'comment'}.first comment_change = changeset.select {|change| change.field == 'comment'}.first
n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''), n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
:created_on => time :created_on => time
n.user = find_or_create_user(changeset.first.author) n.user = find_or_create_user(changeset.first.author)
n.journalized = i n.journalized = i
if status_change && if status_change &&
STATUS_MAPPING[status_change.oldvalue] && STATUS_MAPPING[status_change.oldvalue] &&
STATUS_MAPPING[status_change.newvalue] && STATUS_MAPPING[status_change.newvalue] &&
(STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue]) (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
@ -502,35 +501,39 @@ namespace :redmine do
:value => resolution_change.newvalue) :value => resolution_change.newvalue)
end end
n.save unless n.details.empty? && n.notes.blank? n.save unless n.details.empty? && n.notes.blank?
end end
# Attachments # Attachments
ticket.attachments.each do |attachment| ticket.attachments.each do |attachment|
next unless attachment.exist? next unless attachment.exist?
a = Attachment.new :created_on => attachment.time a = Attachment.new :created_on => attachment.time
a.file = attachment a.file = attachment
a.author = find_or_create_user(attachment.author) a.author = find_or_create_user(attachment.author)
a.container = i a.container = i
a.description = attachment.description a.description = attachment.description
migrated_ticket_attachments += 1 if a.save migrated_ticket_attachments += 1 if a.save
end end
# Custom fields # Custom fields
ticket.customs.each do |custom| custom_values = ticket.customs.inject({}) do |h, custom|
next if custom_field_map[custom.name].nil? if custom_field = custom_field_map[custom.name]
v = CustomValue.new :custom_field => custom_field_map[custom.name], h[custom_field.id] = custom.value
:value => custom.value
v.customized = i
next unless v.save
migrated_custom_values += 1 migrated_custom_values += 1
end end
h
end
if custom_field_map['resolution'] && !ticket.resolution.blank?
custom_values[custom_field_map['resolution'].id] = ticket.resolution
end
i.custom_field_values = custom_values
i.save_custom_field_values
end end
# update issue id sequence if needed (postgresql) # update issue id sequence if needed (postgresql)
Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!') Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
puts puts
# Wiki # Wiki
print "Migrating wiki" print "Migrating wiki"
if wiki.save if wiki.save
TracWikiPage.find(:all, :order => 'name, version').each do |page| TracWikiPage.find(:all, :order => 'name, version').each do |page|
@ -545,10 +548,10 @@ namespace :redmine do
p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac' p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
p.content.comments = page.comment p.content.comments = page.comment
Time.fake(page.time) { p.new_record? ? p.save : p.content.save } Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
next if p.content.new_record? next if p.content.new_record?
migrated_wiki_edits += 1 migrated_wiki_edits += 1
# Attachments # Attachments
page.attachments.each do |attachment| page.attachments.each do |attachment|
next unless attachment.exist? next unless attachment.exist?
@ -561,7 +564,7 @@ namespace :redmine do
migrated_wiki_attachments += 1 if a.save migrated_wiki_attachments += 1 if a.save
end end
end end
wiki.reload wiki.reload
wiki.pages.each do |page| wiki.pages.each do |page|
page.content.text = convert_wiki_text(page.content.text) page.content.text = convert_wiki_text(page.content.text)
@ -569,7 +572,7 @@ namespace :redmine do
end end
end end
puts puts
puts puts
puts "Components: #{migrated_components}/#{TracComponent.count}" puts "Components: #{migrated_components}/#{TracComponent.count}"
puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}" puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
@ -579,18 +582,18 @@ namespace :redmine do
puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}" puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
end end
def self.limit_for(klass, attribute) def self.limit_for(klass, attribute)
klass.columns_hash[attribute.to_s].limit klass.columns_hash[attribute.to_s].limit
end end
def self.encoding(charset) def self.encoding(charset)
@ic = Iconv.new('UTF-8', charset) @ic = Iconv.new('UTF-8', charset)
rescue Iconv::InvalidEncoding rescue Iconv::InvalidEncoding
puts "Invalid encoding!" puts "Invalid encoding!"
return false return false
end end
def self.set_trac_directory(path) def self.set_trac_directory(path)
@@trac_directory = path @@trac_directory = path
raise "This directory doesn't exist!" unless File.directory?(path) raise "This directory doesn't exist!" unless File.directory?(path)
@ -615,7 +618,7 @@ namespace :redmine do
puts e puts e
return false return false
end end
def self.set_trac_db_host(host) def self.set_trac_db_host(host)
return nil if host.blank? return nil if host.blank?
@@trac_db_host = host @@trac_db_host = host
@ -625,7 +628,7 @@ namespace :redmine do
return nil if port.to_i == 0 return nil if port.to_i == 0
@@trac_db_port = port.to_i @@trac_db_port = port.to_i
end end
def self.set_trac_db_name(name) def self.set_trac_db_name(name)
return nil if name.blank? return nil if name.blank?
@@trac_db_name = name @@trac_db_name = name
@ -634,22 +637,22 @@ namespace :redmine do
def self.set_trac_db_username(username) def self.set_trac_db_username(username)
@@trac_db_username = username @@trac_db_username = username
end end
def self.set_trac_db_password(password) def self.set_trac_db_password(password)
@@trac_db_password = password @@trac_db_password = password
end end
def self.set_trac_db_schema(schema) def self.set_trac_db_schema(schema)
@@trac_db_schema = schema @@trac_db_schema = schema
end end
mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
def self.trac_db_path; "#{trac_directory}/db/trac.db" end def self.trac_db_path; "#{trac_directory}/db/trac.db" end
def self.trac_attachments_directory; "#{trac_directory}/attachments" end def self.trac_attachments_directory; "#{trac_directory}/attachments" end
def self.target_project_identifier(identifier) def self.target_project_identifier(identifier)
project = Project.find_by_identifier(identifier) project = Project.find_by_identifier(identifier)
if !project if !project
# create the target project # create the target project
project = Project.new :name => identifier.humanize, project = Project.new :name => identifier.humanize,
@ -662,16 +665,16 @@ namespace :redmine do
puts puts
puts "This project already exists in your Redmine database." puts "This project already exists in your Redmine database."
print "Are you sure you want to append data to this project ? [Y/n] " print "Are you sure you want to append data to this project ? [Y/n] "
exit if STDIN.gets.match(/^n$/i) exit if STDIN.gets.match(/^n$/i)
end end
project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG) project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE) project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
@target_project = project.new_record? ? nil : project @target_project = project.new_record? ? nil : project
end end
def self.connection_params def self.connection_params
if %w(sqlite sqlite3).include?(trac_adapter) if %w(sqlite sqlite3).include?(trac_adapter)
{:adapter => trac_adapter, {:adapter => trac_adapter,
:database => trac_db_path} :database => trac_db_path}
else else
{:adapter => trac_adapter, {:adapter => trac_adapter,
@ -684,7 +687,7 @@ namespace :redmine do
} }
end end
end end
def self.establish_connection def self.establish_connection
constants.each do |const| constants.each do |const|
klass = const_get(const) klass = const_get(const)
@ -692,7 +695,7 @@ namespace :redmine do
klass.establish_connection connection_params klass.establish_connection connection_params
end end
end end
private private
def self.encode(text) def self.encode(text)
@ic.iconv text @ic.iconv text
@ -700,7 +703,7 @@ namespace :redmine do
text text
end end
end end
puts puts
if Redmine::DefaultData::Loader.no_data? if Redmine::DefaultData::Loader.no_data?
puts "Redmine configuration need to be loaded before importing data." puts "Redmine configuration need to be loaded before importing data."
@ -709,10 +712,10 @@ namespace :redmine do
puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\"" puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
exit exit
end end
puts "WARNING: a new project will be added to Redmine during this process." puts "WARNING: a new project will be added to Redmine 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
def prompt(text, options = {}, &block) def prompt(text, options = {}, &block)
@ -724,9 +727,9 @@ namespace :redmine do
break if yield value break if yield value
end end
end end
DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432} DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip} prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter} prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter) unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
@ -740,7 +743,7 @@ namespace :redmine do
prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding} prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier} prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
puts puts
TracMigrate.migrate TracMigrate.migrate
end end
end end