##// END OF EJS Templates
Fixed: Trac migration of ticket:123 or [ticket:34] do not work (#2053)....
Jean-Philippe Lang -
r2011:d545ca736070
parent child
Show More
@@ -1,737 +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:
271 # Situations like the following:
272 # [ticket:234 Text],[ticket:234 This is a test]
272 # [ticket:234 Text],[ticket:234 This is a test]
273 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '[[#\1|\2]]')
273 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
274 # Situations like:
274 # Situations like:
275 # ticket:1234
275 # ticket:1234
276 # #1 is working cause Redmine uses the same syntax.
276 # #1 is working cause Redmine uses the same syntax.
277 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
277 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
278 # Internal Links
278 # Internal Links
279 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
280 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
280 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
281 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
281 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
282 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
282 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
283 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
283 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
284 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
284 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
285
285
286 # Links to pages UsingJustWikiCaps
286 # Links to pages UsingJustWikiCaps
287 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]]')
288 # Normalize things that were supposed to not be links
288 # Normalize things that were supposed to not be links
289 # like !NotALink
289 # like !NotALink
290 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
290 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
291 # Revisions links
291 # Revisions links
292 text = text.gsub(/\[(\d+)\]/, 'r\1')
292 text = text.gsub(/\[(\d+)\]/, 'r\1')
293 # Ticket number re-writing
293 # Ticket number re-writing
294 text = text.gsub(/#(\d+)/) do |s|
294 text = text.gsub(/#(\d+)/) do |s|
295 if $1.length < 10
295 if $1.length < 10
296 TICKET_MAP[$1.to_i] ||= $1
296 TICKET_MAP[$1.to_i] ||= $1
297 "\##{TICKET_MAP[$1.to_i] || $1}"
297 "\##{TICKET_MAP[$1.to_i] || $1}"
298 else
298 else
299 s
299 s
300 end
300 end
301 end
301 end
302 # We would like to convert the Code highlighting too
302 # We would like to convert the Code highlighting too
303 # This will go into the next line.
303 # This will go into the next line.
304 shebang_line = false
304 shebang_line = false
305 # Reguar expression for start of code
305 # Reguar expression for start of code
306 pre_re = /\{\{\{/
306 pre_re = /\{\{\{/
307 # Code hightlighing...
307 # Code hightlighing...
308 shebang_re = /^\#\!([a-z]+)/
308 shebang_re = /^\#\!([a-z]+)/
309 # Regular expression for end of code
309 # Regular expression for end of code
310 pre_end_re = /\}\}\}/
310 pre_end_re = /\}\}\}/
311
311
312 # Go through the whole text..extract it line by line
312 # Go through the whole text..extract it line by line
313 text = text.gsub(/^(.*)$/) do |line|
313 text = text.gsub(/^(.*)$/) do |line|
314 m_pre = pre_re.match(line)
314 m_pre = pre_re.match(line)
315 if m_pre
315 if m_pre
316 line = '<pre>'
316 line = '<pre>'
317 else
317 else
318 m_sl = shebang_re.match(line)
318 m_sl = shebang_re.match(line)
319 if m_sl
319 if m_sl
320 shebang_line = true
320 shebang_line = true
321 line = '<code class="' + m_sl[1] + '">'
321 line = '<code class="' + m_sl[1] + '">'
322 end
322 end
323 m_pre_end = pre_end_re.match(line)
323 m_pre_end = pre_end_re.match(line)
324 if m_pre_end
324 if m_pre_end
325 line = '</pre>'
325 line = '</pre>'
326 if shebang_line
326 if shebang_line
327 line = '</code>' + line
327 line = '</code>' + line
328 end
328 end
329 end
329 end
330 end
330 end
331 line
331 line
332 end
332 end
333
333
334 # Highlighting
334 # Highlighting
335 text = text.gsub(/'''''([^\s])/, '_*\1')
335 text = text.gsub(/'''''([^\s])/, '_*\1')
336 text = text.gsub(/([^\s])'''''/, '\1*_')
336 text = text.gsub(/([^\s])'''''/, '\1*_')
337 text = text.gsub(/'''/, '*')
337 text = text.gsub(/'''/, '*')
338 text = text.gsub(/''/, '_')
338 text = text.gsub(/''/, '_')
339 text = text.gsub(/__/, '+')
339 text = text.gsub(/__/, '+')
340 text = text.gsub(/~~/, '-')
340 text = text.gsub(/~~/, '-')
341 text = text.gsub(/`/, '@')
341 text = text.gsub(/`/, '@')
342 text = text.gsub(/,,/, '~')
342 text = text.gsub(/,,/, '~')
343 # Lists
343 # Lists
344 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
344 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
345
345
346 text
346 text
347 end
347 end
348
348
349 def self.migrate
349 def self.migrate
350 establish_connection
350 establish_connection
351
351
352 # Quick database test
352 # Quick database test
353 TracComponent.count
353 TracComponent.count
354
354
355 migrated_components = 0
355 migrated_components = 0
356 migrated_milestones = 0
356 migrated_milestones = 0
357 migrated_tickets = 0
357 migrated_tickets = 0
358 migrated_custom_values = 0
358 migrated_custom_values = 0
359 migrated_ticket_attachments = 0
359 migrated_ticket_attachments = 0
360 migrated_wiki_edits = 0
360 migrated_wiki_edits = 0
361 migrated_wiki_attachments = 0
361 migrated_wiki_attachments = 0
362
362
363 #Wiki system initializing...
363 #Wiki system initializing...
364 @target_project.wiki.destroy if @target_project.wiki
364 @target_project.wiki.destroy if @target_project.wiki
365 @target_project.reload
365 @target_project.reload
366 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
366 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
367 wiki_edit_count = 0
367 wiki_edit_count = 0
368
368
369 # Components
369 # Components
370 print "Migrating components"
370 print "Migrating components"
371 issues_category_map = {}
371 issues_category_map = {}
372 TracComponent.find(:all).each do |component|
372 TracComponent.find(:all).each do |component|
373 print '.'
373 print '.'
374 STDOUT.flush
374 STDOUT.flush
375 c = IssueCategory.new :project => @target_project,
375 c = IssueCategory.new :project => @target_project,
376 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
376 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
377 next unless c.save
377 next unless c.save
378 issues_category_map[component.name] = c
378 issues_category_map[component.name] = c
379 migrated_components += 1
379 migrated_components += 1
380 end
380 end
381 puts
381 puts
382
382
383 # Milestones
383 # Milestones
384 print "Migrating milestones"
384 print "Migrating milestones"
385 version_map = {}
385 version_map = {}
386 TracMilestone.find(:all).each do |milestone|
386 TracMilestone.find(:all).each do |milestone|
387 print '.'
387 print '.'
388 STDOUT.flush
388 STDOUT.flush
389 # First we try to find the wiki page...
389 # First we try to find the wiki page...
390 p = wiki.find_or_new_page(milestone.name.to_s)
390 p = wiki.find_or_new_page(milestone.name.to_s)
391 p.content = WikiContent.new(:page => p) if p.new_record?
391 p.content = WikiContent.new(:page => p) if p.new_record?
392 p.content.text = milestone.description.to_s
392 p.content.text = milestone.description.to_s
393 p.content.author = find_or_create_user('trac')
393 p.content.author = find_or_create_user('trac')
394 p.content.comments = 'Milestone'
394 p.content.comments = 'Milestone'
395 p.save
395 p.save
396
396
397 v = Version.new :project => @target_project,
397 v = Version.new :project => @target_project,
398 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
398 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
399 :description => nil,
399 :description => nil,
400 :wiki_page_title => milestone.name.to_s,
400 :wiki_page_title => milestone.name.to_s,
401 :effective_date => milestone.completed
401 :effective_date => milestone.completed
402
402
403 next unless v.save
403 next unless v.save
404 version_map[milestone.name] = v
404 version_map[milestone.name] = v
405 migrated_milestones += 1
405 migrated_milestones += 1
406 end
406 end
407 puts
407 puts
408
408
409 # Custom fields
409 # Custom fields
410 # TODO: read trac.ini instead
410 # TODO: read trac.ini instead
411 print "Migrating custom fields"
411 print "Migrating custom fields"
412 custom_field_map = {}
412 custom_field_map = {}
413 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|
414 print '.'
414 print '.'
415 STDOUT.flush
415 STDOUT.flush
416 # Redmine custom field name
416 # Redmine custom field name
417 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
417 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
418 # Find if the custom already exists in Redmine
418 # Find if the custom already exists in Redmine
419 f = IssueCustomField.find_by_name(field_name)
419 f = IssueCustomField.find_by_name(field_name)
420 # Or create a new one
420 # Or create a new one
421 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,
422 :field_format => 'string')
422 :field_format => 'string')
423
423
424 next if f.new_record?
424 next if f.new_record?
425 f.trackers = Tracker.find(:all)
425 f.trackers = Tracker.find(:all)
426 f.projects << @target_project
426 f.projects << @target_project
427 custom_field_map[field.name] = f
427 custom_field_map[field.name] = f
428 end
428 end
429 puts
429 puts
430
430
431 # Trac 'resolution' field as a Redmine custom field
431 # Trac 'resolution' field as a Redmine custom field
432 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
432 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
433 r = IssueCustomField.new(:name => 'Resolution',
433 r = IssueCustomField.new(:name => 'Resolution',
434 :field_format => 'list',
434 :field_format => 'list',
435 :is_filter => true) if r.nil?
435 :is_filter => true) if r.nil?
436 r.trackers = Tracker.find(:all)
436 r.trackers = Tracker.find(:all)
437 r.projects << @target_project
437 r.projects << @target_project
438 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
439 r.save!
439 r.save!
440 custom_field_map['resolution'] = r
440 custom_field_map['resolution'] = r
441
441
442 # Tickets
442 # Tickets
443 print "Migrating tickets"
443 print "Migrating tickets"
444 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
444 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
445 print '.'
445 print '.'
446 STDOUT.flush
446 STDOUT.flush
447 i = Issue.new :project => @target_project,
447 i = Issue.new :project => @target_project,
448 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
448 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
449 :description => convert_wiki_text(encode(ticket.description)),
449 :description => convert_wiki_text(encode(ticket.description)),
450 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
450 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
451 :created_on => ticket.time
451 :created_on => ticket.time
452 i.author = find_or_create_user(ticket.reporter)
452 i.author = find_or_create_user(ticket.reporter)
453 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
453 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
454 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
454 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
455 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
455 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
456 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
456 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
457 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?
458 i.id = ticket.id unless Issue.exists?(ticket.id)
458 i.id = ticket.id unless Issue.exists?(ticket.id)
459 next unless Time.fake(ticket.changetime) { i.save }
459 next unless Time.fake(ticket.changetime) { i.save }
460 TICKET_MAP[ticket.id] = i.id
460 TICKET_MAP[ticket.id] = i.id
461 migrated_tickets += 1
461 migrated_tickets += 1
462
462
463 # Owner
463 # Owner
464 unless ticket.owner.blank?
464 unless ticket.owner.blank?
465 i.assigned_to = find_or_create_user(ticket.owner, true)
465 i.assigned_to = find_or_create_user(ticket.owner, true)
466 Time.fake(ticket.changetime) { i.save }
466 Time.fake(ticket.changetime) { i.save }
467 end
467 end
468
468
469 # Comments and status/resolution changes
469 # Comments and status/resolution changes
470 ticket.changes.group_by(&:time).each do |time, changeset|
470 ticket.changes.group_by(&:time).each do |time, changeset|
471 status_change = changeset.select {|change| change.field == 'status'}.first
471 status_change = changeset.select {|change| change.field == 'status'}.first
472 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
472 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
473 comment_change = changeset.select {|change| change.field == 'comment'}.first
473 comment_change = changeset.select {|change| change.field == 'comment'}.first
474
474
475 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)) : ''),
476 :created_on => time
476 :created_on => time
477 n.user = find_or_create_user(changeset.first.author)
477 n.user = find_or_create_user(changeset.first.author)
478 n.journalized = i
478 n.journalized = i
479 if status_change &&
479 if status_change &&
480 STATUS_MAPPING[status_change.oldvalue] &&
480 STATUS_MAPPING[status_change.oldvalue] &&
481 STATUS_MAPPING[status_change.newvalue] &&
481 STATUS_MAPPING[status_change.newvalue] &&
482 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
482 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
483 n.details << JournalDetail.new(:property => 'attr',
483 n.details << JournalDetail.new(:property => 'attr',
484 :prop_key => 'status_id',
484 :prop_key => 'status_id',
485 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
485 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
486 :value => STATUS_MAPPING[status_change.newvalue].id)
486 :value => STATUS_MAPPING[status_change.newvalue].id)
487 end
487 end
488 if resolution_change
488 if resolution_change
489 n.details << JournalDetail.new(:property => 'cf',
489 n.details << JournalDetail.new(:property => 'cf',
490 :prop_key => custom_field_map['resolution'].id,
490 :prop_key => custom_field_map['resolution'].id,
491 :old_value => resolution_change.oldvalue,
491 :old_value => resolution_change.oldvalue,
492 :value => resolution_change.newvalue)
492 :value => resolution_change.newvalue)
493 end
493 end
494 n.save unless n.details.empty? && n.notes.blank?
494 n.save unless n.details.empty? && n.notes.blank?
495 end
495 end
496
496
497 # Attachments
497 # Attachments
498 ticket.attachments.each do |attachment|
498 ticket.attachments.each do |attachment|
499 next unless attachment.exist?
499 next unless attachment.exist?
500 a = Attachment.new :created_on => attachment.time
500 a = Attachment.new :created_on => attachment.time
501 a.file = attachment
501 a.file = attachment
502 a.author = find_or_create_user(attachment.author)
502 a.author = find_or_create_user(attachment.author)
503 a.container = i
503 a.container = i
504 a.description = attachment.description
504 a.description = attachment.description
505 migrated_ticket_attachments += 1 if a.save
505 migrated_ticket_attachments += 1 if a.save
506 end
506 end
507
507
508 # Custom fields
508 # Custom fields
509 ticket.customs.each do |custom|
509 ticket.customs.each do |custom|
510 next if custom_field_map[custom.name].nil?
510 next if custom_field_map[custom.name].nil?
511 v = CustomValue.new :custom_field => custom_field_map[custom.name],
511 v = CustomValue.new :custom_field => custom_field_map[custom.name],
512 :value => custom.value
512 :value => custom.value
513 v.customized = i
513 v.customized = i
514 next unless v.save
514 next unless v.save
515 migrated_custom_values += 1
515 migrated_custom_values += 1
516 end
516 end
517 end
517 end
518
518
519 # update issue id sequence if needed (postgresql)
519 # update issue id sequence if needed (postgresql)
520 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!')
521 puts
521 puts
522
522
523 # Wiki
523 # Wiki
524 print "Migrating wiki"
524 print "Migrating wiki"
525 if wiki.save
525 if wiki.save
526 TracWikiPage.find(:all, :order => 'name, version').each do |page|
526 TracWikiPage.find(:all, :order => 'name, version').each do |page|
527 # Do not migrate Trac manual wiki pages
527 # Do not migrate Trac manual wiki pages
528 next if TRAC_WIKI_PAGES.include?(page.name)
528 next if TRAC_WIKI_PAGES.include?(page.name)
529 wiki_edit_count += 1
529 wiki_edit_count += 1
530 print '.'
530 print '.'
531 STDOUT.flush
531 STDOUT.flush
532 p = wiki.find_or_new_page(page.name)
532 p = wiki.find_or_new_page(page.name)
533 p.content = WikiContent.new(:page => p) if p.new_record?
533 p.content = WikiContent.new(:page => p) if p.new_record?
534 p.content.text = page.text
534 p.content.text = page.text
535 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'
536 p.content.comments = page.comment
536 p.content.comments = page.comment
537 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 }
538
538
539 next if p.content.new_record?
539 next if p.content.new_record?
540 migrated_wiki_edits += 1
540 migrated_wiki_edits += 1
541
541
542 # Attachments
542 # Attachments
543 page.attachments.each do |attachment|
543 page.attachments.each do |attachment|
544 next unless attachment.exist?
544 next unless attachment.exist?
545 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
546 a = Attachment.new :created_on => attachment.time
546 a = Attachment.new :created_on => attachment.time
547 a.file = attachment
547 a.file = attachment
548 a.author = find_or_create_user(attachment.author)
548 a.author = find_or_create_user(attachment.author)
549 a.description = attachment.description
549 a.description = attachment.description
550 a.container = p
550 a.container = p
551 migrated_wiki_attachments += 1 if a.save
551 migrated_wiki_attachments += 1 if a.save
552 end
552 end
553 end
553 end
554
554
555 wiki.reload
555 wiki.reload
556 wiki.pages.each do |page|
556 wiki.pages.each do |page|
557 page.content.text = convert_wiki_text(page.content.text)
557 page.content.text = convert_wiki_text(page.content.text)
558 Time.fake(page.content.updated_on) { page.content.save }
558 Time.fake(page.content.updated_on) { page.content.save }
559 end
559 end
560 end
560 end
561 puts
561 puts
562
562
563 puts
563 puts
564 puts "Components: #{migrated_components}/#{TracComponent.count}"
564 puts "Components: #{migrated_components}/#{TracComponent.count}"
565 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
565 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
566 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
566 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
567 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
568 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
568 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
569 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
569 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
570 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
571 end
571 end
572
572
573 def self.limit_for(klass, attribute)
573 def self.limit_for(klass, attribute)
574 klass.columns_hash[attribute.to_s].limit
574 klass.columns_hash[attribute.to_s].limit
575 end
575 end
576
576
577 def self.encoding(charset)
577 def self.encoding(charset)
578 @ic = Iconv.new('UTF-8', charset)
578 @ic = Iconv.new('UTF-8', charset)
579 rescue Iconv::InvalidEncoding
579 rescue Iconv::InvalidEncoding
580 puts "Invalid encoding!"
580 puts "Invalid encoding!"
581 return false
581 return false
582 end
582 end
583
583
584 def self.set_trac_directory(path)
584 def self.set_trac_directory(path)
585 @@trac_directory = path
585 @@trac_directory = path
586 raise "This directory doesn't exist!" unless File.directory?(path)
586 raise "This directory doesn't exist!" unless File.directory?(path)
587 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)
588 @@trac_directory
588 @@trac_directory
589 rescue Exception => e
589 rescue Exception => e
590 puts e
590 puts e
591 return false
591 return false
592 end
592 end
593
593
594 def self.trac_directory
594 def self.trac_directory
595 @@trac_directory
595 @@trac_directory
596 end
596 end
597
597
598 def self.set_trac_adapter(adapter)
598 def self.set_trac_adapter(adapter)
599 return false if adapter.blank?
599 return false if adapter.blank?
600 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)
601 # 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
602 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)
603 @@trac_adapter = adapter
603 @@trac_adapter = adapter
604 rescue Exception => e
604 rescue Exception => e
605 puts e
605 puts e
606 return false
606 return false
607 end
607 end
608
608
609 def self.set_trac_db_host(host)
609 def self.set_trac_db_host(host)
610 return nil if host.blank?
610 return nil if host.blank?
611 @@trac_db_host = host
611 @@trac_db_host = host
612 end
612 end
613
613
614 def self.set_trac_db_port(port)
614 def self.set_trac_db_port(port)
615 return nil if port.to_i == 0
615 return nil if port.to_i == 0
616 @@trac_db_port = port.to_i
616 @@trac_db_port = port.to_i
617 end
617 end
618
618
619 def self.set_trac_db_name(name)
619 def self.set_trac_db_name(name)
620 return nil if name.blank?
620 return nil if name.blank?
621 @@trac_db_name = name
621 @@trac_db_name = name
622 end
622 end
623
623
624 def self.set_trac_db_username(username)
624 def self.set_trac_db_username(username)
625 @@trac_db_username = username
625 @@trac_db_username = username
626 end
626 end
627
627
628 def self.set_trac_db_password(password)
628 def self.set_trac_db_password(password)
629 @@trac_db_password = password
629 @@trac_db_password = password
630 end
630 end
631
631
632 def self.set_trac_db_schema(schema)
632 def self.set_trac_db_schema(schema)
633 @@trac_db_schema = schema
633 @@trac_db_schema = schema
634 end
634 end
635
635
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
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
637
637
638 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
638 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
639 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
639 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
640
640
641 def self.target_project_identifier(identifier)
641 def self.target_project_identifier(identifier)
642 project = Project.find_by_identifier(identifier)
642 project = Project.find_by_identifier(identifier)
643 if !project
643 if !project
644 # create the target project
644 # create the target project
645 project = Project.new :name => identifier.humanize,
645 project = Project.new :name => identifier.humanize,
646 :description => ''
646 :description => ''
647 project.identifier = identifier
647 project.identifier = identifier
648 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
649 # enable issues and wiki for the created project
649 # enable issues and wiki for the created project
650 project.enabled_module_names = ['issue_tracking', 'wiki']
650 project.enabled_module_names = ['issue_tracking', 'wiki']
651 else
651 else
652 puts
652 puts
653 puts "This project already exists in your Redmine database."
653 puts "This project already exists in your Redmine database."
654 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] "
655 exit if STDIN.gets.match(/^n$/i)
655 exit if STDIN.gets.match(/^n$/i)
656 end
656 end
657 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
657 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
658 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
658 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
659 @target_project = project.new_record? ? nil : project
659 @target_project = project.new_record? ? nil : project
660 end
660 end
661
661
662 def self.connection_params
662 def self.connection_params
663 if %w(sqlite sqlite3).include?(trac_adapter)
663 if %w(sqlite sqlite3).include?(trac_adapter)
664 {:adapter => trac_adapter,
664 {:adapter => trac_adapter,
665 :database => trac_db_path}
665 :database => trac_db_path}
666 else
666 else
667 {:adapter => trac_adapter,
667 {:adapter => trac_adapter,
668 :database => trac_db_name,
668 :database => trac_db_name,
669 :host => trac_db_host,
669 :host => trac_db_host,
670 :port => trac_db_port,
670 :port => trac_db_port,
671 :username => trac_db_username,
671 :username => trac_db_username,
672 :password => trac_db_password,
672 :password => trac_db_password,
673 :schema_search_path => trac_db_schema
673 :schema_search_path => trac_db_schema
674 }
674 }
675 end
675 end
676 end
676 end
677
677
678 def self.establish_connection
678 def self.establish_connection
679 constants.each do |const|
679 constants.each do |const|
680 klass = const_get(const)
680 klass = const_get(const)
681 next unless klass.respond_to? 'establish_connection'
681 next unless klass.respond_to? 'establish_connection'
682 klass.establish_connection connection_params
682 klass.establish_connection connection_params
683 end
683 end
684 end
684 end
685
685
686 private
686 private
687 def self.encode(text)
687 def self.encode(text)
688 @ic.iconv text
688 @ic.iconv text
689 rescue
689 rescue
690 text
690 text
691 end
691 end
692 end
692 end
693
693
694 puts
694 puts
695 if Redmine::DefaultData::Loader.no_data?
695 if Redmine::DefaultData::Loader.no_data?
696 puts "Redmine configuration need to be loaded before importing data."
696 puts "Redmine configuration need to be loaded before importing data."
697 puts "Please, run this first:"
697 puts "Please, run this first:"
698 puts
698 puts
699 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
699 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
700 exit
700 exit
701 end
701 end
702
702
703 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."
704 print "Are you sure you want to continue ? [y/N] "
704 print "Are you sure you want to continue ? [y/N] "
705 break unless STDIN.gets.match(/^y$/i)
705 break unless STDIN.gets.match(/^y$/i)
706 puts
706 puts
707
707
708 def prompt(text, options = {}, &block)
708 def prompt(text, options = {}, &block)
709 default = options[:default] || ''
709 default = options[:default] || ''
710 while true
710 while true
711 print "#{text} [#{default}]: "
711 print "#{text} [#{default}]: "
712 value = STDIN.gets.chomp!
712 value = STDIN.gets.chomp!
713 value = default if value.blank?
713 value = default if value.blank?
714 break if yield value
714 break if yield value
715 end
715 end
716 end
716 end
717
717
718 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
718 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
719
719
720 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
720 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
721 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}
722 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
722 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
723 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}
724 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}
725 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
725 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
726 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}
727 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
727 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
728 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
728 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
729 end
729 end
730 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
730 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
731 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
731 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
732 puts
732 puts
733
733
734 TracMigrate.migrate
734 TracMigrate.migrate
735 end
735 end
736 end
736 end
737
737
General Comments 0
You need to be logged in to leave comments. Login now