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