##// END OF EJS Templates
Don't use iconv with ruby1.9 (#12787)....
Jean-Philippe Lang -
r11210:59c704dcd2a2
parent child
Show More
@@ -1,511 +1,512
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 desc 'Mantis migration script'
18 desc 'Mantis migration script'
19
19
20 require 'active_record'
20 require 'active_record'
21 require 'iconv'
21 require 'iconv' if RUBY_VERSION < '1.9'
22 require 'pp'
22 require 'pp'
23
23
24 namespace :redmine do
24 namespace :redmine do
25 task :migrate_from_mantis => :environment do
25 task :migrate_from_mantis => :environment do
26
26
27 module MantisMigrate
27 module MantisMigrate
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.where(:is_closed => true).first
33 closed_status = IssueStatus.where(:is_closed => true).first
34 STATUS_MAPPING = {10 => DEFAULT_STATUS, # new
34 STATUS_MAPPING = {10 => DEFAULT_STATUS, # new
35 20 => feedback_status, # feedback
35 20 => feedback_status, # feedback
36 30 => DEFAULT_STATUS, # acknowledged
36 30 => DEFAULT_STATUS, # acknowledged
37 40 => DEFAULT_STATUS, # confirmed
37 40 => DEFAULT_STATUS, # confirmed
38 50 => assigned_status, # assigned
38 50 => assigned_status, # assigned
39 80 => resolved_status, # resolved
39 80 => resolved_status, # resolved
40 90 => closed_status # closed
40 90 => closed_status # closed
41 }
41 }
42
42
43 priorities = IssuePriority.all
43 priorities = IssuePriority.all
44 DEFAULT_PRIORITY = priorities[2]
44 DEFAULT_PRIORITY = priorities[2]
45 PRIORITY_MAPPING = {10 => priorities[1], # none
45 PRIORITY_MAPPING = {10 => priorities[1], # none
46 20 => priorities[1], # low
46 20 => priorities[1], # low
47 30 => priorities[2], # normal
47 30 => priorities[2], # normal
48 40 => priorities[3], # high
48 40 => priorities[3], # high
49 50 => priorities[4], # urgent
49 50 => priorities[4], # urgent
50 60 => priorities[5] # immediate
50 60 => priorities[5] # immediate
51 }
51 }
52
52
53 TRACKER_BUG = Tracker.find_by_position(1)
53 TRACKER_BUG = Tracker.find_by_position(1)
54 TRACKER_FEATURE = Tracker.find_by_position(2)
54 TRACKER_FEATURE = Tracker.find_by_position(2)
55
55
56 roles = Role.where(:builtin => 0).order('position ASC').all
56 roles = Role.where(:builtin => 0).order('position ASC').all
57 manager_role = roles[0]
57 manager_role = roles[0]
58 developer_role = roles[1]
58 developer_role = roles[1]
59 DEFAULT_ROLE = roles.last
59 DEFAULT_ROLE = roles.last
60 ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer
60 ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer
61 25 => DEFAULT_ROLE, # reporter
61 25 => DEFAULT_ROLE, # reporter
62 40 => DEFAULT_ROLE, # updater
62 40 => DEFAULT_ROLE, # updater
63 55 => developer_role, # developer
63 55 => developer_role, # developer
64 70 => manager_role, # manager
64 70 => manager_role, # manager
65 90 => manager_role # administrator
65 90 => manager_role # administrator
66 }
66 }
67
67
68 CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String
68 CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String
69 1 => 'int', # Numeric
69 1 => 'int', # Numeric
70 2 => 'int', # Float
70 2 => 'int', # Float
71 3 => 'list', # Enumeration
71 3 => 'list', # Enumeration
72 4 => 'string', # Email
72 4 => 'string', # Email
73 5 => 'bool', # Checkbox
73 5 => 'bool', # Checkbox
74 6 => 'list', # List
74 6 => 'list', # List
75 7 => 'list', # Multiselection list
75 7 => 'list', # Multiselection list
76 8 => 'date', # Date
76 8 => 'date', # Date
77 }
77 }
78
78
79 RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to
79 RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to
80 2 => IssueRelation::TYPE_RELATES, # parent of
80 2 => IssueRelation::TYPE_RELATES, # parent of
81 3 => IssueRelation::TYPE_RELATES, # child of
81 3 => IssueRelation::TYPE_RELATES, # child of
82 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
82 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
83 4 => IssueRelation::TYPE_DUPLICATES # has duplicate
83 4 => IssueRelation::TYPE_DUPLICATES # has duplicate
84 }
84 }
85
85
86 class MantisUser < ActiveRecord::Base
86 class MantisUser < ActiveRecord::Base
87 self.table_name = :mantis_user_table
87 self.table_name = :mantis_user_table
88
88
89 def firstname
89 def firstname
90 @firstname = realname.blank? ? username : realname.split.first[0..29]
90 @firstname = realname.blank? ? username : realname.split.first[0..29]
91 @firstname
91 @firstname
92 end
92 end
93
93
94 def lastname
94 def lastname
95 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
95 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
96 @lastname = '-' if @lastname.blank?
96 @lastname = '-' if @lastname.blank?
97 @lastname
97 @lastname
98 end
98 end
99
99
100 def email
100 def email
101 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
101 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
102 !User.find_by_mail(read_attribute(:email))
102 !User.find_by_mail(read_attribute(:email))
103 @email = read_attribute(:email)
103 @email = read_attribute(:email)
104 else
104 else
105 @email = "#{username}@foo.bar"
105 @email = "#{username}@foo.bar"
106 end
106 end
107 end
107 end
108
108
109 def username
109 def username
110 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
110 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
111 end
111 end
112 end
112 end
113
113
114 class MantisProject < ActiveRecord::Base
114 class MantisProject < ActiveRecord::Base
115 self.table_name = :mantis_project_table
115 self.table_name = :mantis_project_table
116 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
116 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
117 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
117 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
118 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
118 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
119 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
119 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
120
120
121 def identifier
121 def identifier
122 read_attribute(:name).gsub(/[^a-z0-9\-]+/, '-').slice(0, Project::IDENTIFIER_MAX_LENGTH)
122 read_attribute(:name).gsub(/[^a-z0-9\-]+/, '-').slice(0, Project::IDENTIFIER_MAX_LENGTH)
123 end
123 end
124 end
124 end
125
125
126 class MantisVersion < ActiveRecord::Base
126 class MantisVersion < ActiveRecord::Base
127 self.table_name = :mantis_project_version_table
127 self.table_name = :mantis_project_version_table
128
128
129 def version
129 def version
130 read_attribute(:version)[0..29]
130 read_attribute(:version)[0..29]
131 end
131 end
132
132
133 def description
133 def description
134 read_attribute(:description)[0..254]
134 read_attribute(:description)[0..254]
135 end
135 end
136 end
136 end
137
137
138 class MantisCategory < ActiveRecord::Base
138 class MantisCategory < ActiveRecord::Base
139 self.table_name = :mantis_project_category_table
139 self.table_name = :mantis_project_category_table
140 end
140 end
141
141
142 class MantisProjectUser < ActiveRecord::Base
142 class MantisProjectUser < ActiveRecord::Base
143 self.table_name = :mantis_project_user_list_table
143 self.table_name = :mantis_project_user_list_table
144 end
144 end
145
145
146 class MantisBug < ActiveRecord::Base
146 class MantisBug < ActiveRecord::Base
147 self.table_name = :mantis_bug_table
147 self.table_name = :mantis_bug_table
148 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
148 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
149 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
149 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
150 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
150 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
151 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
151 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
152 end
152 end
153
153
154 class MantisBugText < ActiveRecord::Base
154 class MantisBugText < ActiveRecord::Base
155 self.table_name = :mantis_bug_text_table
155 self.table_name = :mantis_bug_text_table
156
156
157 # Adds Mantis steps_to_reproduce and additional_information fields
157 # Adds Mantis steps_to_reproduce and additional_information fields
158 # to description if any
158 # to description if any
159 def full_description
159 def full_description
160 full_description = description
160 full_description = description
161 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
161 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
162 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
162 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
163 full_description
163 full_description
164 end
164 end
165 end
165 end
166
166
167 class MantisBugNote < ActiveRecord::Base
167 class MantisBugNote < ActiveRecord::Base
168 self.table_name = :mantis_bugnote_table
168 self.table_name = :mantis_bugnote_table
169 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
169 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
170 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
170 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
171 end
171 end
172
172
173 class MantisBugNoteText < ActiveRecord::Base
173 class MantisBugNoteText < ActiveRecord::Base
174 self.table_name = :mantis_bugnote_text_table
174 self.table_name = :mantis_bugnote_text_table
175 end
175 end
176
176
177 class MantisBugFile < ActiveRecord::Base
177 class MantisBugFile < ActiveRecord::Base
178 self.table_name = :mantis_bug_file_table
178 self.table_name = :mantis_bug_file_table
179
179
180 def size
180 def size
181 filesize
181 filesize
182 end
182 end
183
183
184 def original_filename
184 def original_filename
185 MantisMigrate.encode(filename)
185 MantisMigrate.encode(filename)
186 end
186 end
187
187
188 def content_type
188 def content_type
189 file_type
189 file_type
190 end
190 end
191
191
192 def read(*args)
192 def read(*args)
193 if @read_finished
193 if @read_finished
194 nil
194 nil
195 else
195 else
196 @read_finished = true
196 @read_finished = true
197 content
197 content
198 end
198 end
199 end
199 end
200 end
200 end
201
201
202 class MantisBugRelationship < ActiveRecord::Base
202 class MantisBugRelationship < ActiveRecord::Base
203 self.table_name = :mantis_bug_relationship_table
203 self.table_name = :mantis_bug_relationship_table
204 end
204 end
205
205
206 class MantisBugMonitor < ActiveRecord::Base
206 class MantisBugMonitor < ActiveRecord::Base
207 self.table_name = :mantis_bug_monitor_table
207 self.table_name = :mantis_bug_monitor_table
208 end
208 end
209
209
210 class MantisNews < ActiveRecord::Base
210 class MantisNews < ActiveRecord::Base
211 self.table_name = :mantis_news_table
211 self.table_name = :mantis_news_table
212 end
212 end
213
213
214 class MantisCustomField < ActiveRecord::Base
214 class MantisCustomField < ActiveRecord::Base
215 self.table_name = :mantis_custom_field_table
215 self.table_name = :mantis_custom_field_table
216 set_inheritance_column :none
216 set_inheritance_column :none
217 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
217 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
218 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
218 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
219
219
220 def format
220 def format
221 read_attribute :type
221 read_attribute :type
222 end
222 end
223
223
224 def name
224 def name
225 read_attribute(:name)[0..29]
225 read_attribute(:name)[0..29]
226 end
226 end
227 end
227 end
228
228
229 class MantisCustomFieldProject < ActiveRecord::Base
229 class MantisCustomFieldProject < ActiveRecord::Base
230 self.table_name = :mantis_custom_field_project_table
230 self.table_name = :mantis_custom_field_project_table
231 end
231 end
232
232
233 class MantisCustomFieldString < ActiveRecord::Base
233 class MantisCustomFieldString < ActiveRecord::Base
234 self.table_name = :mantis_custom_field_string_table
234 self.table_name = :mantis_custom_field_string_table
235 end
235 end
236
236
237 def self.migrate
237 def self.migrate
238
238
239 # Users
239 # Users
240 print "Migrating users"
240 print "Migrating users"
241 User.delete_all "login <> 'admin'"
241 User.delete_all "login <> 'admin'"
242 users_map = {}
242 users_map = {}
243 users_migrated = 0
243 users_migrated = 0
244 MantisUser.all.each do |user|
244 MantisUser.all.each do |user|
245 u = User.new :firstname => encode(user.firstname),
245 u = User.new :firstname => encode(user.firstname),
246 :lastname => encode(user.lastname),
246 :lastname => encode(user.lastname),
247 :mail => user.email,
247 :mail => user.email,
248 :last_login_on => user.last_visit
248 :last_login_on => user.last_visit
249 u.login = user.username
249 u.login = user.username
250 u.password = 'mantis'
250 u.password = 'mantis'
251 u.status = User::STATUS_LOCKED if user.enabled != 1
251 u.status = User::STATUS_LOCKED if user.enabled != 1
252 u.admin = true if user.access_level == 90
252 u.admin = true if user.access_level == 90
253 next unless u.save!
253 next unless u.save!
254 users_migrated += 1
254 users_migrated += 1
255 users_map[user.id] = u.id
255 users_map[user.id] = u.id
256 print '.'
256 print '.'
257 end
257 end
258 puts
258 puts
259
259
260 # Projects
260 # Projects
261 print "Migrating projects"
261 print "Migrating projects"
262 Project.destroy_all
262 Project.destroy_all
263 projects_map = {}
263 projects_map = {}
264 versions_map = {}
264 versions_map = {}
265 categories_map = {}
265 categories_map = {}
266 MantisProject.all.each do |project|
266 MantisProject.all.each do |project|
267 p = Project.new :name => encode(project.name),
267 p = Project.new :name => encode(project.name),
268 :description => encode(project.description)
268 :description => encode(project.description)
269 p.identifier = project.identifier
269 p.identifier = project.identifier
270 next unless p.save
270 next unless p.save
271 projects_map[project.id] = p.id
271 projects_map[project.id] = p.id
272 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
272 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
273 p.trackers << TRACKER_BUG unless p.trackers.include?(TRACKER_BUG)
273 p.trackers << TRACKER_BUG unless p.trackers.include?(TRACKER_BUG)
274 p.trackers << TRACKER_FEATURE unless p.trackers.include?(TRACKER_FEATURE)
274 p.trackers << TRACKER_FEATURE unless p.trackers.include?(TRACKER_FEATURE)
275 print '.'
275 print '.'
276
276
277 # Project members
277 # Project members
278 project.members.each do |member|
278 project.members.each do |member|
279 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
279 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
280 :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
280 :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
281 m.project = p
281 m.project = p
282 m.save
282 m.save
283 end
283 end
284
284
285 # Project versions
285 # Project versions
286 project.versions.each do |version|
286 project.versions.each do |version|
287 v = Version.new :name => encode(version.version),
287 v = Version.new :name => encode(version.version),
288 :description => encode(version.description),
288 :description => encode(version.description),
289 :effective_date => (version.date_order ? version.date_order.to_date : nil)
289 :effective_date => (version.date_order ? version.date_order.to_date : nil)
290 v.project = p
290 v.project = p
291 v.save
291 v.save
292 versions_map[version.id] = v.id
292 versions_map[version.id] = v.id
293 end
293 end
294
294
295 # Project categories
295 # Project categories
296 project.categories.each do |category|
296 project.categories.each do |category|
297 g = IssueCategory.new :name => category.category[0,30]
297 g = IssueCategory.new :name => category.category[0,30]
298 g.project = p
298 g.project = p
299 g.save
299 g.save
300 categories_map[category.category] = g.id
300 categories_map[category.category] = g.id
301 end
301 end
302 end
302 end
303 puts
303 puts
304
304
305 # Bugs
305 # Bugs
306 print "Migrating bugs"
306 print "Migrating bugs"
307 Issue.destroy_all
307 Issue.destroy_all
308 issues_map = {}
308 issues_map = {}
309 keep_bug_ids = (Issue.count == 0)
309 keep_bug_ids = (Issue.count == 0)
310 MantisBug.find_each(:batch_size => 200) do |bug|
310 MantisBug.find_each(:batch_size => 200) do |bug|
311 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
311 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
312 i = Issue.new :project_id => projects_map[bug.project_id],
312 i = Issue.new :project_id => projects_map[bug.project_id],
313 :subject => encode(bug.summary),
313 :subject => encode(bug.summary),
314 :description => encode(bug.bug_text.full_description),
314 :description => encode(bug.bug_text.full_description),
315 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
315 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
316 :created_on => bug.date_submitted,
316 :created_on => bug.date_submitted,
317 :updated_on => bug.last_updated
317 :updated_on => bug.last_updated
318 i.author = User.find_by_id(users_map[bug.reporter_id])
318 i.author = User.find_by_id(users_map[bug.reporter_id])
319 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
319 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
320 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
320 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
321 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
321 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
322 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
322 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
323 i.id = bug.id if keep_bug_ids
323 i.id = bug.id if keep_bug_ids
324 next unless i.save
324 next unless i.save
325 issues_map[bug.id] = i.id
325 issues_map[bug.id] = i.id
326 print '.'
326 print '.'
327 STDOUT.flush
327 STDOUT.flush
328
328
329 # Assignee
329 # Assignee
330 # Redmine checks that the assignee is a project member
330 # Redmine checks that the assignee is a project member
331 if (bug.handler_id && users_map[bug.handler_id])
331 if (bug.handler_id && users_map[bug.handler_id])
332 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
332 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
333 i.save(:validate => false)
333 i.save(:validate => false)
334 end
334 end
335
335
336 # Bug notes
336 # Bug notes
337 bug.bug_notes.each do |note|
337 bug.bug_notes.each do |note|
338 next unless users_map[note.reporter_id]
338 next unless users_map[note.reporter_id]
339 n = Journal.new :notes => encode(note.bug_note_text.note),
339 n = Journal.new :notes => encode(note.bug_note_text.note),
340 :created_on => note.date_submitted
340 :created_on => note.date_submitted
341 n.user = User.find_by_id(users_map[note.reporter_id])
341 n.user = User.find_by_id(users_map[note.reporter_id])
342 n.journalized = i
342 n.journalized = i
343 n.save
343 n.save
344 end
344 end
345
345
346 # Bug files
346 # Bug files
347 bug.bug_files.each do |file|
347 bug.bug_files.each do |file|
348 a = Attachment.new :created_on => file.date_added
348 a = Attachment.new :created_on => file.date_added
349 a.file = file
349 a.file = file
350 a.author = User.first
350 a.author = User.first
351 a.container = i
351 a.container = i
352 a.save
352 a.save
353 end
353 end
354
354
355 # Bug monitors
355 # Bug monitors
356 bug.bug_monitors.each do |monitor|
356 bug.bug_monitors.each do |monitor|
357 next unless users_map[monitor.user_id]
357 next unless users_map[monitor.user_id]
358 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
358 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
359 end
359 end
360 end
360 end
361
361
362 # update issue id sequence if needed (postgresql)
362 # update issue id sequence if needed (postgresql)
363 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
363 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
364 puts
364 puts
365
365
366 # Bug relationships
366 # Bug relationships
367 print "Migrating bug relations"
367 print "Migrating bug relations"
368 MantisBugRelationship.all.each do |relation|
368 MantisBugRelationship.all.each do |relation|
369 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
369 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
370 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
370 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
371 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
371 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
372 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
372 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
373 pp r unless r.save
373 pp r unless r.save
374 print '.'
374 print '.'
375 STDOUT.flush
375 STDOUT.flush
376 end
376 end
377 puts
377 puts
378
378
379 # News
379 # News
380 print "Migrating news"
380 print "Migrating news"
381 News.destroy_all
381 News.destroy_all
382 MantisNews.where('project_id > 0').all.each do |news|
382 MantisNews.where('project_id > 0').all.each do |news|
383 next unless projects_map[news.project_id]
383 next unless projects_map[news.project_id]
384 n = News.new :project_id => projects_map[news.project_id],
384 n = News.new :project_id => projects_map[news.project_id],
385 :title => encode(news.headline[0..59]),
385 :title => encode(news.headline[0..59]),
386 :description => encode(news.body),
386 :description => encode(news.body),
387 :created_on => news.date_posted
387 :created_on => news.date_posted
388 n.author = User.find_by_id(users_map[news.poster_id])
388 n.author = User.find_by_id(users_map[news.poster_id])
389 n.save
389 n.save
390 print '.'
390 print '.'
391 STDOUT.flush
391 STDOUT.flush
392 end
392 end
393 puts
393 puts
394
394
395 # Custom fields
395 # Custom fields
396 print "Migrating custom fields"
396 print "Migrating custom fields"
397 IssueCustomField.destroy_all
397 IssueCustomField.destroy_all
398 MantisCustomField.all.each do |field|
398 MantisCustomField.all.each do |field|
399 f = IssueCustomField.new :name => field.name[0..29],
399 f = IssueCustomField.new :name => field.name[0..29],
400 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
400 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
401 :min_length => field.length_min,
401 :min_length => field.length_min,
402 :max_length => field.length_max,
402 :max_length => field.length_max,
403 :regexp => field.valid_regexp,
403 :regexp => field.valid_regexp,
404 :possible_values => field.possible_values.split('|'),
404 :possible_values => field.possible_values.split('|'),
405 :is_required => field.require_report?
405 :is_required => field.require_report?
406 next unless f.save
406 next unless f.save
407 print '.'
407 print '.'
408 STDOUT.flush
408 STDOUT.flush
409 # Trackers association
409 # Trackers association
410 f.trackers = Tracker.all
410 f.trackers = Tracker.all
411
411
412 # Projects association
412 # Projects association
413 field.projects.each do |project|
413 field.projects.each do |project|
414 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
414 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
415 end
415 end
416
416
417 # Values
417 # Values
418 field.values.each do |value|
418 field.values.each do |value|
419 v = CustomValue.new :custom_field_id => f.id,
419 v = CustomValue.new :custom_field_id => f.id,
420 :value => value.value
420 :value => value.value
421 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
421 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
422 v.save
422 v.save
423 end unless f.new_record?
423 end unless f.new_record?
424 end
424 end
425 puts
425 puts
426
426
427 puts
427 puts
428 puts "Users: #{users_migrated}/#{MantisUser.count}"
428 puts "Users: #{users_migrated}/#{MantisUser.count}"
429 puts "Projects: #{Project.count}/#{MantisProject.count}"
429 puts "Projects: #{Project.count}/#{MantisProject.count}"
430 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
430 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
431 puts "Versions: #{Version.count}/#{MantisVersion.count}"
431 puts "Versions: #{Version.count}/#{MantisVersion.count}"
432 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
432 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
433 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
433 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
434 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
434 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
435 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
435 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
436 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
436 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
437 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
437 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
438 puts "News: #{News.count}/#{MantisNews.count}"
438 puts "News: #{News.count}/#{MantisNews.count}"
439 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
439 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
440 end
440 end
441
441
442 def self.encoding(charset)
442 def self.encoding(charset)
443 @ic = Iconv.new('UTF-8', charset)
443 @charset = charset
444 rescue Iconv::InvalidEncoding
445 return false
446 end
444 end
447
445
448 def self.establish_connection(params)
446 def self.establish_connection(params)
449 constants.each do |const|
447 constants.each do |const|
450 klass = const_get(const)
448 klass = const_get(const)
451 next unless klass.respond_to? 'establish_connection'
449 next unless klass.respond_to? 'establish_connection'
452 klass.establish_connection params
450 klass.establish_connection params
453 end
451 end
454 end
452 end
455
453
456 def self.encode(text)
454 def self.encode(text)
455 if RUBY_VERSION < '1.9'
456 @ic ||= Iconv.new('UTF-8', @charset)
457 @ic.iconv text
457 @ic.iconv text
458 rescue
458 else
459 text
459 text.to_s.force_encoding(@charset).encode('UTF-8')
460 end
460 end
461 end
461 end
462 end
462
463
463 puts
464 puts
464 if Redmine::DefaultData::Loader.no_data?
465 if Redmine::DefaultData::Loader.no_data?
465 puts "Redmine configuration need to be loaded before importing data."
466 puts "Redmine configuration need to be loaded before importing data."
466 puts "Please, run this first:"
467 puts "Please, run this first:"
467 puts
468 puts
468 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
469 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
469 exit
470 exit
470 end
471 end
471
472
472 puts "WARNING: Your Redmine data will be deleted during this process."
473 puts "WARNING: Your Redmine data will be deleted during this process."
473 print "Are you sure you want to continue ? [y/N] "
474 print "Are you sure you want to continue ? [y/N] "
474 STDOUT.flush
475 STDOUT.flush
475 break unless STDIN.gets.match(/^y$/i)
476 break unless STDIN.gets.match(/^y$/i)
476
477
477 # Default Mantis database settings
478 # Default Mantis database settings
478 db_params = {:adapter => 'mysql2',
479 db_params = {:adapter => 'mysql2',
479 :database => 'bugtracker',
480 :database => 'bugtracker',
480 :host => 'localhost',
481 :host => 'localhost',
481 :username => 'root',
482 :username => 'root',
482 :password => '' }
483 :password => '' }
483
484
484 puts
485 puts
485 puts "Please enter settings for your Mantis database"
486 puts "Please enter settings for your Mantis database"
486 [:adapter, :host, :database, :username, :password].each do |param|
487 [:adapter, :host, :database, :username, :password].each do |param|
487 print "#{param} [#{db_params[param]}]: "
488 print "#{param} [#{db_params[param]}]: "
488 value = STDIN.gets.chomp!
489 value = STDIN.gets.chomp!
489 db_params[param] = value unless value.blank?
490 db_params[param] = value unless value.blank?
490 end
491 end
491
492
492 while true
493 while true
493 print "encoding [UTF-8]: "
494 print "encoding [UTF-8]: "
494 STDOUT.flush
495 STDOUT.flush
495 encoding = STDIN.gets.chomp!
496 encoding = STDIN.gets.chomp!
496 encoding = 'UTF-8' if encoding.blank?
497 encoding = 'UTF-8' if encoding.blank?
497 break if MantisMigrate.encoding encoding
498 break if MantisMigrate.encoding encoding
498 puts "Invalid encoding!"
499 puts "Invalid encoding!"
499 end
500 end
500 puts
501 puts
501
502
502 # Make sure bugs can refer bugs in other projects
503 # Make sure bugs can refer bugs in other projects
503 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
504 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
504
505
505 # Turn off email notifications
506 # Turn off email notifications
506 Setting.notified_events = []
507 Setting.notified_events = []
507
508
508 MantisMigrate.establish_connection db_params
509 MantisMigrate.establish_connection db_params
509 MantisMigrate.migrate
510 MantisMigrate.migrate
510 end
511 end
511 end
512 end
@@ -1,772 +1,771
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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' if RUBY_VERSION < '1.9'
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.where(:is_closed => true).first
33 closed_status = IssueStatus.where(:is_closed => true).first
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 = IssuePriority.all
40 priorities = IssuePriority.all
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.where(:builtin => 0).order('position ASC').all
64 roles = Role.where(:builtin => 0).order('position ASC').all
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 ::Time
72 class ::Time
73 class << self
73 class << self
74 alias :real_now :now
74 alias :real_now :now
75 def now
75 def now
76 real_now - @fake_diff.to_i
76 real_now - @fake_diff.to_i
77 end
77 end
78 def fake(time)
78 def fake(time)
79 @fake_diff = real_now - time
79 @fake_diff = real_now - time
80 res = yield
80 res = yield
81 @fake_diff = 0
81 @fake_diff = 0
82 res
82 res
83 end
83 end
84 end
84 end
85 end
85 end
86
86
87 class TracComponent < ActiveRecord::Base
87 class TracComponent < ActiveRecord::Base
88 self.table_name = :component
88 self.table_name = :component
89 end
89 end
90
90
91 class TracMilestone < ActiveRecord::Base
91 class TracMilestone < ActiveRecord::Base
92 self.table_name = :milestone
92 self.table_name = :milestone
93 # If this attribute is set a milestone has a defined target timepoint
93 # If this attribute is set a milestone has a defined target timepoint
94 def due
94 def due
95 if read_attribute(:due) && read_attribute(:due) > 0
95 if read_attribute(:due) && read_attribute(:due) > 0
96 Time.at(read_attribute(:due)).to_date
96 Time.at(read_attribute(:due)).to_date
97 else
97 else
98 nil
98 nil
99 end
99 end
100 end
100 end
101 # This is the real timepoint at which the milestone has finished.
101 # This is the real timepoint at which the milestone has finished.
102 def completed
102 def completed
103 if read_attribute(:completed) && read_attribute(:completed) > 0
103 if read_attribute(:completed) && read_attribute(:completed) > 0
104 Time.at(read_attribute(:completed)).to_date
104 Time.at(read_attribute(:completed)).to_date
105 else
105 else
106 nil
106 nil
107 end
107 end
108 end
108 end
109
109
110 def description
110 def description
111 # Attribute is named descr in Trac v0.8.x
111 # Attribute is named descr in Trac v0.8.x
112 has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
112 has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
113 end
113 end
114 end
114 end
115
115
116 class TracTicketCustom < ActiveRecord::Base
116 class TracTicketCustom < ActiveRecord::Base
117 self.table_name = :ticket_custom
117 self.table_name = :ticket_custom
118 end
118 end
119
119
120 class TracAttachment < ActiveRecord::Base
120 class TracAttachment < ActiveRecord::Base
121 self.table_name = :attachment
121 self.table_name = :attachment
122 set_inheritance_column :none
122 set_inheritance_column :none
123
123
124 def time; Time.at(read_attribute(:time)) end
124 def time; Time.at(read_attribute(:time)) end
125
125
126 def original_filename
126 def original_filename
127 filename
127 filename
128 end
128 end
129
129
130 def content_type
130 def content_type
131 ''
131 ''
132 end
132 end
133
133
134 def exist?
134 def exist?
135 File.file? trac_fullpath
135 File.file? trac_fullpath
136 end
136 end
137
137
138 def open
138 def open
139 File.open("#{trac_fullpath}", 'rb') {|f|
139 File.open("#{trac_fullpath}", 'rb') {|f|
140 @file = f
140 @file = f
141 yield self
141 yield self
142 }
142 }
143 end
143 end
144
144
145 def read(*args)
145 def read(*args)
146 @file.read(*args)
146 @file.read(*args)
147 end
147 end
148
148
149 def description
149 def description
150 read_attribute(:description).to_s.slice(0,255)
150 read_attribute(:description).to_s.slice(0,255)
151 end
151 end
152
152
153 private
153 private
154 def trac_fullpath
154 def trac_fullpath
155 attachment_type = read_attribute(:type)
155 attachment_type = read_attribute(:type)
156 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
156 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
157 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
157 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
158 end
158 end
159 end
159 end
160
160
161 class TracTicket < ActiveRecord::Base
161 class TracTicket < ActiveRecord::Base
162 self.table_name = :ticket
162 self.table_name = :ticket
163 set_inheritance_column :none
163 set_inheritance_column :none
164
164
165 # ticket changes: only migrate status changes and comments
165 # ticket changes: only migrate status changes and comments
166 has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
166 has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
167 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
167 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
168
168
169 def attachments
169 def attachments
170 TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
170 TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
171 end
171 end
172
172
173 def ticket_type
173 def ticket_type
174 read_attribute(:type)
174 read_attribute(:type)
175 end
175 end
176
176
177 def summary
177 def summary
178 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
178 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
179 end
179 end
180
180
181 def description
181 def description
182 read_attribute(:description).blank? ? summary : read_attribute(:description)
182 read_attribute(:description).blank? ? summary : read_attribute(:description)
183 end
183 end
184
184
185 def time; Time.at(read_attribute(:time)) end
185 def time; Time.at(read_attribute(:time)) end
186 def changetime; Time.at(read_attribute(:changetime)) end
186 def changetime; Time.at(read_attribute(:changetime)) end
187 end
187 end
188
188
189 class TracTicketChange < ActiveRecord::Base
189 class TracTicketChange < ActiveRecord::Base
190 self.table_name = :ticket_change
190 self.table_name = :ticket_change
191
191
192 def self.columns
192 def self.columns
193 # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
193 # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
194 super.select {|column| column.name.to_s != 'field'}
194 super.select {|column| column.name.to_s != 'field'}
195 end
195 end
196
196
197 def time; Time.at(read_attribute(:time)) end
197 def time; Time.at(read_attribute(:time)) end
198 end
198 end
199
199
200 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
200 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
201 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
201 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
202 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
202 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
203 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
203 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
204 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
204 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
205 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
205 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
206 CamelCase TitleIndex)
206 CamelCase TitleIndex)
207
207
208 class TracWikiPage < ActiveRecord::Base
208 class TracWikiPage < ActiveRecord::Base
209 self.table_name = :wiki
209 self.table_name = :wiki
210 set_primary_key :name
210 set_primary_key :name
211
211
212 def self.columns
212 def self.columns
213 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
213 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
214 super.select {|column| column.name.to_s != 'readonly'}
214 super.select {|column| column.name.to_s != 'readonly'}
215 end
215 end
216
216
217 def attachments
217 def attachments
218 TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
218 TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
219 end
219 end
220
220
221 def time; Time.at(read_attribute(:time)) end
221 def time; Time.at(read_attribute(:time)) end
222 end
222 end
223
223
224 class TracPermission < ActiveRecord::Base
224 class TracPermission < ActiveRecord::Base
225 self.table_name = :permission
225 self.table_name = :permission
226 end
226 end
227
227
228 class TracSessionAttribute < ActiveRecord::Base
228 class TracSessionAttribute < ActiveRecord::Base
229 self.table_name = :session_attribute
229 self.table_name = :session_attribute
230 end
230 end
231
231
232 def self.find_or_create_user(username, project_member = false)
232 def self.find_or_create_user(username, project_member = false)
233 return User.anonymous if username.blank?
233 return User.anonymous if username.blank?
234
234
235 u = User.find_by_login(username)
235 u = User.find_by_login(username)
236 if !u
236 if !u
237 # Create a new user if not found
237 # Create a new user if not found
238 mail = username[0, User::MAIL_LENGTH_LIMIT]
238 mail = username[0, User::MAIL_LENGTH_LIMIT]
239 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
239 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
240 mail = mail_attr.value
240 mail = mail_attr.value
241 end
241 end
242 mail = "#{mail}@foo.bar" unless mail.include?("@")
242 mail = "#{mail}@foo.bar" unless mail.include?("@")
243
243
244 name = username
244 name = username
245 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
245 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
246 name = name_attr.value
246 name = name_attr.value
247 end
247 end
248 name =~ (/(.*)(\s+\w+)?/)
248 name =~ (/(.*)(\s+\w+)?/)
249 fn = $1.strip
249 fn = $1.strip
250 ln = ($2 || '-').strip
250 ln = ($2 || '-').strip
251
251
252 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
252 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
253 :firstname => fn[0, limit_for(User, 'firstname')],
253 :firstname => fn[0, limit_for(User, 'firstname')],
254 :lastname => ln[0, limit_for(User, 'lastname')]
254 :lastname => ln[0, limit_for(User, 'lastname')]
255
255
256 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
256 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
257 u.password = 'trac'
257 u.password = 'trac'
258 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
258 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
259 # finally, a default user is used if the new user is not valid
259 # finally, a default user is used if the new user is not valid
260 u = User.first unless u.save
260 u = User.first unless u.save
261 end
261 end
262 # Make sure he is a member of the project
262 # Make sure he is a member of the project
263 if project_member && !u.member_of?(@target_project)
263 if project_member && !u.member_of?(@target_project)
264 role = DEFAULT_ROLE
264 role = DEFAULT_ROLE
265 if u.admin
265 if u.admin
266 role = ROLE_MAPPING['admin']
266 role = ROLE_MAPPING['admin']
267 elsif TracPermission.find_by_username_and_action(username, 'developer')
267 elsif TracPermission.find_by_username_and_action(username, 'developer')
268 role = ROLE_MAPPING['developer']
268 role = ROLE_MAPPING['developer']
269 end
269 end
270 Member.create(:user => u, :project => @target_project, :roles => [role])
270 Member.create(:user => u, :project => @target_project, :roles => [role])
271 u.reload
271 u.reload
272 end
272 end
273 u
273 u
274 end
274 end
275
275
276 # Basic wiki syntax conversion
276 # Basic wiki syntax conversion
277 def self.convert_wiki_text(text)
277 def self.convert_wiki_text(text)
278 # Titles
278 # Titles
279 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
279 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
280 # External Links
280 # External Links
281 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
281 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
282 # Ticket links:
282 # Ticket links:
283 # [ticket:234 Text],[ticket:234 This is a test]
283 # [ticket:234 Text],[ticket:234 This is a test]
284 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
284 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
285 # ticket:1234
285 # ticket:1234
286 # #1 is working cause Redmine uses the same syntax.
286 # #1 is working cause Redmine uses the same syntax.
287 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
287 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
288 # Milestone links:
288 # Milestone links:
289 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
289 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
290 # The text "Milestone 0.1.0 (Mercury)" is not converted,
290 # The text "Milestone 0.1.0 (Mercury)" is not converted,
291 # cause Redmine's wiki does not support this.
291 # cause Redmine's wiki does not support this.
292 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
292 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
293 # [milestone:"0.1.0 Mercury"]
293 # [milestone:"0.1.0 Mercury"]
294 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
294 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
295 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
295 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
296 # milestone:0.1.0
296 # milestone:0.1.0
297 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
297 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
298 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
298 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
299 # Internal Links
299 # Internal Links
300 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
300 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
301 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
301 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
302 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
302 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
303 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
303 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
304 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
304 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
305 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
305 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
306
306
307 # Links to pages UsingJustWikiCaps
307 # Links to pages UsingJustWikiCaps
308 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
308 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
309 # Normalize things that were supposed to not be links
309 # Normalize things that were supposed to not be links
310 # like !NotALink
310 # like !NotALink
311 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
311 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
312 # Revisions links
312 # Revisions links
313 text = text.gsub(/\[(\d+)\]/, 'r\1')
313 text = text.gsub(/\[(\d+)\]/, 'r\1')
314 # Ticket number re-writing
314 # Ticket number re-writing
315 text = text.gsub(/#(\d+)/) do |s|
315 text = text.gsub(/#(\d+)/) do |s|
316 if $1.length < 10
316 if $1.length < 10
317 # TICKET_MAP[$1.to_i] ||= $1
317 # TICKET_MAP[$1.to_i] ||= $1
318 "\##{TICKET_MAP[$1.to_i] || $1}"
318 "\##{TICKET_MAP[$1.to_i] || $1}"
319 else
319 else
320 s
320 s
321 end
321 end
322 end
322 end
323 # We would like to convert the Code highlighting too
323 # We would like to convert the Code highlighting too
324 # This will go into the next line.
324 # This will go into the next line.
325 shebang_line = false
325 shebang_line = false
326 # Reguar expression for start of code
326 # Reguar expression for start of code
327 pre_re = /\{\{\{/
327 pre_re = /\{\{\{/
328 # Code hightlighing...
328 # Code hightlighing...
329 shebang_re = /^\#\!([a-z]+)/
329 shebang_re = /^\#\!([a-z]+)/
330 # Regular expression for end of code
330 # Regular expression for end of code
331 pre_end_re = /\}\}\}/
331 pre_end_re = /\}\}\}/
332
332
333 # Go through the whole text..extract it line by line
333 # Go through the whole text..extract it line by line
334 text = text.gsub(/^(.*)$/) do |line|
334 text = text.gsub(/^(.*)$/) do |line|
335 m_pre = pre_re.match(line)
335 m_pre = pre_re.match(line)
336 if m_pre
336 if m_pre
337 line = '<pre>'
337 line = '<pre>'
338 else
338 else
339 m_sl = shebang_re.match(line)
339 m_sl = shebang_re.match(line)
340 if m_sl
340 if m_sl
341 shebang_line = true
341 shebang_line = true
342 line = '<code class="' + m_sl[1] + '">'
342 line = '<code class="' + m_sl[1] + '">'
343 end
343 end
344 m_pre_end = pre_end_re.match(line)
344 m_pre_end = pre_end_re.match(line)
345 if m_pre_end
345 if m_pre_end
346 line = '</pre>'
346 line = '</pre>'
347 if shebang_line
347 if shebang_line
348 line = '</code>' + line
348 line = '</code>' + line
349 end
349 end
350 end
350 end
351 end
351 end
352 line
352 line
353 end
353 end
354
354
355 # Highlighting
355 # Highlighting
356 text = text.gsub(/'''''([^\s])/, '_*\1')
356 text = text.gsub(/'''''([^\s])/, '_*\1')
357 text = text.gsub(/([^\s])'''''/, '\1*_')
357 text = text.gsub(/([^\s])'''''/, '\1*_')
358 text = text.gsub(/'''/, '*')
358 text = text.gsub(/'''/, '*')
359 text = text.gsub(/''/, '_')
359 text = text.gsub(/''/, '_')
360 text = text.gsub(/__/, '+')
360 text = text.gsub(/__/, '+')
361 text = text.gsub(/~~/, '-')
361 text = text.gsub(/~~/, '-')
362 text = text.gsub(/`/, '@')
362 text = text.gsub(/`/, '@')
363 text = text.gsub(/,,/, '~')
363 text = text.gsub(/,,/, '~')
364 # Lists
364 # Lists
365 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
365 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
366
366
367 text
367 text
368 end
368 end
369
369
370 def self.migrate
370 def self.migrate
371 establish_connection
371 establish_connection
372
372
373 # Quick database test
373 # Quick database test
374 TracComponent.count
374 TracComponent.count
375
375
376 migrated_components = 0
376 migrated_components = 0
377 migrated_milestones = 0
377 migrated_milestones = 0
378 migrated_tickets = 0
378 migrated_tickets = 0
379 migrated_custom_values = 0
379 migrated_custom_values = 0
380 migrated_ticket_attachments = 0
380 migrated_ticket_attachments = 0
381 migrated_wiki_edits = 0
381 migrated_wiki_edits = 0
382 migrated_wiki_attachments = 0
382 migrated_wiki_attachments = 0
383
383
384 #Wiki system initializing...
384 #Wiki system initializing...
385 @target_project.wiki.destroy if @target_project.wiki
385 @target_project.wiki.destroy if @target_project.wiki
386 @target_project.reload
386 @target_project.reload
387 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
387 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
388 wiki_edit_count = 0
388 wiki_edit_count = 0
389
389
390 # Components
390 # Components
391 print "Migrating components"
391 print "Migrating components"
392 issues_category_map = {}
392 issues_category_map = {}
393 TracComponent.all.each do |component|
393 TracComponent.all.each do |component|
394 print '.'
394 print '.'
395 STDOUT.flush
395 STDOUT.flush
396 c = IssueCategory.new :project => @target_project,
396 c = IssueCategory.new :project => @target_project,
397 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
397 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
398 next unless c.save
398 next unless c.save
399 issues_category_map[component.name] = c
399 issues_category_map[component.name] = c
400 migrated_components += 1
400 migrated_components += 1
401 end
401 end
402 puts
402 puts
403
403
404 # Milestones
404 # Milestones
405 print "Migrating milestones"
405 print "Migrating milestones"
406 version_map = {}
406 version_map = {}
407 TracMilestone.all.each do |milestone|
407 TracMilestone.all.each do |milestone|
408 print '.'
408 print '.'
409 STDOUT.flush
409 STDOUT.flush
410 # First we try to find the wiki page...
410 # First we try to find the wiki page...
411 p = wiki.find_or_new_page(milestone.name.to_s)
411 p = wiki.find_or_new_page(milestone.name.to_s)
412 p.content = WikiContent.new(:page => p) if p.new_record?
412 p.content = WikiContent.new(:page => p) if p.new_record?
413 p.content.text = milestone.description.to_s
413 p.content.text = milestone.description.to_s
414 p.content.author = find_or_create_user('trac')
414 p.content.author = find_or_create_user('trac')
415 p.content.comments = 'Milestone'
415 p.content.comments = 'Milestone'
416 p.save
416 p.save
417
417
418 v = Version.new :project => @target_project,
418 v = Version.new :project => @target_project,
419 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
419 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
420 :description => nil,
420 :description => nil,
421 :wiki_page_title => milestone.name.to_s,
421 :wiki_page_title => milestone.name.to_s,
422 :effective_date => milestone.completed
422 :effective_date => milestone.completed
423
423
424 next unless v.save
424 next unless v.save
425 version_map[milestone.name] = v
425 version_map[milestone.name] = v
426 migrated_milestones += 1
426 migrated_milestones += 1
427 end
427 end
428 puts
428 puts
429
429
430 # Custom fields
430 # Custom fields
431 # TODO: read trac.ini instead
431 # TODO: read trac.ini instead
432 print "Migrating custom fields"
432 print "Migrating custom fields"
433 custom_field_map = {}
433 custom_field_map = {}
434 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
434 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
435 print '.'
435 print '.'
436 STDOUT.flush
436 STDOUT.flush
437 # Redmine custom field name
437 # Redmine custom field name
438 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
438 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
439 # Find if the custom already exists in Redmine
439 # Find if the custom already exists in Redmine
440 f = IssueCustomField.find_by_name(field_name)
440 f = IssueCustomField.find_by_name(field_name)
441 # Or create a new one
441 # Or create a new one
442 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
442 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
443 :field_format => 'string')
443 :field_format => 'string')
444
444
445 next if f.new_record?
445 next if f.new_record?
446 f.trackers = Tracker.all
446 f.trackers = Tracker.all
447 f.projects << @target_project
447 f.projects << @target_project
448 custom_field_map[field.name] = f
448 custom_field_map[field.name] = f
449 end
449 end
450 puts
450 puts
451
451
452 # Trac 'resolution' field as a Redmine custom field
452 # Trac 'resolution' field as a Redmine custom field
453 r = IssueCustomField.where(:name => "Resolution").first
453 r = IssueCustomField.where(:name => "Resolution").first
454 r = IssueCustomField.new(:name => 'Resolution',
454 r = IssueCustomField.new(:name => 'Resolution',
455 :field_format => 'list',
455 :field_format => 'list',
456 :is_filter => true) if r.nil?
456 :is_filter => true) if r.nil?
457 r.trackers = Tracker.all
457 r.trackers = Tracker.all
458 r.projects << @target_project
458 r.projects << @target_project
459 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
459 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
460 r.save!
460 r.save!
461 custom_field_map['resolution'] = r
461 custom_field_map['resolution'] = r
462
462
463 # Tickets
463 # Tickets
464 print "Migrating tickets"
464 print "Migrating tickets"
465 TracTicket.find_each(:batch_size => 200) do |ticket|
465 TracTicket.find_each(:batch_size => 200) do |ticket|
466 print '.'
466 print '.'
467 STDOUT.flush
467 STDOUT.flush
468 i = Issue.new :project => @target_project,
468 i = Issue.new :project => @target_project,
469 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
469 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
470 :description => convert_wiki_text(encode(ticket.description)),
470 :description => convert_wiki_text(encode(ticket.description)),
471 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
471 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
472 :created_on => ticket.time
472 :created_on => ticket.time
473 i.author = find_or_create_user(ticket.reporter)
473 i.author = find_or_create_user(ticket.reporter)
474 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
474 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
475 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
475 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
476 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
476 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
477 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
477 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
478 i.id = ticket.id unless Issue.exists?(ticket.id)
478 i.id = ticket.id unless Issue.exists?(ticket.id)
479 next unless Time.fake(ticket.changetime) { i.save }
479 next unless Time.fake(ticket.changetime) { i.save }
480 TICKET_MAP[ticket.id] = i.id
480 TICKET_MAP[ticket.id] = i.id
481 migrated_tickets += 1
481 migrated_tickets += 1
482
482
483 # Owner
483 # Owner
484 unless ticket.owner.blank?
484 unless ticket.owner.blank?
485 i.assigned_to = find_or_create_user(ticket.owner, true)
485 i.assigned_to = find_or_create_user(ticket.owner, true)
486 Time.fake(ticket.changetime) { i.save }
486 Time.fake(ticket.changetime) { i.save }
487 end
487 end
488
488
489 # Comments and status/resolution changes
489 # Comments and status/resolution changes
490 ticket.ticket_changes.group_by(&:time).each do |time, changeset|
490 ticket.ticket_changes.group_by(&:time).each do |time, changeset|
491 status_change = changeset.select {|change| change.field == 'status'}.first
491 status_change = changeset.select {|change| change.field == 'status'}.first
492 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
492 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
493 comment_change = changeset.select {|change| change.field == 'comment'}.first
493 comment_change = changeset.select {|change| change.field == 'comment'}.first
494
494
495 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
495 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
496 :created_on => time
496 :created_on => time
497 n.user = find_or_create_user(changeset.first.author)
497 n.user = find_or_create_user(changeset.first.author)
498 n.journalized = i
498 n.journalized = i
499 if status_change &&
499 if status_change &&
500 STATUS_MAPPING[status_change.oldvalue] &&
500 STATUS_MAPPING[status_change.oldvalue] &&
501 STATUS_MAPPING[status_change.newvalue] &&
501 STATUS_MAPPING[status_change.newvalue] &&
502 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
502 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
503 n.details << JournalDetail.new(:property => 'attr',
503 n.details << JournalDetail.new(:property => 'attr',
504 :prop_key => 'status_id',
504 :prop_key => 'status_id',
505 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
505 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
506 :value => STATUS_MAPPING[status_change.newvalue].id)
506 :value => STATUS_MAPPING[status_change.newvalue].id)
507 end
507 end
508 if resolution_change
508 if resolution_change
509 n.details << JournalDetail.new(:property => 'cf',
509 n.details << JournalDetail.new(:property => 'cf',
510 :prop_key => custom_field_map['resolution'].id,
510 :prop_key => custom_field_map['resolution'].id,
511 :old_value => resolution_change.oldvalue,
511 :old_value => resolution_change.oldvalue,
512 :value => resolution_change.newvalue)
512 :value => resolution_change.newvalue)
513 end
513 end
514 n.save unless n.details.empty? && n.notes.blank?
514 n.save unless n.details.empty? && n.notes.blank?
515 end
515 end
516
516
517 # Attachments
517 # Attachments
518 ticket.attachments.each do |attachment|
518 ticket.attachments.each do |attachment|
519 next unless attachment.exist?
519 next unless attachment.exist?
520 attachment.open {
520 attachment.open {
521 a = Attachment.new :created_on => attachment.time
521 a = Attachment.new :created_on => attachment.time
522 a.file = attachment
522 a.file = attachment
523 a.author = find_or_create_user(attachment.author)
523 a.author = find_or_create_user(attachment.author)
524 a.container = i
524 a.container = i
525 a.description = attachment.description
525 a.description = attachment.description
526 migrated_ticket_attachments += 1 if a.save
526 migrated_ticket_attachments += 1 if a.save
527 }
527 }
528 end
528 end
529
529
530 # Custom fields
530 # Custom fields
531 custom_values = ticket.customs.inject({}) do |h, custom|
531 custom_values = ticket.customs.inject({}) do |h, custom|
532 if custom_field = custom_field_map[custom.name]
532 if custom_field = custom_field_map[custom.name]
533 h[custom_field.id] = custom.value
533 h[custom_field.id] = custom.value
534 migrated_custom_values += 1
534 migrated_custom_values += 1
535 end
535 end
536 h
536 h
537 end
537 end
538 if custom_field_map['resolution'] && !ticket.resolution.blank?
538 if custom_field_map['resolution'] && !ticket.resolution.blank?
539 custom_values[custom_field_map['resolution'].id] = ticket.resolution
539 custom_values[custom_field_map['resolution'].id] = ticket.resolution
540 end
540 end
541 i.custom_field_values = custom_values
541 i.custom_field_values = custom_values
542 i.save_custom_field_values
542 i.save_custom_field_values
543 end
543 end
544
544
545 # update issue id sequence if needed (postgresql)
545 # update issue id sequence if needed (postgresql)
546 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
546 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
547 puts
547 puts
548
548
549 # Wiki
549 # Wiki
550 print "Migrating wiki"
550 print "Migrating wiki"
551 if wiki.save
551 if wiki.save
552 TracWikiPage.order('name, version').all.each do |page|
552 TracWikiPage.order('name, version').all.each do |page|
553 # Do not migrate Trac manual wiki pages
553 # Do not migrate Trac manual wiki pages
554 next if TRAC_WIKI_PAGES.include?(page.name)
554 next if TRAC_WIKI_PAGES.include?(page.name)
555 wiki_edit_count += 1
555 wiki_edit_count += 1
556 print '.'
556 print '.'
557 STDOUT.flush
557 STDOUT.flush
558 p = wiki.find_or_new_page(page.name)
558 p = wiki.find_or_new_page(page.name)
559 p.content = WikiContent.new(:page => p) if p.new_record?
559 p.content = WikiContent.new(:page => p) if p.new_record?
560 p.content.text = page.text
560 p.content.text = page.text
561 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
561 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
562 p.content.comments = page.comment
562 p.content.comments = page.comment
563 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
563 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
564
564
565 next if p.content.new_record?
565 next if p.content.new_record?
566 migrated_wiki_edits += 1
566 migrated_wiki_edits += 1
567
567
568 # Attachments
568 # Attachments
569 page.attachments.each do |attachment|
569 page.attachments.each do |attachment|
570 next unless attachment.exist?
570 next unless attachment.exist?
571 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
571 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
572 attachment.open {
572 attachment.open {
573 a = Attachment.new :created_on => attachment.time
573 a = Attachment.new :created_on => attachment.time
574 a.file = attachment
574 a.file = attachment
575 a.author = find_or_create_user(attachment.author)
575 a.author = find_or_create_user(attachment.author)
576 a.description = attachment.description
576 a.description = attachment.description
577 a.container = p
577 a.container = p
578 migrated_wiki_attachments += 1 if a.save
578 migrated_wiki_attachments += 1 if a.save
579 }
579 }
580 end
580 end
581 end
581 end
582
582
583 wiki.reload
583 wiki.reload
584 wiki.pages.each do |page|
584 wiki.pages.each do |page|
585 page.content.text = convert_wiki_text(page.content.text)
585 page.content.text = convert_wiki_text(page.content.text)
586 Time.fake(page.content.updated_on) { page.content.save }
586 Time.fake(page.content.updated_on) { page.content.save }
587 end
587 end
588 end
588 end
589 puts
589 puts
590
590
591 puts
591 puts
592 puts "Components: #{migrated_components}/#{TracComponent.count}"
592 puts "Components: #{migrated_components}/#{TracComponent.count}"
593 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
593 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
594 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
594 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
595 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
595 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
596 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
596 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
597 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
597 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
598 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
598 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
599 end
599 end
600
600
601 def self.limit_for(klass, attribute)
601 def self.limit_for(klass, attribute)
602 klass.columns_hash[attribute.to_s].limit
602 klass.columns_hash[attribute.to_s].limit
603 end
603 end
604
604
605 def self.encoding(charset)
605 def self.encoding(charset)
606 @ic = Iconv.new('UTF-8', charset)
606 @charset = charset
607 rescue Iconv::InvalidEncoding
608 puts "Invalid encoding!"
609 return false
610 end
607 end
611
608
612 def self.set_trac_directory(path)
609 def self.set_trac_directory(path)
613 @@trac_directory = path
610 @@trac_directory = path
614 raise "This directory doesn't exist!" unless File.directory?(path)
611 raise "This directory doesn't exist!" unless File.directory?(path)
615 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
612 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
616 @@trac_directory
613 @@trac_directory
617 rescue Exception => e
614 rescue Exception => e
618 puts e
615 puts e
619 return false
616 return false
620 end
617 end
621
618
622 def self.trac_directory
619 def self.trac_directory
623 @@trac_directory
620 @@trac_directory
624 end
621 end
625
622
626 def self.set_trac_adapter(adapter)
623 def self.set_trac_adapter(adapter)
627 return false if adapter.blank?
624 return false if adapter.blank?
628 raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
625 raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
629 # If adapter is sqlite or sqlite3, make sure that trac.db exists
626 # If adapter is sqlite or sqlite3, make sure that trac.db exists
630 raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
627 raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
631 @@trac_adapter = adapter
628 @@trac_adapter = adapter
632 rescue Exception => e
629 rescue Exception => e
633 puts e
630 puts e
634 return false
631 return false
635 end
632 end
636
633
637 def self.set_trac_db_host(host)
634 def self.set_trac_db_host(host)
638 return nil if host.blank?
635 return nil if host.blank?
639 @@trac_db_host = host
636 @@trac_db_host = host
640 end
637 end
641
638
642 def self.set_trac_db_port(port)
639 def self.set_trac_db_port(port)
643 return nil if port.to_i == 0
640 return nil if port.to_i == 0
644 @@trac_db_port = port.to_i
641 @@trac_db_port = port.to_i
645 end
642 end
646
643
647 def self.set_trac_db_name(name)
644 def self.set_trac_db_name(name)
648 return nil if name.blank?
645 return nil if name.blank?
649 @@trac_db_name = name
646 @@trac_db_name = name
650 end
647 end
651
648
652 def self.set_trac_db_username(username)
649 def self.set_trac_db_username(username)
653 @@trac_db_username = username
650 @@trac_db_username = username
654 end
651 end
655
652
656 def self.set_trac_db_password(password)
653 def self.set_trac_db_password(password)
657 @@trac_db_password = password
654 @@trac_db_password = password
658 end
655 end
659
656
660 def self.set_trac_db_schema(schema)
657 def self.set_trac_db_schema(schema)
661 @@trac_db_schema = schema
658 @@trac_db_schema = schema
662 end
659 end
663
660
664 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
661 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
665
662
666 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
663 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
667 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
664 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
668
665
669 def self.target_project_identifier(identifier)
666 def self.target_project_identifier(identifier)
670 project = Project.find_by_identifier(identifier)
667 project = Project.find_by_identifier(identifier)
671 if !project
668 if !project
672 # create the target project
669 # create the target project
673 project = Project.new :name => identifier.humanize,
670 project = Project.new :name => identifier.humanize,
674 :description => ''
671 :description => ''
675 project.identifier = identifier
672 project.identifier = identifier
676 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
673 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
677 # enable issues and wiki for the created project
674 # enable issues and wiki for the created project
678 project.enabled_module_names = ['issue_tracking', 'wiki']
675 project.enabled_module_names = ['issue_tracking', 'wiki']
679 else
676 else
680 puts
677 puts
681 puts "This project already exists in your Redmine database."
678 puts "This project already exists in your Redmine database."
682 print "Are you sure you want to append data to this project ? [Y/n] "
679 print "Are you sure you want to append data to this project ? [Y/n] "
683 STDOUT.flush
680 STDOUT.flush
684 exit if STDIN.gets.match(/^n$/i)
681 exit if STDIN.gets.match(/^n$/i)
685 end
682 end
686 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
683 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
687 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
684 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
688 @target_project = project.new_record? ? nil : project
685 @target_project = project.new_record? ? nil : project
689 @target_project.reload
686 @target_project.reload
690 end
687 end
691
688
692 def self.connection_params
689 def self.connection_params
693 if trac_adapter == 'sqlite3'
690 if trac_adapter == 'sqlite3'
694 {:adapter => 'sqlite3',
691 {:adapter => 'sqlite3',
695 :database => trac_db_path}
692 :database => trac_db_path}
696 else
693 else
697 {:adapter => trac_adapter,
694 {:adapter => trac_adapter,
698 :database => trac_db_name,
695 :database => trac_db_name,
699 :host => trac_db_host,
696 :host => trac_db_host,
700 :port => trac_db_port,
697 :port => trac_db_port,
701 :username => trac_db_username,
698 :username => trac_db_username,
702 :password => trac_db_password,
699 :password => trac_db_password,
703 :schema_search_path => trac_db_schema
700 :schema_search_path => trac_db_schema
704 }
701 }
705 end
702 end
706 end
703 end
707
704
708 def self.establish_connection
705 def self.establish_connection
709 constants.each do |const|
706 constants.each do |const|
710 klass = const_get(const)
707 klass = const_get(const)
711 next unless klass.respond_to? 'establish_connection'
708 next unless klass.respond_to? 'establish_connection'
712 klass.establish_connection connection_params
709 klass.establish_connection connection_params
713 end
710 end
714 end
711 end
715
712
716 private
717 def self.encode(text)
713 def self.encode(text)
714 if RUBY_VERSION < '1.9'
715 @ic ||= Iconv.new('UTF-8', @charset)
718 @ic.iconv text
716 @ic.iconv text
719 rescue
717 else
720 text
718 text.to_s.force_encoding(@charset).encode('UTF-8')
719 end
721 end
720 end
722 end
721 end
723
722
724 puts
723 puts
725 if Redmine::DefaultData::Loader.no_data?
724 if Redmine::DefaultData::Loader.no_data?
726 puts "Redmine configuration need to be loaded before importing data."
725 puts "Redmine configuration need to be loaded before importing data."
727 puts "Please, run this first:"
726 puts "Please, run this first:"
728 puts
727 puts
729 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
728 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
730 exit
729 exit
731 end
730 end
732
731
733 puts "WARNING: a new project will be added to Redmine during this process."
732 puts "WARNING: a new project will be added to Redmine during this process."
734 print "Are you sure you want to continue ? [y/N] "
733 print "Are you sure you want to continue ? [y/N] "
735 STDOUT.flush
734 STDOUT.flush
736 break unless STDIN.gets.match(/^y$/i)
735 break unless STDIN.gets.match(/^y$/i)
737 puts
736 puts
738
737
739 def prompt(text, options = {}, &block)
738 def prompt(text, options = {}, &block)
740 default = options[:default] || ''
739 default = options[:default] || ''
741 while true
740 while true
742 print "#{text} [#{default}]: "
741 print "#{text} [#{default}]: "
743 STDOUT.flush
742 STDOUT.flush
744 value = STDIN.gets.chomp!
743 value = STDIN.gets.chomp!
745 value = default if value.blank?
744 value = default if value.blank?
746 break if yield value
745 break if yield value
747 end
746 end
748 end
747 end
749
748
750 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
749 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
751
750
752 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
751 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
753 prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
752 prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
754 unless %w(sqlite3).include?(TracMigrate.trac_adapter)
753 unless %w(sqlite3).include?(TracMigrate.trac_adapter)
755 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
754 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
756 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
755 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
757 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
756 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
758 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
757 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
759 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
758 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
760 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
759 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
761 end
760 end
762 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
761 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
763 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
762 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
764 puts
763 puts
765
764
766 # Turn off email notifications
765 # Turn off email notifications
767 Setting.notified_events = []
766 Setting.notified_events = []
768
767
769 TracMigrate.migrate
768 TracMigrate.migrate
770 end
769 end
771 end
770 end
772
771
General Comments 0
You need to be logged in to leave comments. Login now