##// END OF EJS Templates
gender neutral source comment at lib/tasks/migrate_from_trac.rake...
Toshi MARUYAMA -
r11760:c5ca4e2622df
parent child
Show More
@@ -1,781 +1,781
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'active_record'
19 19 require 'iconv' if RUBY_VERSION < '1.9'
20 20 require 'pp'
21 21
22 22 namespace :redmine do
23 23 desc 'Trac migration script'
24 24 task :migrate_from_trac => :environment do
25 25
26 26 module TracMigrate
27 27 TICKET_MAP = []
28 28
29 29 DEFAULT_STATUS = IssueStatus.default
30 30 assigned_status = IssueStatus.find_by_position(2)
31 31 resolved_status = IssueStatus.find_by_position(3)
32 32 feedback_status = IssueStatus.find_by_position(4)
33 33 closed_status = IssueStatus.where(:is_closed => true).first
34 34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
35 35 'reopened' => feedback_status,
36 36 'assigned' => assigned_status,
37 37 'closed' => closed_status
38 38 }
39 39
40 40 priorities = IssuePriority.all
41 41 DEFAULT_PRIORITY = priorities[0]
42 42 PRIORITY_MAPPING = {'lowest' => priorities[0],
43 43 'low' => priorities[0],
44 44 'normal' => priorities[1],
45 45 'high' => priorities[2],
46 46 'highest' => priorities[3],
47 47 # ---
48 48 'trivial' => priorities[0],
49 49 'minor' => priorities[1],
50 50 'major' => priorities[2],
51 51 'critical' => priorities[3],
52 52 'blocker' => priorities[4]
53 53 }
54 54
55 55 TRACKER_BUG = Tracker.find_by_position(1)
56 56 TRACKER_FEATURE = Tracker.find_by_position(2)
57 57 DEFAULT_TRACKER = TRACKER_BUG
58 58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
59 59 'enhancement' => TRACKER_FEATURE,
60 60 'task' => TRACKER_FEATURE,
61 61 'patch' =>TRACKER_FEATURE
62 62 }
63 63
64 64 roles = Role.where(:builtin => 0).order('position ASC').all
65 65 manager_role = roles[0]
66 66 developer_role = roles[1]
67 67 DEFAULT_ROLE = roles.last
68 68 ROLE_MAPPING = {'admin' => manager_role,
69 69 'developer' => developer_role
70 70 }
71 71
72 72 class ::Time
73 73 class << self
74 74 alias :real_now :now
75 75 def now
76 76 real_now - @fake_diff.to_i
77 77 end
78 78 def fake(time)
79 79 @fake_diff = real_now - time
80 80 res = yield
81 81 @fake_diff = 0
82 82 res
83 83 end
84 84 end
85 85 end
86 86
87 87 class TracComponent < ActiveRecord::Base
88 88 self.table_name = :component
89 89 end
90 90
91 91 class TracMilestone < ActiveRecord::Base
92 92 self.table_name = :milestone
93 93 # If this attribute is set a milestone has a defined target timepoint
94 94 def due
95 95 if read_attribute(:due) && read_attribute(:due) > 0
96 96 Time.at(read_attribute(:due)).to_date
97 97 else
98 98 nil
99 99 end
100 100 end
101 101 # This is the real timepoint at which the milestone has finished.
102 102 def completed
103 103 if read_attribute(:completed) && read_attribute(:completed) > 0
104 104 Time.at(read_attribute(:completed)).to_date
105 105 else
106 106 nil
107 107 end
108 108 end
109 109
110 110 def description
111 111 # Attribute is named descr in Trac v0.8.x
112 112 has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
113 113 end
114 114 end
115 115
116 116 class TracTicketCustom < ActiveRecord::Base
117 117 self.table_name = :ticket_custom
118 118 end
119 119
120 120 class TracAttachment < ActiveRecord::Base
121 121 self.table_name = :attachment
122 122 set_inheritance_column :none
123 123
124 124 def time; Time.at(read_attribute(:time)) end
125 125
126 126 def original_filename
127 127 filename
128 128 end
129 129
130 130 def content_type
131 131 ''
132 132 end
133 133
134 134 def exist?
135 135 File.file? trac_fullpath
136 136 end
137 137
138 138 def open
139 139 File.open("#{trac_fullpath}", 'rb') {|f|
140 140 @file = f
141 141 yield self
142 142 }
143 143 end
144 144
145 145 def read(*args)
146 146 @file.read(*args)
147 147 end
148 148
149 149 def description
150 150 read_attribute(:description).to_s.slice(0,255)
151 151 end
152 152
153 153 private
154 154 def trac_fullpath
155 155 attachment_type = read_attribute(:type)
156 156 #replace exotic characters with their hex representation to avoid invalid filenames
157 157 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) do |x|
158 158 codepoint = RUBY_VERSION < '1.9' ? x[0] : x.codepoints.to_a[0]
159 159 sprintf('%%%02x', codepoint)
160 160 end
161 161 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
162 162 end
163 163 end
164 164
165 165 class TracTicket < ActiveRecord::Base
166 166 self.table_name = :ticket
167 167 set_inheritance_column :none
168 168
169 169 # ticket changes: only migrate status changes and comments
170 170 has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
171 171 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
172 172
173 173 def attachments
174 174 TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
175 175 end
176 176
177 177 def ticket_type
178 178 read_attribute(:type)
179 179 end
180 180
181 181 def summary
182 182 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
183 183 end
184 184
185 185 def description
186 186 read_attribute(:description).blank? ? summary : read_attribute(:description)
187 187 end
188 188
189 189 def time; Time.at(read_attribute(:time)) end
190 190 def changetime; Time.at(read_attribute(:changetime)) end
191 191 end
192 192
193 193 class TracTicketChange < ActiveRecord::Base
194 194 self.table_name = :ticket_change
195 195
196 196 def self.columns
197 197 # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
198 198 super.select {|column| column.name.to_s != 'field'}
199 199 end
200 200
201 201 def time; Time.at(read_attribute(:time)) end
202 202 end
203 203
204 204 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
205 205 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
206 206 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
207 207 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
208 208 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
209 209 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
210 210 CamelCase TitleIndex)
211 211
212 212 class TracWikiPage < ActiveRecord::Base
213 213 self.table_name = :wiki
214 214 set_primary_key :name
215 215
216 216 def self.columns
217 217 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
218 218 super.select {|column| column.name.to_s != 'readonly'}
219 219 end
220 220
221 221 def attachments
222 222 TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
223 223 end
224 224
225 225 def time; Time.at(read_attribute(:time)) end
226 226 end
227 227
228 228 class TracPermission < ActiveRecord::Base
229 229 self.table_name = :permission
230 230 end
231 231
232 232 class TracSessionAttribute < ActiveRecord::Base
233 233 self.table_name = :session_attribute
234 234 end
235 235
236 236 def self.find_or_create_user(username, project_member = false)
237 237 return User.anonymous if username.blank?
238 238
239 239 u = User.find_by_login(username)
240 240 if !u
241 241 # Create a new user if not found
242 242 mail = username[0, User::MAIL_LENGTH_LIMIT]
243 243 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
244 244 mail = mail_attr.value
245 245 end
246 246 mail = "#{mail}@foo.bar" unless mail.include?("@")
247 247
248 248 name = username
249 249 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
250 250 name = name_attr.value
251 251 end
252 252 name =~ (/(.*)(\s+\w+)?/)
253 253 fn = $1.strip
254 254 ln = ($2 || '-').strip
255 255
256 256 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
257 257 :firstname => fn[0, limit_for(User, 'firstname')],
258 258 :lastname => ln[0, limit_for(User, 'lastname')]
259 259
260 260 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
261 261 u.password = 'trac'
262 262 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
263 263 # finally, a default user is used if the new user is not valid
264 264 u = User.first unless u.save
265 265 end
266 # Make sure he is a member of the project
266 # Make sure user is a member of the project
267 267 if project_member && !u.member_of?(@target_project)
268 268 role = DEFAULT_ROLE
269 269 if u.admin
270 270 role = ROLE_MAPPING['admin']
271 271 elsif TracPermission.find_by_username_and_action(username, 'developer')
272 272 role = ROLE_MAPPING['developer']
273 273 end
274 274 Member.create(:user => u, :project => @target_project, :roles => [role])
275 275 u.reload
276 276 end
277 277 u
278 278 end
279 279
280 280 # Basic wiki syntax conversion
281 281 def self.convert_wiki_text(text)
282 282 # Titles
283 283 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
284 284 # External Links
285 285 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
286 286 # Ticket links:
287 287 # [ticket:234 Text],[ticket:234 This is a test]
288 288 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
289 289 # ticket:1234
290 290 # #1 is working cause Redmine uses the same syntax.
291 291 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
292 292 # Milestone links:
293 293 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
294 294 # The text "Milestone 0.1.0 (Mercury)" is not converted,
295 295 # cause Redmine's wiki does not support this.
296 296 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
297 297 # [milestone:"0.1.0 Mercury"]
298 298 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
299 299 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
300 300 # milestone:0.1.0
301 301 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
302 302 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
303 303 # Internal Links
304 304 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
305 305 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
306 306 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
307 307 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
308 308 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
309 309 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
310 310
311 311 # Links to pages UsingJustWikiCaps
312 312 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
313 313 # Normalize things that were supposed to not be links
314 314 # like !NotALink
315 315 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
316 316 # Revisions links
317 317 text = text.gsub(/\[(\d+)\]/, 'r\1')
318 318 # Ticket number re-writing
319 319 text = text.gsub(/#(\d+)/) do |s|
320 320 if $1.length < 10
321 321 # TICKET_MAP[$1.to_i] ||= $1
322 322 "\##{TICKET_MAP[$1.to_i] || $1}"
323 323 else
324 324 s
325 325 end
326 326 end
327 327 # We would like to convert the Code highlighting too
328 328 # This will go into the next line.
329 329 shebang_line = false
330 330 # Reguar expression for start of code
331 331 pre_re = /\{\{\{/
332 332 # Code hightlighing...
333 333 shebang_re = /^\#\!([a-z]+)/
334 334 # Regular expression for end of code
335 335 pre_end_re = /\}\}\}/
336 336
337 337 # Go through the whole text..extract it line by line
338 338 text = text.gsub(/^(.*)$/) do |line|
339 339 m_pre = pre_re.match(line)
340 340 if m_pre
341 341 line = '<pre>'
342 342 else
343 343 m_sl = shebang_re.match(line)
344 344 if m_sl
345 345 shebang_line = true
346 346 line = '<code class="' + m_sl[1] + '">'
347 347 end
348 348 m_pre_end = pre_end_re.match(line)
349 349 if m_pre_end
350 350 line = '</pre>'
351 351 if shebang_line
352 352 line = '</code>' + line
353 353 end
354 354 end
355 355 end
356 356 line
357 357 end
358 358
359 359 # Highlighting
360 360 text = text.gsub(/'''''([^\s])/, '_*\1')
361 361 text = text.gsub(/([^\s])'''''/, '\1*_')
362 362 text = text.gsub(/'''/, '*')
363 363 text = text.gsub(/''/, '_')
364 364 text = text.gsub(/__/, '+')
365 365 text = text.gsub(/~~/, '-')
366 366 text = text.gsub(/`/, '@')
367 367 text = text.gsub(/,,/, '~')
368 368 # Lists
369 369 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
370 370
371 371 text
372 372 end
373 373
374 374 def self.migrate
375 375 establish_connection
376 376
377 377 # Quick database test
378 378 TracComponent.count
379 379
380 380 migrated_components = 0
381 381 migrated_milestones = 0
382 382 migrated_tickets = 0
383 383 migrated_custom_values = 0
384 384 migrated_ticket_attachments = 0
385 385 migrated_wiki_edits = 0
386 386 migrated_wiki_attachments = 0
387 387
388 388 #Wiki system initializing...
389 389 @target_project.wiki.destroy if @target_project.wiki
390 390 @target_project.reload
391 391 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
392 392 wiki_edit_count = 0
393 393
394 394 # Components
395 395 print "Migrating components"
396 396 issues_category_map = {}
397 397 TracComponent.all.each do |component|
398 398 print '.'
399 399 STDOUT.flush
400 400 c = IssueCategory.new :project => @target_project,
401 401 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
402 402 next unless c.save
403 403 issues_category_map[component.name] = c
404 404 migrated_components += 1
405 405 end
406 406 puts
407 407
408 408 # Milestones
409 409 print "Migrating milestones"
410 410 version_map = {}
411 411 TracMilestone.all.each do |milestone|
412 412 print '.'
413 413 STDOUT.flush
414 414 # First we try to find the wiki page...
415 415 p = wiki.find_or_new_page(milestone.name.to_s)
416 416 p.content = WikiContent.new(:page => p) if p.new_record?
417 417 p.content.text = milestone.description.to_s
418 418 p.content.author = find_or_create_user('trac')
419 419 p.content.comments = 'Milestone'
420 420 p.save
421 421
422 422 v = Version.new :project => @target_project,
423 423 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
424 424 :description => nil,
425 425 :wiki_page_title => milestone.name.to_s,
426 426 :effective_date => milestone.completed
427 427
428 428 next unless v.save
429 429 version_map[milestone.name] = v
430 430 migrated_milestones += 1
431 431 end
432 432 puts
433 433
434 434 # Custom fields
435 435 # TODO: read trac.ini instead
436 436 print "Migrating custom fields"
437 437 custom_field_map = {}
438 438 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
439 439 print '.'
440 440 STDOUT.flush
441 441 # Redmine custom field name
442 442 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
443 443 # Find if the custom already exists in Redmine
444 444 f = IssueCustomField.find_by_name(field_name)
445 445 # Or create a new one
446 446 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
447 447 :field_format => 'string')
448 448
449 449 next if f.new_record?
450 450 f.trackers = Tracker.all
451 451 f.projects << @target_project
452 452 custom_field_map[field.name] = f
453 453 end
454 454 puts
455 455
456 456 # Trac 'resolution' field as a Redmine custom field
457 457 r = IssueCustomField.where(:name => "Resolution").first
458 458 r = IssueCustomField.new(:name => 'Resolution',
459 459 :field_format => 'list',
460 460 :is_filter => true) if r.nil?
461 461 r.trackers = Tracker.all
462 462 r.projects << @target_project
463 463 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
464 464 r.save!
465 465 custom_field_map['resolution'] = r
466 466
467 467 # Tickets
468 468 print "Migrating tickets"
469 469 TracTicket.find_each(:batch_size => 200) do |ticket|
470 470 print '.'
471 471 STDOUT.flush
472 472 i = Issue.new :project => @target_project,
473 473 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
474 474 :description => convert_wiki_text(encode(ticket.description)),
475 475 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
476 476 :created_on => ticket.time
477 477 i.author = find_or_create_user(ticket.reporter)
478 478 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
479 479 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
480 480 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
481 481 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
482 482 i.id = ticket.id unless Issue.exists?(ticket.id)
483 483 next unless Time.fake(ticket.changetime) { i.save }
484 484 TICKET_MAP[ticket.id] = i.id
485 485 migrated_tickets += 1
486 486
487 487 # Owner
488 488 unless ticket.owner.blank?
489 489 i.assigned_to = find_or_create_user(ticket.owner, true)
490 490 Time.fake(ticket.changetime) { i.save }
491 491 end
492 492
493 493 # Comments and status/resolution changes
494 494 ticket.ticket_changes.group_by(&:time).each do |time, changeset|
495 495 status_change = changeset.select {|change| change.field == 'status'}.first
496 496 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
497 497 comment_change = changeset.select {|change| change.field == 'comment'}.first
498 498
499 499 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
500 500 :created_on => time
501 501 n.user = find_or_create_user(changeset.first.author)
502 502 n.journalized = i
503 503 if status_change &&
504 504 STATUS_MAPPING[status_change.oldvalue] &&
505 505 STATUS_MAPPING[status_change.newvalue] &&
506 506 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
507 507 n.details << JournalDetail.new(:property => 'attr',
508 508 :prop_key => 'status_id',
509 509 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
510 510 :value => STATUS_MAPPING[status_change.newvalue].id)
511 511 end
512 512 if resolution_change
513 513 n.details << JournalDetail.new(:property => 'cf',
514 514 :prop_key => custom_field_map['resolution'].id,
515 515 :old_value => resolution_change.oldvalue,
516 516 :value => resolution_change.newvalue)
517 517 end
518 518 n.save unless n.details.empty? && n.notes.blank?
519 519 end
520 520
521 521 # Attachments
522 522 ticket.attachments.each do |attachment|
523 523 next unless attachment.exist?
524 524 attachment.open {
525 525 a = Attachment.new :created_on => attachment.time
526 526 a.file = attachment
527 527 a.author = find_or_create_user(attachment.author)
528 528 a.container = i
529 529 a.description = attachment.description
530 530 migrated_ticket_attachments += 1 if a.save
531 531 }
532 532 end
533 533
534 534 # Custom fields
535 535 custom_values = ticket.customs.inject({}) do |h, custom|
536 536 if custom_field = custom_field_map[custom.name]
537 537 h[custom_field.id] = custom.value
538 538 migrated_custom_values += 1
539 539 end
540 540 h
541 541 end
542 542 if custom_field_map['resolution'] && !ticket.resolution.blank?
543 543 custom_values[custom_field_map['resolution'].id] = ticket.resolution
544 544 end
545 545 i.custom_field_values = custom_values
546 546 i.save_custom_field_values
547 547 end
548 548
549 549 # update issue id sequence if needed (postgresql)
550 550 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
551 551 puts
552 552
553 553 # Wiki
554 554 print "Migrating wiki"
555 555 if wiki.save
556 556 TracWikiPage.order('name, version').all.each do |page|
557 557 # Do not migrate Trac manual wiki pages
558 558 next if TRAC_WIKI_PAGES.include?(page.name)
559 559 wiki_edit_count += 1
560 560 print '.'
561 561 STDOUT.flush
562 562 p = wiki.find_or_new_page(page.name)
563 563 p.content = WikiContent.new(:page => p) if p.new_record?
564 564 p.content.text = page.text
565 565 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
566 566 p.content.comments = page.comment
567 567 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
568 568
569 569 next if p.content.new_record?
570 570 migrated_wiki_edits += 1
571 571
572 572 # Attachments
573 573 page.attachments.each do |attachment|
574 574 next unless attachment.exist?
575 575 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
576 576 attachment.open {
577 577 a = Attachment.new :created_on => attachment.time
578 578 a.file = attachment
579 579 a.author = find_or_create_user(attachment.author)
580 580 a.description = attachment.description
581 581 a.container = p
582 582 migrated_wiki_attachments += 1 if a.save
583 583 }
584 584 end
585 585 end
586 586
587 587 wiki.reload
588 588 wiki.pages.each do |page|
589 589 page.content.text = convert_wiki_text(page.content.text)
590 590 Time.fake(page.content.updated_on) { page.content.save }
591 591 end
592 592 end
593 593 puts
594 594
595 595 puts
596 596 puts "Components: #{migrated_components}/#{TracComponent.count}"
597 597 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
598 598 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
599 599 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
600 600 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
601 601 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
602 602 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
603 603 end
604 604
605 605 def self.limit_for(klass, attribute)
606 606 klass.columns_hash[attribute.to_s].limit
607 607 end
608 608
609 609 def self.encoding(charset)
610 610 @charset = charset
611 611 end
612 612
613 613 def self.set_trac_directory(path)
614 614 @@trac_directory = path
615 615 raise "This directory doesn't exist!" unless File.directory?(path)
616 616 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
617 617 @@trac_directory
618 618 rescue Exception => e
619 619 puts e
620 620 return false
621 621 end
622 622
623 623 def self.trac_directory
624 624 @@trac_directory
625 625 end
626 626
627 627 def self.set_trac_adapter(adapter)
628 628 return false if adapter.blank?
629 629 raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
630 630 # If adapter is sqlite or sqlite3, make sure that trac.db exists
631 631 raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
632 632 @@trac_adapter = adapter
633 633 rescue Exception => e
634 634 puts e
635 635 return false
636 636 end
637 637
638 638 def self.set_trac_db_host(host)
639 639 return nil if host.blank?
640 640 @@trac_db_host = host
641 641 end
642 642
643 643 def self.set_trac_db_port(port)
644 644 return nil if port.to_i == 0
645 645 @@trac_db_port = port.to_i
646 646 end
647 647
648 648 def self.set_trac_db_name(name)
649 649 return nil if name.blank?
650 650 @@trac_db_name = name
651 651 end
652 652
653 653 def self.set_trac_db_username(username)
654 654 @@trac_db_username = username
655 655 end
656 656
657 657 def self.set_trac_db_password(password)
658 658 @@trac_db_password = password
659 659 end
660 660
661 661 def self.set_trac_db_schema(schema)
662 662 @@trac_db_schema = schema
663 663 end
664 664
665 665 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
666 666
667 667 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
668 668 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
669 669
670 670 def self.target_project_identifier(identifier)
671 671 project = Project.find_by_identifier(identifier)
672 672 if !project
673 673 # create the target project
674 674 project = Project.new :name => identifier.humanize,
675 675 :description => ''
676 676 project.identifier = identifier
677 677 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
678 678 # enable issues and wiki for the created project
679 679 project.enabled_module_names = ['issue_tracking', 'wiki']
680 680 else
681 681 puts
682 682 puts "This project already exists in your Redmine database."
683 683 print "Are you sure you want to append data to this project ? [Y/n] "
684 684 STDOUT.flush
685 685 exit if STDIN.gets.match(/^n$/i)
686 686 end
687 687 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
688 688 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
689 689 @target_project = project.new_record? ? nil : project
690 690 @target_project.reload
691 691 end
692 692
693 693 def self.connection_params
694 694 if trac_adapter == 'sqlite3'
695 695 {:adapter => 'sqlite3',
696 696 :database => trac_db_path}
697 697 else
698 698 {:adapter => trac_adapter,
699 699 :database => trac_db_name,
700 700 :host => trac_db_host,
701 701 :port => trac_db_port,
702 702 :username => trac_db_username,
703 703 :password => trac_db_password,
704 704 :schema_search_path => trac_db_schema
705 705 }
706 706 end
707 707 end
708 708
709 709 def self.establish_connection
710 710 constants.each do |const|
711 711 klass = const_get(const)
712 712 next unless klass.respond_to? 'establish_connection'
713 713 klass.establish_connection connection_params
714 714 end
715 715 end
716 716
717 717 def self.encode(text)
718 718 if RUBY_VERSION < '1.9'
719 719 @ic ||= Iconv.new('UTF-8', @charset)
720 720 @ic.iconv text
721 721 else
722 722 text.to_s.force_encoding(@charset).encode('UTF-8')
723 723 end
724 724 end
725 725 end
726 726
727 727 puts
728 728 if Redmine::DefaultData::Loader.no_data?
729 729 puts "Redmine configuration need to be loaded before importing data."
730 730 puts "Please, run this first:"
731 731 puts
732 732 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
733 733 exit
734 734 end
735 735
736 736 puts "WARNING: a new project will be added to Redmine during this process."
737 737 print "Are you sure you want to continue ? [y/N] "
738 738 STDOUT.flush
739 739 break unless STDIN.gets.match(/^y$/i)
740 740 puts
741 741
742 742 def prompt(text, options = {}, &block)
743 743 default = options[:default] || ''
744 744 while true
745 745 print "#{text} [#{default}]: "
746 746 STDOUT.flush
747 747 value = STDIN.gets.chomp!
748 748 value = default if value.blank?
749 749 break if yield value
750 750 end
751 751 end
752 752
753 753 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
754 754
755 755 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
756 756 prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
757 757 unless %w(sqlite3).include?(TracMigrate.trac_adapter)
758 758 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
759 759 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
760 760 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
761 761 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
762 762 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
763 763 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
764 764 end
765 765 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
766 766 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
767 767 puts
768 768
769 769 old_notified_events = Setting.notified_events
770 770 begin
771 771 # Turn off email notifications temporarily
772 772 Setting.notified_events = []
773 773 # Run the migration
774 774 TracMigrate.migrate
775 775 ensure
776 776 # Restore previous notification settings even if the migration fails
777 777 Setting.notified_events = old_notified_events
778 778 end
779 779 end
780 780 end
781 781
General Comments 0
You need to be logged in to leave comments. Login now