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