17 |
17 |
|
18 |
18 |
require 'active_record'
|
19 |
19 |
require 'pp'
|
|
20 |
require 'digest/sha1'
|
20 |
21 |
|
21 |
22 |
namespace :redmine do
|
22 |
23 |
desc 'Trac migration script'
|
... | ... | |
71 |
72 |
class ::Time
|
72 |
73 |
class << self
|
73 |
74 |
alias :real_now :now
|
|
75 |
alias :real_at :at
|
74 |
76 |
def now
|
75 |
77 |
real_now - @fake_diff.to_i
|
76 |
78 |
end
|
... | ... | |
80 |
82 |
@fake_diff = 0
|
81 |
83 |
res
|
82 |
84 |
end
|
|
85 |
def at(time)
|
|
86 |
# In Trac ticket #6466, timestamps
|
|
87 |
# were changed from seconds since the epoch
|
|
88 |
# to microseconds since the epoch. The
|
|
89 |
# Trac database version was bumped to 23 for this.
|
|
90 |
if TracMigrate.database_version > 22
|
|
91 |
Time.real_at(time / 1000000)
|
|
92 |
else
|
|
93 |
Time.real_at(time)
|
|
94 |
end
|
|
95 |
end
|
83 |
96 |
end
|
84 |
97 |
end
|
85 |
98 |
|
|
99 |
class TracSystem < ActiveRecord::Base
|
|
100 |
self.table_name = :system
|
|
101 |
end
|
|
102 |
|
86 |
103 |
class TracComponent < ActiveRecord::Base
|
87 |
104 |
self.table_name = :component
|
88 |
105 |
end
|
... | ... | |
118 |
135 |
|
119 |
136 |
class TracAttachment < ActiveRecord::Base
|
120 |
137 |
self.table_name = :attachment
|
121 |
|
set_inheritance_column :none
|
|
138 |
self.inheritance_column = :none
|
122 |
139 |
|
123 |
140 |
def time; Time.at(read_attribute(:time)) end
|
124 |
141 |
|
... | ... | |
150 |
167 |
end
|
151 |
168 |
|
152 |
169 |
private
|
|
170 |
|
|
171 |
def sha1(s)
|
|
172 |
return Digest::SHA1.hexdigest(s)
|
|
173 |
end
|
|
174 |
|
|
175 |
def get_path(ticket_id, filename)
|
|
176 |
t = sha1(ticket_id.to_s)
|
|
177 |
f = sha1(filename)
|
|
178 |
ext = File.extname(filename)
|
|
179 |
a = [ t[0..2], "/", t, "/", f, ext ]
|
|
180 |
return a.join("")
|
|
181 |
end
|
|
182 |
|
153 |
183 |
def trac_fullpath
|
154 |
|
attachment_type = read_attribute(:type)
|
155 |
|
#replace exotic characters with their hex representation to avoid invalid filenames
|
156 |
|
trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) do |x|
|
157 |
|
codepoint = x.codepoints.to_a[0]
|
158 |
|
sprintf('%%%02x', codepoint)
|
159 |
|
end
|
160 |
|
"#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
|
|
184 |
attachment_type = read_attribute(:type)
|
|
185 |
ticket_id = read_attribute(:id)
|
|
186 |
filename = read_attribute(:filename)
|
|
187 |
path = get_path(id, filename)
|
|
188 |
"#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{path}"
|
161 |
189 |
end
|
162 |
190 |
end
|
163 |
191 |
|
164 |
192 |
class TracTicket < ActiveRecord::Base
|
165 |
193 |
self.table_name = :ticket
|
166 |
|
set_inheritance_column :none
|
|
194 |
self.inheritance_column = :none
|
167 |
195 |
|
168 |
196 |
# ticket changes: only migrate status changes and comments
|
169 |
197 |
has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
|
170 |
198 |
has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
|
171 |
199 |
|
172 |
200 |
def attachments
|
173 |
|
TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
|
|
201 |
TracMigrate::TracAttachment.where("type = 'ticket' AND id = :id", id: self.id.to_s)
|
174 |
202 |
end
|
175 |
203 |
|
176 |
204 |
def ticket_type
|
... | ... | |
210 |
238 |
|
211 |
239 |
class TracWikiPage < ActiveRecord::Base
|
212 |
240 |
self.table_name = :wiki
|
213 |
|
set_primary_key :name
|
|
241 |
self.primary_key = 'name'
|
214 |
242 |
|
215 |
243 |
def self.columns
|
216 |
244 |
# Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
|
... | ... | |
218 |
246 |
end
|
219 |
247 |
|
220 |
248 |
def attachments
|
221 |
|
TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
|
|
249 |
TracMigrate::TracAttachment.where("type = 'wiki' AND id = :id", id: self.id.to_s)
|
222 |
250 |
end
|
223 |
251 |
|
224 |
252 |
def time; Time.at(read_attribute(:time)) end
|
... | ... | |
376 |
404 |
# Quick database test
|
377 |
405 |
TracComponent.count
|
378 |
406 |
|
|
407 |
lookup_database_version
|
|
408 |
print "Trac database version is: ", database_version, "\n"
|
379 |
409 |
migrated_components = 0
|
380 |
410 |
migrated_milestones = 0
|
381 |
411 |
migrated_tickets = 0
|
... | ... | |
419 |
449 |
p.save
|
420 |
450 |
|
421 |
451 |
v = Version.new :project => @target_project,
|
422 |
|
:name => encode(milestone.name[0, limit_for(Version, 'name')]),
|
|
452 |
:name => encode(milestone.name),
|
423 |
453 |
:description => nil,
|
424 |
454 |
:wiki_page_title => milestone.name.to_s,
|
425 |
455 |
:effective_date => milestone.completed
|
... | ... | |
469 |
499 |
print '.'
|
470 |
500 |
STDOUT.flush
|
471 |
501 |
i = Issue.new :project => @target_project,
|
472 |
|
:subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
|
|
502 |
:subject => encode(ticket.summary),
|
473 |
503 |
:description => convert_wiki_text(encode(ticket.description)),
|
474 |
504 |
:priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
|
475 |
505 |
:created_on => ticket.time
|
... | ... | |
595 |
625 |
puts "Components: #{migrated_components}/#{TracComponent.count}"
|
596 |
626 |
puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
|
597 |
627 |
puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
|
598 |
|
puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
|
|
628 |
puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.where(type: 'ticket').count().to_s
|
599 |
629 |
puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
|
600 |
630 |
puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
|
601 |
|
puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
|
|
631 |
puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.where(type: 'wiki').count().to_s
|
602 |
632 |
end
|
603 |
633 |
|
604 |
634 |
def self.limit_for(klass, attribute)
|
... | ... | |
609 |
639 |
@charset = charset
|
610 |
640 |
end
|
611 |
641 |
|
|
642 |
def self.lookup_database_version
|
|
643 |
f = TracSystem.find_by_name("database_version")
|
|
644 |
@@database_version = f.value.to_i
|
|
645 |
end
|
|
646 |
|
|
647 |
def self.database_version
|
|
648 |
@@database_version
|
|
649 |
end
|
|
650 |
|
612 |
651 |
def self.set_trac_directory(path)
|
613 |
652 |
@@trac_directory = path
|
614 |
653 |
raise "This directory doesn't exist!" unless File.directory?(path)
|
... | ... | |
664 |
703 |
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 |
704 |
|
666 |
705 |
def self.trac_db_path; "#{trac_directory}/db/trac.db" end
|
667 |
|
def self.trac_attachments_directory; "#{trac_directory}/attachments" end
|
|
706 |
def self.trac_attachments_directory; "#{trac_directory}/files/attachments" end
|
668 |
707 |
|
669 |
708 |
def self.target_project_identifier(identifier)
|
670 |
709 |
project = Project.find_by_identifier(identifier)
|