##// END OF EJS Templates
Fixed: migrate_from_trac doesn't import timestamps of wiki and tickets (patch #882 by Andreas Neuhaus slightly edited)....
Jean-Philippe Lang -
r1287:1bb51f743a35
parent child
Show More
@@ -1,627 +1,645
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'active_record'
18 require 'active_record'
19 require 'iconv'
19 require 'iconv'
20 require 'pp'
20 require 'pp'
21
21
22 namespace :redmine do
22 namespace :redmine do
23 desc 'Trac migration script'
23 desc 'Trac migration script'
24 task :migrate_from_trac => :environment do
24 task :migrate_from_trac => :environment do
25
25
26 module TracMigrate
26 module TracMigrate
27 TICKET_MAP = []
27 TICKET_MAP = []
28
28
29 DEFAULT_STATUS = IssueStatus.default
29 DEFAULT_STATUS = IssueStatus.default
30 assigned_status = IssueStatus.find_by_position(2)
30 assigned_status = IssueStatus.find_by_position(2)
31 resolved_status = IssueStatus.find_by_position(3)
31 resolved_status = IssueStatus.find_by_position(3)
32 feedback_status = IssueStatus.find_by_position(4)
32 feedback_status = IssueStatus.find_by_position(4)
33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
35 'reopened' => feedback_status,
35 'reopened' => feedback_status,
36 'assigned' => assigned_status,
36 'assigned' => assigned_status,
37 'closed' => closed_status
37 'closed' => closed_status
38 }
38 }
39
39
40 priorities = Enumeration.get_values('IPRI')
40 priorities = Enumeration.get_values('IPRI')
41 DEFAULT_PRIORITY = priorities[0]
41 DEFAULT_PRIORITY = priorities[0]
42 PRIORITY_MAPPING = {'lowest' => priorities[0],
42 PRIORITY_MAPPING = {'lowest' => priorities[0],
43 'low' => priorities[0],
43 'low' => priorities[0],
44 'normal' => priorities[1],
44 'normal' => priorities[1],
45 'high' => priorities[2],
45 'high' => priorities[2],
46 'highest' => priorities[3],
46 'highest' => priorities[3],
47 # ---
47 # ---
48 'trivial' => priorities[0],
48 'trivial' => priorities[0],
49 'minor' => priorities[1],
49 'minor' => priorities[1],
50 'major' => priorities[2],
50 'major' => priorities[2],
51 'critical' => priorities[3],
51 'critical' => priorities[3],
52 'blocker' => priorities[4]
52 'blocker' => priorities[4]
53 }
53 }
54
54
55 TRACKER_BUG = Tracker.find_by_position(1)
55 TRACKER_BUG = Tracker.find_by_position(1)
56 TRACKER_FEATURE = Tracker.find_by_position(2)
56 TRACKER_FEATURE = Tracker.find_by_position(2)
57 DEFAULT_TRACKER = TRACKER_BUG
57 DEFAULT_TRACKER = TRACKER_BUG
58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
59 'enhancement' => TRACKER_FEATURE,
59 'enhancement' => TRACKER_FEATURE,
60 'task' => TRACKER_FEATURE,
60 'task' => TRACKER_FEATURE,
61 'patch' =>TRACKER_FEATURE
61 'patch' =>TRACKER_FEATURE
62 }
62 }
63
63
64 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
64 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
65 manager_role = roles[0]
65 manager_role = roles[0]
66 developer_role = roles[1]
66 developer_role = roles[1]
67 DEFAULT_ROLE = roles.last
67 DEFAULT_ROLE = roles.last
68 ROLE_MAPPING = {'admin' => manager_role,
68 ROLE_MAPPING = {'admin' => manager_role,
69 'developer' => developer_role
69 'developer' => developer_role
70 }
70 }
71
71
72 class ::Time
73 class << self
74 alias :real_now :now
75 def now
76 real_now - @fake_diff.to_i
77 end
78 def fake(time)
79 @fake_diff = real_now - time
80 res = yield
81 @fake_diff = 0
82 res
83 end
84 end
85 end
86
72 class TracComponent < ActiveRecord::Base
87 class TracComponent < ActiveRecord::Base
73 set_table_name :component
88 set_table_name :component
74 end
89 end
75
90
76 class TracMilestone < ActiveRecord::Base
91 class TracMilestone < ActiveRecord::Base
77 set_table_name :milestone
92 set_table_name :milestone
78
93
79 def due
94 def due
80 if read_attribute(:due) > 0
95 if read_attribute(:due) > 0
81 Time.at(read_attribute(:due)).to_date
96 Time.at(read_attribute(:due)).to_date
82 else
97 else
83 nil
98 nil
84 end
99 end
85 end
100 end
86 end
101 end
87
102
88 class TracTicketCustom < ActiveRecord::Base
103 class TracTicketCustom < ActiveRecord::Base
89 set_table_name :ticket_custom
104 set_table_name :ticket_custom
90 end
105 end
91
106
92 class TracAttachment < ActiveRecord::Base
107 class TracAttachment < ActiveRecord::Base
93 set_table_name :attachment
108 set_table_name :attachment
94 set_inheritance_column :none
109 set_inheritance_column :none
95
110
96 def time; Time.at(read_attribute(:time)) end
111 def time; Time.at(read_attribute(:time)) end
97
112
98 def original_filename
113 def original_filename
99 filename
114 filename
100 end
115 end
101
116
102 def content_type
117 def content_type
103 Redmine::MimeType.of(filename) || ''
118 Redmine::MimeType.of(filename) || ''
104 end
119 end
105
120
106 def exist?
121 def exist?
107 File.file? trac_fullpath
122 File.file? trac_fullpath
108 end
123 end
109
124
110 def read
125 def read
111 File.open("#{trac_fullpath}", 'rb').read
126 File.open("#{trac_fullpath}", 'rb').read
112 end
127 end
113
128
114 private
129 private
115 def trac_fullpath
130 def trac_fullpath
116 attachment_type = read_attribute(:type)
131 attachment_type = read_attribute(:type)
117 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
132 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
118 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
133 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
119 end
134 end
120 end
135 end
121
136
122 class TracTicket < ActiveRecord::Base
137 class TracTicket < ActiveRecord::Base
123 set_table_name :ticket
138 set_table_name :ticket
124 set_inheritance_column :none
139 set_inheritance_column :none
125
140
126 # ticket changes: only migrate status changes and comments
141 # ticket changes: only migrate status changes and comments
127 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
142 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
128 has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'ticket'"
143 has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'ticket'"
129 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
144 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
130
145
131 def ticket_type
146 def ticket_type
132 read_attribute(:type)
147 read_attribute(:type)
133 end
148 end
134
149
135 def summary
150 def summary
136 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
151 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
137 end
152 end
138
153
139 def description
154 def description
140 read_attribute(:description).blank? ? summary : read_attribute(:description)
155 read_attribute(:description).blank? ? summary : read_attribute(:description)
141 end
156 end
142
157
143 def time; Time.at(read_attribute(:time)) end
158 def time; Time.at(read_attribute(:time)) end
159 def changetime; Time.at(read_attribute(:changetime)) end
144 end
160 end
145
161
146 class TracTicketChange < ActiveRecord::Base
162 class TracTicketChange < ActiveRecord::Base
147 set_table_name :ticket_change
163 set_table_name :ticket_change
148
164
149 def time; Time.at(read_attribute(:time)) end
165 def time; Time.at(read_attribute(:time)) end
150 end
166 end
151
167
152 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
168 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
153 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
169 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
154 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
170 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
155 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
171 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
156 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
172 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
157 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
173 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
158 CamelCase TitleIndex)
174 CamelCase TitleIndex)
159
175
160 class TracWikiPage < ActiveRecord::Base
176 class TracWikiPage < ActiveRecord::Base
161 set_table_name :wiki
177 set_table_name :wiki
162 set_primary_key :name
178 set_primary_key :name
163
179
164 has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'wiki'"
180 has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'wiki'"
165
181
166 def self.columns
182 def self.columns
167 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
183 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
168 super.select {|column| column.name.to_s != 'readonly'}
184 super.select {|column| column.name.to_s != 'readonly'}
169 end
185 end
186
187 def time; Time.at(read_attribute(:time)) end
170 end
188 end
171
189
172 class TracPermission < ActiveRecord::Base
190 class TracPermission < ActiveRecord::Base
173 set_table_name :permission
191 set_table_name :permission
174 end
192 end
175
193
176 def self.find_or_create_user(username, project_member = false)
194 def self.find_or_create_user(username, project_member = false)
177 return User.anonymous if username.blank?
195 return User.anonymous if username.blank?
178
196
179 u = User.find_by_login(username)
197 u = User.find_by_login(username)
180 if !u
198 if !u
181 # Create a new user if not found
199 # Create a new user if not found
182 mail = username[0,limit_for(User, 'mail')]
200 mail = username[0,limit_for(User, 'mail')]
183 mail = "#{mail}@foo.bar" unless mail.include?("@")
201 mail = "#{mail}@foo.bar" unless mail.include?("@")
184 u = User.new :firstname => username[0,limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
202 u = User.new :firstname => username[0,limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
185 :lastname => '-',
203 :lastname => '-',
186 :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-')
204 :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-')
187 u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
205 u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
188 u.password = 'trac'
206 u.password = 'trac'
189 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
207 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
190 # finally, a default user is used if the new user is not valid
208 # finally, a default user is used if the new user is not valid
191 u = User.find(:first) unless u.save
209 u = User.find(:first) unless u.save
192 end
210 end
193 # Make sure he is a member of the project
211 # Make sure he is a member of the project
194 if project_member && !u.member_of?(@target_project)
212 if project_member && !u.member_of?(@target_project)
195 role = DEFAULT_ROLE
213 role = DEFAULT_ROLE
196 if u.admin
214 if u.admin
197 role = ROLE_MAPPING['admin']
215 role = ROLE_MAPPING['admin']
198 elsif TracPermission.find_by_username_and_action(username, 'developer')
216 elsif TracPermission.find_by_username_and_action(username, 'developer')
199 role = ROLE_MAPPING['developer']
217 role = ROLE_MAPPING['developer']
200 end
218 end
201 Member.create(:user => u, :project => @target_project, :role => role)
219 Member.create(:user => u, :project => @target_project, :role => role)
202 u.reload
220 u.reload
203 end
221 end
204 u
222 u
205 end
223 end
206
224
207 # Basic wiki syntax conversion
225 # Basic wiki syntax conversion
208 def self.convert_wiki_text(text)
226 def self.convert_wiki_text(text)
209 # Titles
227 # Titles
210 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
228 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
211 # External Links
229 # External Links
212 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
230 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
213 # Internal Links
231 # Internal Links
214 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
232 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
215 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
233 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
216 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
234 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
217 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
235 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
218 text = text.gsub(/\[wiki:([^\s\]]+).*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
236 text = text.gsub(/\[wiki:([^\s\]]+).*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
219
237
220 # Links to pages UsingJustWikiCaps
238 # Links to pages UsingJustWikiCaps
221 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
239 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
222 # Normalize things that were supposed to not be links
240 # Normalize things that were supposed to not be links
223 # like !NotALink
241 # like !NotALink
224 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
242 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
225 # Revisions links
243 # Revisions links
226 text = text.gsub(/\[(\d+)\]/, 'r\1')
244 text = text.gsub(/\[(\d+)\]/, 'r\1')
227 # Ticket number re-writing
245 # Ticket number re-writing
228 text = text.gsub(/#(\d+)/) do |s|
246 text = text.gsub(/#(\d+)/) do |s|
229 if $1.length < 10
247 if $1.length < 10
230 TICKET_MAP[$1.to_i] ||= $1
248 TICKET_MAP[$1.to_i] ||= $1
231 "\##{TICKET_MAP[$1.to_i] || $1}"
249 "\##{TICKET_MAP[$1.to_i] || $1}"
232 else
250 else
233 s
251 s
234 end
252 end
235 end
253 end
236 # Preformatted blocks
254 # Preformatted blocks
237 text = text.gsub(/\{\{\{/, '<pre>')
255 text = text.gsub(/\{\{\{/, '<pre>')
238 text = text.gsub(/\}\}\}/, '</pre>')
256 text = text.gsub(/\}\}\}/, '</pre>')
239 # Highlighting
257 # Highlighting
240 text = text.gsub(/'''''([^\s])/, '_*\1')
258 text = text.gsub(/'''''([^\s])/, '_*\1')
241 text = text.gsub(/([^\s])'''''/, '\1*_')
259 text = text.gsub(/([^\s])'''''/, '\1*_')
242 text = text.gsub(/'''/, '*')
260 text = text.gsub(/'''/, '*')
243 text = text.gsub(/''/, '_')
261 text = text.gsub(/''/, '_')
244 text = text.gsub(/__/, '+')
262 text = text.gsub(/__/, '+')
245 text = text.gsub(/~~/, '-')
263 text = text.gsub(/~~/, '-')
246 text = text.gsub(/`/, '@')
264 text = text.gsub(/`/, '@')
247 text = text.gsub(/,,/, '~')
265 text = text.gsub(/,,/, '~')
248 # Lists
266 # Lists
249 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
267 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
250
268
251 text
269 text
252 end
270 end
253
271
254 def self.migrate
272 def self.migrate
255 establish_connection
273 establish_connection
256
274
257 # Quick database test
275 # Quick database test
258 TracComponent.count
276 TracComponent.count
259
277
260 migrated_components = 0
278 migrated_components = 0
261 migrated_milestones = 0
279 migrated_milestones = 0
262 migrated_tickets = 0
280 migrated_tickets = 0
263 migrated_custom_values = 0
281 migrated_custom_values = 0
264 migrated_ticket_attachments = 0
282 migrated_ticket_attachments = 0
265 migrated_wiki_edits = 0
283 migrated_wiki_edits = 0
266 migrated_wiki_attachments = 0
284 migrated_wiki_attachments = 0
267
285
268 # Components
286 # Components
269 print "Migrating components"
287 print "Migrating components"
270 issues_category_map = {}
288 issues_category_map = {}
271 TracComponent.find(:all).each do |component|
289 TracComponent.find(:all).each do |component|
272 print '.'
290 print '.'
273 STDOUT.flush
291 STDOUT.flush
274 c = IssueCategory.new :project => @target_project,
292 c = IssueCategory.new :project => @target_project,
275 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
293 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
276 next unless c.save
294 next unless c.save
277 issues_category_map[component.name] = c
295 issues_category_map[component.name] = c
278 migrated_components += 1
296 migrated_components += 1
279 end
297 end
280 puts
298 puts
281
299
282 # Milestones
300 # Milestones
283 print "Migrating milestones"
301 print "Migrating milestones"
284 version_map = {}
302 version_map = {}
285 TracMilestone.find(:all).each do |milestone|
303 TracMilestone.find(:all).each do |milestone|
286 print '.'
304 print '.'
287 STDOUT.flush
305 STDOUT.flush
288 v = Version.new :project => @target_project,
306 v = Version.new :project => @target_project,
289 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
307 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
290 :description => encode(milestone.description.to_s[0, limit_for(Version, 'description')]),
308 :description => encode(milestone.description.to_s[0, limit_for(Version, 'description')]),
291 :effective_date => milestone.due
309 :effective_date => milestone.due
292 next unless v.save
310 next unless v.save
293 version_map[milestone.name] = v
311 version_map[milestone.name] = v
294 migrated_milestones += 1
312 migrated_milestones += 1
295 end
313 end
296 puts
314 puts
297
315
298 # Custom fields
316 # Custom fields
299 # TODO: read trac.ini instead
317 # TODO: read trac.ini instead
300 print "Migrating custom fields"
318 print "Migrating custom fields"
301 custom_field_map = {}
319 custom_field_map = {}
302 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
320 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
303 print '.'
321 print '.'
304 STDOUT.flush
322 STDOUT.flush
305 # Redmine custom field name
323 # Redmine custom field name
306 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
324 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
307 # Find if the custom already exists in Redmine
325 # Find if the custom already exists in Redmine
308 f = IssueCustomField.find_by_name(field_name)
326 f = IssueCustomField.find_by_name(field_name)
309 # Or create a new one
327 # Or create a new one
310 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
328 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
311 :field_format => 'string')
329 :field_format => 'string')
312
330
313 next if f.new_record?
331 next if f.new_record?
314 f.trackers = Tracker.find(:all)
332 f.trackers = Tracker.find(:all)
315 f.projects << @target_project
333 f.projects << @target_project
316 custom_field_map[field.name] = f
334 custom_field_map[field.name] = f
317 end
335 end
318 puts
336 puts
319
337
320 # Trac 'resolution' field as a Redmine custom field
338 # Trac 'resolution' field as a Redmine custom field
321 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
339 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
322 r = IssueCustomField.new(:name => 'Resolution',
340 r = IssueCustomField.new(:name => 'Resolution',
323 :field_format => 'list',
341 :field_format => 'list',
324 :is_filter => true) if r.nil?
342 :is_filter => true) if r.nil?
325 r.trackers = Tracker.find(:all)
343 r.trackers = Tracker.find(:all)
326 r.projects << @target_project
344 r.projects << @target_project
327 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
345 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
328 r.save!
346 r.save!
329 custom_field_map['resolution'] = r
347 custom_field_map['resolution'] = r
330
348
331 # Tickets
349 # Tickets
332 print "Migrating tickets"
350 print "Migrating tickets"
333 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
351 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
334 print '.'
352 print '.'
335 STDOUT.flush
353 STDOUT.flush
336 i = Issue.new :project => @target_project,
354 i = Issue.new :project => @target_project,
337 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
355 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
338 :description => convert_wiki_text(encode(ticket.description)),
356 :description => convert_wiki_text(encode(ticket.description)),
339 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
357 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
340 :created_on => ticket.time
358 :created_on => ticket.time
341 i.author = find_or_create_user(ticket.reporter)
359 i.author = find_or_create_user(ticket.reporter)
342 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
360 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
343 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
361 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
344 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
362 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
345 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
363 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
346 i.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank?
364 i.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank?
347 i.id = ticket.id unless Issue.exists?(ticket.id)
365 i.id = ticket.id unless Issue.exists?(ticket.id)
348 next unless i.save
366 next unless Time.fake(ticket.changetime) { i.save }
349 TICKET_MAP[ticket.id] = i.id
367 TICKET_MAP[ticket.id] = i.id
350 migrated_tickets += 1
368 migrated_tickets += 1
351
369
352 # Owner
370 # Owner
353 unless ticket.owner.blank?
371 unless ticket.owner.blank?
354 i.assigned_to = find_or_create_user(ticket.owner, true)
372 i.assigned_to = find_or_create_user(ticket.owner, true)
355 i.save
373 Time.fake(ticket.changetime) { i.save }
356 end
374 end
357
375
358 # Comments and status/resolution changes
376 # Comments and status/resolution changes
359 ticket.changes.group_by(&:time).each do |time, changeset|
377 ticket.changes.group_by(&:time).each do |time, changeset|
360 status_change = changeset.select {|change| change.field == 'status'}.first
378 status_change = changeset.select {|change| change.field == 'status'}.first
361 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
379 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
362 comment_change = changeset.select {|change| change.field == 'comment'}.first
380 comment_change = changeset.select {|change| change.field == 'comment'}.first
363
381
364 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
382 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
365 :created_on => time
383 :created_on => time
366 n.user = find_or_create_user(changeset.first.author)
384 n.user = find_or_create_user(changeset.first.author)
367 n.journalized = i
385 n.journalized = i
368 if status_change &&
386 if status_change &&
369 STATUS_MAPPING[status_change.oldvalue] &&
387 STATUS_MAPPING[status_change.oldvalue] &&
370 STATUS_MAPPING[status_change.newvalue] &&
388 STATUS_MAPPING[status_change.newvalue] &&
371 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
389 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
372 n.details << JournalDetail.new(:property => 'attr',
390 n.details << JournalDetail.new(:property => 'attr',
373 :prop_key => 'status_id',
391 :prop_key => 'status_id',
374 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
392 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
375 :value => STATUS_MAPPING[status_change.newvalue].id)
393 :value => STATUS_MAPPING[status_change.newvalue].id)
376 end
394 end
377 if resolution_change
395 if resolution_change
378 n.details << JournalDetail.new(:property => 'cf',
396 n.details << JournalDetail.new(:property => 'cf',
379 :prop_key => custom_field_map['resolution'].id,
397 :prop_key => custom_field_map['resolution'].id,
380 :old_value => resolution_change.oldvalue,
398 :old_value => resolution_change.oldvalue,
381 :value => resolution_change.newvalue)
399 :value => resolution_change.newvalue)
382 end
400 end
383 n.save unless n.details.empty? && n.notes.blank?
401 n.save unless n.details.empty? && n.notes.blank?
384 end
402 end
385
403
386 # Attachments
404 # Attachments
387 ticket.attachments.each do |attachment|
405 ticket.attachments.each do |attachment|
388 next unless attachment.exist?
406 next unless attachment.exist?
389 a = Attachment.new :created_on => attachment.time
407 a = Attachment.new :created_on => attachment.time
390 a.file = attachment
408 a.file = attachment
391 a.author = find_or_create_user(attachment.author)
409 a.author = find_or_create_user(attachment.author)
392 a.container = i
410 a.container = i
393 migrated_ticket_attachments += 1 if a.save
411 migrated_ticket_attachments += 1 if a.save
394 end
412 end
395
413
396 # Custom fields
414 # Custom fields
397 ticket.customs.each do |custom|
415 ticket.customs.each do |custom|
398 next if custom_field_map[custom.name].nil?
416 next if custom_field_map[custom.name].nil?
399 v = CustomValue.new :custom_field => custom_field_map[custom.name],
417 v = CustomValue.new :custom_field => custom_field_map[custom.name],
400 :value => custom.value
418 :value => custom.value
401 v.customized = i
419 v.customized = i
402 next unless v.save
420 next unless v.save
403 migrated_custom_values += 1
421 migrated_custom_values += 1
404 end
422 end
405 end
423 end
406
424
407 # update issue id sequence if needed (postgresql)
425 # update issue id sequence if needed (postgresql)
408 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
426 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
409 puts
427 puts
410
428
411 # Wiki
429 # Wiki
412 print "Migrating wiki"
430 print "Migrating wiki"
413 @target_project.wiki.destroy if @target_project.wiki
431 @target_project.wiki.destroy if @target_project.wiki
414 @target_project.reload
432 @target_project.reload
415 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
433 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
416 wiki_edit_count = 0
434 wiki_edit_count = 0
417 if wiki.save
435 if wiki.save
418 TracWikiPage.find(:all, :order => 'name, version').each do |page|
436 TracWikiPage.find(:all, :order => 'name, version').each do |page|
419 # Do not migrate Trac manual wiki pages
437 # Do not migrate Trac manual wiki pages
420 next if TRAC_WIKI_PAGES.include?(page.name)
438 next if TRAC_WIKI_PAGES.include?(page.name)
421 wiki_edit_count += 1
439 wiki_edit_count += 1
422 print '.'
440 print '.'
423 STDOUT.flush
441 STDOUT.flush
424 p = wiki.find_or_new_page(page.name)
442 p = wiki.find_or_new_page(page.name)
425 p.content = WikiContent.new(:page => p) if p.new_record?
443 p.content = WikiContent.new(:page => p) if p.new_record?
426 p.content.text = page.text
444 p.content.text = page.text
427 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
445 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
428 p.content.comments = page.comment
446 p.content.comments = page.comment
429 p.new_record? ? p.save : p.content.save
447 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
430
448
431 next if p.content.new_record?
449 next if p.content.new_record?
432 migrated_wiki_edits += 1
450 migrated_wiki_edits += 1
433
451
434 # Attachments
452 # Attachments
435 page.attachments.each do |attachment|
453 page.attachments.each do |attachment|
436 next unless attachment.exist?
454 next unless attachment.exist?
437 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
455 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
438 a = Attachment.new :created_on => attachment.time
456 a = Attachment.new :created_on => attachment.time
439 a.file = attachment
457 a.file = attachment
440 a.author = find_or_create_user(attachment.author)
458 a.author = find_or_create_user(attachment.author)
441 a.container = p
459 a.container = p
442 migrated_wiki_attachments += 1 if a.save
460 migrated_wiki_attachments += 1 if a.save
443 end
461 end
444 end
462 end
445
463
446 wiki.reload
464 wiki.reload
447 wiki.pages.each do |page|
465 wiki.pages.each do |page|
448 page.content.text = convert_wiki_text(page.content.text)
466 page.content.text = convert_wiki_text(page.content.text)
449 page.content.save
467 Time.fake(page.content.updated_on) { page.content.save }
450 end
468 end
451 end
469 end
452 puts
470 puts
453
471
454 puts
472 puts
455 puts "Components: #{migrated_components}/#{TracComponent.count}"
473 puts "Components: #{migrated_components}/#{TracComponent.count}"
456 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
474 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
457 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
475 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
458 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
476 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
459 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
477 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
460 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
478 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
461 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
479 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
462 end
480 end
463
481
464 def self.limit_for(klass, attribute)
482 def self.limit_for(klass, attribute)
465 klass.columns_hash[attribute.to_s].limit
483 klass.columns_hash[attribute.to_s].limit
466 end
484 end
467
485
468 def self.encoding(charset)
486 def self.encoding(charset)
469 @ic = Iconv.new('UTF-8', charset)
487 @ic = Iconv.new('UTF-8', charset)
470 rescue Iconv::InvalidEncoding
488 rescue Iconv::InvalidEncoding
471 puts "Invalid encoding!"
489 puts "Invalid encoding!"
472 return false
490 return false
473 end
491 end
474
492
475 def self.set_trac_directory(path)
493 def self.set_trac_directory(path)
476 @@trac_directory = path
494 @@trac_directory = path
477 raise "This directory doesn't exist!" unless File.directory?(path)
495 raise "This directory doesn't exist!" unless File.directory?(path)
478 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
496 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
479 @@trac_directory
497 @@trac_directory
480 rescue Exception => e
498 rescue Exception => e
481 puts e
499 puts e
482 return false
500 return false
483 end
501 end
484
502
485 def self.trac_directory
503 def self.trac_directory
486 @@trac_directory
504 @@trac_directory
487 end
505 end
488
506
489 def self.set_trac_adapter(adapter)
507 def self.set_trac_adapter(adapter)
490 return false if adapter.blank?
508 return false if adapter.blank?
491 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
509 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
492 # If adapter is sqlite or sqlite3, make sure that trac.db exists
510 # If adapter is sqlite or sqlite3, make sure that trac.db exists
493 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
511 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
494 @@trac_adapter = adapter
512 @@trac_adapter = adapter
495 rescue Exception => e
513 rescue Exception => e
496 puts e
514 puts e
497 return false
515 return false
498 end
516 end
499
517
500 def self.set_trac_db_host(host)
518 def self.set_trac_db_host(host)
501 return nil if host.blank?
519 return nil if host.blank?
502 @@trac_db_host = host
520 @@trac_db_host = host
503 end
521 end
504
522
505 def self.set_trac_db_port(port)
523 def self.set_trac_db_port(port)
506 return nil if port.to_i == 0
524 return nil if port.to_i == 0
507 @@trac_db_port = port.to_i
525 @@trac_db_port = port.to_i
508 end
526 end
509
527
510 def self.set_trac_db_name(name)
528 def self.set_trac_db_name(name)
511 return nil if name.blank?
529 return nil if name.blank?
512 @@trac_db_name = name
530 @@trac_db_name = name
513 end
531 end
514
532
515 def self.set_trac_db_username(username)
533 def self.set_trac_db_username(username)
516 @@trac_db_username = username
534 @@trac_db_username = username
517 end
535 end
518
536
519 def self.set_trac_db_password(password)
537 def self.set_trac_db_password(password)
520 @@trac_db_password = password
538 @@trac_db_password = password
521 end
539 end
522
540
523 def self.set_trac_db_schema(schema)
541 def self.set_trac_db_schema(schema)
524 @@trac_db_schema = schema
542 @@trac_db_schema = schema
525 end
543 end
526
544
527 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
545 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
528
546
529 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
547 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
530 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
548 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
531
549
532 def self.target_project_identifier(identifier)
550 def self.target_project_identifier(identifier)
533 project = Project.find_by_identifier(identifier)
551 project = Project.find_by_identifier(identifier)
534 if !project
552 if !project
535 # create the target project
553 # create the target project
536 project = Project.new :name => identifier.humanize,
554 project = Project.new :name => identifier.humanize,
537 :description => ''
555 :description => ''
538 project.identifier = identifier
556 project.identifier = identifier
539 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
557 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
540 # enable issues and wiki for the created project
558 # enable issues and wiki for the created project
541 project.enabled_module_names = ['issue_tracking', 'wiki']
559 project.enabled_module_names = ['issue_tracking', 'wiki']
542 else
560 else
543 puts
561 puts
544 puts "This project already exists in your Redmine database."
562 puts "This project already exists in your Redmine database."
545 print "Are you sure you want to append data to this project ? [Y/n] "
563 print "Are you sure you want to append data to this project ? [Y/n] "
546 exit if STDIN.gets.match(/^n$/i)
564 exit if STDIN.gets.match(/^n$/i)
547 end
565 end
548 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
566 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
549 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
567 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
550 @target_project = project.new_record? ? nil : project
568 @target_project = project.new_record? ? nil : project
551 end
569 end
552
570
553 def self.connection_params
571 def self.connection_params
554 if %w(sqlite sqlite3).include?(trac_adapter)
572 if %w(sqlite sqlite3).include?(trac_adapter)
555 {:adapter => trac_adapter,
573 {:adapter => trac_adapter,
556 :database => trac_db_path}
574 :database => trac_db_path}
557 else
575 else
558 {:adapter => trac_adapter,
576 {:adapter => trac_adapter,
559 :database => trac_db_name,
577 :database => trac_db_name,
560 :host => trac_db_host,
578 :host => trac_db_host,
561 :port => trac_db_port,
579 :port => trac_db_port,
562 :username => trac_db_username,
580 :username => trac_db_username,
563 :password => trac_db_password,
581 :password => trac_db_password,
564 :schema_search_path => trac_db_schema
582 :schema_search_path => trac_db_schema
565 }
583 }
566 end
584 end
567 end
585 end
568
586
569 def self.establish_connection
587 def self.establish_connection
570 constants.each do |const|
588 constants.each do |const|
571 klass = const_get(const)
589 klass = const_get(const)
572 next unless klass.respond_to? 'establish_connection'
590 next unless klass.respond_to? 'establish_connection'
573 klass.establish_connection connection_params
591 klass.establish_connection connection_params
574 end
592 end
575 end
593 end
576
594
577 private
595 private
578 def self.encode(text)
596 def self.encode(text)
579 @ic.iconv text
597 @ic.iconv text
580 rescue
598 rescue
581 text
599 text
582 end
600 end
583 end
601 end
584
602
585 puts
603 puts
586 if Redmine::DefaultData::Loader.no_data?
604 if Redmine::DefaultData::Loader.no_data?
587 puts "Redmine configuration need to be loaded before importing data."
605 puts "Redmine configuration need to be loaded before importing data."
588 puts "Please, run this first:"
606 puts "Please, run this first:"
589 puts
607 puts
590 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
608 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
591 exit
609 exit
592 end
610 end
593
611
594 puts "WARNING: a new project will be added to Redmine during this process."
612 puts "WARNING: a new project will be added to Redmine during this process."
595 print "Are you sure you want to continue ? [y/N] "
613 print "Are you sure you want to continue ? [y/N] "
596 break unless STDIN.gets.match(/^y$/i)
614 break unless STDIN.gets.match(/^y$/i)
597 puts
615 puts
598
616
599 def prompt(text, options = {}, &block)
617 def prompt(text, options = {}, &block)
600 default = options[:default] || ''
618 default = options[:default] || ''
601 while true
619 while true
602 print "#{text} [#{default}]: "
620 print "#{text} [#{default}]: "
603 value = STDIN.gets.chomp!
621 value = STDIN.gets.chomp!
604 value = default if value.blank?
622 value = default if value.blank?
605 break if yield value
623 break if yield value
606 end
624 end
607 end
625 end
608
626
609 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
627 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
610
628
611 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
629 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
612 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
630 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
613 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
631 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
614 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
632 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
615 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
633 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
616 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
634 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
617 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
635 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
618 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
636 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
619 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
637 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
620 end
638 end
621 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
639 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
622 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
640 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
623 puts
641 puts
624
642
625 TracMigrate.migrate
643 TracMigrate.migrate
626 end
644 end
627 end
645 end
General Comments 0
You need to be logged in to leave comments. Login now