##// END OF EJS Templates
Trac importer: improves wiki link conversion (#1287)....
Jean-Philippe Lang -
r1480:8aa57058cf0e
parent child
Show More
@@ -1,651 +1,652
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.get_values('IPRI')
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
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 end
102 102
103 103 class TracTicketCustom < ActiveRecord::Base
104 104 set_table_name :ticket_custom
105 105 end
106 106
107 107 class TracAttachment < ActiveRecord::Base
108 108 set_table_name :attachment
109 109 set_inheritance_column :none
110 110
111 111 def time; Time.at(read_attribute(:time)) end
112 112
113 113 def original_filename
114 114 filename
115 115 end
116 116
117 117 def content_type
118 118 Redmine::MimeType.of(filename) || ''
119 119 end
120 120
121 121 def exist?
122 122 File.file? trac_fullpath
123 123 end
124 124
125 125 def read
126 126 File.open("#{trac_fullpath}", 'rb').read
127 127 end
128 128
129 129 def description
130 130 read_attribute(:description).to_s.slice(0,255)
131 131 end
132 132
133 133 private
134 134 def trac_fullpath
135 135 attachment_type = read_attribute(:type)
136 136 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
137 137 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
138 138 end
139 139 end
140 140
141 141 class TracTicket < ActiveRecord::Base
142 142 set_table_name :ticket
143 143 set_inheritance_column :none
144 144
145 145 # ticket changes: only migrate status changes and comments
146 146 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
147 147 has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'ticket'"
148 148 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
149 149
150 150 def ticket_type
151 151 read_attribute(:type)
152 152 end
153 153
154 154 def summary
155 155 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
156 156 end
157 157
158 158 def description
159 159 read_attribute(:description).blank? ? summary : read_attribute(:description)
160 160 end
161 161
162 162 def time; Time.at(read_attribute(:time)) end
163 163 def changetime; Time.at(read_attribute(:changetime)) end
164 164 end
165 165
166 166 class TracTicketChange < ActiveRecord::Base
167 167 set_table_name :ticket_change
168 168
169 169 def time; Time.at(read_attribute(:time)) end
170 170 end
171 171
172 172 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
173 173 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
174 174 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
175 175 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
176 176 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
177 177 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
178 178 CamelCase TitleIndex)
179 179
180 180 class TracWikiPage < ActiveRecord::Base
181 181 set_table_name :wiki
182 182 set_primary_key :name
183 183
184 184 has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'wiki'"
185 185
186 186 def self.columns
187 187 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
188 188 super.select {|column| column.name.to_s != 'readonly'}
189 189 end
190 190
191 191 def time; Time.at(read_attribute(:time)) end
192 192 end
193 193
194 194 class TracPermission < ActiveRecord::Base
195 195 set_table_name :permission
196 196 end
197 197
198 198 def self.find_or_create_user(username, project_member = false)
199 199 return User.anonymous if username.blank?
200 200
201 201 u = User.find_by_login(username)
202 202 if !u
203 203 # Create a new user if not found
204 204 mail = username[0,limit_for(User, 'mail')]
205 205 mail = "#{mail}@foo.bar" unless mail.include?("@")
206 206 u = User.new :firstname => username[0,limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
207 207 :lastname => '-',
208 208 :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-')
209 209 u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
210 210 u.password = 'trac'
211 211 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
212 212 # finally, a default user is used if the new user is not valid
213 213 u = User.find(:first) unless u.save
214 214 end
215 215 # Make sure he is a member of the project
216 216 if project_member && !u.member_of?(@target_project)
217 217 role = DEFAULT_ROLE
218 218 if u.admin
219 219 role = ROLE_MAPPING['admin']
220 220 elsif TracPermission.find_by_username_and_action(username, 'developer')
221 221 role = ROLE_MAPPING['developer']
222 222 end
223 223 Member.create(:user => u, :project => @target_project, :role => role)
224 224 u.reload
225 225 end
226 226 u
227 227 end
228 228
229 229 # Basic wiki syntax conversion
230 230 def self.convert_wiki_text(text)
231 231 # Titles
232 232 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
233 233 # External Links
234 234 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
235 235 # Internal Links
236 236 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
237 237 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
238 238 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
239 239 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
240 text = text.gsub(/\[wiki:([^\s\]]+).*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
240 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
241 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
241 242
242 243 # Links to pages UsingJustWikiCaps
243 244 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
244 245 # Normalize things that were supposed to not be links
245 246 # like !NotALink
246 247 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
247 248 # Revisions links
248 249 text = text.gsub(/\[(\d+)\]/, 'r\1')
249 250 # Ticket number re-writing
250 251 text = text.gsub(/#(\d+)/) do |s|
251 252 if $1.length < 10
252 253 TICKET_MAP[$1.to_i] ||= $1
253 254 "\##{TICKET_MAP[$1.to_i] || $1}"
254 255 else
255 256 s
256 257 end
257 258 end
258 259 # Preformatted blocks
259 260 text = text.gsub(/\{\{\{/, '<pre>')
260 261 text = text.gsub(/\}\}\}/, '</pre>')
261 262 # Highlighting
262 263 text = text.gsub(/'''''([^\s])/, '_*\1')
263 264 text = text.gsub(/([^\s])'''''/, '\1*_')
264 265 text = text.gsub(/'''/, '*')
265 266 text = text.gsub(/''/, '_')
266 267 text = text.gsub(/__/, '+')
267 268 text = text.gsub(/~~/, '-')
268 269 text = text.gsub(/`/, '@')
269 270 text = text.gsub(/,,/, '~')
270 271 # Lists
271 272 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
272 273
273 274 text
274 275 end
275 276
276 277 def self.migrate
277 278 establish_connection
278 279
279 280 # Quick database test
280 281 TracComponent.count
281 282
282 283 migrated_components = 0
283 284 migrated_milestones = 0
284 285 migrated_tickets = 0
285 286 migrated_custom_values = 0
286 287 migrated_ticket_attachments = 0
287 288 migrated_wiki_edits = 0
288 289 migrated_wiki_attachments = 0
289 290
290 291 # Components
291 292 print "Migrating components"
292 293 issues_category_map = {}
293 294 TracComponent.find(:all).each do |component|
294 295 print '.'
295 296 STDOUT.flush
296 297 c = IssueCategory.new :project => @target_project,
297 298 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
298 299 next unless c.save
299 300 issues_category_map[component.name] = c
300 301 migrated_components += 1
301 302 end
302 303 puts
303 304
304 305 # Milestones
305 306 print "Migrating milestones"
306 307 version_map = {}
307 308 TracMilestone.find(:all).each do |milestone|
308 309 print '.'
309 310 STDOUT.flush
310 311 v = Version.new :project => @target_project,
311 312 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
312 313 :description => encode(milestone.description.to_s[0, limit_for(Version, 'description')]),
313 314 :effective_date => milestone.due
314 315 next unless v.save
315 316 version_map[milestone.name] = v
316 317 migrated_milestones += 1
317 318 end
318 319 puts
319 320
320 321 # Custom fields
321 322 # TODO: read trac.ini instead
322 323 print "Migrating custom fields"
323 324 custom_field_map = {}
324 325 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
325 326 print '.'
326 327 STDOUT.flush
327 328 # Redmine custom field name
328 329 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
329 330 # Find if the custom already exists in Redmine
330 331 f = IssueCustomField.find_by_name(field_name)
331 332 # Or create a new one
332 333 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
333 334 :field_format => 'string')
334 335
335 336 next if f.new_record?
336 337 f.trackers = Tracker.find(:all)
337 338 f.projects << @target_project
338 339 custom_field_map[field.name] = f
339 340 end
340 341 puts
341 342
342 343 # Trac 'resolution' field as a Redmine custom field
343 344 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
344 345 r = IssueCustomField.new(:name => 'Resolution',
345 346 :field_format => 'list',
346 347 :is_filter => true) if r.nil?
347 348 r.trackers = Tracker.find(:all)
348 349 r.projects << @target_project
349 350 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
350 351 r.save!
351 352 custom_field_map['resolution'] = r
352 353
353 354 # Tickets
354 355 print "Migrating tickets"
355 356 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
356 357 print '.'
357 358 STDOUT.flush
358 359 i = Issue.new :project => @target_project,
359 360 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
360 361 :description => convert_wiki_text(encode(ticket.description)),
361 362 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
362 363 :created_on => ticket.time
363 364 i.author = find_or_create_user(ticket.reporter)
364 365 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
365 366 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
366 367 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
367 368 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
368 369 i.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank?
369 370 i.id = ticket.id unless Issue.exists?(ticket.id)
370 371 next unless Time.fake(ticket.changetime) { i.save }
371 372 TICKET_MAP[ticket.id] = i.id
372 373 migrated_tickets += 1
373 374
374 375 # Owner
375 376 unless ticket.owner.blank?
376 377 i.assigned_to = find_or_create_user(ticket.owner, true)
377 378 Time.fake(ticket.changetime) { i.save }
378 379 end
379 380
380 381 # Comments and status/resolution changes
381 382 ticket.changes.group_by(&:time).each do |time, changeset|
382 383 status_change = changeset.select {|change| change.field == 'status'}.first
383 384 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
384 385 comment_change = changeset.select {|change| change.field == 'comment'}.first
385 386
386 387 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
387 388 :created_on => time
388 389 n.user = find_or_create_user(changeset.first.author)
389 390 n.journalized = i
390 391 if status_change &&
391 392 STATUS_MAPPING[status_change.oldvalue] &&
392 393 STATUS_MAPPING[status_change.newvalue] &&
393 394 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
394 395 n.details << JournalDetail.new(:property => 'attr',
395 396 :prop_key => 'status_id',
396 397 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
397 398 :value => STATUS_MAPPING[status_change.newvalue].id)
398 399 end
399 400 if resolution_change
400 401 n.details << JournalDetail.new(:property => 'cf',
401 402 :prop_key => custom_field_map['resolution'].id,
402 403 :old_value => resolution_change.oldvalue,
403 404 :value => resolution_change.newvalue)
404 405 end
405 406 n.save unless n.details.empty? && n.notes.blank?
406 407 end
407 408
408 409 # Attachments
409 410 ticket.attachments.each do |attachment|
410 411 next unless attachment.exist?
411 412 a = Attachment.new :created_on => attachment.time
412 413 a.file = attachment
413 414 a.author = find_or_create_user(attachment.author)
414 415 a.container = i
415 416 a.description = attachment.description
416 417 migrated_ticket_attachments += 1 if a.save
417 418 end
418 419
419 420 # Custom fields
420 421 ticket.customs.each do |custom|
421 422 next if custom_field_map[custom.name].nil?
422 423 v = CustomValue.new :custom_field => custom_field_map[custom.name],
423 424 :value => custom.value
424 425 v.customized = i
425 426 next unless v.save
426 427 migrated_custom_values += 1
427 428 end
428 429 end
429 430
430 431 # update issue id sequence if needed (postgresql)
431 432 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
432 433 puts
433 434
434 435 # Wiki
435 436 print "Migrating wiki"
436 437 @target_project.wiki.destroy if @target_project.wiki
437 438 @target_project.reload
438 439 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
439 440 wiki_edit_count = 0
440 441 if wiki.save
441 442 TracWikiPage.find(:all, :order => 'name, version').each do |page|
442 443 # Do not migrate Trac manual wiki pages
443 444 next if TRAC_WIKI_PAGES.include?(page.name)
444 445 wiki_edit_count += 1
445 446 print '.'
446 447 STDOUT.flush
447 448 p = wiki.find_or_new_page(page.name)
448 449 p.content = WikiContent.new(:page => p) if p.new_record?
449 450 p.content.text = page.text
450 451 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
451 452 p.content.comments = page.comment
452 453 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
453 454
454 455 next if p.content.new_record?
455 456 migrated_wiki_edits += 1
456 457
457 458 # Attachments
458 459 page.attachments.each do |attachment|
459 460 next unless attachment.exist?
460 461 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
461 462 a = Attachment.new :created_on => attachment.time
462 463 a.file = attachment
463 464 a.author = find_or_create_user(attachment.author)
464 465 a.description = attachment.description
465 466 a.container = p
466 467 migrated_wiki_attachments += 1 if a.save
467 468 end
468 469 end
469 470
470 471 wiki.reload
471 472 wiki.pages.each do |page|
472 473 page.content.text = convert_wiki_text(page.content.text)
473 474 Time.fake(page.content.updated_on) { page.content.save }
474 475 end
475 476 end
476 477 puts
477 478
478 479 puts
479 480 puts "Components: #{migrated_components}/#{TracComponent.count}"
480 481 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
481 482 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
482 483 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
483 484 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
484 485 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
485 486 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
486 487 end
487 488
488 489 def self.limit_for(klass, attribute)
489 490 klass.columns_hash[attribute.to_s].limit
490 491 end
491 492
492 493 def self.encoding(charset)
493 494 @ic = Iconv.new('UTF-8', charset)
494 495 rescue Iconv::InvalidEncoding
495 496 puts "Invalid encoding!"
496 497 return false
497 498 end
498 499
499 500 def self.set_trac_directory(path)
500 501 @@trac_directory = path
501 502 raise "This directory doesn't exist!" unless File.directory?(path)
502 503 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
503 504 @@trac_directory
504 505 rescue Exception => e
505 506 puts e
506 507 return false
507 508 end
508 509
509 510 def self.trac_directory
510 511 @@trac_directory
511 512 end
512 513
513 514 def self.set_trac_adapter(adapter)
514 515 return false if adapter.blank?
515 516 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
516 517 # If adapter is sqlite or sqlite3, make sure that trac.db exists
517 518 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
518 519 @@trac_adapter = adapter
519 520 rescue Exception => e
520 521 puts e
521 522 return false
522 523 end
523 524
524 525 def self.set_trac_db_host(host)
525 526 return nil if host.blank?
526 527 @@trac_db_host = host
527 528 end
528 529
529 530 def self.set_trac_db_port(port)
530 531 return nil if port.to_i == 0
531 532 @@trac_db_port = port.to_i
532 533 end
533 534
534 535 def self.set_trac_db_name(name)
535 536 return nil if name.blank?
536 537 @@trac_db_name = name
537 538 end
538 539
539 540 def self.set_trac_db_username(username)
540 541 @@trac_db_username = username
541 542 end
542 543
543 544 def self.set_trac_db_password(password)
544 545 @@trac_db_password = password
545 546 end
546 547
547 548 def self.set_trac_db_schema(schema)
548 549 @@trac_db_schema = schema
549 550 end
550 551
551 552 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
552 553
553 554 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
554 555 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
555 556
556 557 def self.target_project_identifier(identifier)
557 558 project = Project.find_by_identifier(identifier)
558 559 if !project
559 560 # create the target project
560 561 project = Project.new :name => identifier.humanize,
561 562 :description => ''
562 563 project.identifier = identifier
563 564 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
564 565 # enable issues and wiki for the created project
565 566 project.enabled_module_names = ['issue_tracking', 'wiki']
566 567 else
567 568 puts
568 569 puts "This project already exists in your Redmine database."
569 570 print "Are you sure you want to append data to this project ? [Y/n] "
570 571 exit if STDIN.gets.match(/^n$/i)
571 572 end
572 573 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
573 574 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
574 575 @target_project = project.new_record? ? nil : project
575 576 end
576 577
577 578 def self.connection_params
578 579 if %w(sqlite sqlite3).include?(trac_adapter)
579 580 {:adapter => trac_adapter,
580 581 :database => trac_db_path}
581 582 else
582 583 {:adapter => trac_adapter,
583 584 :database => trac_db_name,
584 585 :host => trac_db_host,
585 586 :port => trac_db_port,
586 587 :username => trac_db_username,
587 588 :password => trac_db_password,
588 589 :schema_search_path => trac_db_schema
589 590 }
590 591 end
591 592 end
592 593
593 594 def self.establish_connection
594 595 constants.each do |const|
595 596 klass = const_get(const)
596 597 next unless klass.respond_to? 'establish_connection'
597 598 klass.establish_connection connection_params
598 599 end
599 600 end
600 601
601 602 private
602 603 def self.encode(text)
603 604 @ic.iconv text
604 605 rescue
605 606 text
606 607 end
607 608 end
608 609
609 610 puts
610 611 if Redmine::DefaultData::Loader.no_data?
611 612 puts "Redmine configuration need to be loaded before importing data."
612 613 puts "Please, run this first:"
613 614 puts
614 615 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
615 616 exit
616 617 end
617 618
618 619 puts "WARNING: a new project will be added to Redmine during this process."
619 620 print "Are you sure you want to continue ? [y/N] "
620 621 break unless STDIN.gets.match(/^y$/i)
621 622 puts
622 623
623 624 def prompt(text, options = {}, &block)
624 625 default = options[:default] || ''
625 626 while true
626 627 print "#{text} [#{default}]: "
627 628 value = STDIN.gets.chomp!
628 629 value = default if value.blank?
629 630 break if yield value
630 631 end
631 632 end
632 633
633 634 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
634 635
635 636 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
636 637 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
637 638 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
638 639 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
639 640 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
640 641 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
641 642 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
642 643 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
643 644 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
644 645 end
645 646 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
646 647 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
647 648 puts
648 649
649 650 TracMigrate.migrate
650 651 end
651 652 end
General Comments 0
You need to be logged in to leave comments. Login now