##// END OF EJS Templates
Fixed: Trac migration of ticket:123 or [ticket:34] do not work (#2053)....
Jean-Philippe Lang -
r2011:d545ca736070
parent child
Show More
@@ -1,737 +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 271 # Situations like the following:
272 272 # [ticket:234 Text],[ticket:234 This is a test]
273 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '[[#\1|\2]]')
273 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
274 274 # Situations like:
275 275 # ticket:1234
276 276 # #1 is working cause Redmine uses the same syntax.
277 277 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
278 278 # Internal Links
279 279 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
280 280 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
281 281 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
282 282 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
283 283 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
284 284 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
285 285
286 286 # Links to pages UsingJustWikiCaps
287 287 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
288 288 # Normalize things that were supposed to not be links
289 289 # like !NotALink
290 290 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
291 291 # Revisions links
292 292 text = text.gsub(/\[(\d+)\]/, 'r\1')
293 293 # Ticket number re-writing
294 294 text = text.gsub(/#(\d+)/) do |s|
295 295 if $1.length < 10
296 296 TICKET_MAP[$1.to_i] ||= $1
297 297 "\##{TICKET_MAP[$1.to_i] || $1}"
298 298 else
299 299 s
300 300 end
301 301 end
302 302 # We would like to convert the Code highlighting too
303 303 # This will go into the next line.
304 304 shebang_line = false
305 305 # Reguar expression for start of code
306 306 pre_re = /\{\{\{/
307 307 # Code hightlighing...
308 308 shebang_re = /^\#\!([a-z]+)/
309 309 # Regular expression for end of code
310 310 pre_end_re = /\}\}\}/
311 311
312 312 # Go through the whole text..extract it line by line
313 313 text = text.gsub(/^(.*)$/) do |line|
314 314 m_pre = pre_re.match(line)
315 315 if m_pre
316 316 line = '<pre>'
317 317 else
318 318 m_sl = shebang_re.match(line)
319 319 if m_sl
320 320 shebang_line = true
321 321 line = '<code class="' + m_sl[1] + '">'
322 322 end
323 323 m_pre_end = pre_end_re.match(line)
324 324 if m_pre_end
325 325 line = '</pre>'
326 326 if shebang_line
327 327 line = '</code>' + line
328 328 end
329 329 end
330 330 end
331 331 line
332 332 end
333 333
334 334 # Highlighting
335 335 text = text.gsub(/'''''([^\s])/, '_*\1')
336 336 text = text.gsub(/([^\s])'''''/, '\1*_')
337 337 text = text.gsub(/'''/, '*')
338 338 text = text.gsub(/''/, '_')
339 339 text = text.gsub(/__/, '+')
340 340 text = text.gsub(/~~/, '-')
341 341 text = text.gsub(/`/, '@')
342 342 text = text.gsub(/,,/, '~')
343 343 # Lists
344 344 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
345 345
346 346 text
347 347 end
348 348
349 349 def self.migrate
350 350 establish_connection
351 351
352 352 # Quick database test
353 353 TracComponent.count
354 354
355 355 migrated_components = 0
356 356 migrated_milestones = 0
357 357 migrated_tickets = 0
358 358 migrated_custom_values = 0
359 359 migrated_ticket_attachments = 0
360 360 migrated_wiki_edits = 0
361 361 migrated_wiki_attachments = 0
362 362
363 363 #Wiki system initializing...
364 364 @target_project.wiki.destroy if @target_project.wiki
365 365 @target_project.reload
366 366 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
367 367 wiki_edit_count = 0
368 368
369 369 # Components
370 370 print "Migrating components"
371 371 issues_category_map = {}
372 372 TracComponent.find(:all).each do |component|
373 373 print '.'
374 374 STDOUT.flush
375 375 c = IssueCategory.new :project => @target_project,
376 376 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
377 377 next unless c.save
378 378 issues_category_map[component.name] = c
379 379 migrated_components += 1
380 380 end
381 381 puts
382 382
383 383 # Milestones
384 384 print "Migrating milestones"
385 385 version_map = {}
386 386 TracMilestone.find(:all).each do |milestone|
387 387 print '.'
388 388 STDOUT.flush
389 389 # First we try to find the wiki page...
390 390 p = wiki.find_or_new_page(milestone.name.to_s)
391 391 p.content = WikiContent.new(:page => p) if p.new_record?
392 392 p.content.text = milestone.description.to_s
393 393 p.content.author = find_or_create_user('trac')
394 394 p.content.comments = 'Milestone'
395 395 p.save
396 396
397 397 v = Version.new :project => @target_project,
398 398 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
399 399 :description => nil,
400 400 :wiki_page_title => milestone.name.to_s,
401 401 :effective_date => milestone.completed
402 402
403 403 next unless v.save
404 404 version_map[milestone.name] = v
405 405 migrated_milestones += 1
406 406 end
407 407 puts
408 408
409 409 # Custom fields
410 410 # TODO: read trac.ini instead
411 411 print "Migrating custom fields"
412 412 custom_field_map = {}
413 413 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
414 414 print '.'
415 415 STDOUT.flush
416 416 # Redmine custom field name
417 417 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
418 418 # Find if the custom already exists in Redmine
419 419 f = IssueCustomField.find_by_name(field_name)
420 420 # Or create a new one
421 421 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
422 422 :field_format => 'string')
423 423
424 424 next if f.new_record?
425 425 f.trackers = Tracker.find(:all)
426 426 f.projects << @target_project
427 427 custom_field_map[field.name] = f
428 428 end
429 429 puts
430 430
431 431 # Trac 'resolution' field as a Redmine custom field
432 432 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
433 433 r = IssueCustomField.new(:name => 'Resolution',
434 434 :field_format => 'list',
435 435 :is_filter => true) if r.nil?
436 436 r.trackers = Tracker.find(:all)
437 437 r.projects << @target_project
438 438 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
439 439 r.save!
440 440 custom_field_map['resolution'] = r
441 441
442 442 # Tickets
443 443 print "Migrating tickets"
444 444 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
445 445 print '.'
446 446 STDOUT.flush
447 447 i = Issue.new :project => @target_project,
448 448 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
449 449 :description => convert_wiki_text(encode(ticket.description)),
450 450 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
451 451 :created_on => ticket.time
452 452 i.author = find_or_create_user(ticket.reporter)
453 453 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
454 454 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
455 455 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
456 456 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
457 457 i.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank?
458 458 i.id = ticket.id unless Issue.exists?(ticket.id)
459 459 next unless Time.fake(ticket.changetime) { i.save }
460 460 TICKET_MAP[ticket.id] = i.id
461 461 migrated_tickets += 1
462 462
463 463 # Owner
464 464 unless ticket.owner.blank?
465 465 i.assigned_to = find_or_create_user(ticket.owner, true)
466 466 Time.fake(ticket.changetime) { i.save }
467 467 end
468 468
469 469 # Comments and status/resolution changes
470 470 ticket.changes.group_by(&:time).each do |time, changeset|
471 471 status_change = changeset.select {|change| change.field == 'status'}.first
472 472 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
473 473 comment_change = changeset.select {|change| change.field == 'comment'}.first
474 474
475 475 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
476 476 :created_on => time
477 477 n.user = find_or_create_user(changeset.first.author)
478 478 n.journalized = i
479 479 if status_change &&
480 480 STATUS_MAPPING[status_change.oldvalue] &&
481 481 STATUS_MAPPING[status_change.newvalue] &&
482 482 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
483 483 n.details << JournalDetail.new(:property => 'attr',
484 484 :prop_key => 'status_id',
485 485 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
486 486 :value => STATUS_MAPPING[status_change.newvalue].id)
487 487 end
488 488 if resolution_change
489 489 n.details << JournalDetail.new(:property => 'cf',
490 490 :prop_key => custom_field_map['resolution'].id,
491 491 :old_value => resolution_change.oldvalue,
492 492 :value => resolution_change.newvalue)
493 493 end
494 494 n.save unless n.details.empty? && n.notes.blank?
495 495 end
496 496
497 497 # Attachments
498 498 ticket.attachments.each do |attachment|
499 499 next unless attachment.exist?
500 500 a = Attachment.new :created_on => attachment.time
501 501 a.file = attachment
502 502 a.author = find_or_create_user(attachment.author)
503 503 a.container = i
504 504 a.description = attachment.description
505 505 migrated_ticket_attachments += 1 if a.save
506 506 end
507 507
508 508 # Custom fields
509 509 ticket.customs.each do |custom|
510 510 next if custom_field_map[custom.name].nil?
511 511 v = CustomValue.new :custom_field => custom_field_map[custom.name],
512 512 :value => custom.value
513 513 v.customized = i
514 514 next unless v.save
515 515 migrated_custom_values += 1
516 516 end
517 517 end
518 518
519 519 # update issue id sequence if needed (postgresql)
520 520 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
521 521 puts
522 522
523 523 # Wiki
524 524 print "Migrating wiki"
525 525 if wiki.save
526 526 TracWikiPage.find(:all, :order => 'name, version').each do |page|
527 527 # Do not migrate Trac manual wiki pages
528 528 next if TRAC_WIKI_PAGES.include?(page.name)
529 529 wiki_edit_count += 1
530 530 print '.'
531 531 STDOUT.flush
532 532 p = wiki.find_or_new_page(page.name)
533 533 p.content = WikiContent.new(:page => p) if p.new_record?
534 534 p.content.text = page.text
535 535 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
536 536 p.content.comments = page.comment
537 537 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
538 538
539 539 next if p.content.new_record?
540 540 migrated_wiki_edits += 1
541 541
542 542 # Attachments
543 543 page.attachments.each do |attachment|
544 544 next unless attachment.exist?
545 545 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
546 546 a = Attachment.new :created_on => attachment.time
547 547 a.file = attachment
548 548 a.author = find_or_create_user(attachment.author)
549 549 a.description = attachment.description
550 550 a.container = p
551 551 migrated_wiki_attachments += 1 if a.save
552 552 end
553 553 end
554 554
555 555 wiki.reload
556 556 wiki.pages.each do |page|
557 557 page.content.text = convert_wiki_text(page.content.text)
558 558 Time.fake(page.content.updated_on) { page.content.save }
559 559 end
560 560 end
561 561 puts
562 562
563 563 puts
564 564 puts "Components: #{migrated_components}/#{TracComponent.count}"
565 565 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
566 566 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
567 567 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
568 568 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
569 569 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
570 570 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
571 571 end
572 572
573 573 def self.limit_for(klass, attribute)
574 574 klass.columns_hash[attribute.to_s].limit
575 575 end
576 576
577 577 def self.encoding(charset)
578 578 @ic = Iconv.new('UTF-8', charset)
579 579 rescue Iconv::InvalidEncoding
580 580 puts "Invalid encoding!"
581 581 return false
582 582 end
583 583
584 584 def self.set_trac_directory(path)
585 585 @@trac_directory = path
586 586 raise "This directory doesn't exist!" unless File.directory?(path)
587 587 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
588 588 @@trac_directory
589 589 rescue Exception => e
590 590 puts e
591 591 return false
592 592 end
593 593
594 594 def self.trac_directory
595 595 @@trac_directory
596 596 end
597 597
598 598 def self.set_trac_adapter(adapter)
599 599 return false if adapter.blank?
600 600 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
601 601 # If adapter is sqlite or sqlite3, make sure that trac.db exists
602 602 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
603 603 @@trac_adapter = adapter
604 604 rescue Exception => e
605 605 puts e
606 606 return false
607 607 end
608 608
609 609 def self.set_trac_db_host(host)
610 610 return nil if host.blank?
611 611 @@trac_db_host = host
612 612 end
613 613
614 614 def self.set_trac_db_port(port)
615 615 return nil if port.to_i == 0
616 616 @@trac_db_port = port.to_i
617 617 end
618 618
619 619 def self.set_trac_db_name(name)
620 620 return nil if name.blank?
621 621 @@trac_db_name = name
622 622 end
623 623
624 624 def self.set_trac_db_username(username)
625 625 @@trac_db_username = username
626 626 end
627 627
628 628 def self.set_trac_db_password(password)
629 629 @@trac_db_password = password
630 630 end
631 631
632 632 def self.set_trac_db_schema(schema)
633 633 @@trac_db_schema = schema
634 634 end
635 635
636 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
637 637
638 638 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
639 639 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
640 640
641 641 def self.target_project_identifier(identifier)
642 642 project = Project.find_by_identifier(identifier)
643 643 if !project
644 644 # create the target project
645 645 project = Project.new :name => identifier.humanize,
646 646 :description => ''
647 647 project.identifier = identifier
648 648 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
649 649 # enable issues and wiki for the created project
650 650 project.enabled_module_names = ['issue_tracking', 'wiki']
651 651 else
652 652 puts
653 653 puts "This project already exists in your Redmine database."
654 654 print "Are you sure you want to append data to this project ? [Y/n] "
655 655 exit if STDIN.gets.match(/^n$/i)
656 656 end
657 657 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
658 658 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
659 659 @target_project = project.new_record? ? nil : project
660 660 end
661 661
662 662 def self.connection_params
663 663 if %w(sqlite sqlite3).include?(trac_adapter)
664 664 {:adapter => trac_adapter,
665 665 :database => trac_db_path}
666 666 else
667 667 {:adapter => trac_adapter,
668 668 :database => trac_db_name,
669 669 :host => trac_db_host,
670 670 :port => trac_db_port,
671 671 :username => trac_db_username,
672 672 :password => trac_db_password,
673 673 :schema_search_path => trac_db_schema
674 674 }
675 675 end
676 676 end
677 677
678 678 def self.establish_connection
679 679 constants.each do |const|
680 680 klass = const_get(const)
681 681 next unless klass.respond_to? 'establish_connection'
682 682 klass.establish_connection connection_params
683 683 end
684 684 end
685 685
686 686 private
687 687 def self.encode(text)
688 688 @ic.iconv text
689 689 rescue
690 690 text
691 691 end
692 692 end
693 693
694 694 puts
695 695 if Redmine::DefaultData::Loader.no_data?
696 696 puts "Redmine configuration need to be loaded before importing data."
697 697 puts "Please, run this first:"
698 698 puts
699 699 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
700 700 exit
701 701 end
702 702
703 703 puts "WARNING: a new project will be added to Redmine during this process."
704 704 print "Are you sure you want to continue ? [y/N] "
705 705 break unless STDIN.gets.match(/^y$/i)
706 706 puts
707 707
708 708 def prompt(text, options = {}, &block)
709 709 default = options[:default] || ''
710 710 while true
711 711 print "#{text} [#{default}]: "
712 712 value = STDIN.gets.chomp!
713 713 value = default if value.blank?
714 714 break if yield value
715 715 end
716 716 end
717 717
718 718 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
719 719
720 720 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
721 721 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
722 722 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
723 723 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
724 724 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
725 725 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
726 726 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
727 727 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
728 728 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
729 729 end
730 730 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
731 731 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
732 732 puts
733 733
734 734 TracMigrate.migrate
735 735 end
736 736 end
737 737
General Comments 0
You need to be logged in to leave comments. Login now