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