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