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