##// END OF EJS Templates
Trac importer: prevent validation failure due to the default value when saving the Resolution custom field if it already exists (#869)....
Jean-Philippe Lang -
r1271:7fb72e671b86
parent child
Show More
@@ -1,626 +1,627
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
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'
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
25
25
26 module TracMigrate
26 module TracMigrate
27 TICKET_MAP = []
27 TICKET_MAP = []
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.find :first, :conditions => { :is_closed => true }
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 }
39
39
40 priorities = Enumeration.get_values('IPRI')
40 priorities = Enumeration.get_values('IPRI')
41 DEFAULT_PRIORITY = priorities[0]
41 DEFAULT_PRIORITY = priorities[0]
42 PRIORITY_MAPPING = {'lowest' => priorities[0],
42 PRIORITY_MAPPING = {'lowest' => priorities[0],
43 'low' => priorities[0],
43 'low' => priorities[0],
44 'normal' => priorities[1],
44 'normal' => priorities[1],
45 'high' => priorities[2],
45 'high' => priorities[2],
46 'highest' => priorities[3],
46 'highest' => priorities[3],
47 # ---
47 # ---
48 'trivial' => priorities[0],
48 'trivial' => priorities[0],
49 'minor' => priorities[1],
49 'minor' => priorities[1],
50 'major' => priorities[2],
50 'major' => priorities[2],
51 'critical' => priorities[3],
51 'critical' => priorities[3],
52 'blocker' => priorities[4]
52 'blocker' => priorities[4]
53 }
53 }
54
54
55 TRACKER_BUG = Tracker.find_by_position(1)
55 TRACKER_BUG = Tracker.find_by_position(1)
56 TRACKER_FEATURE = Tracker.find_by_position(2)
56 TRACKER_FEATURE = Tracker.find_by_position(2)
57 DEFAULT_TRACKER = TRACKER_BUG
57 DEFAULT_TRACKER = TRACKER_BUG
58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
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.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
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
70 }
70 }
71
71
72 class TracComponent < ActiveRecord::Base
72 class TracComponent < ActiveRecord::Base
73 set_table_name :component
73 set_table_name :component
74 end
74 end
75
75
76 class TracMilestone < ActiveRecord::Base
76 class TracMilestone < ActiveRecord::Base
77 set_table_name :milestone
77 set_table_name :milestone
78
78
79 def due
79 def due
80 if read_attribute(:due) > 0
80 if read_attribute(:due) > 0
81 Time.at(read_attribute(:due)).to_date
81 Time.at(read_attribute(:due)).to_date
82 else
82 else
83 nil
83 nil
84 end
84 end
85 end
85 end
86 end
86 end
87
87
88 class TracTicketCustom < ActiveRecord::Base
88 class TracTicketCustom < ActiveRecord::Base
89 set_table_name :ticket_custom
89 set_table_name :ticket_custom
90 end
90 end
91
91
92 class TracAttachment < ActiveRecord::Base
92 class TracAttachment < ActiveRecord::Base
93 set_table_name :attachment
93 set_table_name :attachment
94 set_inheritance_column :none
94 set_inheritance_column :none
95
95
96 def time; Time.at(read_attribute(:time)) end
96 def time; Time.at(read_attribute(:time)) end
97
97
98 def original_filename
98 def original_filename
99 filename
99 filename
100 end
100 end
101
101
102 def content_type
102 def content_type
103 Redmine::MimeType.of(filename) || ''
103 Redmine::MimeType.of(filename) || ''
104 end
104 end
105
105
106 def exist?
106 def exist?
107 File.file? trac_fullpath
107 File.file? trac_fullpath
108 end
108 end
109
109
110 def read
110 def read
111 File.open("#{trac_fullpath}", 'rb').read
111 File.open("#{trac_fullpath}", 'rb').read
112 end
112 end
113
113
114 private
114 private
115 def trac_fullpath
115 def trac_fullpath
116 attachment_type = read_attribute(:type)
116 attachment_type = read_attribute(:type)
117 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
117 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
118 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
118 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
119 end
119 end
120 end
120 end
121
121
122 class TracTicket < ActiveRecord::Base
122 class TracTicket < ActiveRecord::Base
123 set_table_name :ticket
123 set_table_name :ticket
124 set_inheritance_column :none
124 set_inheritance_column :none
125
125
126 # ticket changes: only migrate status changes and comments
126 # ticket changes: only migrate status changes and comments
127 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
127 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
128 has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'ticket'"
128 has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'ticket'"
129 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
129 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
130
130
131 def ticket_type
131 def ticket_type
132 read_attribute(:type)
132 read_attribute(:type)
133 end
133 end
134
134
135 def summary
135 def summary
136 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
136 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
137 end
137 end
138
138
139 def description
139 def description
140 read_attribute(:description).blank? ? summary : read_attribute(:description)
140 read_attribute(:description).blank? ? summary : read_attribute(:description)
141 end
141 end
142
142
143 def time; Time.at(read_attribute(:time)) end
143 def time; Time.at(read_attribute(:time)) end
144 end
144 end
145
145
146 class TracTicketChange < ActiveRecord::Base
146 class TracTicketChange < ActiveRecord::Base
147 set_table_name :ticket_change
147 set_table_name :ticket_change
148
148
149 def time; Time.at(read_attribute(:time)) end
149 def time; Time.at(read_attribute(:time)) end
150 end
150 end
151
151
152 TRAC_WIKI_PAGES = %w(TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
152 TRAC_WIKI_PAGES = %w(TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
153 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
153 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
154 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
154 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
155 TracReports TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
155 TracReports TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
156 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
156 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
157 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
157 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
158 CamelCase TitleIndex)
158 CamelCase TitleIndex)
159
159
160 class TracWikiPage < ActiveRecord::Base
160 class TracWikiPage < ActiveRecord::Base
161 set_table_name :wiki
161 set_table_name :wiki
162 set_primary_key :name
162 set_primary_key :name
163
163
164 has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'wiki'"
164 has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'wiki'"
165
165
166 def self.columns
166 def self.columns
167 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
167 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
168 super.select {|column| column.name.to_s != 'readonly'}
168 super.select {|column| column.name.to_s != 'readonly'}
169 end
169 end
170 end
170 end
171
171
172 class TracPermission < ActiveRecord::Base
172 class TracPermission < ActiveRecord::Base
173 set_table_name :permission
173 set_table_name :permission
174 end
174 end
175
175
176 def self.find_or_create_user(username, project_member = false)
176 def self.find_or_create_user(username, project_member = false)
177 return User.anonymous if username.blank?
177 return User.anonymous if username.blank?
178
178
179 u = User.find_by_login(username)
179 u = User.find_by_login(username)
180 if !u
180 if !u
181 # Create a new user if not found
181 # Create a new user if not found
182 mail = username[0,limit_for(User, 'mail')]
182 mail = username[0,limit_for(User, 'mail')]
183 mail = "#{mail}@foo.bar" unless mail.include?("@")
183 mail = "#{mail}@foo.bar" unless mail.include?("@")
184 u = User.new :firstname => username[0,limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
184 u = User.new :firstname => username[0,limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
185 :lastname => '-',
185 :lastname => '-',
186 :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-')
186 :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-')
187 u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
187 u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
188 u.password = 'trac'
188 u.password = 'trac'
189 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
189 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
190 # finally, a default user is used if the new user is not valid
190 # finally, a default user is used if the new user is not valid
191 u = User.find(:first) unless u.save
191 u = User.find(:first) unless u.save
192 end
192 end
193 # Make sure he is a member of the project
193 # Make sure he is a member of the project
194 if project_member && !u.member_of?(@target_project)
194 if project_member && !u.member_of?(@target_project)
195 role = DEFAULT_ROLE
195 role = DEFAULT_ROLE
196 if u.admin
196 if u.admin
197 role = ROLE_MAPPING['admin']
197 role = ROLE_MAPPING['admin']
198 elsif TracPermission.find_by_username_and_action(username, 'developer')
198 elsif TracPermission.find_by_username_and_action(username, 'developer')
199 role = ROLE_MAPPING['developer']
199 role = ROLE_MAPPING['developer']
200 end
200 end
201 Member.create(:user => u, :project => @target_project, :role => role)
201 Member.create(:user => u, :project => @target_project, :role => role)
202 u.reload
202 u.reload
203 end
203 end
204 u
204 u
205 end
205 end
206
206
207 # Basic wiki syntax conversion
207 # Basic wiki syntax conversion
208 def self.convert_wiki_text(text)
208 def self.convert_wiki_text(text)
209 # Titles
209 # Titles
210 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
210 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
211 # External Links
211 # External Links
212 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
212 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
213 # Internal Links
213 # Internal Links
214 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
214 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
215 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
215 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
216 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
216 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
217 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
217 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
218 text = text.gsub(/\[wiki:([^\s\]]+).*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
218 text = text.gsub(/\[wiki:([^\s\]]+).*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
219
219
220 # Links to pages UsingJustWikiCaps
220 # Links to pages UsingJustWikiCaps
221 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
221 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
222 # Normalize things that were supposed to not be links
222 # Normalize things that were supposed to not be links
223 # like !NotALink
223 # like !NotALink
224 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
224 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
225 # Revisions links
225 # Revisions links
226 text = text.gsub(/\[(\d+)\]/, 'r\1')
226 text = text.gsub(/\[(\d+)\]/, 'r\1')
227 # Ticket number re-writing
227 # Ticket number re-writing
228 text = text.gsub(/#(\d+)/) do |s|
228 text = text.gsub(/#(\d+)/) do |s|
229 if $1.length < 10
229 if $1.length < 10
230 TICKET_MAP[$1.to_i] ||= $1
230 TICKET_MAP[$1.to_i] ||= $1
231 "\##{TICKET_MAP[$1.to_i] || $1}"
231 "\##{TICKET_MAP[$1.to_i] || $1}"
232 else
232 else
233 s
233 s
234 end
234 end
235 end
235 end
236 # Preformatted blocks
236 # Preformatted blocks
237 text = text.gsub(/\{\{\{/, '<pre>')
237 text = text.gsub(/\{\{\{/, '<pre>')
238 text = text.gsub(/\}\}\}/, '</pre>')
238 text = text.gsub(/\}\}\}/, '</pre>')
239 # Highlighting
239 # Highlighting
240 text = text.gsub(/'''''([^\s])/, '_*\1')
240 text = text.gsub(/'''''([^\s])/, '_*\1')
241 text = text.gsub(/([^\s])'''''/, '\1*_')
241 text = text.gsub(/([^\s])'''''/, '\1*_')
242 text = text.gsub(/'''/, '*')
242 text = text.gsub(/'''/, '*')
243 text = text.gsub(/''/, '_')
243 text = text.gsub(/''/, '_')
244 text = text.gsub(/__/, '+')
244 text = text.gsub(/__/, '+')
245 text = text.gsub(/~~/, '-')
245 text = text.gsub(/~~/, '-')
246 text = text.gsub(/`/, '@')
246 text = text.gsub(/`/, '@')
247 text = text.gsub(/,,/, '~')
247 text = text.gsub(/,,/, '~')
248 # Lists
248 # Lists
249 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
249 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
250
250
251 text
251 text
252 end
252 end
253
253
254 def self.migrate
254 def self.migrate
255 establish_connection
255 establish_connection
256
256
257 # Quick database test
257 # Quick database test
258 TracComponent.count
258 TracComponent.count
259
259
260 migrated_components = 0
260 migrated_components = 0
261 migrated_milestones = 0
261 migrated_milestones = 0
262 migrated_tickets = 0
262 migrated_tickets = 0
263 migrated_custom_values = 0
263 migrated_custom_values = 0
264 migrated_ticket_attachments = 0
264 migrated_ticket_attachments = 0
265 migrated_wiki_edits = 0
265 migrated_wiki_edits = 0
266 migrated_wiki_attachments = 0
266 migrated_wiki_attachments = 0
267
267
268 # Components
268 # Components
269 print "Migrating components"
269 print "Migrating components"
270 issues_category_map = {}
270 issues_category_map = {}
271 TracComponent.find(:all).each do |component|
271 TracComponent.find(:all).each do |component|
272 print '.'
272 print '.'
273 STDOUT.flush
273 STDOUT.flush
274 c = IssueCategory.new :project => @target_project,
274 c = IssueCategory.new :project => @target_project,
275 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
275 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
276 next unless c.save
276 next unless c.save
277 issues_category_map[component.name] = c
277 issues_category_map[component.name] = c
278 migrated_components += 1
278 migrated_components += 1
279 end
279 end
280 puts
280 puts
281
281
282 # Milestones
282 # Milestones
283 print "Migrating milestones"
283 print "Migrating milestones"
284 version_map = {}
284 version_map = {}
285 TracMilestone.find(:all).each do |milestone|
285 TracMilestone.find(:all).each do |milestone|
286 print '.'
286 print '.'
287 STDOUT.flush
287 STDOUT.flush
288 v = Version.new :project => @target_project,
288 v = Version.new :project => @target_project,
289 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
289 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
290 :description => encode(milestone.description.to_s[0, limit_for(Version, 'description')]),
290 :description => encode(milestone.description.to_s[0, limit_for(Version, 'description')]),
291 :effective_date => milestone.due
291 :effective_date => milestone.due
292 next unless v.save
292 next unless v.save
293 version_map[milestone.name] = v
293 version_map[milestone.name] = v
294 migrated_milestones += 1
294 migrated_milestones += 1
295 end
295 end
296 puts
296 puts
297
297
298 # Custom fields
298 # Custom fields
299 # TODO: read trac.ini instead
299 # TODO: read trac.ini instead
300 print "Migrating custom fields"
300 print "Migrating custom fields"
301 custom_field_map = {}
301 custom_field_map = {}
302 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
302 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
303 print '.'
303 print '.'
304 STDOUT.flush
304 STDOUT.flush
305 # Redmine custom field name
305 # Redmine custom field name
306 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
306 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
307 # Find if the custom already exists in Redmine
307 # Find if the custom already exists in Redmine
308 f = IssueCustomField.find_by_name(field_name)
308 f = IssueCustomField.find_by_name(field_name)
309 # Or create a new one
309 # Or create a new one
310 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
310 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
311 :field_format => 'string')
311 :field_format => 'string')
312
312
313 next if f.new_record?
313 next if f.new_record?
314 f.trackers = Tracker.find(:all)
314 f.trackers = Tracker.find(:all)
315 f.projects << @target_project
315 f.projects << @target_project
316 custom_field_map[field.name] = f
316 custom_field_map[field.name] = f
317 end
317 end
318 puts
318 puts
319
319
320 # Trac 'resolution' field as a Redmine custom field
320 # Trac 'resolution' field as a Redmine custom field
321 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
321 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
322 r = IssueCustomField.new(:name => 'Resolution',
322 r = IssueCustomField.new(:name => 'Resolution',
323 :field_format => 'list',
323 :field_format => 'list',
324 :is_filter => true) if r.nil?
324 :is_filter => true) if r.nil?
325 r.trackers = Tracker.find(:all)
325 r.trackers = Tracker.find(:all)
326 r.projects << @target_project
326 r.projects << @target_project
327 r.possible_values = %w(fixed invalid wontfix duplicate worksforme)
327 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
328 custom_field_map['resolution'] = r if r.save
328 r.save!
329 custom_field_map['resolution'] = r
329
330
330 # Tickets
331 # Tickets
331 print "Migrating tickets"
332 print "Migrating tickets"
332 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
333 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
333 print '.'
334 print '.'
334 STDOUT.flush
335 STDOUT.flush
335 i = Issue.new :project => @target_project,
336 i = Issue.new :project => @target_project,
336 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
337 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
337 :description => convert_wiki_text(encode(ticket.description)),
338 :description => convert_wiki_text(encode(ticket.description)),
338 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
339 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
339 :created_on => ticket.time
340 :created_on => ticket.time
340 i.author = find_or_create_user(ticket.reporter)
341 i.author = find_or_create_user(ticket.reporter)
341 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
342 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
342 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
343 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
343 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
344 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
344 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
345 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
345 i.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank?
346 i.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank?
346 i.id = ticket.id unless Issue.exists?(ticket.id)
347 i.id = ticket.id unless Issue.exists?(ticket.id)
347 next unless i.save
348 next unless i.save
348 TICKET_MAP[ticket.id] = i.id
349 TICKET_MAP[ticket.id] = i.id
349 migrated_tickets += 1
350 migrated_tickets += 1
350
351
351 # Owner
352 # Owner
352 unless ticket.owner.blank?
353 unless ticket.owner.blank?
353 i.assigned_to = find_or_create_user(ticket.owner, true)
354 i.assigned_to = find_or_create_user(ticket.owner, true)
354 i.save
355 i.save
355 end
356 end
356
357
357 # Comments and status/resolution changes
358 # Comments and status/resolution changes
358 ticket.changes.group_by(&:time).each do |time, changeset|
359 ticket.changes.group_by(&:time).each do |time, changeset|
359 status_change = changeset.select {|change| change.field == 'status'}.first
360 status_change = changeset.select {|change| change.field == 'status'}.first
360 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
361 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
361 comment_change = changeset.select {|change| change.field == 'comment'}.first
362 comment_change = changeset.select {|change| change.field == 'comment'}.first
362
363
363 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
364 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
364 :created_on => time
365 :created_on => time
365 n.user = find_or_create_user(changeset.first.author)
366 n.user = find_or_create_user(changeset.first.author)
366 n.journalized = i
367 n.journalized = i
367 if status_change &&
368 if status_change &&
368 STATUS_MAPPING[status_change.oldvalue] &&
369 STATUS_MAPPING[status_change.oldvalue] &&
369 STATUS_MAPPING[status_change.newvalue] &&
370 STATUS_MAPPING[status_change.newvalue] &&
370 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
371 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
371 n.details << JournalDetail.new(:property => 'attr',
372 n.details << JournalDetail.new(:property => 'attr',
372 :prop_key => 'status_id',
373 :prop_key => 'status_id',
373 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
374 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
374 :value => STATUS_MAPPING[status_change.newvalue].id)
375 :value => STATUS_MAPPING[status_change.newvalue].id)
375 end
376 end
376 if resolution_change
377 if resolution_change
377 n.details << JournalDetail.new(:property => 'cf',
378 n.details << JournalDetail.new(:property => 'cf',
378 :prop_key => custom_field_map['resolution'].id,
379 :prop_key => custom_field_map['resolution'].id,
379 :old_value => resolution_change.oldvalue,
380 :old_value => resolution_change.oldvalue,
380 :value => resolution_change.newvalue)
381 :value => resolution_change.newvalue)
381 end
382 end
382 n.save unless n.details.empty? && n.notes.blank?
383 n.save unless n.details.empty? && n.notes.blank?
383 end
384 end
384
385
385 # Attachments
386 # Attachments
386 ticket.attachments.each do |attachment|
387 ticket.attachments.each do |attachment|
387 next unless attachment.exist?
388 next unless attachment.exist?
388 a = Attachment.new :created_on => attachment.time
389 a = Attachment.new :created_on => attachment.time
389 a.file = attachment
390 a.file = attachment
390 a.author = find_or_create_user(attachment.author)
391 a.author = find_or_create_user(attachment.author)
391 a.container = i
392 a.container = i
392 migrated_ticket_attachments += 1 if a.save
393 migrated_ticket_attachments += 1 if a.save
393 end
394 end
394
395
395 # Custom fields
396 # Custom fields
396 ticket.customs.each do |custom|
397 ticket.customs.each do |custom|
397 next if custom_field_map[custom.name].nil?
398 next if custom_field_map[custom.name].nil?
398 v = CustomValue.new :custom_field => custom_field_map[custom.name],
399 v = CustomValue.new :custom_field => custom_field_map[custom.name],
399 :value => custom.value
400 :value => custom.value
400 v.customized = i
401 v.customized = i
401 next unless v.save
402 next unless v.save
402 migrated_custom_values += 1
403 migrated_custom_values += 1
403 end
404 end
404 end
405 end
405
406
406 # update issue id sequence if needed (postgresql)
407 # update issue id sequence if needed (postgresql)
407 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
408 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
408 puts
409 puts
409
410
410 # Wiki
411 # Wiki
411 print "Migrating wiki"
412 print "Migrating wiki"
412 @target_project.wiki.destroy if @target_project.wiki
413 @target_project.wiki.destroy if @target_project.wiki
413 @target_project.reload
414 @target_project.reload
414 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
415 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
415 wiki_edit_count = 0
416 wiki_edit_count = 0
416 if wiki.save
417 if wiki.save
417 TracWikiPage.find(:all, :order => 'name, version').each do |page|
418 TracWikiPage.find(:all, :order => 'name, version').each do |page|
418 # Do not migrate Trac manual wiki pages
419 # Do not migrate Trac manual wiki pages
419 next if TRAC_WIKI_PAGES.include?(page.name)
420 next if TRAC_WIKI_PAGES.include?(page.name)
420 wiki_edit_count += 1
421 wiki_edit_count += 1
421 print '.'
422 print '.'
422 STDOUT.flush
423 STDOUT.flush
423 p = wiki.find_or_new_page(page.name)
424 p = wiki.find_or_new_page(page.name)
424 p.content = WikiContent.new(:page => p) if p.new_record?
425 p.content = WikiContent.new(:page => p) if p.new_record?
425 p.content.text = page.text
426 p.content.text = page.text
426 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
427 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
427 p.content.comments = page.comment
428 p.content.comments = page.comment
428 p.new_record? ? p.save : p.content.save
429 p.new_record? ? p.save : p.content.save
429
430
430 next if p.content.new_record?
431 next if p.content.new_record?
431 migrated_wiki_edits += 1
432 migrated_wiki_edits += 1
432
433
433 # Attachments
434 # Attachments
434 page.attachments.each do |attachment|
435 page.attachments.each do |attachment|
435 next unless attachment.exist?
436 next unless attachment.exist?
436 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
437 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
437 a = Attachment.new :created_on => attachment.time
438 a = Attachment.new :created_on => attachment.time
438 a.file = attachment
439 a.file = attachment
439 a.author = find_or_create_user(attachment.author)
440 a.author = find_or_create_user(attachment.author)
440 a.container = p
441 a.container = p
441 migrated_wiki_attachments += 1 if a.save
442 migrated_wiki_attachments += 1 if a.save
442 end
443 end
443 end
444 end
444
445
445 wiki.reload
446 wiki.reload
446 wiki.pages.each do |page|
447 wiki.pages.each do |page|
447 page.content.text = convert_wiki_text(page.content.text)
448 page.content.text = convert_wiki_text(page.content.text)
448 page.content.save
449 page.content.save
449 end
450 end
450 end
451 end
451 puts
452 puts
452
453
453 puts
454 puts
454 puts "Components: #{migrated_components}/#{TracComponent.count}"
455 puts "Components: #{migrated_components}/#{TracComponent.count}"
455 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
456 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
456 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
457 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
457 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
458 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
458 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
459 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
459 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
460 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
460 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
461 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
461 end
462 end
462
463
463 def self.limit_for(klass, attribute)
464 def self.limit_for(klass, attribute)
464 klass.columns_hash[attribute.to_s].limit
465 klass.columns_hash[attribute.to_s].limit
465 end
466 end
466
467
467 def self.encoding(charset)
468 def self.encoding(charset)
468 @ic = Iconv.new('UTF-8', charset)
469 @ic = Iconv.new('UTF-8', charset)
469 rescue Iconv::InvalidEncoding
470 rescue Iconv::InvalidEncoding
470 puts "Invalid encoding!"
471 puts "Invalid encoding!"
471 return false
472 return false
472 end
473 end
473
474
474 def self.set_trac_directory(path)
475 def self.set_trac_directory(path)
475 @@trac_directory = path
476 @@trac_directory = path
476 raise "This directory doesn't exist!" unless File.directory?(path)
477 raise "This directory doesn't exist!" unless File.directory?(path)
477 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
478 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
478 @@trac_directory
479 @@trac_directory
479 rescue Exception => e
480 rescue Exception => e
480 puts e
481 puts e
481 return false
482 return false
482 end
483 end
483
484
484 def self.trac_directory
485 def self.trac_directory
485 @@trac_directory
486 @@trac_directory
486 end
487 end
487
488
488 def self.set_trac_adapter(adapter)
489 def self.set_trac_adapter(adapter)
489 return false if adapter.blank?
490 return false if adapter.blank?
490 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
491 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
491 # If adapter is sqlite or sqlite3, make sure that trac.db exists
492 # If adapter is sqlite or sqlite3, make sure that trac.db exists
492 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
493 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
493 @@trac_adapter = adapter
494 @@trac_adapter = adapter
494 rescue Exception => e
495 rescue Exception => e
495 puts e
496 puts e
496 return false
497 return false
497 end
498 end
498
499
499 def self.set_trac_db_host(host)
500 def self.set_trac_db_host(host)
500 return nil if host.blank?
501 return nil if host.blank?
501 @@trac_db_host = host
502 @@trac_db_host = host
502 end
503 end
503
504
504 def self.set_trac_db_port(port)
505 def self.set_trac_db_port(port)
505 return nil if port.to_i == 0
506 return nil if port.to_i == 0
506 @@trac_db_port = port.to_i
507 @@trac_db_port = port.to_i
507 end
508 end
508
509
509 def self.set_trac_db_name(name)
510 def self.set_trac_db_name(name)
510 return nil if name.blank?
511 return nil if name.blank?
511 @@trac_db_name = name
512 @@trac_db_name = name
512 end
513 end
513
514
514 def self.set_trac_db_username(username)
515 def self.set_trac_db_username(username)
515 @@trac_db_username = username
516 @@trac_db_username = username
516 end
517 end
517
518
518 def self.set_trac_db_password(password)
519 def self.set_trac_db_password(password)
519 @@trac_db_password = password
520 @@trac_db_password = password
520 end
521 end
521
522
522 def self.set_trac_db_schema(schema)
523 def self.set_trac_db_schema(schema)
523 @@trac_db_schema = schema
524 @@trac_db_schema = schema
524 end
525 end
525
526
526 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
527 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
527
528
528 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
529 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
529 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
530 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
530
531
531 def self.target_project_identifier(identifier)
532 def self.target_project_identifier(identifier)
532 project = Project.find_by_identifier(identifier)
533 project = Project.find_by_identifier(identifier)
533 if !project
534 if !project
534 # create the target project
535 # create the target project
535 project = Project.new :name => identifier.humanize,
536 project = Project.new :name => identifier.humanize,
536 :description => ''
537 :description => ''
537 project.identifier = identifier
538 project.identifier = identifier
538 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
539 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
539 # enable issues and wiki for the created project
540 # enable issues and wiki for the created project
540 project.enabled_module_names = ['issue_tracking', 'wiki']
541 project.enabled_module_names = ['issue_tracking', 'wiki']
541 else
542 else
542 puts
543 puts
543 puts "This project already exists in your Redmine database."
544 puts "This project already exists in your Redmine database."
544 print "Are you sure you want to append data to this project ? [Y/n] "
545 print "Are you sure you want to append data to this project ? [Y/n] "
545 exit if STDIN.gets.match(/^n$/i)
546 exit if STDIN.gets.match(/^n$/i)
546 end
547 end
547 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
548 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
548 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
549 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
549 @target_project = project.new_record? ? nil : project
550 @target_project = project.new_record? ? nil : project
550 end
551 end
551
552
552 def self.connection_params
553 def self.connection_params
553 if %w(sqlite sqlite3).include?(trac_adapter)
554 if %w(sqlite sqlite3).include?(trac_adapter)
554 {:adapter => trac_adapter,
555 {:adapter => trac_adapter,
555 :database => trac_db_path}
556 :database => trac_db_path}
556 else
557 else
557 {:adapter => trac_adapter,
558 {:adapter => trac_adapter,
558 :database => trac_db_name,
559 :database => trac_db_name,
559 :host => trac_db_host,
560 :host => trac_db_host,
560 :port => trac_db_port,
561 :port => trac_db_port,
561 :username => trac_db_username,
562 :username => trac_db_username,
562 :password => trac_db_password,
563 :password => trac_db_password,
563 :schema_search_path => trac_db_schema
564 :schema_search_path => trac_db_schema
564 }
565 }
565 end
566 end
566 end
567 end
567
568
568 def self.establish_connection
569 def self.establish_connection
569 constants.each do |const|
570 constants.each do |const|
570 klass = const_get(const)
571 klass = const_get(const)
571 next unless klass.respond_to? 'establish_connection'
572 next unless klass.respond_to? 'establish_connection'
572 klass.establish_connection connection_params
573 klass.establish_connection connection_params
573 end
574 end
574 end
575 end
575
576
576 private
577 private
577 def self.encode(text)
578 def self.encode(text)
578 @ic.iconv text
579 @ic.iconv text
579 rescue
580 rescue
580 text
581 text
581 end
582 end
582 end
583 end
583
584
584 puts
585 puts
585 if Redmine::DefaultData::Loader.no_data?
586 if Redmine::DefaultData::Loader.no_data?
586 puts "Redmine configuration need to be loaded before importing data."
587 puts "Redmine configuration need to be loaded before importing data."
587 puts "Please, run this first:"
588 puts "Please, run this first:"
588 puts
589 puts
589 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
590 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
590 exit
591 exit
591 end
592 end
592
593
593 puts "WARNING: a new project will be added to Redmine during this process."
594 puts "WARNING: a new project will be added to Redmine during this process."
594 print "Are you sure you want to continue ? [y/N] "
595 print "Are you sure you want to continue ? [y/N] "
595 break unless STDIN.gets.match(/^y$/i)
596 break unless STDIN.gets.match(/^y$/i)
596 puts
597 puts
597
598
598 def prompt(text, options = {}, &block)
599 def prompt(text, options = {}, &block)
599 default = options[:default] || ''
600 default = options[:default] || ''
600 while true
601 while true
601 print "#{text} [#{default}]: "
602 print "#{text} [#{default}]: "
602 value = STDIN.gets.chomp!
603 value = STDIN.gets.chomp!
603 value = default if value.blank?
604 value = default if value.blank?
604 break if yield value
605 break if yield value
605 end
606 end
606 end
607 end
607
608
608 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
609 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
609
610
610 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
611 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
611 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
612 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
612 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
613 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
613 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
614 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
614 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
615 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
615 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
616 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
616 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
617 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
617 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
618 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
618 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
619 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
619 end
620 end
620 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
621 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
621 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
622 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
622 puts
623 puts
623
624
624 TracMigrate.migrate
625 TracMigrate.migrate
625 end
626 end
626 end
627 end
General Comments 0
You need to be logged in to leave comments. Login now