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