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