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