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