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