##// END OF EJS Templates
Fixed that Mantis/Trac users are not imported because of password too short (#14590)....
Jean-Philippe Lang -
r11933:5e2eedf2c5fd
parent child
Show More
@@ -1,519 +1,522
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' if RUBY_VERSION < '1.9'
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 @charset = charset
443 @charset = charset
444 end
444 end
445
445
446 def self.establish_connection(params)
446 def self.establish_connection(params)
447 constants.each do |const|
447 constants.each do |const|
448 klass = const_get(const)
448 klass = const_get(const)
449 next unless klass.respond_to? 'establish_connection'
449 next unless klass.respond_to? 'establish_connection'
450 klass.establish_connection params
450 klass.establish_connection params
451 end
451 end
452 end
452 end
453
453
454 def self.encode(text)
454 def self.encode(text)
455 if RUBY_VERSION < '1.9'
455 if RUBY_VERSION < '1.9'
456 @ic ||= Iconv.new('UTF-8', @charset)
456 @ic ||= Iconv.new('UTF-8', @charset)
457 @ic.iconv text
457 @ic.iconv text
458 else
458 else
459 text.to_s.force_encoding(@charset).encode('UTF-8')
459 text.to_s.force_encoding(@charset).encode('UTF-8')
460 end
460 end
461 end
461 end
462 end
462 end
463
463
464 puts
464 puts
465 if Redmine::DefaultData::Loader.no_data?
465 if Redmine::DefaultData::Loader.no_data?
466 puts "Redmine configuration need to be loaded before importing data."
466 puts "Redmine configuration need to be loaded before importing data."
467 puts "Please, run this first:"
467 puts "Please, run this first:"
468 puts
468 puts
469 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
469 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
470 exit
470 exit
471 end
471 end
472
472
473 puts "WARNING: Your Redmine data will be deleted during this process."
473 puts "WARNING: Your Redmine data will be deleted during this process."
474 print "Are you sure you want to continue ? [y/N] "
474 print "Are you sure you want to continue ? [y/N] "
475 STDOUT.flush
475 STDOUT.flush
476 break unless STDIN.gets.match(/^y$/i)
476 break unless STDIN.gets.match(/^y$/i)
477
477
478 # Default Mantis database settings
478 # Default Mantis database settings
479 db_params = {:adapter => 'mysql2',
479 db_params = {:adapter => 'mysql2',
480 :database => 'bugtracker',
480 :database => 'bugtracker',
481 :host => 'localhost',
481 :host => 'localhost',
482 :username => 'root',
482 :username => 'root',
483 :password => '' }
483 :password => '' }
484
484
485 puts
485 puts
486 puts "Please enter settings for your Mantis database"
486 puts "Please enter settings for your Mantis database"
487 [:adapter, :host, :database, :username, :password].each do |param|
487 [:adapter, :host, :database, :username, :password].each do |param|
488 print "#{param} [#{db_params[param]}]: "
488 print "#{param} [#{db_params[param]}]: "
489 value = STDIN.gets.chomp!
489 value = STDIN.gets.chomp!
490 db_params[param] = value unless value.blank?
490 db_params[param] = value unless value.blank?
491 end
491 end
492
492
493 while true
493 while true
494 print "encoding [UTF-8]: "
494 print "encoding [UTF-8]: "
495 STDOUT.flush
495 STDOUT.flush
496 encoding = STDIN.gets.chomp!
496 encoding = STDIN.gets.chomp!
497 encoding = 'UTF-8' if encoding.blank?
497 encoding = 'UTF-8' if encoding.blank?
498 break if MantisMigrate.encoding encoding
498 break if MantisMigrate.encoding encoding
499 puts "Invalid encoding!"
499 puts "Invalid encoding!"
500 end
500 end
501 puts
501 puts
502
502
503 # Make sure bugs can refer bugs in other projects
503 # Make sure bugs can refer bugs in other projects
504 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'
505
505
506 old_notified_events = Setting.notified_events
506 old_notified_events = Setting.notified_events
507 old_password_min_length = Setting.password_min_length
507 begin
508 begin
508 # Turn off email notifications temporarily
509 # Turn off email notifications temporarily
509 Setting.notified_events = []
510 Setting.notified_events = []
511 Setting.password_min_length = 4
510 # Run the migration
512 # Run the migration
511 MantisMigrate.establish_connection db_params
513 MantisMigrate.establish_connection db_params
512 MantisMigrate.migrate
514 MantisMigrate.migrate
513 ensure
515 ensure
514 # Restore previous notification settings even if the migration fails
516 # Restore previous settings
515 Setting.notified_events = old_notified_events
517 Setting.notified_events = old_notified_events
518 Setting.password_min_length = old_password_min_length
516 end
519 end
517
520
518 end
521 end
519 end
522 end
@@ -1,781 +1,784
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' if RUBY_VERSION < '1.9'
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 #replace exotic characters with their hex representation to avoid invalid filenames
156 #replace exotic characters with their hex representation to avoid invalid filenames
157 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) do |x|
157 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) do |x|
158 codepoint = RUBY_VERSION < '1.9' ? x[0] : x.codepoints.to_a[0]
158 codepoint = RUBY_VERSION < '1.9' ? x[0] : x.codepoints.to_a[0]
159 sprintf('%%%02x', codepoint)
159 sprintf('%%%02x', codepoint)
160 end
160 end
161 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
161 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
162 end
162 end
163 end
163 end
164
164
165 class TracTicket < ActiveRecord::Base
165 class TracTicket < ActiveRecord::Base
166 self.table_name = :ticket
166 self.table_name = :ticket
167 set_inheritance_column :none
167 set_inheritance_column :none
168
168
169 # ticket changes: only migrate status changes and comments
169 # ticket changes: only migrate status changes and comments
170 has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
170 has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
171 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
171 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
172
172
173 def attachments
173 def attachments
174 TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
174 TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
175 end
175 end
176
176
177 def ticket_type
177 def ticket_type
178 read_attribute(:type)
178 read_attribute(:type)
179 end
179 end
180
180
181 def summary
181 def summary
182 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
182 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
183 end
183 end
184
184
185 def description
185 def description
186 read_attribute(:description).blank? ? summary : read_attribute(:description)
186 read_attribute(:description).blank? ? summary : read_attribute(:description)
187 end
187 end
188
188
189 def time; Time.at(read_attribute(:time)) end
189 def time; Time.at(read_attribute(:time)) end
190 def changetime; Time.at(read_attribute(:changetime)) end
190 def changetime; Time.at(read_attribute(:changetime)) end
191 end
191 end
192
192
193 class TracTicketChange < ActiveRecord::Base
193 class TracTicketChange < ActiveRecord::Base
194 self.table_name = :ticket_change
194 self.table_name = :ticket_change
195
195
196 def self.columns
196 def self.columns
197 # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
197 # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
198 super.select {|column| column.name.to_s != 'field'}
198 super.select {|column| column.name.to_s != 'field'}
199 end
199 end
200
200
201 def time; Time.at(read_attribute(:time)) end
201 def time; Time.at(read_attribute(:time)) end
202 end
202 end
203
203
204 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
204 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
205 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
205 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
206 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
206 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
207 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
207 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
208 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
208 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
209 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
209 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
210 CamelCase TitleIndex)
210 CamelCase TitleIndex)
211
211
212 class TracWikiPage < ActiveRecord::Base
212 class TracWikiPage < ActiveRecord::Base
213 self.table_name = :wiki
213 self.table_name = :wiki
214 set_primary_key :name
214 set_primary_key :name
215
215
216 def self.columns
216 def self.columns
217 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
217 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
218 super.select {|column| column.name.to_s != 'readonly'}
218 super.select {|column| column.name.to_s != 'readonly'}
219 end
219 end
220
220
221 def attachments
221 def attachments
222 TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
222 TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
223 end
223 end
224
224
225 def time; Time.at(read_attribute(:time)) end
225 def time; Time.at(read_attribute(:time)) end
226 end
226 end
227
227
228 class TracPermission < ActiveRecord::Base
228 class TracPermission < ActiveRecord::Base
229 self.table_name = :permission
229 self.table_name = :permission
230 end
230 end
231
231
232 class TracSessionAttribute < ActiveRecord::Base
232 class TracSessionAttribute < ActiveRecord::Base
233 self.table_name = :session_attribute
233 self.table_name = :session_attribute
234 end
234 end
235
235
236 def self.find_or_create_user(username, project_member = false)
236 def self.find_or_create_user(username, project_member = false)
237 return User.anonymous if username.blank?
237 return User.anonymous if username.blank?
238
238
239 u = User.find_by_login(username)
239 u = User.find_by_login(username)
240 if !u
240 if !u
241 # Create a new user if not found
241 # Create a new user if not found
242 mail = username[0, User::MAIL_LENGTH_LIMIT]
242 mail = username[0, User::MAIL_LENGTH_LIMIT]
243 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
243 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
244 mail = mail_attr.value
244 mail = mail_attr.value
245 end
245 end
246 mail = "#{mail}@foo.bar" unless mail.include?("@")
246 mail = "#{mail}@foo.bar" unless mail.include?("@")
247
247
248 name = username
248 name = username
249 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
249 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
250 name = name_attr.value
250 name = name_attr.value
251 end
251 end
252 name =~ (/(.*)(\s+\w+)?/)
252 name =~ (/(.*)(\s+\w+)?/)
253 fn = $1.strip
253 fn = $1.strip
254 ln = ($2 || '-').strip
254 ln = ($2 || '-').strip
255
255
256 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
256 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
257 :firstname => fn[0, limit_for(User, 'firstname')],
257 :firstname => fn[0, limit_for(User, 'firstname')],
258 :lastname => ln[0, limit_for(User, 'lastname')]
258 :lastname => ln[0, limit_for(User, 'lastname')]
259
259
260 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
260 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
261 u.password = 'trac'
261 u.password = 'trac'
262 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
262 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
263 # finally, a default user is used if the new user is not valid
263 # finally, a default user is used if the new user is not valid
264 u = User.first unless u.save
264 u = User.first unless u.save
265 end
265 end
266 # Make sure user is a member of the project
266 # Make sure user is a member of the project
267 if project_member && !u.member_of?(@target_project)
267 if project_member && !u.member_of?(@target_project)
268 role = DEFAULT_ROLE
268 role = DEFAULT_ROLE
269 if u.admin
269 if u.admin
270 role = ROLE_MAPPING['admin']
270 role = ROLE_MAPPING['admin']
271 elsif TracPermission.find_by_username_and_action(username, 'developer')
271 elsif TracPermission.find_by_username_and_action(username, 'developer')
272 role = ROLE_MAPPING['developer']
272 role = ROLE_MAPPING['developer']
273 end
273 end
274 Member.create(:user => u, :project => @target_project, :roles => [role])
274 Member.create(:user => u, :project => @target_project, :roles => [role])
275 u.reload
275 u.reload
276 end
276 end
277 u
277 u
278 end
278 end
279
279
280 # Basic wiki syntax conversion
280 # Basic wiki syntax conversion
281 def self.convert_wiki_text(text)
281 def self.convert_wiki_text(text)
282 # Titles
282 # Titles
283 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
283 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
284 # External Links
284 # External Links
285 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
285 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
286 # Ticket links:
286 # Ticket links:
287 # [ticket:234 Text],[ticket:234 This is a test]
287 # [ticket:234 Text],[ticket:234 This is a test]
288 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
288 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
289 # ticket:1234
289 # ticket:1234
290 # #1 is working cause Redmine uses the same syntax.
290 # #1 is working cause Redmine uses the same syntax.
291 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
291 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
292 # Milestone links:
292 # Milestone links:
293 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
293 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
294 # The text "Milestone 0.1.0 (Mercury)" is not converted,
294 # The text "Milestone 0.1.0 (Mercury)" is not converted,
295 # cause Redmine's wiki does not support this.
295 # cause Redmine's wiki does not support this.
296 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
296 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
297 # [milestone:"0.1.0 Mercury"]
297 # [milestone:"0.1.0 Mercury"]
298 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
298 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
299 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
299 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
300 # milestone:0.1.0
300 # milestone:0.1.0
301 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
301 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
302 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
302 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
303 # Internal Links
303 # Internal Links
304 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
304 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
305 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
305 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
306 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
306 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
307 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
307 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
308 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
308 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
309 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
309 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
310
310
311 # Links to pages UsingJustWikiCaps
311 # Links to pages UsingJustWikiCaps
312 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
312 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
313 # Normalize things that were supposed to not be links
313 # Normalize things that were supposed to not be links
314 # like !NotALink
314 # like !NotALink
315 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
315 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
316 # Revisions links
316 # Revisions links
317 text = text.gsub(/\[(\d+)\]/, 'r\1')
317 text = text.gsub(/\[(\d+)\]/, 'r\1')
318 # Ticket number re-writing
318 # Ticket number re-writing
319 text = text.gsub(/#(\d+)/) do |s|
319 text = text.gsub(/#(\d+)/) do |s|
320 if $1.length < 10
320 if $1.length < 10
321 # TICKET_MAP[$1.to_i] ||= $1
321 # TICKET_MAP[$1.to_i] ||= $1
322 "\##{TICKET_MAP[$1.to_i] || $1}"
322 "\##{TICKET_MAP[$1.to_i] || $1}"
323 else
323 else
324 s
324 s
325 end
325 end
326 end
326 end
327 # We would like to convert the Code highlighting too
327 # We would like to convert the Code highlighting too
328 # This will go into the next line.
328 # This will go into the next line.
329 shebang_line = false
329 shebang_line = false
330 # Reguar expression for start of code
330 # Reguar expression for start of code
331 pre_re = /\{\{\{/
331 pre_re = /\{\{\{/
332 # Code hightlighing...
332 # Code hightlighing...
333 shebang_re = /^\#\!([a-z]+)/
333 shebang_re = /^\#\!([a-z]+)/
334 # Regular expression for end of code
334 # Regular expression for end of code
335 pre_end_re = /\}\}\}/
335 pre_end_re = /\}\}\}/
336
336
337 # Go through the whole text..extract it line by line
337 # Go through the whole text..extract it line by line
338 text = text.gsub(/^(.*)$/) do |line|
338 text = text.gsub(/^(.*)$/) do |line|
339 m_pre = pre_re.match(line)
339 m_pre = pre_re.match(line)
340 if m_pre
340 if m_pre
341 line = '<pre>'
341 line = '<pre>'
342 else
342 else
343 m_sl = shebang_re.match(line)
343 m_sl = shebang_re.match(line)
344 if m_sl
344 if m_sl
345 shebang_line = true
345 shebang_line = true
346 line = '<code class="' + m_sl[1] + '">'
346 line = '<code class="' + m_sl[1] + '">'
347 end
347 end
348 m_pre_end = pre_end_re.match(line)
348 m_pre_end = pre_end_re.match(line)
349 if m_pre_end
349 if m_pre_end
350 line = '</pre>'
350 line = '</pre>'
351 if shebang_line
351 if shebang_line
352 line = '</code>' + line
352 line = '</code>' + line
353 end
353 end
354 end
354 end
355 end
355 end
356 line
356 line
357 end
357 end
358
358
359 # Highlighting
359 # Highlighting
360 text = text.gsub(/'''''([^\s])/, '_*\1')
360 text = text.gsub(/'''''([^\s])/, '_*\1')
361 text = text.gsub(/([^\s])'''''/, '\1*_')
361 text = text.gsub(/([^\s])'''''/, '\1*_')
362 text = text.gsub(/'''/, '*')
362 text = text.gsub(/'''/, '*')
363 text = text.gsub(/''/, '_')
363 text = text.gsub(/''/, '_')
364 text = text.gsub(/__/, '+')
364 text = text.gsub(/__/, '+')
365 text = text.gsub(/~~/, '-')
365 text = text.gsub(/~~/, '-')
366 text = text.gsub(/`/, '@')
366 text = text.gsub(/`/, '@')
367 text = text.gsub(/,,/, '~')
367 text = text.gsub(/,,/, '~')
368 # Lists
368 # Lists
369 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
369 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
370
370
371 text
371 text
372 end
372 end
373
373
374 def self.migrate
374 def self.migrate
375 establish_connection
375 establish_connection
376
376
377 # Quick database test
377 # Quick database test
378 TracComponent.count
378 TracComponent.count
379
379
380 migrated_components = 0
380 migrated_components = 0
381 migrated_milestones = 0
381 migrated_milestones = 0
382 migrated_tickets = 0
382 migrated_tickets = 0
383 migrated_custom_values = 0
383 migrated_custom_values = 0
384 migrated_ticket_attachments = 0
384 migrated_ticket_attachments = 0
385 migrated_wiki_edits = 0
385 migrated_wiki_edits = 0
386 migrated_wiki_attachments = 0
386 migrated_wiki_attachments = 0
387
387
388 #Wiki system initializing...
388 #Wiki system initializing...
389 @target_project.wiki.destroy if @target_project.wiki
389 @target_project.wiki.destroy if @target_project.wiki
390 @target_project.reload
390 @target_project.reload
391 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
391 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
392 wiki_edit_count = 0
392 wiki_edit_count = 0
393
393
394 # Components
394 # Components
395 print "Migrating components"
395 print "Migrating components"
396 issues_category_map = {}
396 issues_category_map = {}
397 TracComponent.all.each do |component|
397 TracComponent.all.each do |component|
398 print '.'
398 print '.'
399 STDOUT.flush
399 STDOUT.flush
400 c = IssueCategory.new :project => @target_project,
400 c = IssueCategory.new :project => @target_project,
401 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
401 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
402 next unless c.save
402 next unless c.save
403 issues_category_map[component.name] = c
403 issues_category_map[component.name] = c
404 migrated_components += 1
404 migrated_components += 1
405 end
405 end
406 puts
406 puts
407
407
408 # Milestones
408 # Milestones
409 print "Migrating milestones"
409 print "Migrating milestones"
410 version_map = {}
410 version_map = {}
411 TracMilestone.all.each do |milestone|
411 TracMilestone.all.each do |milestone|
412 print '.'
412 print '.'
413 STDOUT.flush
413 STDOUT.flush
414 # First we try to find the wiki page...
414 # First we try to find the wiki page...
415 p = wiki.find_or_new_page(milestone.name.to_s)
415 p = wiki.find_or_new_page(milestone.name.to_s)
416 p.content = WikiContent.new(:page => p) if p.new_record?
416 p.content = WikiContent.new(:page => p) if p.new_record?
417 p.content.text = milestone.description.to_s
417 p.content.text = milestone.description.to_s
418 p.content.author = find_or_create_user('trac')
418 p.content.author = find_or_create_user('trac')
419 p.content.comments = 'Milestone'
419 p.content.comments = 'Milestone'
420 p.save
420 p.save
421
421
422 v = Version.new :project => @target_project,
422 v = Version.new :project => @target_project,
423 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
423 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
424 :description => nil,
424 :description => nil,
425 :wiki_page_title => milestone.name.to_s,
425 :wiki_page_title => milestone.name.to_s,
426 :effective_date => milestone.completed
426 :effective_date => milestone.completed
427
427
428 next unless v.save
428 next unless v.save
429 version_map[milestone.name] = v
429 version_map[milestone.name] = v
430 migrated_milestones += 1
430 migrated_milestones += 1
431 end
431 end
432 puts
432 puts
433
433
434 # Custom fields
434 # Custom fields
435 # TODO: read trac.ini instead
435 # TODO: read trac.ini instead
436 print "Migrating custom fields"
436 print "Migrating custom fields"
437 custom_field_map = {}
437 custom_field_map = {}
438 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
438 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
439 print '.'
439 print '.'
440 STDOUT.flush
440 STDOUT.flush
441 # Redmine custom field name
441 # Redmine custom field name
442 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
442 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
443 # Find if the custom already exists in Redmine
443 # Find if the custom already exists in Redmine
444 f = IssueCustomField.find_by_name(field_name)
444 f = IssueCustomField.find_by_name(field_name)
445 # Or create a new one
445 # Or create a new one
446 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
446 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
447 :field_format => 'string')
447 :field_format => 'string')
448
448
449 next if f.new_record?
449 next if f.new_record?
450 f.trackers = Tracker.all
450 f.trackers = Tracker.all
451 f.projects << @target_project
451 f.projects << @target_project
452 custom_field_map[field.name] = f
452 custom_field_map[field.name] = f
453 end
453 end
454 puts
454 puts
455
455
456 # Trac 'resolution' field as a Redmine custom field
456 # Trac 'resolution' field as a Redmine custom field
457 r = IssueCustomField.where(:name => "Resolution").first
457 r = IssueCustomField.where(:name => "Resolution").first
458 r = IssueCustomField.new(:name => 'Resolution',
458 r = IssueCustomField.new(:name => 'Resolution',
459 :field_format => 'list',
459 :field_format => 'list',
460 :is_filter => true) if r.nil?
460 :is_filter => true) if r.nil?
461 r.trackers = Tracker.all
461 r.trackers = Tracker.all
462 r.projects << @target_project
462 r.projects << @target_project
463 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
463 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
464 r.save!
464 r.save!
465 custom_field_map['resolution'] = r
465 custom_field_map['resolution'] = r
466
466
467 # Tickets
467 # Tickets
468 print "Migrating tickets"
468 print "Migrating tickets"
469 TracTicket.find_each(:batch_size => 200) do |ticket|
469 TracTicket.find_each(:batch_size => 200) do |ticket|
470 print '.'
470 print '.'
471 STDOUT.flush
471 STDOUT.flush
472 i = Issue.new :project => @target_project,
472 i = Issue.new :project => @target_project,
473 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
473 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
474 :description => convert_wiki_text(encode(ticket.description)),
474 :description => convert_wiki_text(encode(ticket.description)),
475 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
475 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
476 :created_on => ticket.time
476 :created_on => ticket.time
477 i.author = find_or_create_user(ticket.reporter)
477 i.author = find_or_create_user(ticket.reporter)
478 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
478 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
479 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
479 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
480 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
480 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
481 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
481 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
482 i.id = ticket.id unless Issue.exists?(ticket.id)
482 i.id = ticket.id unless Issue.exists?(ticket.id)
483 next unless Time.fake(ticket.changetime) { i.save }
483 next unless Time.fake(ticket.changetime) { i.save }
484 TICKET_MAP[ticket.id] = i.id
484 TICKET_MAP[ticket.id] = i.id
485 migrated_tickets += 1
485 migrated_tickets += 1
486
486
487 # Owner
487 # Owner
488 unless ticket.owner.blank?
488 unless ticket.owner.blank?
489 i.assigned_to = find_or_create_user(ticket.owner, true)
489 i.assigned_to = find_or_create_user(ticket.owner, true)
490 Time.fake(ticket.changetime) { i.save }
490 Time.fake(ticket.changetime) { i.save }
491 end
491 end
492
492
493 # Comments and status/resolution changes
493 # Comments and status/resolution changes
494 ticket.ticket_changes.group_by(&:time).each do |time, changeset|
494 ticket.ticket_changes.group_by(&:time).each do |time, changeset|
495 status_change = changeset.select {|change| change.field == 'status'}.first
495 status_change = changeset.select {|change| change.field == 'status'}.first
496 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
496 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
497 comment_change = changeset.select {|change| change.field == 'comment'}.first
497 comment_change = changeset.select {|change| change.field == 'comment'}.first
498
498
499 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
499 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
500 :created_on => time
500 :created_on => time
501 n.user = find_or_create_user(changeset.first.author)
501 n.user = find_or_create_user(changeset.first.author)
502 n.journalized = i
502 n.journalized = i
503 if status_change &&
503 if status_change &&
504 STATUS_MAPPING[status_change.oldvalue] &&
504 STATUS_MAPPING[status_change.oldvalue] &&
505 STATUS_MAPPING[status_change.newvalue] &&
505 STATUS_MAPPING[status_change.newvalue] &&
506 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
506 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
507 n.details << JournalDetail.new(:property => 'attr',
507 n.details << JournalDetail.new(:property => 'attr',
508 :prop_key => 'status_id',
508 :prop_key => 'status_id',
509 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
509 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
510 :value => STATUS_MAPPING[status_change.newvalue].id)
510 :value => STATUS_MAPPING[status_change.newvalue].id)
511 end
511 end
512 if resolution_change
512 if resolution_change
513 n.details << JournalDetail.new(:property => 'cf',
513 n.details << JournalDetail.new(:property => 'cf',
514 :prop_key => custom_field_map['resolution'].id,
514 :prop_key => custom_field_map['resolution'].id,
515 :old_value => resolution_change.oldvalue,
515 :old_value => resolution_change.oldvalue,
516 :value => resolution_change.newvalue)
516 :value => resolution_change.newvalue)
517 end
517 end
518 n.save unless n.details.empty? && n.notes.blank?
518 n.save unless n.details.empty? && n.notes.blank?
519 end
519 end
520
520
521 # Attachments
521 # Attachments
522 ticket.attachments.each do |attachment|
522 ticket.attachments.each do |attachment|
523 next unless attachment.exist?
523 next unless attachment.exist?
524 attachment.open {
524 attachment.open {
525 a = Attachment.new :created_on => attachment.time
525 a = Attachment.new :created_on => attachment.time
526 a.file = attachment
526 a.file = attachment
527 a.author = find_or_create_user(attachment.author)
527 a.author = find_or_create_user(attachment.author)
528 a.container = i
528 a.container = i
529 a.description = attachment.description
529 a.description = attachment.description
530 migrated_ticket_attachments += 1 if a.save
530 migrated_ticket_attachments += 1 if a.save
531 }
531 }
532 end
532 end
533
533
534 # Custom fields
534 # Custom fields
535 custom_values = ticket.customs.inject({}) do |h, custom|
535 custom_values = ticket.customs.inject({}) do |h, custom|
536 if custom_field = custom_field_map[custom.name]
536 if custom_field = custom_field_map[custom.name]
537 h[custom_field.id] = custom.value
537 h[custom_field.id] = custom.value
538 migrated_custom_values += 1
538 migrated_custom_values += 1
539 end
539 end
540 h
540 h
541 end
541 end
542 if custom_field_map['resolution'] && !ticket.resolution.blank?
542 if custom_field_map['resolution'] && !ticket.resolution.blank?
543 custom_values[custom_field_map['resolution'].id] = ticket.resolution
543 custom_values[custom_field_map['resolution'].id] = ticket.resolution
544 end
544 end
545 i.custom_field_values = custom_values
545 i.custom_field_values = custom_values
546 i.save_custom_field_values
546 i.save_custom_field_values
547 end
547 end
548
548
549 # update issue id sequence if needed (postgresql)
549 # update issue id sequence if needed (postgresql)
550 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
550 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
551 puts
551 puts
552
552
553 # Wiki
553 # Wiki
554 print "Migrating wiki"
554 print "Migrating wiki"
555 if wiki.save
555 if wiki.save
556 TracWikiPage.order('name, version').all.each do |page|
556 TracWikiPage.order('name, version').all.each do |page|
557 # Do not migrate Trac manual wiki pages
557 # Do not migrate Trac manual wiki pages
558 next if TRAC_WIKI_PAGES.include?(page.name)
558 next if TRAC_WIKI_PAGES.include?(page.name)
559 wiki_edit_count += 1
559 wiki_edit_count += 1
560 print '.'
560 print '.'
561 STDOUT.flush
561 STDOUT.flush
562 p = wiki.find_or_new_page(page.name)
562 p = wiki.find_or_new_page(page.name)
563 p.content = WikiContent.new(:page => p) if p.new_record?
563 p.content = WikiContent.new(:page => p) if p.new_record?
564 p.content.text = page.text
564 p.content.text = page.text
565 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
565 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
566 p.content.comments = page.comment
566 p.content.comments = page.comment
567 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
567 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
568
568
569 next if p.content.new_record?
569 next if p.content.new_record?
570 migrated_wiki_edits += 1
570 migrated_wiki_edits += 1
571
571
572 # Attachments
572 # Attachments
573 page.attachments.each do |attachment|
573 page.attachments.each do |attachment|
574 next unless attachment.exist?
574 next unless attachment.exist?
575 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
575 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
576 attachment.open {
576 attachment.open {
577 a = Attachment.new :created_on => attachment.time
577 a = Attachment.new :created_on => attachment.time
578 a.file = attachment
578 a.file = attachment
579 a.author = find_or_create_user(attachment.author)
579 a.author = find_or_create_user(attachment.author)
580 a.description = attachment.description
580 a.description = attachment.description
581 a.container = p
581 a.container = p
582 migrated_wiki_attachments += 1 if a.save
582 migrated_wiki_attachments += 1 if a.save
583 }
583 }
584 end
584 end
585 end
585 end
586
586
587 wiki.reload
587 wiki.reload
588 wiki.pages.each do |page|
588 wiki.pages.each do |page|
589 page.content.text = convert_wiki_text(page.content.text)
589 page.content.text = convert_wiki_text(page.content.text)
590 Time.fake(page.content.updated_on) { page.content.save }
590 Time.fake(page.content.updated_on) { page.content.save }
591 end
591 end
592 end
592 end
593 puts
593 puts
594
594
595 puts
595 puts
596 puts "Components: #{migrated_components}/#{TracComponent.count}"
596 puts "Components: #{migrated_components}/#{TracComponent.count}"
597 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
597 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
598 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
598 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
599 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
599 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
600 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
600 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
601 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
601 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
602 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
602 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
603 end
603 end
604
604
605 def self.limit_for(klass, attribute)
605 def self.limit_for(klass, attribute)
606 klass.columns_hash[attribute.to_s].limit
606 klass.columns_hash[attribute.to_s].limit
607 end
607 end
608
608
609 def self.encoding(charset)
609 def self.encoding(charset)
610 @charset = charset
610 @charset = charset
611 end
611 end
612
612
613 def self.set_trac_directory(path)
613 def self.set_trac_directory(path)
614 @@trac_directory = path
614 @@trac_directory = path
615 raise "This directory doesn't exist!" unless File.directory?(path)
615 raise "This directory doesn't exist!" unless File.directory?(path)
616 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
616 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
617 @@trac_directory
617 @@trac_directory
618 rescue Exception => e
618 rescue Exception => e
619 puts e
619 puts e
620 return false
620 return false
621 end
621 end
622
622
623 def self.trac_directory
623 def self.trac_directory
624 @@trac_directory
624 @@trac_directory
625 end
625 end
626
626
627 def self.set_trac_adapter(adapter)
627 def self.set_trac_adapter(adapter)
628 return false if adapter.blank?
628 return false if adapter.blank?
629 raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
629 raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
630 # If adapter is sqlite or sqlite3, make sure that trac.db exists
630 # If adapter is sqlite or sqlite3, make sure that trac.db exists
631 raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
631 raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
632 @@trac_adapter = adapter
632 @@trac_adapter = adapter
633 rescue Exception => e
633 rescue Exception => e
634 puts e
634 puts e
635 return false
635 return false
636 end
636 end
637
637
638 def self.set_trac_db_host(host)
638 def self.set_trac_db_host(host)
639 return nil if host.blank?
639 return nil if host.blank?
640 @@trac_db_host = host
640 @@trac_db_host = host
641 end
641 end
642
642
643 def self.set_trac_db_port(port)
643 def self.set_trac_db_port(port)
644 return nil if port.to_i == 0
644 return nil if port.to_i == 0
645 @@trac_db_port = port.to_i
645 @@trac_db_port = port.to_i
646 end
646 end
647
647
648 def self.set_trac_db_name(name)
648 def self.set_trac_db_name(name)
649 return nil if name.blank?
649 return nil if name.blank?
650 @@trac_db_name = name
650 @@trac_db_name = name
651 end
651 end
652
652
653 def self.set_trac_db_username(username)
653 def self.set_trac_db_username(username)
654 @@trac_db_username = username
654 @@trac_db_username = username
655 end
655 end
656
656
657 def self.set_trac_db_password(password)
657 def self.set_trac_db_password(password)
658 @@trac_db_password = password
658 @@trac_db_password = password
659 end
659 end
660
660
661 def self.set_trac_db_schema(schema)
661 def self.set_trac_db_schema(schema)
662 @@trac_db_schema = schema
662 @@trac_db_schema = schema
663 end
663 end
664
664
665 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 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
666
666
667 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
667 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
668 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
668 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
669
669
670 def self.target_project_identifier(identifier)
670 def self.target_project_identifier(identifier)
671 project = Project.find_by_identifier(identifier)
671 project = Project.find_by_identifier(identifier)
672 if !project
672 if !project
673 # create the target project
673 # create the target project
674 project = Project.new :name => identifier.humanize,
674 project = Project.new :name => identifier.humanize,
675 :description => ''
675 :description => ''
676 project.identifier = identifier
676 project.identifier = identifier
677 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
677 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
678 # enable issues and wiki for the created project
678 # enable issues and wiki for the created project
679 project.enabled_module_names = ['issue_tracking', 'wiki']
679 project.enabled_module_names = ['issue_tracking', 'wiki']
680 else
680 else
681 puts
681 puts
682 puts "This project already exists in your Redmine database."
682 puts "This project already exists in your Redmine database."
683 print "Are you sure you want to append data to this project ? [Y/n] "
683 print "Are you sure you want to append data to this project ? [Y/n] "
684 STDOUT.flush
684 STDOUT.flush
685 exit if STDIN.gets.match(/^n$/i)
685 exit if STDIN.gets.match(/^n$/i)
686 end
686 end
687 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
687 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
688 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
688 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
689 @target_project = project.new_record? ? nil : project
689 @target_project = project.new_record? ? nil : project
690 @target_project.reload
690 @target_project.reload
691 end
691 end
692
692
693 def self.connection_params
693 def self.connection_params
694 if trac_adapter == 'sqlite3'
694 if trac_adapter == 'sqlite3'
695 {:adapter => 'sqlite3',
695 {:adapter => 'sqlite3',
696 :database => trac_db_path}
696 :database => trac_db_path}
697 else
697 else
698 {:adapter => trac_adapter,
698 {:adapter => trac_adapter,
699 :database => trac_db_name,
699 :database => trac_db_name,
700 :host => trac_db_host,
700 :host => trac_db_host,
701 :port => trac_db_port,
701 :port => trac_db_port,
702 :username => trac_db_username,
702 :username => trac_db_username,
703 :password => trac_db_password,
703 :password => trac_db_password,
704 :schema_search_path => trac_db_schema
704 :schema_search_path => trac_db_schema
705 }
705 }
706 end
706 end
707 end
707 end
708
708
709 def self.establish_connection
709 def self.establish_connection
710 constants.each do |const|
710 constants.each do |const|
711 klass = const_get(const)
711 klass = const_get(const)
712 next unless klass.respond_to? 'establish_connection'
712 next unless klass.respond_to? 'establish_connection'
713 klass.establish_connection connection_params
713 klass.establish_connection connection_params
714 end
714 end
715 end
715 end
716
716
717 def self.encode(text)
717 def self.encode(text)
718 if RUBY_VERSION < '1.9'
718 if RUBY_VERSION < '1.9'
719 @ic ||= Iconv.new('UTF-8', @charset)
719 @ic ||= Iconv.new('UTF-8', @charset)
720 @ic.iconv text
720 @ic.iconv text
721 else
721 else
722 text.to_s.force_encoding(@charset).encode('UTF-8')
722 text.to_s.force_encoding(@charset).encode('UTF-8')
723 end
723 end
724 end
724 end
725 end
725 end
726
726
727 puts
727 puts
728 if Redmine::DefaultData::Loader.no_data?
728 if Redmine::DefaultData::Loader.no_data?
729 puts "Redmine configuration need to be loaded before importing data."
729 puts "Redmine configuration need to be loaded before importing data."
730 puts "Please, run this first:"
730 puts "Please, run this first:"
731 puts
731 puts
732 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
732 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
733 exit
733 exit
734 end
734 end
735
735
736 puts "WARNING: a new project will be added to Redmine during this process."
736 puts "WARNING: a new project will be added to Redmine during this process."
737 print "Are you sure you want to continue ? [y/N] "
737 print "Are you sure you want to continue ? [y/N] "
738 STDOUT.flush
738 STDOUT.flush
739 break unless STDIN.gets.match(/^y$/i)
739 break unless STDIN.gets.match(/^y$/i)
740 puts
740 puts
741
741
742 def prompt(text, options = {}, &block)
742 def prompt(text, options = {}, &block)
743 default = options[:default] || ''
743 default = options[:default] || ''
744 while true
744 while true
745 print "#{text} [#{default}]: "
745 print "#{text} [#{default}]: "
746 STDOUT.flush
746 STDOUT.flush
747 value = STDIN.gets.chomp!
747 value = STDIN.gets.chomp!
748 value = default if value.blank?
748 value = default if value.blank?
749 break if yield value
749 break if yield value
750 end
750 end
751 end
751 end
752
752
753 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
753 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
754
754
755 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
755 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
756 prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
756 prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
757 unless %w(sqlite3).include?(TracMigrate.trac_adapter)
757 unless %w(sqlite3).include?(TracMigrate.trac_adapter)
758 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
758 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
759 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
759 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
760 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
760 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
761 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
761 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
762 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
762 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
763 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
763 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
764 end
764 end
765 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
765 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
766 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
766 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
767 puts
767 puts
768
768
769 old_notified_events = Setting.notified_events
769 old_notified_events = Setting.notified_events
770 old_password_min_length = Setting.password_min_length
770 begin
771 begin
771 # Turn off email notifications temporarily
772 # Turn off email notifications temporarily
772 Setting.notified_events = []
773 Setting.notified_events = []
774 Setting.password_min_length = 4
773 # Run the migration
775 # Run the migration
774 TracMigrate.migrate
776 TracMigrate.migrate
775 ensure
777 ensure
776 # Restore previous notification settings even if the migration fails
778 # Restore previous settings
777 Setting.notified_events = old_notified_events
779 Setting.notified_events = old_notified_events
780 Setting.password_min_length = old_password_min_length
778 end
781 end
779 end
782 end
780 end
783 end
781
784
General Comments 0
You need to be logged in to leave comments. Login now