ChiliProject is not maintained anymore. Please be advised that there will be no more updates.
We do not recommend that you setup new ChiliProject instances and we urge all existing users to migrate their data to a maintained system, e.g. Redmine. We will provide a migration script later. In the meantime, you can use the instructions by Christian Daehn.
migrate_from_trac.rake.patch
migrate_from_trac12.rake | ||
---|---|---|
1 |
#-- encoding: UTF-8 |
|
2 |
#-- copyright |
|
3 |
# ChiliProject is a project management system. |
|
4 |
# |
|
5 |
# Copyright (C) 2010-2012 the ChiliProject Team |
|
1 |
# redMine - project management software |
|
2 |
# Copyright (C) 2006-2007 Jean-Philippe Lang |
|
6 | 3 |
# |
7 | 4 |
# This program is free software; you can redistribute it and/or |
8 | 5 |
# modify it under the terms of the GNU General Public License |
9 | 6 |
# as published by the Free Software Foundation; either version 2 |
10 | 7 |
# of the License, or (at your option) any later version. |
11 | 8 |
# |
12 |
# See doc/COPYRIGHT.rdoc for more details. |
|
13 |
#++ |
|
14 | ||
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
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 |
|
16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
15 | 17 | |
16 | 18 |
require 'active_record' |
17 | 19 |
require 'iconv' |
18 | 20 |
require 'pp' |
19 | 21 | |
22 | ||
23 |
## Default presets |
|
24 |
$DEFAULT_TRAC_DIRECTORY=ENV['TRAC_ENV'] |
|
25 |
$DEFAULT_PROJECT_IDENTIFIER=ENV['TRAC_ENV']).downcase! |
|
26 | ||
27 |
# part of the email after the '@' |
|
28 |
$DEFAULT_MAIL_SUFFIX='foo.bar' |
|
29 | ||
30 | ||
20 | 31 |
namespace :redmine do |
21 | 32 |
desc 'Trac migration script' |
22 |
task :migrate_from_trac => :environment do |
|
33 |
task :migrate_from_trac12 => :environment do
|
|
23 | 34 | |
24 | 35 |
module TracMigrate |
25 | 36 |
TICKET_MAP = [] |
... | ... | |
52 | 63 | |
53 | 64 |
TRACKER_BUG = Tracker.find_by_position(1) |
54 | 65 |
TRACKER_FEATURE = Tracker.find_by_position(2) |
66 |
# Add a fourth issue type for tasks as we use them heavily |
|
67 |
t = Tracker.find_by_name('Task') |
|
68 |
if !t |
|
69 |
t = Tracker.create(:name => 'Task', :is_in_chlog => true, :is_in_roadmap => false, :position => 4) |
|
70 |
t.workflows.copy(Tracker.find(1)) |
|
71 |
end |
|
72 |
TRACKER_TASK = t |
|
55 | 73 |
DEFAULT_TRACKER = TRACKER_BUG |
56 | 74 |
TRACKER_MAPPING = {'defect' => TRACKER_BUG, |
57 | 75 |
'enhancement' => TRACKER_FEATURE, |
58 |
'task' => TRACKER_FEATURE,
|
|
76 |
'task' => TRACKER_TASK,
|
|
59 | 77 |
'patch' =>TRACKER_FEATURE |
60 | 78 |
} |
61 | 79 | |
... | ... | |
66 | 84 |
ROLE_MAPPING = {'admin' => manager_role, |
67 | 85 |
'developer' => developer_role |
68 | 86 |
} |
87 |
# Add an Hash Table for comments' updatable fields |
|
88 |
PROP_MAPPING = {'status' => 'status_id', |
|
89 |
'owner' => 'assigned_to_id', |
|
90 |
'component' => 'category_id', |
|
91 |
'milestone' => 'fixed_version_id', |
|
92 |
'priority' => 'priority_id', |
|
93 |
'summary' => 'subject', |
|
94 |
'type' => 'tracker_id'} |
|
95 |
|
|
96 |
# Hash table to map completion ratio |
|
97 |
RATIO_MAPPING = {'' => 0, |
|
98 |
'fixed' => 100, |
|
99 |
'invalid' => 0, |
|
100 |
'wontfix' => 0, |
|
101 |
'duplicate' => 100, |
|
102 |
'worksforme' => 0} |
|
103 | ||
104 |
@migrate_accounts = false |
|
105 |
@migrate_accounts_valid_email = true |
|
69 | 106 | |
70 | 107 |
class ::Time |
71 | 108 |
class << self |
72 | 109 |
alias :real_now :now |
110 |
alias :old_at_method :at |
|
73 | 111 |
def now |
74 | 112 |
real_now - @fake_diff.to_i |
75 | 113 |
end |
... | ... | |
78 | 116 |
res = yield |
79 | 117 |
@fake_diff = 0 |
80 | 118 |
res |
119 |
end |
|
120 |
def at(t) |
|
121 |
old_at_method(t>1e6? t*1e-6 : t) |
|
81 | 122 |
end |
82 | 123 |
end |
83 | 124 |
end |
... | ... | |
151 | 192 |
private |
152 | 193 |
def trac_fullpath |
153 | 194 |
attachment_type = read_attribute(:type) |
154 |
trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) } |
|
155 |
"#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}" |
|
195 |
trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*]/n ) {|x| sprintf('%%%02X', x[0]) } |
|
196 |
trac_dir = id.gsub( /[^a-zA-Z0-9\-_\.!~*\\\/]/n ) {|x| sprintf('%%%02X', x[0]) } |
|
197 |
"#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{trac_dir}/#{trac_file}" |
|
156 | 198 |
end |
157 | 199 |
end |
158 | 200 | |
... | ... | |
161 | 203 |
set_inheritance_column :none |
162 | 204 | |
163 | 205 |
# ticket changes: only migrate status changes and comments |
164 |
has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket |
|
165 |
has_many :attachments, :class_name => "TracAttachment", |
|
206 |
has_many :changes, :class_name => "TracMigrate::TracTicketChange", :foreign_key => :ticket
|
|
207 |
has_many :attachments, :class_name => "TracMigrate::TracAttachment",
|
|
166 | 208 |
:finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" + |
167 | 209 |
" WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" + |
168 | 210 |
' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\'' |
169 |
has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket |
|
211 |
has_many :customs, :class_name => "TracMigrate::TracTicketCustom", :foreign_key => :ticket
|
|
170 | 212 | |
171 | 213 |
def ticket_type |
172 | 214 |
read_attribute(:type) |
... | ... | |
187 | 229 |
class TracTicketChange < ActiveRecord::Base |
188 | 230 |
set_table_name :ticket_change |
189 | 231 | |
190 |
def time; Time.at(read_attribute(:time)) end |
|
191 |
end |
|
192 | ||
193 |
TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \ |
|
232 |
def time |
|
233 |
Time.at(read_attribute(:time)) |
|
234 |
end |
|
235 |
end |
|
236 | ||
237 |
TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup \ |
|
238 |
TracBrowser TracCgi TracChangeset TracInstallPlatforms TracMultipleProjects TracModWSGI \ |
|
194 | 239 |
TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \ |
195 | 240 |
TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \ |
196 | 241 |
TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \ |
197 | 242 |
TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \ |
198 | 243 |
WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \ |
199 |
CamelCase TitleIndex)
|
|
200 | ||
244 |
CamelCase TitleIndex TracNavigation TracFineGrainedPermissions TracWorkflow TimingAndEstimationPluginUserManual \
|
|
245 |
PageTemplates BadContent TracRepositoryAdmin TracWikiMacros) |
|
201 | 246 |
class TracWikiPage < ActiveRecord::Base |
202 | 247 |
set_table_name :wiki |
203 | 248 |
set_primary_key :name |
204 | 249 | |
205 |
has_many :attachments, :class_name => "TracAttachment", |
|
250 |
has_many :attachments, :class_name => "TracMigrate::TracAttachment",
|
|
206 | 251 |
:finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" + |
207 | 252 |
" WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" + |
208 | 253 |
' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\'' |
... | ... | |
215 | 260 |
def time; Time.at(read_attribute(:time)) end |
216 | 261 |
end |
217 | 262 | |
263 |
class TracAccount < ActiveRecord::Base |
|
264 |
set_table_name :session |
|
265 |
named_scope :authenticated, {:conditions => {:authenticated => 1}} |
|
266 | ||
267 |
end |
|
268 | ||
218 | 269 |
class TracPermission < ActiveRecord::Base |
219 | 270 |
set_table_name :permission |
220 | 271 |
end |
... | ... | |
225 | 276 | |
226 | 277 |
def self.find_or_create_user(username, project_member = false) |
227 | 278 |
return User.anonymous if username.blank? |
228 | ||
229 |
u = User.find_by_login(username) |
|
230 |
if !u |
|
279 |
return User.anonymous if [ |
|
280 |
'admin', 'anonymous', 'asas', 'remote_user', 'somebody', 'trac', 'foo', |
|
281 |
].include? username.downcase |
|
282 |
|
|
283 |
## HACK clean username - if username is an email use first part of the address |
|
284 |
username = username.split("@")[0] if username.include?("@") |
|
285 | ||
286 |
_u = User.find_by_login(username) |
|
287 |
if !_u |
|
231 | 288 |
# Create a new user if not found |
232 | 289 |
mail = username[0,limit_for(User, 'mail')] |
233 | 290 |
if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email') |
234 | 291 |
mail = mail_attr.value |
235 |
end |
|
236 |
mail = "#{mail}@foo.bar" unless mail.include?("@") |
|
292 |
elsif @migrate_accounts_valid_email |
|
293 |
return User.find(:first) |
|
294 |
end |
|
295 |
mail = "#{mail}@#{$DEFAULT_MAIL_SUFFIX}" unless mail.include?("@") |
|
237 | 296 | |
238 | 297 |
name = username |
239 | 298 |
if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name') |
240 | 299 |
name = name_attr.value |
241 | 300 |
end |
242 |
name =~ (/(.*)(\s+\w+)?/)
|
|
301 |
name =~ (/(.+?)(?:[\ \t]+(.+)?|[\ \t]+|)$/)
|
|
243 | 302 |
fn = $1.strip |
303 |
# Add a dash for lastname or the user is not saved (bugfix) |
|
244 | 304 |
ln = ($2 || '-').strip |
245 | 305 | |
246 |
u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'), |
|
306 |
_u = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option, |
|
307 |
:mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'), |
|
247 | 308 |
:firstname => fn[0, limit_for(User, 'firstname')], |
248 |
:lastname => ln[0, limit_for(User, 'lastname')] |
|
249 | ||
250 |
u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-') |
|
251 |
u.password = 'trac'
|
|
252 |
u.admin = true if TracPermission.find_by_username_and_action(username, 'admin') |
|
309 |
:lastname => ln[0, limit_for(User, 'lastname')])
|
|
310 | ||
311 |
_u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
|
|
312 |
_u.password = 'tractrac'
|
|
313 |
_u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
|
|
253 | 314 |
# finally, a default user is used if the new user is not valid |
254 |
u = User.find(:first) unless u.save
|
|
315 |
_u = User.find(:first) unless _u.save
|
|
255 | 316 |
end |
256 | 317 |
# Make sure he is a member of the project |
257 |
if project_member && !u.member_of?(@target_project) |
|
318 |
if project_member && !_u.member_of?(@target_project)
|
|
258 | 319 |
role = DEFAULT_ROLE |
259 |
if u.admin |
|
320 |
if _u.admin
|
|
260 | 321 |
role = ROLE_MAPPING['admin'] |
261 | 322 |
elsif TracPermission.find_by_username_and_action(username, 'developer') |
262 | 323 |
role = ROLE_MAPPING['developer'] |
263 | 324 |
end |
264 |
Member.create(:user => u, :project => @target_project, :roles => [role]) |
|
265 |
u.reload |
|
266 |
end |
|
267 |
u |
|
325 |
Member.create(:user => _u, :project => @target_project, :roles => [role])
|
|
326 |
_u.reload
|
|
327 |
end |
|
328 |
_u
|
|
268 | 329 |
end |
269 | 330 | |
270 | 331 |
# Basic wiki syntax conversion |
271 | 332 |
def self.convert_wiki_text(text) |
272 |
# Titles |
|
273 |
text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"} |
|
274 |
# External Links |
|
275 |
text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"} |
|
276 |
# Ticket links: |
|
277 |
# [ticket:234 Text],[ticket:234 This is a test] |
|
278 |
text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1') |
|
279 |
# ticket:1234 |
|
280 |
# #1 is working cause Redmine uses the same syntax. |
|
281 |
text = text.gsub(/ticket\:([^\ ]+)/, '#\1') |
|
282 |
# Milestone links: |
|
283 |
# [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)] |
|
284 |
# The text "Milestone 0.1.0 (Mercury)" is not converted, |
|
285 |
# cause Redmine's wiki does not support this. |
|
286 |
text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"') |
|
287 |
# [milestone:"0.1.0 Mercury"] |
|
288 |
text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"') |
|
289 |
text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"') |
|
290 |
# milestone:0.1.0 |
|
291 |
text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1') |
|
292 |
text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1') |
|
293 |
# Internal Links |
|
294 |
text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below |
|
295 |
text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} |
|
296 |
text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} |
|
297 |
text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} |
|
298 |
text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} |
|
299 |
text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"} |
|
300 | ||
301 |
# Links to pages UsingJustWikiCaps |
|
302 |
text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]') |
|
303 |
# Normalize things that were supposed to not be links |
|
304 |
# like !NotALink |
|
305 |
text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2') |
|
306 |
# Revisions links |
|
307 |
text = text.gsub(/\[(\d+)\]/, 'r\1') |
|
308 |
# Ticket number re-writing |
|
309 |
text = text.gsub(/#(\d+)/) do |s| |
|
310 |
if $1.length < 10 |
|
311 |
# TICKET_MAP[$1.to_i] ||= $1 |
|
312 |
"\##{TICKET_MAP[$1.to_i] || $1}" |
|
313 |
else |
|
314 |
s |
|
315 |
end |
|
316 |
end |
|
317 |
# We would like to convert the Code highlighting too |
|
318 |
# This will go into the next line. |
|
319 |
shebang_line = false |
|
320 |
# Reguar expression for start of code |
|
321 |
pre_re = /\{\{\{/ |
|
322 |
# Code hightlighing... |
|
323 |
shebang_re = /^\#\!([a-z]+)/ |
|
324 |
# Regular expression for end of code |
|
325 |
pre_end_re = /\}\}\}/ |
|
326 | ||
327 |
# Go through the whole text..extract it line by line |
|
328 |
text = text.gsub(/^(.*)$/) do |line| |
|
329 |
m_pre = pre_re.match(line) |
|
330 |
if m_pre |
|
331 |
line = '<pre>' |
|
332 |
else |
|
333 |
m_sl = shebang_re.match(line) |
|
334 |
if m_sl |
|
335 |
shebang_line = true |
|
336 |
line = '<code class="' + m_sl[1] + '">' |
|
337 |
end |
|
338 |
m_pre_end = pre_end_re.match(line) |
|
339 |
if m_pre_end |
|
340 |
line = '</pre>' |
|
341 |
if shebang_line |
|
342 |
line = '</code>' + line |
|
343 |
end |
|
344 |
end |
|
345 |
end |
|
346 |
line |
|
347 |
end |
|
348 | ||
349 |
# Highlighting |
|
350 |
text = text.gsub(/'''''([^\s])/, '_*\1') |
|
351 |
text = text.gsub(/([^\s])'''''/, '\1*_') |
|
352 |
text = text.gsub(/'''/, '*') |
|
353 |
text = text.gsub(/''/, '_') |
|
354 |
text = text.gsub(/__/, '+') |
|
355 |
text = text.gsub(/~~/, '-') |
|
356 |
text = text.gsub(/`/, '@') |
|
357 |
text = text.gsub(/,,/, '~') |
|
358 |
# Lists |
|
359 |
text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "} |
|
360 | ||
361 |
text |
|
333 |
convert_wiki_text_mapping(text, TICKET_MAP) |
|
362 | 334 |
end |
363 | 335 | |
364 | 336 |
def self.migrate |
... | ... | |
367 | 339 |
# Quick database test |
368 | 340 |
TracComponent.count |
369 | 341 | |
342 |
migrated_accounts = 0 |
|
370 | 343 |
migrated_components = 0 |
371 | 344 |
migrated_milestones = 0 |
372 | 345 |
migrated_tickets = 0 |
... | ... | |
375 | 348 |
migrated_wiki_edits = 0 |
376 | 349 |
migrated_wiki_attachments = 0 |
377 | 350 | |
378 |
#Wiki system initializing... |
|
351 |
# Wiki system initializing...
|
|
379 | 352 |
@target_project.wiki.destroy if @target_project.wiki |
380 | 353 |
@target_project.reload |
381 | 354 |
wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart') |
382 | 355 |
wiki_edit_count = 0 |
383 | 356 | |
357 |
# Accounts |
|
358 |
# SELECT sid FROM session WHERE authenticated=1; |
|
359 |
# SELECT value FROM session_attribute WHERE sid='s0undt3ch' AND name='email'; |
|
360 |
if @migrate_accounts |
|
361 |
who = "Migrating Accounts" |
|
362 |
accounts_total = TracAccount.authenticated.find(:all).count |
|
363 |
TracAccount.authenticated.find(:all).each do |account| |
|
364 |
if account.authenticated == 1 |
|
365 |
a = find_or_create_user(account.sid, true) |
|
366 |
next unless a.save! |
|
367 |
migrated_accounts += 1 |
|
368 |
simplebar(who, migrated_accounts, accounts_total) |
|
369 |
end |
|
370 |
end |
|
371 |
end |
|
372 | ||
373 |
# After this point set migrate_accounts_valid_email to false so that |
|
374 |
# tickets still have a issuer even though they don't have a valid |
|
375 |
# email address |
|
376 |
TracMigrate.set_migrate_accounts_valid_email(false) |
|
377 | ||
384 | 378 |
# Components |
385 |
print "Migrating components"
|
|
379 |
who = "Migrating components"
|
|
386 | 380 |
issues_category_map = {} |
381 |
components_total = TracComponent.count |
|
387 | 382 |
TracComponent.find(:all).each do |component| |
388 |
print '.' |
|
389 |
STDOUT.flush |
|
390 |
c = IssueCategory.new :project => @target_project, |
|
383 |
_c = IssueCategory.new :project => @target_project, |
|
391 | 384 |
:name => encode(component.name[0, limit_for(IssueCategory, 'name')]) |
392 |
next unless c.save |
|
393 |
issues_category_map[component.name] = c |
|
394 |
migrated_components += 1 |
|
395 |
end |
|
396 |
puts |
|
385 |
# Owner |
|
386 |
unless component.owner.blank? |
|
387 |
_c.assigned_to = find_or_create_user(component.owner, true) |
|
388 |
end |
|
389 |
next unless _c.save |
|
390 |
issues_category_map[component.name] = _c |
|
391 |
migrated_components += 1 |
|
392 |
simplebar(who, migrated_components, components_total) |
|
393 |
end |
|
394 |
puts if migrated_components < components_total |
|
397 | 395 | |
398 | 396 |
# Milestones |
399 |
print "Migrating milestones"
|
|
397 |
who = "Migrating milestones"
|
|
400 | 398 |
version_map = {} |
399 |
milestone_wiki = Array.new |
|
400 |
milestones_total = TracMilestone.count |
|
401 | 401 |
TracMilestone.find(:all).each do |milestone| |
402 |
print '.' |
|
403 |
STDOUT.flush |
|
404 | 402 |
# First we try to find the wiki page... |
405 | 403 |
p = wiki.find_or_new_page(milestone.name.to_s) |
406 | 404 |
p.content = WikiContent.new(:page => p) if p.new_record? |
407 |
p.content.text = milestone.description.to_s |
|
405 |
# be sure that milestone text is not empty |
|
406 |
p.content.text = milestone.description.nil? || milestone.description.empty? ? milestone.name.to_s : milestone.description.to_s |
|
408 | 407 |
p.content.author = find_or_create_user('trac') |
409 | 408 |
p.content.comments = 'Milestone' |
410 |
p.save |
|
409 | ||
410 |
status_p_save = p.save |
|
411 |
puts ".status_p_save: " + status_p_save unless status_p_save |
|
411 | 412 | |
412 | 413 |
v = Version.new :project => @target_project, |
413 | 414 |
:name => encode(milestone.name[0, limit_for(Version, 'name')]), |
... | ... | |
417 | 418 | |
418 | 419 |
next unless v.save |
419 | 420 |
version_map[milestone.name] = v |
421 |
milestone_wiki.push(milestone.name); |
|
420 | 422 |
migrated_milestones += 1 |
421 |
end |
|
422 |
puts |
|
423 |
simplebar(who, migrated_milestones, milestones_total) |
|
424 |
end |
|
425 |
puts if migrated_milestones < milestones_total |
|
423 | 426 | |
424 | 427 |
# Custom fields |
425 | 428 |
# TODO: read trac.ini instead |
426 |
print "Migrating custom fields"
|
|
429 |
who = "Migrating custom fields"
|
|
427 | 430 |
custom_field_map = {} |
428 | 431 |
TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field| |
429 |
print '.'
|
|
430 |
STDOUT.flush |
|
432 |
#print '.' # Maybe not needed this out?
|
|
433 |
#STDOUT.flush
|
|
431 | 434 |
# Redmine custom field name |
432 | 435 |
field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize |
433 | 436 |
# Find if the custom already exists in Redmine |
434 | 437 |
f = IssueCustomField.find_by_name(field_name) |
438 |
# Ugly hack to handle billable checkbox. Would require to read the ini file to be cleaner |
|
439 |
if field_name == 'Billable' |
|
440 |
format = 'bool' |
|
441 |
else |
|
442 |
format = 'string' |
|
443 |
end |
|
435 | 444 |
# Or create a new one |
436 | 445 |
f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize, |
437 |
:field_format => 'string')
|
|
446 |
:field_format => format, :default_value => '')
|
|
438 | 447 | |
439 | 448 |
next if f.new_record? |
440 | 449 |
f.trackers = Tracker.find(:all) |
441 | 450 |
f.projects << @target_project |
442 | 451 |
custom_field_map[field.name] = f |
443 | 452 |
end |
444 |
puts |
|
453 |
#puts
|
|
445 | 454 | |
446 | 455 |
# Trac 'resolution' field as a Redmine custom field |
447 | 456 |
r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" }) |
448 | 457 |
r = IssueCustomField.new(:name => 'Resolution', |
449 | 458 |
:field_format => 'list', |
459 |
:default_value => '', |
|
450 | 460 |
:is_filter => true) if r.nil? |
451 | 461 |
r.trackers = Tracker.find(:all) |
452 | 462 |
r.projects << @target_project |
... | ... | |
454 | 464 |
r.save! |
455 | 465 |
custom_field_map['resolution'] = r |
456 | 466 | |
467 |
# Trac 'keywords' field as a Redmine custom field |
|
468 |
k = IssueCustomField.find(:first, :conditions => { :name => "Keywords" }) |
|
469 |
k = IssueCustomField.new(:name => 'Keywords', |
|
470 |
:field_format => 'string', |
|
471 |
:default_value => '', |
|
472 |
:is_filter => true) if k.nil? |
|
473 |
k.trackers = Tracker.find(:all) |
|
474 |
k.projects << @target_project |
|
475 |
k.save! |
|
476 |
custom_field_map['keywords'] = k |
|
477 | ||
478 |
# Trac ticket id as a Redmine custom field |
|
479 |
tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" }) |
|
480 |
tid = IssueCustomField.new(:name => 'TracID', |
|
481 |
:field_format => 'string', |
|
482 |
:default_value => '', |
|
483 |
:is_filter => true) if tid.nil? |
|
484 |
tid.trackers = Tracker.find(:all) |
|
485 |
tid.projects << @target_project |
|
486 |
tid.save! |
|
487 |
custom_field_map['tracid'] = tid |
|
488 |
|
|
457 | 489 |
# Tickets |
458 |
print "Migrating tickets" |
|
459 |
TracTicket.find_each(:batch_size => 200) do |ticket| |
|
460 |
print '.' |
|
461 |
STDOUT.flush |
|
462 |
i = Issue.new :project => @target_project, |
|
490 |
who = "Migrating tickets" |
|
491 |
tickets_total = TracTicket.count |
|
492 |
TracTicket.find_each(:batch_size => 200) do |ticket| |
|
493 |
#puts ".Ticket.time: " + ticket.time.inspect |
|
494 |
begin |
|
495 |
ticket_desc = encode(ticket.description) |
|
496 |
rescue Exception=>e |
|
497 |
ticket_desc = Iconv.conv("UTF8", "LATIN1", ticket.description) |
|
498 |
end |
|
499 |
_i = Issue.new :project => @target_project, |
|
463 | 500 |
:subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]), |
464 |
:description => convert_wiki_text(encode(ticket.description)),
|
|
501 |
:description => ticket_desc,
|
|
465 | 502 |
:priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY, |
466 | 503 |
:created_on => ticket.time |
467 |
i.author = find_or_create_user(ticket.reporter) |
|
468 |
i.category = issues_category_map[ticket.component] unless ticket.component.blank? |
|
469 |
i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank? |
|
470 |
i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS |
|
471 |
i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER |
|
472 |
i.id = ticket.id unless Issue.exists?(ticket.id) |
|
473 |
next unless Time.fake(ticket.changetime) { i.save } |
|
474 |
TICKET_MAP[ticket.id] = i.id |
|
504 | ||
505 |
# Add the ticket's author to project's reporter list (bugfix) |
|
506 |
_i.author = find_or_create_user(ticket.reporter,true) |
|
507 |
# Extrapolate done_ratio from ticket's resolution |
|
508 |
_i.done_ratio = RATIO_MAPPING[ticket.resolution] || 0 |
|
509 |
_i.category = issues_category_map[ticket.component] unless ticket.component.blank? |
|
510 |
_i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank? |
|
511 |
_i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS |
|
512 |
_i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER |
|
513 |
_i.id = ticket.id unless Issue.exists?(ticket.id) |
|
514 |
|
|
515 |
# Owner |
|
516 |
unless ticket.owner.blank? |
|
517 |
_i.assigned_to = find_or_create_user(ticket.owner, true) |
|
518 |
end |
|
519 |
|
|
520 |
Time.fake(ticket.changetime) { _i.save } |
|
521 |
|
|
522 |
TICKET_MAP[ticket.id] = _i.id |
|
475 | 523 |
migrated_tickets += 1 |
476 | ||
477 |
# Owner |
|
478 |
unless ticket.owner.blank? |
|
479 |
i.assigned_to = find_or_create_user(ticket.owner, true) |
|
480 |
Time.fake(ticket.changetime) { i.save } |
|
481 |
end |
|
482 | ||
483 |
# Comments and status/resolution changes |
|
524 |
simplebar(who, migrated_tickets, tickets_total) |
|
525 |
|
|
526 |
|
|
527 |
# Handle CC field |
|
528 |
ticket.cc.split(',').each do |email| |
|
529 |
w = Watcher.new :watchable_type => 'Issue', |
|
530 |
:watchable_id => _i.id, |
|
531 |
:user_id => find_or_create_user(email.strip).id |
|
532 |
status_w_save = w.save |
|
533 |
end |
|
534 | ||
535 |
# Necessary to handle direct link to note from timelogs and putting the right start time in issue |
|
536 |
noteid = 1 |
|
537 |
# Comments and status/resolution/keywords changes |
|
484 | 538 |
ticket.changes.group_by(&:time).each do |time, changeset| |
485 |
status_change = changeset.select {|change| change.field == 'status'}.first |
|
539 |
status_change = changeset.select {|change| change.field == 'status'}.first
|
|
486 | 540 |
resolution_change = changeset.select {|change| change.field == 'resolution'}.first |
487 |
comment_change = changeset.select {|change| change.field == 'comment'}.first |
|
488 | ||
489 |
n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''), |
|
490 |
:created_on => time |
|
491 |
n.user = find_or_create_user(changeset.first.author) |
|
492 |
n.journalized = i |
|
541 |
keywords_change = changeset.select {|change| change.field == 'keywords'}.first |
|
542 |
comment_change = changeset.select {|change| change.field == 'comment'}.first |
|
543 |
# Handle more ticket changes (owner, component, milestone, priority, summary, type, done_ratio and hours) |
|
544 |
assigned_change = changeset.select {|change| change.field == 'owner'}.first |
|
545 |
category_change = changeset.select {|change| change.field == 'component'}.first |
|
546 |
version_change = changeset.select {|change| change.field == 'milestone'}.first |
|
547 |
priority_change = changeset.select {|change| change.field == 'priority'}.first |
|
548 |
subject_change = changeset.select {|change| change.field == 'summary'}.first |
|
549 |
tracker_change = changeset.select {|change| change.field == 'type'}.first |
|
550 |
time_change = changeset.select {|change| change.field == 'hours'}.first |
|
551 | ||
552 |
# If it's the first note then we set the start working time to handle calendar and gantts |
|
553 |
if noteid == 1 |
|
554 |
_i.start_date = time |
|
555 |
end |
|
556 | ||
557 |
n = IssueJournal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''), |
|
558 |
:created_at => time, |
|
559 |
:version => _i.last_journal.version + 1 |
|
560 |
|
|
561 |
n.user = find_or_create_user(changeset.first.author, true) |
|
562 |
n.journalized = _i |
|
563 |
|
|
493 | 564 |
if status_change && |
494 | 565 |
STATUS_MAPPING[status_change.oldvalue] && |
495 | 566 |
STATUS_MAPPING[status_change.newvalue] && |
496 | 567 |
(STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue]) |
497 |
n.details << JournalDetail.new(:property => 'attr', |
|
498 |
:prop_key => 'status_id', |
|
499 |
:old_value => STATUS_MAPPING[status_change.oldvalue].id, |
|
500 |
:value => STATUS_MAPPING[status_change.newvalue].id) |
|
568 |
n.changes = n.changes.merge({ |
|
569 |
'status_id' => [ |
|
570 |
STATUS_MAPPING[status_change.oldvalue].id, |
|
571 |
STATUS_MAPPING[status_change.newvalue].id |
|
572 |
] |
|
573 |
}) |
|
501 | 574 |
end |
502 |
if resolution_change |
|
503 |
n.details << JournalDetail.new(:property => 'cf', |
|
504 |
:prop_key => custom_field_map['resolution'].id, |
|
505 |
:old_value => resolution_change.oldvalue, |
|
506 |
:value => resolution_change.newvalue) |
|
575 |
|
|
576 |
# Handle resolution changes |
|
577 |
if resolution_change |
|
578 |
## BUG not working |
|
579 |
# n.changes = n.changes.merge({ |
|
580 |
# custom_field_map['resolution'].id => [ |
|
581 |
# resolution_change.oldvalue, |
|
582 |
# resolution_change.newvalue |
|
583 |
# ] |
|
584 |
# }) |
|
585 | ||
586 |
# Add a change for the done_ratio |
|
587 |
n.changes = n.changes.merge({ |
|
588 |
'done_ratio' => [ |
|
589 |
RATIO_MAPPING[resolution_change.oldvalue], |
|
590 |
RATIO_MAPPING[resolution_change.newvalue] |
|
591 |
] |
|
592 |
}) |
|
593 |
|
|
594 |
# Arbitrary set the due time to the day the ticket was resolved for calendar and gantts |
|
595 |
case RATIO_MAPPING[resolution_change.newvalue] |
|
596 |
when 0 |
|
597 |
_i.due_date = nil |
|
598 |
when 100 |
|
599 |
_i.due_date = time |
|
600 |
end |
|
507 | 601 |
end |
508 |
n.save unless n.details.empty? && n.notes.blank? |
|
509 |
end |
|
602 |
|
|
603 |
# Handle keyword changes |
|
604 |
if keywords_change |
|
605 |
n.changes = n.changes.merge({ |
|
606 |
custom_field_map['keywords'].id => [ |
|
607 |
keywords_change.oldvalue, |
|
608 |
keywords_change.newvalue |
|
609 |
] |
|
610 |
}) |
|
611 |
end |
|
612 |
|
|
613 |
# Handle assignement/owner changes |
|
614 |
if assigned_change |
|
615 |
n.changes = n.changes.merge({ |
|
616 |
PROP_MAPPING['owner'] => [ |
|
617 |
find_or_create_user(assigned_change.oldvalue, true).id, |
|
618 |
find_or_create_user(assigned_change.newvalue, true).id |
|
619 |
] |
|
620 |
}) |
|
621 |
end |
|
622 |
|
|
623 |
# Handle component/category changes |
|
624 |
if category_change |
|
625 |
n.changes = n.changes.merge({ |
|
626 |
PROP_MAPPING['component'] => [ |
|
627 |
issues_category_map[category_change.oldvalue].nil? ? nil : issues_category_map[category_change.oldvalue].id, |
|
628 |
issues_category_map[category_change.newvalue].nil? ? nil : issues_category_map[category_change.newvalue].id |
|
629 |
] |
|
630 |
}) |
|
631 |
end |
|
632 |
|
|
633 |
# Handle version/milestone changes |
|
634 |
if version_change |
|
635 |
n.changes = n.changes.merge({ |
|
636 |
PROP_MAPPING['milestone'] => [ |
|
637 |
version_map[version_change.oldvalue].nil? ? nil : version_map[version_change.oldvalue].id, |
|
638 |
version_map[version_change.newvalue].nil? ? nil : version_map[version_change.newvalue].id |
|
639 |
] |
|
640 |
}) |
|
641 |
end |
|
642 |
|
|
643 |
# Handle priority changes |
|
644 |
if priority_change |
|
645 |
n.changes = n.changes.merge({ |
|
646 |
PROP_MAPPING['priority'] => [ |
|
647 |
PRIORITY_MAPPING[priority_change.oldvalue].id, |
|
648 |
PRIORITY_MAPPING[priority_change.newvalue].id |
|
649 |
] |
|
650 |
}) |
|
651 |
end |
|
652 |
|
|
653 |
# Handle subject/summary changes |
|
654 |
if subject_change |
|
655 |
n.changes = n.changes.merge({ |
|
656 |
PROP_MAPPING['summary'] => [ |
|
657 |
encode(subject_change.oldvalue[0, limit_for(Issue, 'subject')]), |
|
658 |
encode(subject_change.newvalue[0, limit_for(Issue, 'subject')]) |
|
659 |
] |
|
660 |
}) |
|
661 |
end |
|
662 |
|
|
663 |
# Handle tracker/type (bug, feature) changes |
|
664 |
if tracker_change |
|
665 |
n.changes = n.changes.merge({ |
|
666 |
PROP_MAPPING['type'] => [ |
|
667 |
TRACKER_MAPPING[tracker_change.oldvalue].id || DEFAULT_TRACKER.id, |
|
668 |
TRACKER_MAPPING[tracker_change.newvalue].id || DEFAULT_TRACKER.id |
|
669 |
] |
|
670 |
}) |
|
671 |
end |
|
672 | ||
673 |
# Add timelog entries for each time changes (from timeandestimation plugin) |
|
674 |
if time_change && time_change.newvalue != '0' && time_change.newvalue != '' |
|
675 |
t = TimeEntry.new(:project => @target_project, |
|
676 |
:issue => _i, |
|
677 |
:user => n.user, |
|
678 |
:spent_on => time, |
|
679 |
:hours => time_change.newvalue.to_i.abs, ## HACK, hours has to be positiv |
|
680 |
:created_on => time, |
|
681 |
:updated_on => time, |
|
682 |
:activity_id => TimeEntryActivity.find_by_position(2).id, |
|
683 |
:comments => "#{convert_wiki_text(n.notes.each_line.first.chomp)[0,100] unless !n.notes.each_line.first}... \"more\":/issues/#{_i.id}#note-#{noteid}") |
|
684 |
t.save |
|
685 |
t.errors.each_full{|msg| puts msg } |
|
686 |
end |
|
687 |
|
|
688 |
# saving changes and registering them for the issue |
|
689 |
n.save unless n.changes.empty? && n.notes.blank? |
|
690 |
_i.journals << n unless n.changes.empty? && n.notes.blank? |
|
691 |
|
|
692 |
# Set correct changetime of the issue |
|
693 |
## HACK Unnecessary next unless Time.fake(ticket.changetime) { _i.save } |
|
694 |
|
|
695 |
noteid += 1 |
|
696 |
end |
|
697 |
|
|
698 |
# saving issue changes to database |
|
699 |
Time.fake(ticket.changetime) { _i.save } |
|
700 |
|
|
701 |
#end |
|
510 | 702 | |
511 | 703 |
# Attachments |
512 | 704 |
ticket.attachments.each do |attachment| |
... | ... | |
515 | 707 |
a = Attachment.new :created_on => attachment.time |
516 | 708 |
a.file = attachment |
517 | 709 |
a.author = find_or_create_user(attachment.author) |
518 |
a.container = i |
|
710 |
a.container = _i
|
|
519 | 711 |
a.description = attachment.description |
712 |
|
|
713 |
## TODO DUMMY HACK be sure, that the attachment exists on the disk - copy real files afterwards |
|
714 |
## TODO get attachment file directory from project settings |
|
715 |
result = system( "touch files/" + a.disk_filename ) |
|
716 |
|
|
520 | 717 |
migrated_ticket_attachments += 1 if a.save |
521 | 718 |
} |
522 | 719 |
end |
523 | 720 | |
524 |
# Custom fields |
|
721 |
# Custom fields
|
|
525 | 722 |
custom_values = ticket.customs.inject({}) do |h, custom| |
526 | 723 |
if custom_field = custom_field_map[custom.name] |
527 | 724 |
h[custom_field.id] = custom.value |
... | ... | |
532 | 729 |
if custom_field_map['resolution'] && !ticket.resolution.blank? |
533 | 730 |
custom_values[custom_field_map['resolution'].id] = ticket.resolution |
534 | 731 |
end |
535 |
i.custom_field_values = custom_values |
|
536 |
i.save_custom_field_values |
|
732 |
if custom_field_map['keywords'] && !ticket.keywords.blank? |
|
733 |
custom_values[custom_field_map['keywords'].id] = ticket.keywords |
|
734 |
end |
|
735 |
if custom_field_map['tracid'] |
|
736 |
custom_values[custom_field_map['tracid'].id] = ticket.id |
|
737 |
end |
|
738 |
_i.custom_field_values = custom_values |
|
739 |
_i.save_custom_field_values |
|
740 |
|
|
537 | 741 |
end |
538 | 742 | |
539 | 743 |
# update issue id sequence if needed (postgresql) |
540 | 744 |
Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!') |
541 |
puts |
|
745 |
puts if migrated_tickets < tickets_total
|
|
542 | 746 | |
543 | 747 |
# Wiki |
544 |
print "Migrating wiki"
|
|
748 |
who = "Migrating wiki"
|
|
545 | 749 |
if wiki.save |
750 |
wiki_edits_total = TracWikiPage.count |
|
546 | 751 |
TracWikiPage.find(:all, :order => 'name, version').each do |page| |
547 | 752 |
# Do not migrate Trac manual wiki pages |
548 |
next if TRAC_WIKI_PAGES.include?(page.name)
|
|
549 |
wiki_edit_count += 1
|
|
550 |
print '.'
|
|
551 |
STDOUT.flush
|
|
753 |
if TRAC_WIKI_PAGES.include?(page.name) then
|
|
754 |
wiki_edits_total -= 1
|
|
755 |
next
|
|
756 |
end
|
|
552 | 757 |
p = wiki.find_or_new_page(page.name) |
553 | 758 |
p.content = WikiContent.new(:page => p) if p.new_record? |
554 | 759 |
p.content.text = page.text |
555 | 760 |
p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac' |
556 | 761 |
p.content.comments = page.comment |
557 | 762 |
Time.fake(page.time) { p.new_record? ? p.save : p.content.save } |
763 |
migrated_wiki_edits += 1 |
|
764 |
simplebar(who, migrated_wiki_edits, wiki_edits_total) |
|
558 | 765 | |
559 | 766 |
next if p.content.new_record? |
560 |
migrated_wiki_edits += 1 |
|
561 | 767 | |
562 | 768 |
# Attachments |
563 | 769 |
page.attachments.each do |attachment| |
... | ... | |
574 | 780 |
end |
575 | 781 |
end |
576 | 782 | |
577 |
wiki.reload |
|
578 |
wiki.pages.each do |page| |
|
579 |
page.content.text = convert_wiki_text(page.content.text) |
|
580 |
Time.fake(page.content.updated_on) { page.content.save } |
|
581 |
end |
|
582 |
end |
|
783 |
end |
|
784 |
puts if migrated_wiki_edits < wiki_edits_total |
|
785 | ||
786 |
# Now load each wiki page and transform its content into textile format |
|
787 |
puts "\nTransform texts to textile format:" |
|
788 |
|
|
789 |
wiki_pages_count = 0 |
|
790 |
issues_count = 0 |
|
791 |
milestone_wiki_count = 0 |
|
792 | ||
793 |
who = " in Wiki pages" |
|
794 |
wiki.reload |
|
795 |
wiki_pages_total = wiki.pages.count |
|
796 |
wiki.pages.each do |page| |
|
797 |
page.content.text = convert_wiki_text(page.content.text) |
|
798 |
Time.fake(page.content.updated_on) { page.content.save } |
|
799 |
wiki_pages_count += 1 |
|
800 |
simplebar(who, wiki_pages_count, wiki_pages_total) |
|
801 |
end |
|
802 |
puts if wiki_pages_count < wiki_pages_total |
|
803 |
|
|
804 |
|
|
805 |
who = " in Issues" |
|
806 |
issues_total = TICKET_MAP.count |
|
807 |
TICKET_MAP.each do |newId| |
|
808 |
issues_count += 1 |
|
809 |
simplebar(who, issues_count, issues_total) |
|
810 |
next if newId.nil? |
|
811 |
issue = findIssue(newId) |
|
812 |
next if issue.nil? |
|
813 |
|
|
814 |
# convert issue description |
|
815 |
issue.description = convert_wiki_text(issue.description) |
|
816 |
|
|
817 |
# Converted issue comments had their last updated time set to the day of the migration (bugfix) |
|
818 |
next unless Time.fake(issue.updated_on) { issue.save } |
|
819 |
|
|
820 |
## TODO CHECKEN |
|
821 |
# convert issue journals |
|
822 |
#issue.journals.find(:all).each do |journal| |
|
823 |
# journal.notes = convert_wiki_text(journal.notes) |
|
824 |
# journal.save |
|
825 |
#end |
|
826 |
end |
|
827 |
puts if issues_count < issues_total |
|
828 | ||
829 |
who = " in Milestone descriptions" |
|
830 |
milestone_wiki_total = milestone_wiki.count |
|
831 |
milestone_wiki.each do |name| |
|
832 |
milestone_wiki_count += 1 |
|
833 |
simplebar(who, milestone_wiki_count, milestone_wiki_total) |
|
834 |
p = wiki.find_page(name) |
|
835 |
next if p.nil? |
|
836 |
p.content.text = convert_wiki_text(p.content.text) |
|
837 |
p.content.save |
|
838 |
end |
|
839 |
puts if milestone_wiki_count < milestone_wiki_total |
|
840 | ||
583 | 841 |
puts |
584 | ||
585 |
puts |
|
586 |
puts "Components: #{migrated_components}/#{TracComponent.count}" |
|
587 |
puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}" |
|
588 |
puts "Tickets: #{migrated_tickets}/#{TracTicket.count}" |
|
842 |
puts "Components: #{migrated_components}/#{components_total}" |
|
843 |
puts "Milestones: #{migrated_milestones}/#{milestones_total}" |
|
844 |
puts "Tickets: #{migrated_tickets}/#{tickets_total}" |
|
589 | 845 |
puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s |
590 | 846 |
puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}" |
591 |
puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
|
|
847 |
puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edits_total}"
|
|
592 | 848 |
puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s |
593 | 849 |
end |
594 | ||
850 |
|
|
851 |
def self.findIssue(id) |
|
852 |
return Issue.find(id) |
|
853 |
rescue ActiveRecord::RecordNotFound |
|
854 |
puts "[#{id}] not found" |
|
855 |
nil |
|
856 |
end |
|
857 |
|
|
595 | 858 |
def self.limit_for(klass, attribute) |
596 | 859 |
klass.columns_hash[attribute.to_s].limit |
597 | 860 |
end |
... | ... | |
601 | 864 |
rescue Iconv::InvalidEncoding |
602 | 865 |
puts "Invalid encoding!" |
603 | 866 |
return false |
867 |
end |
|
868 | ||
869 |
def self.set_migrate_accounts(migrate) |
|
870 |
@migrate_accounts = migrate |
|
871 |
@migrate_accounts |
|
872 |
end |
|
873 | ||
874 |
def self.set_migrate_accounts_valid_email(valid_email) |
|
875 |
@migrate_accounts_valid_email = valid_email |
|
876 |
@migrate_accounts_valid_email |
|
604 | 877 |
end |
605 | 878 | |
606 | 879 |
def self.set_trac_directory(path) |
... | ... | |
669 | 942 |
project.identifier = identifier |
670 | 943 |
puts "Unable to create a project with identifier '#{identifier}'!" unless project.save |
671 | 944 |
# enable issues and wiki for the created project |
672 |
project.enabled_module_names = ['issue_tracking', 'wiki'] |
|
945 |
# Enable all project modules by default |
|
946 |
#project.enabled_module_names = ['issue_tracking', 'wiki', 'time_tracking', 'news', 'documents', 'files', 'repository', 'boards', 'calendar', 'gantt'] |
|
947 |
# Only enable modules which might have content from the migrated project |
|
948 |
project.enabled_module_names = ['issue_tracking', 'wiki', 'repository'] |
|
673 | 949 |
else |
674 | 950 |
puts |
675 | 951 |
puts "This project already exists in your Redmine database." |
... | ... | |
679 | 955 |
end |
680 | 956 |
project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG) |
681 | 957 |
project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE) |
958 |
# Add Task type to the project |
|
959 |
project.trackers << TRACKER_TASK unless project.trackers.include?(TRACKER_TASK) |
|
682 | 960 |
@target_project = project.new_record? ? nil : project |
683 | 961 |
@target_project.reload |
684 | 962 |
end |
... | ... | |
715 | 993 |
end |
716 | 994 |
end |
717 | 995 | |
996 |
|
|
718 | 997 |
puts |
719 | 998 |
if Redmine::DefaultData::Loader.no_data? |
720 | 999 |
puts "Redmine configuration need to be loaded before importing data." |
... | ... | |
730 | 1009 |
break unless STDIN.gets.match(/^y$/i) |
731 | 1010 |
puts |
732 | 1011 | |
733 |
def prompt(text, options = {}, &block) |
|
734 |
default = options[:default] || '' |
|
735 |
while true |
|
736 |
print "#{text} [#{default}]: " |
|
737 |
STDOUT.flush |
|
738 |
value = STDIN.gets.chomp! |
|
739 |
value = default if value.blank? |
|
740 |
break if yield value |
|
741 |
end |
|
742 |
end |
|
743 | ||
744 | 1012 |
DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432} |
745 | 1013 | |
746 |
prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip} |
|
747 |
prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter} |
|
1014 |
# Get Trac configuration and migration settings |
|
1015 |
prompt('Trac directory', :default => $DEFAULT_TRAC_DIRECTORY) {|directory| TracMigrate.set_trac_directory directory.strip} |
|
1016 |
prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter} |
|
748 | 1017 |
unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter) |
749 | 1018 |
prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host} |
750 | 1019 |
prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port} |
... | ... | |
754 | 1023 |
prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password} |
755 | 1024 |
end |
756 | 1025 |
prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding} |
757 |
prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier} |
|
1026 |
prompt('Target project identifier', :default => $DEFAULT_PROJECT_IDENTIFIER) {|identifier| TracMigrate.target_project_identifier identifier.downcase} |
|
1027 |
|
|
1028 |
# Ask for user account migration |
|
1029 |
print "Migrate ALL authenticated accounts [y/N]? " |
|
1030 |
STDOUT.flush |
|
1031 |
migrate_accounts = STDIN.gets.match(/^y$/i) |
|
1032 |
TracMigrate.set_migrate_accounts(migrate_accounts) |
|
1033 | ||
1034 |
if migrate_accounts |
|
1035 |
print "Require authenticated accounts to have an email address set [Y/n]? " |
|
1036 |
STDOUT.flush |
|
1037 |
TracMigrate.set_migrate_accounts_valid_email(STDIN.gets.match(/^y$/i)) |
|
1038 |
end |
|
758 | 1039 |
puts |
759 | ||
1040 |
|
|
760 | 1041 |
# Turn off email notifications |
761 | 1042 |
Setting.notified_events = [] |
762 | ||
1043 |
|
|
763 | 1044 |
TracMigrate.migrate |
764 | 1045 |
end |
1046 | ||
1047 | ||
1048 |
# Prompt |
|
1049 |
def prompt(text, options = {}, &block) |
|
1050 |
default = options[:default] || '' |
|
1051 |
while true |
|
1052 |
print "#{text} [#{default}]: " |
|
1053 |
STDOUT.flush |
|
1054 |
value = STDIN.gets.chomp! |
|
1055 |
value = default if value.blank? |
|
1056 |
break if yield value |
|
1057 |
end |
|
1058 |
end |
|
1059 | ||
1060 |
# Basic wiki syntax conversion |
|
1061 |
def convert_wiki_text_mapping(text, ticket_map = []) |
|
1062 |
# Hide links |
|
1063 |
def wiki_links_hide(src) |
|
1064 |
@wiki_links = [] |
|
1065 |
@wiki_links_hash = "####WIKILINKS#{src.hash.to_s}####" |
|
1066 |
src.gsub(/(\[\[.+?\|.+?\]\])/) do |
|
1067 |
@wiki_links << $1 |
|
1068 |
@wiki_links_hash |
|
1069 |
end |
|
1070 |
end |
|
1071 |
# Restore links |
|
1072 |
def wiki_links_restore(src) |
|
1073 |
@wiki_links.each do |s| |
|
1074 |
src = src.sub("#{@wiki_links_hash}", s.to_s) |
|
1075 |
end |
|
1076 |
src |
|
1077 |
end |
|
1078 |
# Hidding code blocks |
|
1079 |
def code_hide(src) |
|
1080 |
@code = [] |
|
1081 |
@code_hash = "####CODEBLOCK#{src.hash.to_s}####" |
|
1082 |
src.gsub(/(\{\{\{.+?\}\}\}|`.+?`)/m) do |
|
1083 |
@code << $1 |
|
1084 |
@code_hash |
|
1085 |
end |
|
1086 |
end |
|
1087 |
# Convert code blocks |
|
1088 |
def code_convert(src) |
|
1089 |
@code.each do |s| |
|
1090 |
s = s.to_s |
|
1091 |
if s =~ (/`(.+?)`/m) || s =~ (/\{\{\{(.+?)\}\}\}/) then |
|
1092 |
# inline code |
|
1093 |
s = s.replace("@#{$1}@") |
|
1094 |
else |
|
1095 |
# We would like to convert the Code highlighting too |
|
1096 |
# This will go into the next line. |
|
1097 |
shebang_line = false |
|
1098 |
# Reguar expression for start of code |
|
1099 |
pre_re = /\{\{\{/ |
|
1100 |
# Code hightlighing... |
|
1101 |
shebang_re = /^\#\!([a-z]+)/ |
|
1102 |
# Regular expression for end of code |
|
1103 |
pre_end_re = /\}\}\}/ |
|
1104 |
|
|
1105 |
# Go through the whole text..extract it line by line |
|
1106 |
s = s.gsub(/^(.*)$/) do |line| |
|
1107 |
m_pre = pre_re.match(line) |
|
1108 |
if m_pre |
|
1109 |
line = '<pre>' |
|
1110 |
else |
|
1111 |
m_sl = shebang_re.match(line) |
|
1112 |
if m_sl |
|
1113 |
shebang_line = true |
|
1114 |
line = '<code class="' + m_sl[1] + '">' |
|
1115 |
end |
|
1116 |
m_pre_end = pre_end_re.match(line) |
|
1117 |
if m_pre_end |
|
1118 |
line = '</pre>' |
|
1119 |
if shebang_line |
|
1120 |
line = '</code>' + line |
|
1121 |
end |
|
1122 |
end |
|
1123 |
end |
|
1124 |
line |
|
1125 |
end |
|
1126 |
end |
|
1127 |
src = src.sub("#{@code_hash}", s) |
|
1128 |
end |
|
1129 |
src |
|
1130 |
end |
|
1131 | ||
1132 |
# Hide code blocks |
|
1133 |
text = code_hide(text) |
|
1134 |
# New line |
|
1135 |
text = text.gsub(/\[\[[Bb][Rr]\]\]/, "\n") # This has to go before the rules below |
|
1136 |
# Titles (only h1. to h6., and remove #...) |
|
1137 |
text = text.gsub(/(?:^|^\ +)(\={1,6})\ (.+)\ (?:\1)(?:\ *(\ \#.*))?/) {|s| "\nh#{$1.length}. #{$2}#{$3}\n"} |
|
1138 |
|
|
1139 |
# External Links: |
|
1140 |
# [http://example.com/] |
|
1141 |
text = text.gsub(/\[((?:https?|s?ftp)\:\S+)\]/, '\1') |
|
1142 |
# [http://example.com/ Example],[http://example.com/ "Example"] |
|
1143 |
# [http://example.com/ "Example for "Example""] -> "Example for 'Example'":http://example.com/ |
|
1144 |
text = text.gsub(/\[((?:https?|s?ftp)\:\S+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":#{$1}"} |
|
1145 |
# [mailto:some@example.com],[mailto:"some@example.com"] |
|
1146 |
text = text.gsub(/\[mailto\:([\"']?)(.+?)\1\]/, '\2') |
|
1147 |
|
|
1148 |
# Ticket links: |
|
1149 |
# [ticket:234 Text],[ticket:234 This is a test],[ticket:234 "This is a test"] |
|
1150 |
# [ticket:234 "Test "with quotes""] -> "Test 'with quotes'":issues/show/234 |
|
1151 |
text = text.gsub(/\[ticket\:(\d+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":/issues/show/#{$1}"} |
|
1152 |
# ticket:1234 |
|
1153 |
# excluding ticket:1234:file.txt (used in macros) |
|
1154 |
# #1 - working cause Redmine uses the same syntax. |
|
1155 |
text = text.gsub(/ticket\:(\d+?)([^\:])/, '#\1\2') |
|
1156 | ||
1157 |
# Source & attachments links: |
|
1158 |
# [source:/trunk/readme.txt Readme File],[source:"/trunk/readme.txt" Readme File], |
|
1159 |
# [source:/trunk/readme.txt],[source:"/trunk/readme.txt"] |
|
1160 |
# The text "Readme File" is not converted, |
|
1161 |
# cause Redmine's wiki does not support this. |
|
1162 |
# Attachments use same syntax. |
|
1163 |
text = text.gsub(/\[(source|attachment)\:([\"']?)([^\"']+?)\2(?:\ +(.+?))?\]/, '\1:"\3"') |
|
1164 |
# source:"/trunk/readme.txt" |
|
1165 |
# source:/trunk/readme.txt - working cause Redmine uses the same syntax. |
|
1166 |
text = text.gsub(/(source|attachment)\:([\"'])([^\"']+?)\2/, '\1:"\3"') |
|
1167 | ||
1168 |
# Milestone links: |
|
1169 |
# [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)], |
|
1170 |
# [milestone:"0.1.0 Mercury"],milestone:"0.1.0 Mercury" |
|
1171 |
# The text "Milestone 0.1.0 (Mercury)" is not converted, |
|
1172 |
# cause Redmine's wiki does not support this. |
|
1173 |
text = text.gsub(/\[milestone\:([\"'])([^\"']+?)\1(?:\ +(.+?))?\]/, 'version:"\2"') |
|
1174 |
text = text.gsub(/milestone\:([\"'])([^\"']+?)\1/, 'version:"\2"') |
|
1175 |
# [milestone:0.1.0],milestone:0.1.0 |
|
1176 |
text = text.gsub(/\[milestone\:([^\ ]+?)\]/, 'version:\1') |
|
1177 |
text = text.gsub(/milestone\:([^\ ]+?)/, 'version:\1') |
|
1178 | ||
1179 |
# Internal Links: |
|
1180 |
# ["Some Link"] |
|
1181 |
text = text.gsub(/\[([\"'])(.+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"} |
|
1182 |
# [wiki:"Some Link" "Link description"],[wiki:"Some Link" Link description] |
|
1183 |
text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1[\ \t]+([\"']?)(.+?)\3\]/) {|s| "[[#{$2.delete(',./?;|:')}|#{$4}]]"} |
|
1184 |
# [wiki:"Some Link"] |
|
1185 |
text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"} |
|
1186 |
# [wiki:SomeLink] |
|
1187 |
text = text.gsub(/\[wiki\:([^\s\]]+?)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} |
|
1188 |
# [wiki:SomeLink Link description],[wiki:SomeLink "Link description"] |
|
1189 |
text = text.gsub(/\[wiki\:([^\s\]\"']+?)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$3}]]"} |
|
1190 | ||
1191 |
# Before convert CamelCase links, must hide wiki links with description. |
|
1192 |
# Like this: [[http://www.freebsd.org|Hello FreeBSD World]] |
|
1193 |
text = wiki_links_hide(text) |
|
1194 |
# Links to CamelCase pages (not work for unicode) |
|
1195 |
# UsingJustWikiCaps,UsingJustWikiCaps/Subpage |
|
1196 |
text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+(?:\/[^\s[:punct:]]+)*)/) {|s| "#{$1}#{$2}[[#{$3.delete('/')}]]"} |
|
1197 |
# Normalize things that were supposed to not be links |
|
1198 |
# like !NotALink |
|
1199 |
text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2') |
|
1200 |
# Now restore hidden links |
|
1201 |
text = wiki_links_restore(text) |
|
1202 |
|
|
1203 |
# Revisions links |
|
1204 |
text = text.gsub(/\[(\d+)\]/, 'r\1') |
|
1205 |
# Ticket number re-writing |
|
1206 |
text = text.gsub(/#(\d+)/) do |s| |
|
1207 |
if $1.length < 10 |
|
1208 |
#ticket_map[$1.to_i] ||= $1 |
|
1209 |
"\##{ticket_map[$1.to_i] || $1}" |
|
1210 |
else |
|
1211 |
s |
|
1212 |
end |
|
1213 |
end |
|
1214 |
|
|
1215 |
# Highlighting |
|
1216 |
text = text.gsub(/'''''([^\s])/, '_*\1') |
|
1217 |
text = text.gsub(/([^\s])'''''/, '\1*_') |
|
1218 |
text = text.gsub(/'''/, '*') |
|
1219 |
text = text.gsub(/''/, '_') |
|
1220 |
text = text.gsub(/__/, '+') |
|
1221 |
text = text.gsub(/~~/, '-') |
|
1222 |
text = text.gsub(/,,/, '~') |
|
1223 |
# Tables |
|
1224 |
text = text.gsub(/\|\|/, '|') |
|
1225 |
# Lists: |
|
1226 |
# bullet |
|
1227 |
text = text.gsub(/^(\ +)[\*-] /) {|s| '*' * $1.length + " "} |
|
1228 |
# numbered |
|
1229 |
text = text.gsub(/^(\ +)\d+\. /) {|s| '#' * $1.length + " "} |
|
1230 |
# Images (work for only attached in current page [[Image(picture.gif)]]) |
|
1231 |
# need rules for: * [[Image(wiki:WikiFormatting:picture.gif)]] (referring to attachment on another page) |
|
1232 |
# * [[Image(ticket:1:picture.gif)]] (file attached to a ticket) |
|
1233 |
# * [[Image(htdocs:picture.gif)]] (referring to a file inside project htdocs) |
|
1234 |
# * [[Image(source:/trunk/trac/htdocs/trac_logo_mini.png)]] (a file in repository) |
|
1235 |
text = text.gsub(/\[\[image\((.+?)(?:,.+?)?\)\]\]/i, '!\1!') |
|
1236 |
# TOC (is right-aligned, because that in Trac) |
|
1237 |
text = text.gsub(/\[\[TOC(?:\((.*?)\))?\]\]/m) {|s| "{{>toc}}\n"} |
|
1238 | ||
1239 |
# Restore and convert code blocks |
|
1240 |
text = code_convert(text) |
|
1241 | ||
1242 |
text |
|
1243 |
end |
|
1244 |
|
|
1245 |
# Simple progress bar |
|
1246 |
def simplebar(title, current, total, out = STDOUT) |
|
1247 |
def get_width |
|
1248 |
default_width = 80 |
|
1249 |
begin |
|
1250 |
tiocgwinsz = 0x5413 |
|
1251 |
data = [0, 0, 0, 0].pack("SSSS") |
|
1252 |
if out.ioctl(tiocgwinsz, data) >= 0 then |
|
1253 |
rows, cols, xpixels, ypixels = data.unpack("SSSS") |
|
1254 |
if cols >= 0 then cols else default_width end |
|
1255 |
else |
|
1256 |
default_width |
|
1257 |
end |
|
1258 |
rescue Exception |
|
1259 |
default_width |
|
1260 |
end |
|
1261 |
end |
|
1262 |
mark = "*" |
|
1263 |
title_width = 40 |
|
1264 |
max = get_width - title_width - 10 |
|
1265 |
format = "%-#{title_width}s [%-#{max}s] %3d%% [%d/%d]%s" |
|
1266 |
bar = current * max / total |
|
1267 |
percentage = bar * 100 / max |
|
1268 |
current == total ? eol = "\n" : eol ="\r" |
|
1269 |
printf(format, title, mark * bar, percentage, current, total, eol) |
|
1270 |
out.flush |
|
1271 |
end |
|
765 | 1272 |
end |
766 |