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