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