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