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