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