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