##// END OF EJS Templates
Merged r2280 from trunk (#2506)....
Jean-Philippe Lang -
r2305:1c1755d2783e
parent child
Show More
@@ -1,747 +1,750
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 # If this attribute is set a milestone has a defined target timepoint
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 set_table_name :permission
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 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 274 # ticket:1234
275 275 # #1 is working cause Redmine uses the same syntax.
276 276 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
277 277 # Milestone links:
278 278 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
279 279 # The text "Milestone 0.1.0 (Mercury)" is not converted,
280 280 # cause Redmine's wiki does not support this.
281 281 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
282 282 # [milestone:"0.1.0 Mercury"]
283 283 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
284 284 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
285 # milestone:0.1.0
285 # milestone:0.1.0
286 286 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
287 287 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
288 288 # Internal Links
289 289 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
290 290 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
291 291 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
292 292 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
293 293 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
294 294 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
295 295
296 # Links to pages UsingJustWikiCaps
297 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
298 # Normalize things that were supposed to not be links
299 # like !NotALink
300 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
296 # Links to pages UsingJustWikiCaps
297 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
298 # Normalize things that were supposed to not be links
299 # like !NotALink
300 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
301 301 # Revisions links
302 302 text = text.gsub(/\[(\d+)\]/, 'r\1')
303 303 # Ticket number re-writing
304 304 text = text.gsub(/#(\d+)/) do |s|
305 305 if $1.length < 10
306 306 TICKET_MAP[$1.to_i] ||= $1
307 307 "\##{TICKET_MAP[$1.to_i] || $1}"
308 308 else
309 309 s
310 310 end
311 311 end
312 312 # We would like to convert the Code highlighting too
313 313 # This will go into the next line.
314 314 shebang_line = false
315 315 # Reguar expression for start of code
316 316 pre_re = /\{\{\{/
317 317 # Code hightlighing...
318 318 shebang_re = /^\#\!([a-z]+)/
319 319 # Regular expression for end of code
320 320 pre_end_re = /\}\}\}/
321
321
322 322 # Go through the whole text..extract it line by line
323 323 text = text.gsub(/^(.*)$/) do |line|
324 324 m_pre = pre_re.match(line)
325 325 if m_pre
326 326 line = '<pre>'
327 327 else
328 328 m_sl = shebang_re.match(line)
329 329 if m_sl
330 330 shebang_line = true
331 331 line = '<code class="' + m_sl[1] + '">'
332 332 end
333 333 m_pre_end = pre_end_re.match(line)
334 334 if m_pre_end
335 335 line = '</pre>'
336 336 if shebang_line
337 337 line = '</code>' + line
338 338 end
339 339 end
340 340 end
341 line
341 line
342 342 end
343 343
344 344 # Highlighting
345 345 text = text.gsub(/'''''([^\s])/, '_*\1')
346 346 text = text.gsub(/([^\s])'''''/, '\1*_')
347 347 text = text.gsub(/'''/, '*')
348 348 text = text.gsub(/''/, '_')
349 349 text = text.gsub(/__/, '+')
350 350 text = text.gsub(/~~/, '-')
351 351 text = text.gsub(/`/, '@')
352 text = text.gsub(/,,/, '~')
352 text = text.gsub(/,,/, '~')
353 353 # Lists
354 354 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
355 355
356 356 text
357 357 end
358
358
359 359 def self.migrate
360 360 establish_connection
361 361
362 362 # Quick database test
363 363 TracComponent.count
364
364
365 365 migrated_components = 0
366 366 migrated_milestones = 0
367 367 migrated_tickets = 0
368 368 migrated_custom_values = 0
369 369 migrated_ticket_attachments = 0
370 migrated_wiki_edits = 0
370 migrated_wiki_edits = 0
371 371 migrated_wiki_attachments = 0
372 372
373 373 #Wiki system initializing...
374 374 @target_project.wiki.destroy if @target_project.wiki
375 375 @target_project.reload
376 376 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
377 377 wiki_edit_count = 0
378
378
379 379 # Components
380 380 print "Migrating components"
381 381 issues_category_map = {}
382 382 TracComponent.find(:all).each do |component|
383 print '.'
384 STDOUT.flush
383 print '.'
384 STDOUT.flush
385 385 c = IssueCategory.new :project => @target_project,
386 386 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
387 next unless c.save
388 issues_category_map[component.name] = c
389 migrated_components += 1
387 next unless c.save
388 issues_category_map[component.name] = c
389 migrated_components += 1
390 390 end
391 391 puts
392
392
393 393 # Milestones
394 394 print "Migrating milestones"
395 395 version_map = {}
396 396 TracMilestone.find(:all).each do |milestone|
397 397 print '.'
398 398 STDOUT.flush
399 399 # First we try to find the wiki page...
400 400 p = wiki.find_or_new_page(milestone.name.to_s)
401 401 p.content = WikiContent.new(:page => p) if p.new_record?
402 402 p.content.text = milestone.description.to_s
403 403 p.content.author = find_or_create_user('trac')
404 404 p.content.comments = 'Milestone'
405 405 p.save
406 406
407 407 v = Version.new :project => @target_project,
408 408 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
409 409 :description => nil,
410 410 :wiki_page_title => milestone.name.to_s,
411 411 :effective_date => milestone.completed
412 412
413 413 next unless v.save
414 414 version_map[milestone.name] = v
415 415 migrated_milestones += 1
416 416 end
417 417 puts
418
418
419 419 # Custom fields
420 420 # TODO: read trac.ini instead
421 421 print "Migrating custom fields"
422 422 custom_field_map = {}
423 423 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
424 424 print '.'
425 425 STDOUT.flush
426 426 # Redmine custom field name
427 427 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
428 428 # Find if the custom already exists in Redmine
429 429 f = IssueCustomField.find_by_name(field_name)
430 430 # Or create a new one
431 431 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
432 432 :field_format => 'string')
433
433
434 434 next if f.new_record?
435 435 f.trackers = Tracker.find(:all)
436 436 f.projects << @target_project
437 437 custom_field_map[field.name] = f
438 438 end
439 439 puts
440
440
441 441 # Trac 'resolution' field as a Redmine custom field
442 442 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
443 443 r = IssueCustomField.new(:name => 'Resolution',
444 444 :field_format => 'list',
445 445 :is_filter => true) if r.nil?
446 446 r.trackers = Tracker.find(:all)
447 447 r.projects << @target_project
448 448 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
449 449 r.save!
450 450 custom_field_map['resolution'] = r
451
451
452 452 # Tickets
453 453 print "Migrating tickets"
454 454 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
455 print '.'
456 STDOUT.flush
457 i = Issue.new :project => @target_project,
455 print '.'
456 STDOUT.flush
457 i = Issue.new :project => @target_project,
458 458 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
459 459 :description => convert_wiki_text(encode(ticket.description)),
460 460 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
461 461 :created_on => ticket.time
462 i.author = find_or_create_user(ticket.reporter)
463 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
464 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
465 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
466 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
467 i.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank?
468 i.id = ticket.id unless Issue.exists?(ticket.id)
469 next unless Time.fake(ticket.changetime) { i.save }
470 TICKET_MAP[ticket.id] = i.id
471 migrated_tickets += 1
472
473 # Owner
462 i.author = find_or_create_user(ticket.reporter)
463 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
464 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
465 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
466 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
467 i.id = ticket.id unless Issue.exists?(ticket.id)
468 next unless Time.fake(ticket.changetime) { i.save }
469 TICKET_MAP[ticket.id] = i.id
470 migrated_tickets += 1
471
472 # Owner
474 473 unless ticket.owner.blank?
475 474 i.assigned_to = find_or_create_user(ticket.owner, true)
476 475 Time.fake(ticket.changetime) { i.save }
477 476 end
478
479 # Comments and status/resolution changes
480 ticket.changes.group_by(&:time).each do |time, changeset|
477
478 # Comments and status/resolution changes
479 ticket.changes.group_by(&:time).each do |time, changeset|
481 480 status_change = changeset.select {|change| change.field == 'status'}.first
482 481 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
483 482 comment_change = changeset.select {|change| change.field == 'comment'}.first
484
483
485 484 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
486 485 :created_on => time
487 486 n.user = find_or_create_user(changeset.first.author)
488 487 n.journalized = i
489 if status_change &&
488 if status_change &&
490 489 STATUS_MAPPING[status_change.oldvalue] &&
491 490 STATUS_MAPPING[status_change.newvalue] &&
492 491 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
493 492 n.details << JournalDetail.new(:property => 'attr',
494 493 :prop_key => 'status_id',
495 494 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
496 495 :value => STATUS_MAPPING[status_change.newvalue].id)
497 496 end
498 497 if resolution_change
499 498 n.details << JournalDetail.new(:property => 'cf',
500 499 :prop_key => custom_field_map['resolution'].id,
501 500 :old_value => resolution_change.oldvalue,
502 501 :value => resolution_change.newvalue)
503 502 end
504 503 n.save unless n.details.empty? && n.notes.blank?
505 end
506
507 # Attachments
508 ticket.attachments.each do |attachment|
509 next unless attachment.exist?
504 end
505
506 # Attachments
507 ticket.attachments.each do |attachment|
508 next unless attachment.exist?
510 509 a = Attachment.new :created_on => attachment.time
511 510 a.file = attachment
512 511 a.author = find_or_create_user(attachment.author)
513 512 a.container = i
514 513 a.description = attachment.description
515 514 migrated_ticket_attachments += 1 if a.save
516 end
517
518 # Custom fields
519 ticket.customs.each do |custom|
520 next if custom_field_map[custom.name].nil?
521 v = CustomValue.new :custom_field => custom_field_map[custom.name],
522 :value => custom.value
523 v.customized = i
524 next unless v.save
515 end
516
517 # Custom fields
518 custom_values = ticket.customs.inject({}) do |h, custom|
519 if custom_field = custom_field_map[custom.name]
520 h[custom_field.id] = custom.value
525 521 migrated_custom_values += 1
526 end
522 end
523 h
524 end
525 if custom_field_map['resolution'] && !ticket.resolution.blank?
526 custom_values[custom_field_map['resolution'].id] = ticket.resolution
527 end
528 i.custom_field_values = custom_values
529 i.save_custom_field_values
527 530 end
528
531
529 532 # update issue id sequence if needed (postgresql)
530 533 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
531 534 puts
532
533 # Wiki
535
536 # Wiki
534 537 print "Migrating wiki"
535 538 if wiki.save
536 539 TracWikiPage.find(:all, :order => 'name, version').each do |page|
537 540 # Do not migrate Trac manual wiki pages
538 541 next if TRAC_WIKI_PAGES.include?(page.name)
539 542 wiki_edit_count += 1
540 543 print '.'
541 544 STDOUT.flush
542 545 p = wiki.find_or_new_page(page.name)
543 546 p.content = WikiContent.new(:page => p) if p.new_record?
544 547 p.content.text = page.text
545 548 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
546 549 p.content.comments = page.comment
547 550 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
548
551
549 552 next if p.content.new_record?
550 migrated_wiki_edits += 1
551
553 migrated_wiki_edits += 1
554
552 555 # Attachments
553 556 page.attachments.each do |attachment|
554 557 next unless attachment.exist?
555 558 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
556 559 a = Attachment.new :created_on => attachment.time
557 560 a.file = attachment
558 561 a.author = find_or_create_user(attachment.author)
559 562 a.description = attachment.description
560 563 a.container = p
561 564 migrated_wiki_attachments += 1 if a.save
562 565 end
563 566 end
564
567
565 568 wiki.reload
566 569 wiki.pages.each do |page|
567 570 page.content.text = convert_wiki_text(page.content.text)
568 571 Time.fake(page.content.updated_on) { page.content.save }
569 572 end
570 573 end
571 574 puts
572
575
573 576 puts
574 577 puts "Components: #{migrated_components}/#{TracComponent.count}"
575 578 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
576 579 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
577 580 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
578 581 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
579 582 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
580 583 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
581 584 end
582
585
583 586 def self.limit_for(klass, attribute)
584 587 klass.columns_hash[attribute.to_s].limit
585 588 end
586
589
587 590 def self.encoding(charset)
588 591 @ic = Iconv.new('UTF-8', charset)
589 592 rescue Iconv::InvalidEncoding
590 593 puts "Invalid encoding!"
591 594 return false
592 595 end
593
596
594 597 def self.set_trac_directory(path)
595 598 @@trac_directory = path
596 599 raise "This directory doesn't exist!" unless File.directory?(path)
597 600 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
598 601 @@trac_directory
599 602 rescue Exception => e
600 603 puts e
601 604 return false
602 605 end
603 606
604 607 def self.trac_directory
605 608 @@trac_directory
606 609 end
607 610
608 611 def self.set_trac_adapter(adapter)
609 612 return false if adapter.blank?
610 613 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
611 614 # If adapter is sqlite or sqlite3, make sure that trac.db exists
612 615 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
613 616 @@trac_adapter = adapter
614 617 rescue Exception => e
615 618 puts e
616 619 return false
617 620 end
618
621
619 622 def self.set_trac_db_host(host)
620 623 return nil if host.blank?
621 624 @@trac_db_host = host
622 625 end
623 626
624 627 def self.set_trac_db_port(port)
625 628 return nil if port.to_i == 0
626 629 @@trac_db_port = port.to_i
627 630 end
628
631
629 632 def self.set_trac_db_name(name)
630 633 return nil if name.blank?
631 634 @@trac_db_name = name
632 635 end
633 636
634 637 def self.set_trac_db_username(username)
635 638 @@trac_db_username = username
636 639 end
637
640
638 641 def self.set_trac_db_password(password)
639 642 @@trac_db_password = password
640 643 end
641
644
642 645 def self.set_trac_db_schema(schema)
643 646 @@trac_db_schema = schema
644 647 end
645 648
646 649 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
647
650
648 651 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
649 652 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
650
653
651 654 def self.target_project_identifier(identifier)
652 project = Project.find_by_identifier(identifier)
655 project = Project.find_by_identifier(identifier)
653 656 if !project
654 657 # create the target project
655 658 project = Project.new :name => identifier.humanize,
656 659 :description => ''
657 660 project.identifier = identifier
658 661 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
659 662 # enable issues and wiki for the created project
660 663 project.enabled_module_names = ['issue_tracking', 'wiki']
661 664 else
662 665 puts
663 666 puts "This project already exists in your Redmine database."
664 667 print "Are you sure you want to append data to this project ? [Y/n] "
665 exit if STDIN.gets.match(/^n$/i)
668 exit if STDIN.gets.match(/^n$/i)
666 669 end
667 670 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
668 671 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
669 672 @target_project = project.new_record? ? nil : project
670 673 end
671
674
672 675 def self.connection_params
673 676 if %w(sqlite sqlite3).include?(trac_adapter)
674 {:adapter => trac_adapter,
677 {:adapter => trac_adapter,
675 678 :database => trac_db_path}
676 679 else
677 680 {:adapter => trac_adapter,
678 681 :database => trac_db_name,
679 682 :host => trac_db_host,
680 683 :port => trac_db_port,
681 684 :username => trac_db_username,
682 685 :password => trac_db_password,
683 686 :schema_search_path => trac_db_schema
684 687 }
685 688 end
686 689 end
687
690
688 691 def self.establish_connection
689 692 constants.each do |const|
690 693 klass = const_get(const)
691 694 next unless klass.respond_to? 'establish_connection'
692 695 klass.establish_connection connection_params
693 696 end
694 697 end
695
698
696 699 private
697 700 def self.encode(text)
698 701 @ic.iconv text
699 702 rescue
700 703 text
701 704 end
702 705 end
703
706
704 707 puts
705 708 if Redmine::DefaultData::Loader.no_data?
706 709 puts "Redmine configuration need to be loaded before importing data."
707 710 puts "Please, run this first:"
708 711 puts
709 712 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
710 713 exit
711 714 end
712
715
713 716 puts "WARNING: a new project will be added to Redmine during this process."
714 717 print "Are you sure you want to continue ? [y/N] "
715 break unless STDIN.gets.match(/^y$/i)
718 break unless STDIN.gets.match(/^y$/i)
716 719 puts
717 720
718 721 def prompt(text, options = {}, &block)
719 722 default = options[:default] || ''
720 723 while true
721 724 print "#{text} [#{default}]: "
722 725 value = STDIN.gets.chomp!
723 726 value = default if value.blank?
724 727 break if yield value
725 728 end
726 729 end
727
730
728 731 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
729
732
730 733 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
731 734 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
732 735 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
733 736 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
734 737 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
735 738 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
736 739 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
737 740 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
738 741 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
739 742 end
740 743 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
741 744 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
742 745 puts
743
746
744 747 TracMigrate.migrate
745 748 end
746 749 end
747 750
General Comments 0
You need to be logged in to leave comments. Login now