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