54 |
54 |
|
55 |
55 |
TRACKER_BUG = Tracker.find_by_position(1)
|
56 |
56 |
TRACKER_FEATURE = Tracker.find_by_position(2)
|
|
57 |
# Add a fourth issue type for tasks as we use them heavily
|
|
58 |
t = Tracker.find_by_name('Task')
|
|
59 |
if !t
|
|
60 |
t = Tracker.create(:name => 'Task', :is_in_chlog => true, :is_in_roadmap => false, :position => 4)
|
|
61 |
t.workflows.copy(Tracker.find(1))
|
|
62 |
end
|
|
63 |
TRACKER_TASK = t
|
57 |
64 |
DEFAULT_TRACKER = TRACKER_BUG
|
58 |
65 |
TRACKER_MAPPING = {'defect' => TRACKER_BUG,
|
59 |
66 |
'enhancement' => TRACKER_FEATURE,
|
60 |
|
'task' => TRACKER_FEATURE,
|
|
67 |
'task' => TRACKER_TASK,
|
61 |
68 |
'patch' =>TRACKER_FEATURE
|
62 |
69 |
}
|
63 |
70 |
|
... | ... | |
68 |
75 |
ROLE_MAPPING = {'admin' => manager_role,
|
69 |
76 |
'developer' => developer_role
|
70 |
77 |
}
|
|
78 |
# Add an Hash Table for comments' updatable fields
|
|
79 |
PROP_MAPPING = {'status' => 'status_id',
|
|
80 |
'owner' => 'assigned_to_id',
|
|
81 |
'component' => 'category_id',
|
|
82 |
'milestone' => 'fixed_version_id',
|
|
83 |
'priority' => 'priority_id',
|
|
84 |
'summary' => 'subject',
|
|
85 |
'type' => 'tracker_id'}
|
|
86 |
|
|
87 |
# Hash table to map completion ratio
|
|
88 |
RATIO_MAPPING = {'' => 0,
|
|
89 |
'fixed' => 100,
|
|
90 |
'invalid' => 0,
|
|
91 |
'wontfix' => 0,
|
|
92 |
'duplicate' => 100,
|
|
93 |
'worksforme' => 0}
|
71 |
94 |
|
72 |
95 |
class ::Time
|
73 |
96 |
class << self
|
... | ... | |
153 |
176 |
private
|
154 |
177 |
def trac_fullpath
|
155 |
178 |
attachment_type = read_attribute(:type)
|
156 |
|
trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
|
157 |
|
"#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
|
|
179 |
trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*]/n ) {|x| sprintf('%%%02X', x[0]) }
|
|
180 |
trac_dir = id.gsub( /[^a-zA-Z0-9\-_\.!~*\\\/]/n ) {|x| sprintf('%%%02X', x[0]) }
|
|
181 |
"#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{trac_dir}/#{trac_file}"
|
158 |
182 |
end
|
159 |
183 |
end
|
160 |
184 |
|
... | ... | |
192 |
216 |
def time; Time.at(read_attribute(:time)) end
|
193 |
217 |
end
|
194 |
218 |
|
195 |
|
TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
|
|
219 |
TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup \
|
|
220 |
TracBrowser TracCgi TracChangeset TracInstallPlatforms TracMultipleProjects TracModWSGI \
|
196 |
221 |
TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
|
197 |
222 |
TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
|
198 |
223 |
TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
|
199 |
224 |
TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
|
200 |
225 |
WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
|
201 |
|
CamelCase TitleIndex)
|
202 |
|
|
|
226 |
CamelCase TitleIndex TracNavigation TracFineGrainedPermissions TracWorkflow TimingAndEstimationPluginUserManual \
|
|
227 |
PageTemplates)
|
203 |
228 |
class TracWikiPage < ActiveRecord::Base
|
204 |
229 |
set_table_name :wiki
|
205 |
230 |
set_primary_key :name
|
... | ... | |
241 |
266 |
if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
|
242 |
267 |
name = name_attr.value
|
243 |
268 |
end
|
244 |
|
name =~ (/(.*)(\s+\w+)?/)
|
|
269 |
name =~ (/(.+?)(?:[\ \t]+(.+)?|[\ \t]+|)$/)
|
245 |
270 |
fn = $1.strip
|
|
271 |
# Add a dash for lastname or the user is not saved (bugfix)
|
246 |
272 |
ln = ($2 || '-').strip
|
247 |
273 |
|
248 |
274 |
u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
|
... | ... | |
271 |
297 |
|
272 |
298 |
# Basic wiki syntax conversion
|
273 |
299 |
def self.convert_wiki_text(text)
|
274 |
|
# Titles
|
275 |
|
text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
|
276 |
|
# External Links
|
277 |
|
text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
|
278 |
|
# Ticket links:
|
279 |
|
# [ticket:234 Text],[ticket:234 This is a test]
|
280 |
|
text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
|
281 |
|
# ticket:1234
|
282 |
|
# #1 is working cause Redmine uses the same syntax.
|
283 |
|
text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
|
284 |
|
# Milestone links:
|
285 |
|
# [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
|
286 |
|
# The text "Milestone 0.1.0 (Mercury)" is not converted,
|
287 |
|
# cause Redmine's wiki does not support this.
|
288 |
|
text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
|
289 |
|
# [milestone:"0.1.0 Mercury"]
|
290 |
|
text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
|
291 |
|
text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
|
292 |
|
# milestone:0.1.0
|
293 |
|
text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
|
294 |
|
text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
|
295 |
|
# Internal Links
|
296 |
|
text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
|
297 |
|
text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
|
298 |
|
text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
|
299 |
|
text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
|
300 |
|
text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
|
301 |
|
text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
|
302 |
|
|
303 |
|
# Links to pages UsingJustWikiCaps
|
304 |
|
text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
|
305 |
|
# Normalize things that were supposed to not be links
|
306 |
|
# like !NotALink
|
307 |
|
text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
|
308 |
|
# Revisions links
|
309 |
|
text = text.gsub(/\[(\d+)\]/, 'r\1')
|
310 |
|
# Ticket number re-writing
|
311 |
|
text = text.gsub(/#(\d+)/) do |s|
|
312 |
|
if $1.length < 10
|
313 |
|
# TICKET_MAP[$1.to_i] ||= $1
|
314 |
|
"\##{TICKET_MAP[$1.to_i] || $1}"
|
315 |
|
else
|
316 |
|
s
|
317 |
|
end
|
318 |
|
end
|
319 |
|
# We would like to convert the Code highlighting too
|
320 |
|
# This will go into the next line.
|
321 |
|
shebang_line = false
|
322 |
|
# Reguar expression for start of code
|
323 |
|
pre_re = /\{\{\{/
|
324 |
|
# Code hightlighing...
|
325 |
|
shebang_re = /^\#\!([a-z]+)/
|
326 |
|
# Regular expression for end of code
|
327 |
|
pre_end_re = /\}\}\}/
|
328 |
|
|
329 |
|
# Go through the whole text..extract it line by line
|
330 |
|
text = text.gsub(/^(.*)$/) do |line|
|
331 |
|
m_pre = pre_re.match(line)
|
332 |
|
if m_pre
|
333 |
|
line = '<pre>'
|
334 |
|
else
|
335 |
|
m_sl = shebang_re.match(line)
|
336 |
|
if m_sl
|
337 |
|
shebang_line = true
|
338 |
|
line = '<code class="' + m_sl[1] + '">'
|
339 |
|
end
|
340 |
|
m_pre_end = pre_end_re.match(line)
|
341 |
|
if m_pre_end
|
342 |
|
line = '</pre>'
|
343 |
|
if shebang_line
|
344 |
|
line = '</code>' + line
|
345 |
|
end
|
346 |
|
end
|
347 |
|
end
|
348 |
|
line
|
349 |
|
end
|
350 |
|
|
351 |
|
# Highlighting
|
352 |
|
text = text.gsub(/'''''([^\s])/, '_*\1')
|
353 |
|
text = text.gsub(/([^\s])'''''/, '\1*_')
|
354 |
|
text = text.gsub(/'''/, '*')
|
355 |
|
text = text.gsub(/''/, '_')
|
356 |
|
text = text.gsub(/__/, '+')
|
357 |
|
text = text.gsub(/~~/, '-')
|
358 |
|
text = text.gsub(/`/, '@')
|
359 |
|
text = text.gsub(/,,/, '~')
|
360 |
|
# Lists
|
361 |
|
text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
|
362 |
|
|
363 |
|
text
|
|
300 |
convert_wiki_text_mapping(text, TICKET_MAP)
|
364 |
301 |
end
|
365 |
302 |
|
366 |
303 |
def self.migrate
|
... | ... | |
377 |
314 |
migrated_wiki_edits = 0
|
378 |
315 |
migrated_wiki_attachments = 0
|
379 |
316 |
|
380 |
|
#Wiki system initializing...
|
|
317 |
# Wiki system initializing...
|
381 |
318 |
@target_project.wiki.destroy if @target_project.wiki
|
382 |
319 |
@target_project.reload
|
383 |
320 |
wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
|
384 |
321 |
wiki_edit_count = 0
|
385 |
322 |
|
386 |
323 |
# Components
|
387 |
|
print "Migrating components"
|
|
324 |
who = "Migrating components"
|
388 |
325 |
issues_category_map = {}
|
|
326 |
components_total = TracComponent.count
|
389 |
327 |
TracComponent.find(:all).each do |component|
|
390 |
|
print '.'
|
391 |
|
STDOUT.flush
|
392 |
328 |
c = IssueCategory.new :project => @target_project,
|
393 |
329 |
:name => encode(component.name[0, limit_for(IssueCategory, 'name')])
|
|
330 |
# Owner
|
|
331 |
unless component.owner.blank?
|
|
332 |
c.assigned_to = find_or_create_user(component.owner, true)
|
|
333 |
end
|
394 |
334 |
next unless c.save
|
395 |
335 |
issues_category_map[component.name] = c
|
396 |
336 |
migrated_components += 1
|
|
337 |
simplebar(who, migrated_components, components_total)
|
397 |
338 |
end
|
398 |
|
puts
|
|
339 |
puts if migrated_components < components_total
|
399 |
340 |
|
400 |
341 |
# Milestones
|
401 |
|
print "Migrating milestones"
|
|
342 |
who = "Migrating milestones"
|
402 |
343 |
version_map = {}
|
|
344 |
milestone_wiki = Array.new
|
|
345 |
milestones_total = TracMilestone.count
|
403 |
346 |
TracMilestone.find(:all).each do |milestone|
|
404 |
|
print '.'
|
405 |
|
STDOUT.flush
|
406 |
347 |
# First we try to find the wiki page...
|
407 |
348 |
p = wiki.find_or_new_page(milestone.name.to_s)
|
408 |
349 |
p.content = WikiContent.new(:page => p) if p.new_record?
|
... | ... | |
419 |
360 |
|
420 |
361 |
next unless v.save
|
421 |
362 |
version_map[milestone.name] = v
|
|
363 |
milestone_wiki.push(milestone.name);
|
422 |
364 |
migrated_milestones += 1
|
|
365 |
simplebar(who, migrated_milestones, milestones_total)
|
423 |
366 |
end
|
424 |
|
puts
|
|
367 |
puts if migrated_milestones < milestones_total
|
425 |
368 |
|
426 |
369 |
# Custom fields
|
427 |
370 |
# TODO: read trac.ini instead
|
428 |
|
print "Migrating custom fields"
|
|
371 |
#print "Migrating custom fields"
|
429 |
372 |
custom_field_map = {}
|
430 |
373 |
TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
|
431 |
|
print '.'
|
432 |
|
STDOUT.flush
|
|
374 |
#print '.' # Maybe not needed this out?
|
|
375 |
#STDOUT.flush
|
433 |
376 |
# Redmine custom field name
|
434 |
377 |
field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
|
435 |
378 |
# Find if the custom already exists in Redmine
|
436 |
379 |
f = IssueCustomField.find_by_name(field_name)
|
|
380 |
# Ugly hack to handle billable checkbox. Would require to read the ini file to be cleaner
|
|
381 |
if field_name == 'Billable'
|
|
382 |
format = 'bool'
|
|
383 |
else
|
|
384 |
format = 'string'
|
|
385 |
end
|
437 |
386 |
# Or create a new one
|
438 |
387 |
f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
|
439 |
|
:field_format => 'string')
|
|
388 |
:field_format => format)
|
440 |
389 |
|
441 |
390 |
next if f.new_record?
|
442 |
391 |
f.trackers = Tracker.find(:all)
|
443 |
392 |
f.projects << @target_project
|
444 |
393 |
custom_field_map[field.name] = f
|
445 |
394 |
end
|
446 |
|
puts
|
|
395 |
#puts
|
447 |
396 |
|
448 |
397 |
# Trac 'resolution' field as a Redmine custom field
|
449 |
398 |
r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
|
... | ... | |
456 |
405 |
r.save!
|
457 |
406 |
custom_field_map['resolution'] = r
|
458 |
407 |
|
|
408 |
# Trac 'keywords' field as a Redmine custom field
|
|
409 |
k = IssueCustomField.find(:first, :conditions => { :name => "Keywords" })
|
|
410 |
k = IssueCustomField.new(:name => 'Keywords',
|
|
411 |
:field_format => 'string',
|
|
412 |
:is_filter => true) if k.nil?
|
|
413 |
k.trackers = Tracker.find(:all)
|
|
414 |
k.projects << @target_project
|
|
415 |
k.save!
|
|
416 |
custom_field_map['keywords'] = k
|
|
417 |
|
|
418 |
# Trac ticket id as a Redmine custom field
|
|
419 |
tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
|
|
420 |
tid = IssueCustomField.new(:name => 'TracID',
|
|
421 |
:field_format => 'string',
|
|
422 |
:is_filter => true) if tid.nil?
|
|
423 |
tid.trackers = Tracker.find(:all)
|
|
424 |
tid.projects << @target_project
|
|
425 |
tid.save!
|
|
426 |
custom_field_map['tracid'] = tid
|
|
427 |
|
459 |
428 |
# Tickets
|
460 |
|
print "Migrating tickets"
|
|
429 |
who = "Migrating tickets"
|
|
430 |
tickets_total = TracTicket.count
|
461 |
431 |
TracTicket.find_each(:batch_size => 200) do |ticket|
|
462 |
|
print '.'
|
463 |
|
STDOUT.flush
|
464 |
432 |
i = Issue.new :project => @target_project,
|
465 |
433 |
:subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
|
466 |
|
:description => convert_wiki_text(encode(ticket.description)),
|
|
434 |
:description => encode(ticket.description),
|
467 |
435 |
:priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
|
468 |
436 |
:created_on => ticket.time
|
469 |
|
i.author = find_or_create_user(ticket.reporter)
|
|
437 |
# Add the ticket's author to project's reporter list (bugfix)
|
|
438 |
i.author = find_or_create_user(ticket.reporter,true)
|
|
439 |
# Extrapolate done_ratio from ticket's resolution
|
|
440 |
i.done_ratio = RATIO_MAPPING[ticket.resolution] || 0
|
470 |
441 |
i.category = issues_category_map[ticket.component] unless ticket.component.blank?
|
471 |
442 |
i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
|
472 |
443 |
i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
|
... | ... | |
475 |
446 |
next unless Time.fake(ticket.changetime) { i.save }
|
476 |
447 |
TICKET_MAP[ticket.id] = i.id
|
477 |
448 |
migrated_tickets += 1
|
478 |
|
|
|
449 |
simplebar(who, migrated_tickets, tickets_total)
|
479 |
450 |
# Owner
|
480 |
451 |
unless ticket.owner.blank?
|
481 |
452 |
i.assigned_to = find_or_create_user(ticket.owner, true)
|
482 |
453 |
Time.fake(ticket.changetime) { i.save }
|
483 |
454 |
end
|
|
455 |
# Handle CC field
|
|
456 |
ticket.cc.split(',').each do |email|
|
|
457 |
w = Watcher.new :watchable_type => 'Issue',
|
|
458 |
:watchable_id => i.id,
|
|
459 |
:user_id => find_or_create_user(email.strip).id
|
|
460 |
w.save
|
|
461 |
end
|
484 |
462 |
|
485 |
|
# Comments and status/resolution changes
|
|
463 |
# Necessary to handle direct link to note from timelogs and putting the right start time in issue
|
|
464 |
noteid = 1
|
|
465 |
# Comments and status/resolution/keywords changes
|
486 |
466 |
ticket.changes.group_by(&:time).each do |time, changeset|
|
487 |
467 |
status_change = changeset.select {|change| change.field == 'status'}.first
|
488 |
468 |
resolution_change = changeset.select {|change| change.field == 'resolution'}.first
|
|
469 |
keywords_change = changeset.select {|change| change.field == 'keywords'}.first
|
489 |
470 |
comment_change = changeset.select {|change| change.field == 'comment'}.first
|
|
471 |
# Handle more ticket changes (owner, component, milestone, priority, summary, type, done_ratio and hours)
|
|
472 |
assigned_change = changeset.select {|change| change.field == 'owner'}.first
|
|
473 |
category_change = changeset.select {|change| change.field == 'component'}.first
|
|
474 |
version_change = changeset.select {|change| change.field == 'milestone'}.first
|
|
475 |
priority_change = changeset.select {|change| change.field == 'priority'}.first
|
|
476 |
subject_change = changeset.select {|change| change.field == 'summary'}.first
|
|
477 |
tracker_change = changeset.select {|change| change.field == 'type'}.first
|
|
478 |
time_change = changeset.select {|change| change.field == 'hours'}.first
|
490 |
479 |
|
491 |
|
n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
|
|
480 |
# If it's the first note then we set the start working time to handle calendar and gantts
|
|
481 |
if noteid == 1
|
|
482 |
i.start_date = time
|
|
483 |
end
|
|
484 |
|
|
485 |
n = Journal.new :notes => (comment_change ? encode(comment_change.newvalue) : ''),
|
492 |
486 |
:created_on => time
|
493 |
487 |
n.user = find_or_create_user(changeset.first.author)
|
494 |
488 |
n.journalized = i
|
... | ... | |
497 |
491 |
STATUS_MAPPING[status_change.newvalue] &&
|
498 |
492 |
(STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
|
499 |
493 |
n.details << JournalDetail.new(:property => 'attr',
|
500 |
|
:prop_key => 'status_id',
|
|
494 |
:prop_key => PROP_MAPPING['status'],
|
501 |
495 |
:old_value => STATUS_MAPPING[status_change.oldvalue].id,
|
502 |
496 |
:value => STATUS_MAPPING[status_change.newvalue].id)
|
503 |
497 |
end
|
... | ... | |
506 |
500 |
:prop_key => custom_field_map['resolution'].id,
|
507 |
501 |
:old_value => resolution_change.oldvalue,
|
508 |
502 |
:value => resolution_change.newvalue)
|
|
503 |
# Add a change for the done_ratio
|
|
504 |
n.details << JournalDetail.new(:property => 'attr',
|
|
505 |
:prop_key => 'done_ratio',
|
|
506 |
:old_value => RATIO_MAPPING[resolution_change.oldvalue],
|
|
507 |
:value => RATIO_MAPPING[resolution_change.newvalue])
|
|
508 |
# Arbitrary set the due time to the day the ticket was resolved for calendar and gantts
|
|
509 |
case RATIO_MAPPING[resolution_change.newvalue]
|
|
510 |
when 0
|
|
511 |
i.due_date = nil
|
|
512 |
when 100
|
|
513 |
i.due_date = time
|
|
514 |
end
|
509 |
515 |
end
|
|
516 |
if keywords_change
|
|
517 |
n.details << JournalDetail.new(:property => 'cf',
|
|
518 |
:prop_key => custom_field_map['keywords'].id,
|
|
519 |
:old_value => keywords_change.oldvalue,
|
|
520 |
:value => keywords_change.newvalue)
|
|
521 |
end
|
|
522 |
# Handle assignement/owner changes
|
|
523 |
if assigned_change
|
|
524 |
n.details << JournalDetail.new(:property => 'attr',
|
|
525 |
:prop_key => PROP_MAPPING['owner'],
|
|
526 |
:old_value => find_or_create_user(assigned_change.oldvalue, true),
|
|
527 |
:value => find_or_create_user(assigned_change.newvalue, true))
|
|
528 |
end
|
|
529 |
# Handle component/category changes
|
|
530 |
if category_change
|
|
531 |
n.details << JournalDetail.new(:property => 'attr',
|
|
532 |
:prop_key => PROP_MAPPING['component'],
|
|
533 |
:old_value => issues_category_map[category_change.oldvalue],
|
|
534 |
:value => issues_category_map[category_change.newvalue])
|
|
535 |
end
|
|
536 |
# Handle version/mileston changes
|
|
537 |
if version_change
|
|
538 |
n.details << JournalDetail.new(:property => 'attr',
|
|
539 |
:prop_key => PROP_MAPPING['milestone'],
|
|
540 |
:old_value => version_map[version_change.oldvalue],
|
|
541 |
:value => version_map[version_change.newvalue])
|
|
542 |
end
|
|
543 |
# Handle priority changes
|
|
544 |
if priority_change
|
|
545 |
n.details << JournalDetail.new(:property => 'attr',
|
|
546 |
:prop_key => PROP_MAPPING['priority'],
|
|
547 |
:old_value => PRIORITY_MAPPING[priority_change.oldvalue],
|
|
548 |
:value => PRIORITY_MAPPING[priority_change.newvalue])
|
|
549 |
end
|
|
550 |
# Handle subject/summary changes
|
|
551 |
if subject_change
|
|
552 |
n.details << JournalDetail.new(:property => 'attr',
|
|
553 |
:prop_key => PROP_MAPPING['summary'],
|
|
554 |
:old_value => encode(subject_change.oldvalue[0, limit_for(Issue, 'subject')]),
|
|
555 |
:value => encode(subject_change.newvalue[0, limit_for(Issue, 'subject')]))
|
|
556 |
end
|
|
557 |
# Handle tracker/type (bug, feature) changes
|
|
558 |
if tracker_change
|
|
559 |
n.details << JournalDetail.new(:property => 'attr',
|
|
560 |
:prop_key => PROP_MAPPING['type'],
|
|
561 |
:old_value => TRACKER_MAPPING[tracker_change.oldvalue] || DEFAULT_TRACKER,
|
|
562 |
:value => TRACKER_MAPPING[tracker_change.newvalue] || DEFAULT_TRACKER)
|
|
563 |
end
|
|
564 |
# Add timelog entries for each time changes (from timeandestimation plugin)
|
|
565 |
if time_change && time_change.newvalue != '0' && time_change.newvalue != ''
|
|
566 |
t = TimeEntry.new(:project => @target_project,
|
|
567 |
:issue => i,
|
|
568 |
:user => n.user,
|
|
569 |
:spent_on => time,
|
|
570 |
:hours => time_change.newvalue,
|
|
571 |
:created_on => time,
|
|
572 |
:updated_on => time,
|
|
573 |
:activity_id => TimeEntryActivity.find_by_position(2).id,
|
|
574 |
:comments => "#{convert_wiki_text(n.notes.each_line.first.chomp)[0,100] unless !n.notes.each_line.first}... \"more\":/issues/#{i.id}#note-#{noteid}")
|
|
575 |
t.save
|
|
576 |
t.errors.each_full{|msg| puts msg }
|
|
577 |
end
|
|
578 |
# Set correct changetime of the issue
|
|
579 |
next unless Time.fake(ticket.changetime) { i.save }
|
510 |
580 |
n.save unless n.details.empty? && n.notes.blank?
|
|
581 |
noteid += 1
|
511 |
582 |
end
|
512 |
583 |
|
513 |
584 |
# Attachments
|
... | ... | |
534 |
605 |
if custom_field_map['resolution'] && !ticket.resolution.blank?
|
535 |
606 |
custom_values[custom_field_map['resolution'].id] = ticket.resolution
|
536 |
607 |
end
|
|
608 |
if custom_field_map['keywords'] && !ticket.keywords.blank?
|
|
609 |
custom_values[custom_field_map['keywords'].id] = ticket.keywords
|
|
610 |
end
|
|
611 |
if custom_field_map['tracid']
|
|
612 |
custom_values[custom_field_map['tracid'].id] = ticket.id
|
|
613 |
end
|
537 |
614 |
i.custom_field_values = custom_values
|
538 |
615 |
i.save_custom_field_values
|
539 |
616 |
end
|
540 |
617 |
|
541 |
618 |
# update issue id sequence if needed (postgresql)
|
542 |
619 |
Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
|
543 |
|
puts
|
|
620 |
puts if migrated_tickets < tickets_total
|
544 |
621 |
|
545 |
622 |
# Wiki
|
546 |
|
print "Migrating wiki"
|
|
623 |
who = "Migrating wiki"
|
547 |
624 |
if wiki.save
|
|
625 |
wiki_edits_total = TracWikiPage.count
|
548 |
626 |
TracWikiPage.find(:all, :order => 'name, version').each do |page|
|
549 |
627 |
# Do not migrate Trac manual wiki pages
|
550 |
|
next if TRAC_WIKI_PAGES.include?(page.name)
|
551 |
|
wiki_edit_count += 1
|
552 |
|
print '.'
|
553 |
|
STDOUT.flush
|
|
628 |
if TRAC_WIKI_PAGES.include?(page.name) then
|
|
629 |
wiki_edits_total -= 1
|
|
630 |
next
|
|
631 |
end
|
554 |
632 |
p = wiki.find_or_new_page(page.name)
|
555 |
633 |
p.content = WikiContent.new(:page => p) if p.new_record?
|
556 |
634 |
p.content.text = page.text
|
557 |
635 |
p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
|
558 |
636 |
p.content.comments = page.comment
|
559 |
637 |
Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
|
|
638 |
migrated_wiki_edits += 1
|
|
639 |
simplebar(who, migrated_wiki_edits, wiki_edits_total)
|
560 |
640 |
|
561 |
641 |
next if p.content.new_record?
|
562 |
|
migrated_wiki_edits += 1
|
563 |
642 |
|
564 |
643 |
# Attachments
|
565 |
644 |
page.attachments.each do |attachment|
|
... | ... | |
576 |
655 |
end
|
577 |
656 |
end
|
578 |
657 |
|
579 |
|
wiki.reload
|
580 |
|
wiki.pages.each do |page|
|
581 |
|
page.content.text = convert_wiki_text(page.content.text)
|
582 |
|
Time.fake(page.content.updated_on) { page.content.save }
|
|
658 |
end
|
|
659 |
puts if migrated_wiki_edits < wiki_edits_total
|
|
660 |
|
|
661 |
# Now load each wiki page and transform its content into textile format
|
|
662 |
puts "\nTransform texts to textile format:"
|
|
663 |
|
|
664 |
wiki_pages_count = 0
|
|
665 |
issues_count = 0
|
|
666 |
milestone_wiki_count = 0
|
|
667 |
|
|
668 |
who = " in Wiki pages"
|
|
669 |
wiki.reload
|
|
670 |
wiki_pages_total = wiki.pages.count
|
|
671 |
wiki.pages.each do |page|
|
|
672 |
page.content.text = convert_wiki_text(page.content.text)
|
|
673 |
Time.fake(page.content.updated_on) { page.content.save }
|
|
674 |
wiki_pages_count += 1
|
|
675 |
simplebar(who, wiki_pages_count, wiki_pages_total)
|
|
676 |
end
|
|
677 |
puts if wiki_pages_count < wiki_pages_total
|
|
678 |
|
|
679 |
who = " in Issues"
|
|
680 |
issues_total = TICKET_MAP.count
|
|
681 |
TICKET_MAP.each do |newId|
|
|
682 |
issues_count += 1
|
|
683 |
simplebar(who, issues_count, issues_total)
|
|
684 |
next if newId.nil?
|
|
685 |
issue = findIssue(newId)
|
|
686 |
next if issue.nil?
|
|
687 |
# convert issue description
|
|
688 |
issue.description = convert_wiki_text(issue.description)
|
|
689 |
# Converted issue comments had their last updated time set to the day of the migration (bugfix)
|
|
690 |
next unless Time.fake(issue.updated_on) { issue.save }
|
|
691 |
# convert issue journals
|
|
692 |
issue.journals.find(:all).each do |journal|
|
|
693 |
journal.notes = convert_wiki_text(journal.notes)
|
|
694 |
journal.save
|
583 |
695 |
end
|
584 |
696 |
end
|
585 |
|
puts
|
|
697 |
puts if issues_count < issues_total
|
586 |
698 |
|
|
699 |
who = " in Milestone descriptions"
|
|
700 |
milestone_wiki_total = milestone_wiki.count
|
|
701 |
milestone_wiki.each do |name|
|
|
702 |
milestone_wiki_count += 1
|
|
703 |
simplebar(who, milestone_wiki_count, milestone_wiki_total)
|
|
704 |
p = wiki.find_page(name)
|
|
705 |
next if p.nil?
|
|
706 |
p.content.text = convert_wiki_text(p.content.text)
|
|
707 |
p.content.save
|
|
708 |
end
|
|
709 |
puts if milestone_wiki_count < milestone_wiki_total
|
|
710 |
|
587 |
711 |
puts
|
588 |
|
puts "Components: #{migrated_components}/#{TracComponent.count}"
|
589 |
|
puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
|
590 |
|
puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
|
|
712 |
puts "Components: #{migrated_components}/#{components_total}"
|
|
713 |
puts "Milestones: #{migrated_milestones}/#{milestones_total}"
|
|
714 |
puts "Tickets: #{migrated_tickets}/#{tickets_total}"
|
591 |
715 |
puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
|
592 |
716 |
puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
|
593 |
|
puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
|
|
717 |
puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edits_total}"
|
594 |
718 |
puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
|
595 |
719 |
end
|
596 |
|
|
|
720 |
|
|
721 |
def self.findIssue(id)
|
|
722 |
return Issue.find(id)
|
|
723 |
rescue ActiveRecord::RecordNotFound
|
|
724 |
puts "[#{id}] not found"
|
|
725 |
nil
|
|
726 |
end
|
|
727 |
|
597 |
728 |
def self.limit_for(klass, attribute)
|
598 |
729 |
klass.columns_hash[attribute.to_s].limit
|
599 |
730 |
end
|
... | ... | |
671 |
802 |
project.identifier = identifier
|
672 |
803 |
puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
|
673 |
804 |
# enable issues and wiki for the created project
|
674 |
|
project.enabled_module_names = ['issue_tracking', 'wiki']
|
|
805 |
# Enable all project modules by default
|
|
806 |
project.enabled_module_names = ['issue_tracking', 'wiki', 'time_tracking', 'news', 'documents', 'files', 'repository', 'boards', 'calendar', 'gantt']
|
675 |
807 |
else
|
676 |
808 |
puts
|
677 |
809 |
puts "This project already exists in your Redmine database."
|
... | ... | |
681 |
813 |
end
|
682 |
814 |
project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
|
683 |
815 |
project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
|
|
816 |
# Add Task type to the project
|
|
817 |
project.trackers << TRACKER_TASK unless project.trackers.include?(TRACKER_TASK)
|
684 |
818 |
@target_project = project.new_record? ? nil : project
|
685 |
819 |
@target_project.reload
|
686 |
820 |
end
|
... | ... | |
732 |
866 |
break unless STDIN.gets.match(/^y$/i)
|
733 |
867 |
puts
|
734 |
868 |
|
735 |
|
def prompt(text, options = {}, &block)
|
736 |
|
default = options[:default] || ''
|
737 |
|
while true
|
738 |
|
print "#{text} [#{default}]: "
|
739 |
|
STDOUT.flush
|
740 |
|
value = STDIN.gets.chomp!
|
741 |
|
value = default if value.blank?
|
742 |
|
break if yield value
|
743 |
|
end
|
744 |
|
end
|
745 |
|
|
746 |
869 |
DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
|
747 |
870 |
|
748 |
871 |
prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
|
749 |
|
prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
|
|
872 |
prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
|
750 |
873 |
unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
|
751 |
874 |
prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
|
752 |
875 |
prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
|
... | ... | |
756 |
879 |
prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
|
757 |
880 |
end
|
758 |
881 |
prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
|
759 |
|
prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
|
|
882 |
prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier.downcase}
|
760 |
883 |
puts
|
761 |
884 |
|
762 |
885 |
# Turn off email notifications
|
... | ... | |
764 |
887 |
|
765 |
888 |
TracMigrate.migrate
|
766 |
889 |
end
|
|
890 |
|
|
891 |
|
|
892 |
desc 'Subversion migration script'
|
|
893 |
task :migrate_from_trac_svn => :environment do
|
|
894 |
|
|
895 |
require 'redmine/scm/adapters/abstract_adapter'
|
|
896 |
require 'redmine/scm/adapters/subversion_adapter'
|
|
897 |
require 'rexml/document'
|
|
898 |
require 'uri'
|
|
899 |
require 'tempfile'
|
|
900 |
|
|
901 |
module SvnMigrate
|
|
902 |
TICKET_MAP = []
|
|
903 |
|
|
904 |
class Commit
|
|
905 |
attr_accessor :revision, :message
|
|
906 |
|
|
907 |
def initialize(attributes={})
|
|
908 |
self.message = attributes[:message] || ""
|
|
909 |
self.revision = attributes[:revision]
|
|
910 |
end
|
|
911 |
end
|
|
912 |
|
|
913 |
class SvnExtendedAdapter < Redmine::Scm::Adapters::SubversionAdapter
|
|
914 |
|
|
915 |
def set_message(path=nil, revision=nil, msg=nil)
|
|
916 |
path ||= ''
|
|
917 |
|
|
918 |
Tempfile.open('msg') do |tempfile|
|
|
919 |
|
|
920 |
# This is a weird thing. We need to cleanup cr/lf so we have uniform line separators
|
|
921 |
tempfile.print msg.gsub(/\r\n/,'\n')
|
|
922 |
tempfile.flush
|
|
923 |
|
|
924 |
filePath = tempfile.path.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
|
|
925 |
|
|
926 |
cmd = "#{SVN_BIN} propset svn:log --quiet --revprop -r #{revision} -F \"#{filePath}\" "
|
|
927 |
cmd << credentials_string
|
|
928 |
cmd << ' ' + target(URI.escape(path))
|
|
929 |
|
|
930 |
shellout(cmd) do |io|
|
|
931 |
begin
|
|
932 |
loop do
|
|
933 |
line = io.readline
|
|
934 |
puts line
|
|
935 |
end
|
|
936 |
rescue EOFError
|
|
937 |
end
|
|
938 |
end
|
|
939 |
|
|
940 |
raise if $? && $?.exitstatus != 0
|
|
941 |
|
|
942 |
end
|
|
943 |
|
|
944 |
end
|
|
945 |
|
|
946 |
def messages(path=nil)
|
|
947 |
path ||= ''
|
|
948 |
|
|
949 |
commits = Array.new
|
|
950 |
|
|
951 |
cmd = "#{SVN_BIN} log --xml -r 1:HEAD"
|
|
952 |
cmd << credentials_string
|
|
953 |
cmd << ' ' + target(URI.escape(path))
|
|
954 |
|
|
955 |
shellout(cmd) do |io|
|
|
956 |
begin
|
|
957 |
doc = REXML::Document.new(io)
|
|
958 |
doc.elements.each("log/logentry") do |logentry|
|
|
959 |
|
|
960 |
commits << Commit.new(
|
|
961 |
{
|
|
962 |
:revision => logentry.attributes['revision'].to_i,
|
|
963 |
:message => logentry.elements['msg'].text
|
|
964 |
})
|
|
965 |
end
|
|
966 |
rescue => e
|
|
967 |
puts"Error !!!"
|
|
968 |
puts e
|
|
969 |
end
|
|
970 |
end
|
|
971 |
return nil if $? && $?.exitstatus != 0
|
|
972 |
commits
|
|
973 |
end
|
|
974 |
|
|
975 |
end
|
|
976 |
|
|
977 |
def self.migrate
|
|
978 |
|
|
979 |
project = Project.find(@@redmine_project)
|
|
980 |
if !project
|
|
981 |
puts "Could not find project identifier '#{@@redmine_project}'"
|
|
982 |
raise
|
|
983 |
end
|
|
984 |
|
|
985 |
tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
|
|
986 |
if !tid
|
|
987 |
puts "Could not find issue custom field 'TracID'"
|
|
988 |
raise
|
|
989 |
end
|
|
990 |
|
|
991 |
Issue.find( :all, :conditions => { :project_id => project }).each do |issue|
|
|
992 |
val = nil
|
|
993 |
issue.custom_values.each do |value|
|
|
994 |
if value.custom_field.id == tid.id
|
|
995 |
val = value
|
|
996 |
break
|
|
997 |
end
|
|
998 |
end
|
|
999 |
|
|
1000 |
TICKET_MAP[val.value.to_i] = issue.id if !val.nil?
|
|
1001 |
end
|
|
1002 |
|
|
1003 |
svn = self.scm
|
|
1004 |
msgs = svn.messages(@svn_url)
|
|
1005 |
msgs.each do |commit|
|
|
1006 |
|
|
1007 |
newText = convert_wiki_text(commit.message)
|
|
1008 |
|
|
1009 |
if newText != commit.message
|
|
1010 |
puts "Updating message #{commit.revision}"
|
|
1011 |
scm.set_message(@svn_url, commit.revision, newText)
|
|
1012 |
end
|
|
1013 |
end
|
|
1014 |
|
|
1015 |
|
|
1016 |
end
|
|
1017 |
|
|
1018 |
# Basic wiki syntax conversion
|
|
1019 |
def self.convert_wiki_text(text)
|
|
1020 |
convert_wiki_text_mapping(text, TICKET_MAP)
|
|
1021 |
end
|
|
1022 |
|
|
1023 |
def self.set_svn_url(url)
|
|
1024 |
@@svn_url = url
|
|
1025 |
end
|
|
1026 |
|
|
1027 |
def self.set_svn_username(username)
|
|
1028 |
@@svn_username = username
|
|
1029 |
end
|
|
1030 |
|
|
1031 |
def self.set_svn_password(password)
|
|
1032 |
@@svn_password = password
|
|
1033 |
end
|
|
1034 |
|
|
1035 |
def self.set_redmine_project_identifier(identifier)
|
|
1036 |
@@redmine_project = identifier
|
|
1037 |
end
|
|
1038 |
|
|
1039 |
def self.scm
|
|
1040 |
# Bugfix, with redmine 1.0.1 (Debian's) it wasn't working anymore
|
|
1041 |
@scm ||= SvnExtendedAdapter.new @@svn_url, @@svn_url, @@svn_username, @@svn_password
|
|
1042 |
@scm
|
|
1043 |
end
|
|
1044 |
end
|
|
1045 |
|
|
1046 |
puts
|
|
1047 |
if Redmine::DefaultData::Loader.no_data?
|
|
1048 |
puts "Redmine configuration need to be loaded before importing data."
|
|
1049 |
puts "Please, run this first:"
|
|
1050 |
puts
|
|
1051 |
puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
|
|
1052 |
exit
|
|
1053 |
end
|
|
1054 |
|
|
1055 |
puts "WARNING: all commit messages with references to trac pages will be modified"
|
|
1056 |
print "Are you sure you want to continue ? [y/N] "
|
|
1057 |
break unless STDIN.gets.match(/^y$/i)
|
|
1058 |
puts
|
|
1059 |
|
|
1060 |
prompt('Subversion repository url') {|repository| SvnMigrate.set_svn_url repository.strip}
|
|
1061 |
prompt('Subversion repository username') {|username| SvnMigrate.set_svn_username username}
|
|
1062 |
prompt('Subversion repository password') {|password| SvnMigrate.set_svn_password password}
|
|
1063 |
prompt('Redmine project identifier') {|identifier| SvnMigrate.set_redmine_project_identifier identifier}
|
|
1064 |
puts
|
|
1065 |
|
|
1066 |
SvnMigrate.migrate
|
|
1067 |
|
|
1068 |
end
|
|
1069 |
|
|
1070 |
# Prompt
|
|
1071 |
def prompt(text, options = {}, &block)
|
|
1072 |
default = options[:default] || ''
|
|
1073 |
while true
|
|
1074 |
print "#{text} [#{default}]: "
|
|
1075 |
STDOUT.flush
|
|
1076 |
value = STDIN.gets.chomp!
|
|
1077 |
value = default if value.blank?
|
|
1078 |
break if yield value
|
|
1079 |
end
|
|
1080 |
end
|
|
1081 |
|
|
1082 |
# Basic wiki syntax conversion
|
|
1083 |
def convert_wiki_text_mapping(text, ticket_map = [])
|
|
1084 |
# Hide links
|
|
1085 |
def wiki_links_hide(src)
|
|
1086 |
@wiki_links = []
|
|
1087 |
@wiki_links_hash = "####WIKILINKS#{src.hash.to_s}####"
|
|
1088 |
src.gsub(/(\[\[.+?\|.+?\]\])/) do
|
|
1089 |
@wiki_links << $1
|
|
1090 |
@wiki_links_hash
|
|
1091 |
end
|
|
1092 |
end
|
|
1093 |
# Restore links
|
|
1094 |
def wiki_links_restore(src)
|
|
1095 |
@wiki_links.each do |s|
|
|
1096 |
src = src.sub("#{@wiki_links_hash}", s.to_s)
|
|
1097 |
end
|
|
1098 |
src
|
|
1099 |
end
|
|
1100 |
# Hidding code blocks
|
|
1101 |
def code_hide(src)
|
|
1102 |
@code = []
|
|
1103 |
@code_hash = "####CODEBLOCK#{src.hash.to_s}####"
|
|
1104 |
src.gsub(/(\{\{\{.+?\}\}\}|`.+?`)/m) do
|
|
1105 |
@code << $1
|
|
1106 |
@code_hash
|
|
1107 |
end
|
|
1108 |
end
|
|
1109 |
# Convert code blocks
|
|
1110 |
def code_convert(src)
|
|
1111 |
@code.each do |s|
|
|
1112 |
s = s.to_s
|
|
1113 |
if s =~ (/`(.+?)`/m) || s =~ (/\{\{\{(.+?)\}\}\}/) then
|
|
1114 |
# inline code
|
|
1115 |
s = s.replace("@#{$1}@")
|
|
1116 |
else
|
|
1117 |
# We would like to convert the Code highlighting too
|
|
1118 |
# This will go into the next line.
|
|
1119 |
shebang_line = false
|
|
1120 |
# Reguar expression for start of code
|
|
1121 |
pre_re = /\{\{\{/
|
|
1122 |
# Code hightlighing...
|
|
1123 |
shebang_re = /^\#\!([a-z]+)/
|
|
1124 |
# Regular expression for end of code
|
|
1125 |
pre_end_re = /\}\}\}/
|
|
1126 |
|
|
1127 |
# Go through the whole text..extract it line by line
|
|
1128 |
s = s.gsub(/^(.*)$/) do |line|
|
|
1129 |
m_pre = pre_re.match(line)
|
|
1130 |
if m_pre
|
|
1131 |
line = '<pre>'
|
|
1132 |
else
|
|
1133 |
m_sl = shebang_re.match(line)
|
|
1134 |
if m_sl
|
|
1135 |
shebang_line = true
|
|
1136 |
line = '<code class="' + m_sl[1] + '">'
|
|
1137 |
end
|
|
1138 |
m_pre_end = pre_end_re.match(line)
|
|
1139 |
if m_pre_end
|
|
1140 |
line = '</pre>'
|
|
1141 |
if shebang_line
|
|
1142 |
line = '</code>' + line
|
|
1143 |
end
|
|
1144 |
end
|
|
1145 |
end
|
|
1146 |
line
|
|
1147 |
end
|
|
1148 |
end
|
|
1149 |
src = src.sub("#{@code_hash}", s)
|
|
1150 |
end
|
|
1151 |
src
|
|
1152 |
end
|
|
1153 |
|
|
1154 |
# Hide code blocks
|
|
1155 |
text = code_hide(text)
|
|
1156 |
# New line
|
|
1157 |
text = text.gsub(/\[\[[Bb][Rr]\]\]/, "\n") # This has to go before the rules below
|
|
1158 |
# Titles (only h1. to h6., and remove #...)
|
|
1159 |
text = text.gsub(/(?:^|^\ +)(\={1,6})\ (.+)\ (?:\1)(?:\ *(\ \#.*))?/) {|s| "\nh#{$1.length}. #{$2}#{$3}\n"}
|
|
1160 |
|
|
1161 |
# External Links:
|
|
1162 |
# [http://example.com/]
|
|
1163 |
text = text.gsub(/\[((?:https?|s?ftp)\:\S+)\]/, '\1')
|
|
1164 |
# [http://example.com/ Example],[http://example.com/ "Example"]
|
|
1165 |
# [http://example.com/ "Example for "Example""] -> "Example for 'Example'":http://example.com/
|
|
1166 |
text = text.gsub(/\[((?:https?|s?ftp)\:\S+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":#{$1}"}
|
|
1167 |
# [mailto:some@example.com],[mailto:"some@example.com"]
|
|
1168 |
text = text.gsub(/\[mailto\:([\"']?)(.+?)\1\]/, '\2')
|
|
1169 |
|
|
1170 |
# Ticket links:
|
|
1171 |
# [ticket:234 Text],[ticket:234 This is a test],[ticket:234 "This is a test"]
|
|
1172 |
# [ticket:234 "Test "with quotes""] -> "Test 'with quotes'":issues/show/234
|
|
1173 |
text = text.gsub(/\[ticket\:(\d+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":/issues/show/#{$1}"}
|
|
1174 |
# ticket:1234
|
|
1175 |
# excluding ticket:1234:file.txt (used in macros)
|
|
1176 |
# #1 - working cause Redmine uses the same syntax.
|
|
1177 |
text = text.gsub(/ticket\:(\d+?)([^\:])/, '#\1\2')
|
|
1178 |
|
|
1179 |
# Source & attachments links:
|
|
1180 |
# [source:/trunk/readme.txt Readme File],[source:"/trunk/readme.txt" Readme File],
|
|
1181 |
# [source:/trunk/readme.txt],[source:"/trunk/readme.txt"]
|
|
1182 |
# The text "Readme File" is not converted,
|
|
1183 |
# cause Redmine's wiki does not support this.
|
|
1184 |
# Attachments use same syntax.
|
|
1185 |
text = text.gsub(/\[(source|attachment)\:([\"']?)([^\"']+?)\2(?:\ +(.+?))?\]/, '\1:"\3"')
|
|
1186 |
# source:"/trunk/readme.txt"
|
|
1187 |
# source:/trunk/readme.txt - working cause Redmine uses the same syntax.
|
|
1188 |
text = text.gsub(/(source|attachment)\:([\"'])([^\"']+?)\2/, '\1:"\3"')
|
|
1189 |
|
|
1190 |
# Milestone links:
|
|
1191 |
# [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)],
|
|
1192 |
# [milestone:"0.1.0 Mercury"],milestone:"0.1.0 Mercury"
|
|
1193 |
# The text "Milestone 0.1.0 (Mercury)" is not converted,
|
|
1194 |
# cause Redmine's wiki does not support this.
|
|
1195 |
text = text.gsub(/\[milestone\:([\"'])([^\"']+?)\1(?:\ +(.+?))?\]/, 'version:"\2"')
|
|
1196 |
text = text.gsub(/milestone\:([\"'])([^\"']+?)\1/, 'version:"\2"')
|
|
1197 |
# [milestone:0.1.0],milestone:0.1.0
|
|
1198 |
text = text.gsub(/\[milestone\:([^\ ]+?)\]/, 'version:\1')
|
|
1199 |
text = text.gsub(/milestone\:([^\ ]+?)/, 'version:\1')
|
|
1200 |
|
|
1201 |
# Internal Links:
|
|
1202 |
# ["Some Link"]
|
|
1203 |
text = text.gsub(/\[([\"'])(.+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
|
|
1204 |
# [wiki:"Some Link" "Link description"],[wiki:"Some Link" Link description]
|
|
1205 |
text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1[\ \t]+([\"']?)(.+?)\3\]/) {|s| "[[#{$2.delete(',./?;|:')}|#{$4}]]"}
|
|
1206 |
# [wiki:"Some Link"]
|
|
1207 |
text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
|
|
1208 |
# [wiki:SomeLink]
|
|
1209 |
text = text.gsub(/\[wiki\:([^\s\]]+?)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
|
|
1210 |
# [wiki:SomeLink Link description],[wiki:SomeLink "Link description"]
|
|
1211 |
text = text.gsub(/\[wiki\:([^\s\]\"']+?)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$3}]]"}
|
|
1212 |
|
|
1213 |
# Before convert CamelCase links, must hide wiki links with description.
|
|
1214 |
# Like this: [[http://www.freebsd.org|Hello FreeBSD World]]
|
|
1215 |
text = wiki_links_hide(text)
|
|
1216 |
# Links to CamelCase pages (not work for unicode)
|
|
1217 |
# UsingJustWikiCaps,UsingJustWikiCaps/Subpage
|
|
1218 |
text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+(?:\/[^\s[:punct:]]+)*)/) {|s| "#{$1}#{$2}[[#{$3.delete('/')}]]"}
|
|
1219 |
# Normalize things that were supposed to not be links
|
|
1220 |
# like !NotALink
|
|
1221 |
text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
|
|
1222 |
# Now restore hidden links
|
|
1223 |
text = wiki_links_restore(text)
|
|
1224 |
|
|
1225 |
# Revisions links
|
|
1226 |
text = text.gsub(/\[(\d+)\]/, 'r\1')
|
|
1227 |
# Ticket number re-writing
|
|
1228 |
text = text.gsub(/#(\d+)/) do |s|
|
|
1229 |
if $1.length < 10
|
|
1230 |
#ticket_map[$1.to_i] ||= $1
|
|
1231 |
"\##{ticket_map[$1.to_i] || $1}"
|
|
1232 |
else
|
|
1233 |
s
|
|
1234 |
end
|
|
1235 |
end
|
|
1236 |
|
|
1237 |
# Highlighting
|
|
1238 |
text = text.gsub(/'''''([^\s])/, '_*\1')
|
|
1239 |
text = text.gsub(/([^\s])'''''/, '\1*_')
|
|
1240 |
text = text.gsub(/'''/, '*')
|
|
1241 |
text = text.gsub(/''/, '_')
|
|
1242 |
text = text.gsub(/__/, '+')
|
|
1243 |
text = text.gsub(/~~/, '-')
|
|
1244 |
text = text.gsub(/,,/, '~')
|
|
1245 |
# Tables
|
|
1246 |
text = text.gsub(/\|\|/, '|')
|
|
1247 |
# Lists:
|
|
1248 |
# bullet
|
|
1249 |
text = text.gsub(/^(\ +)[\*-] /) {|s| '*' * $1.length + " "}
|
|
1250 |
# numbered
|
|
1251 |
text = text.gsub(/^(\ +)\d+\. /) {|s| '#' * $1.length + " "}
|
|
1252 |
# Images (work for only attached in current page [[Image(picture.gif)]])
|
|
1253 |
# need rules for: * [[Image(wiki:WikiFormatting:picture.gif)]] (referring to attachment on another page)
|
|
1254 |
# * [[Image(ticket:1:picture.gif)]] (file attached to a ticket)
|
|
1255 |
# * [[Image(htdocs:picture.gif)]] (referring to a file inside project htdocs)
|
|
1256 |
# * [[Image(source:/trunk/trac/htdocs/trac_logo_mini.png)]] (a file in repository)
|
|
1257 |
text = text.gsub(/\[\[image\((.+?)(?:,.+?)?\)\]\]/i, '!\1!')
|
|
1258 |
# TOC (is right-aligned, because that in Trac)
|
|
1259 |
text = text.gsub(/\[\[TOC(?:\((.*?)\))?\]\]/m) {|s| "{{>toc}}\n"}
|
|
1260 |
|
|
1261 |
# Restore and convert code blocks
|
|
1262 |
text = code_convert(text)
|
|
1263 |
|
|
1264 |
text
|
|
1265 |
end
|
|
1266 |
|
|
1267 |
# Simple progress bar
|
|
1268 |
def simplebar(title, current, total, out = STDOUT)
|
|
1269 |
def get_width
|
|
1270 |
default_width = 80
|
|
1271 |
begin
|
|
1272 |
tiocgwinsz = 0x5413
|
|
1273 |
data = [0, 0, 0, 0].pack("SSSS")
|
|
1274 |
if out.ioctl(tiocgwinsz, data) >= 0 then
|
|
1275 |
rows, cols, xpixels, ypixels = data.unpack("SSSS")
|
|
1276 |
if cols >= 0 then cols else default_width end
|
|
1277 |
else
|
|
1278 |
default_width
|
|
1279 |
end
|
|
1280 |
rescue Exception
|
|
1281 |
default_width
|
|
1282 |
end
|
|
1283 |
end
|
|
1284 |
mark = "*"
|
|
1285 |
title_width = 40
|
|
1286 |
max = get_width - title_width - 10
|
|
1287 |
format = "%-#{title_width}s [%-#{max}s] %3d%% %s"
|
|
1288 |
bar = current * max / total
|
|
1289 |
percentage = bar * 100 / max
|
|
1290 |
current == total ? eol = "\n" : eol ="\r"
|
|
1291 |
printf(format, title, mark * bar, percentage, eol)
|
|
1292 |
out.flush
|
|
1293 |
end
|
767 |
1294 |
end
|
768 |
1295 |
|