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