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