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