19 |
19 |
require 'iconv'
|
20 |
20 |
require 'pp'
|
21 |
21 |
|
|
22 |
require 'redmine/scm/adapters/abstract_adapter'
|
|
23 |
require 'redmine/scm/adapters/subversion_adapter'
|
|
24 |
require 'rexml/document'
|
|
25 |
require 'uri'
|
|
26 |
require 'tempfile'
|
|
27 |
|
22 |
28 |
namespace :redmine do
|
23 |
29 |
desc 'Trac migration script'
|
24 |
30 |
task :migrate_from_trac => :environment do
|
... | ... | |
192 |
198 |
def time; Time.at(read_attribute(:time)) end
|
193 |
199 |
end
|
194 |
200 |
|
195 |
|
TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
|
|
201 |
TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup \
|
|
202 |
TracBrowser TracCgi TracChangeset TracInstallPlatforms TracMultipleProjects TracModWSGI \
|
196 |
203 |
TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
|
197 |
204 |
TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
|
198 |
205 |
TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
|
199 |
206 |
TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
|
200 |
207 |
WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
|
201 |
208 |
CamelCase TitleIndex)
|
202 |
|
|
203 |
209 |
class TracWikiPage < ActiveRecord::Base
|
204 |
210 |
set_table_name :wiki
|
205 |
211 |
set_primary_key :name
|
... | ... | |
241 |
247 |
if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
|
242 |
248 |
name = name_attr.value
|
243 |
249 |
end
|
244 |
|
name =~ (/(.*)(\s+\w+)?/)
|
|
250 |
name =~ (/(.+?)(?:[\ \t]+(.+)?|[\ \t]+|)$/)
|
245 |
251 |
fn = $1.strip
|
246 |
252 |
ln = ($2 || '-').strip
|
247 |
253 |
|
... | ... | |
271 |
277 |
|
272 |
278 |
# Basic wiki syntax conversion
|
273 |
279 |
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
|
|
280 |
convert_wiki_text_mapping(text, TICKET_MAP)
|
364 |
281 |
end
|
365 |
282 |
|
366 |
283 |
def self.migrate
|
... | ... | |
400 |
317 |
# Milestones
|
401 |
318 |
print "Migrating milestones"
|
402 |
319 |
version_map = {}
|
|
320 |
milestone_wiki = Array.new
|
403 |
321 |
TracMilestone.find(:all).each do |milestone|
|
404 |
322 |
print '.'
|
405 |
323 |
STDOUT.flush
|
... | ... | |
419 |
337 |
|
420 |
338 |
next unless v.save
|
421 |
339 |
version_map[milestone.name] = v
|
|
340 |
milestone_wiki.push(milestone.name);
|
422 |
341 |
migrated_milestones += 1
|
423 |
342 |
end
|
424 |
343 |
puts
|
... | ... | |
456 |
375 |
r.save!
|
457 |
376 |
custom_field_map['resolution'] = r
|
458 |
377 |
|
|
378 |
# Trac 'keywords' field as a Redmine custom field
|
|
379 |
k = IssueCustomField.find(:first, :conditions => { :name => "Keywords" })
|
|
380 |
k = IssueCustomField.new(:name => 'Keywords',
|
|
381 |
:field_format => 'string',
|
|
382 |
:is_filter => true) if k.nil?
|
|
383 |
k.trackers = Tracker.find(:all)
|
|
384 |
k.projects << @target_project
|
|
385 |
k.save!
|
|
386 |
custom_field_map['keywords'] = k
|
|
387 |
|
|
388 |
# Trac ticket id as a Redmine custom field
|
|
389 |
tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
|
|
390 |
tid = IssueCustomField.new(:name => 'TracID',
|
|
391 |
:field_format => 'string',
|
|
392 |
:is_filter => true) if tid.nil?
|
|
393 |
tid.trackers = Tracker.find(:all)
|
|
394 |
tid.projects << @target_project
|
|
395 |
tid.save!
|
|
396 |
custom_field_map['tracid'] = tid
|
|
397 |
|
459 |
398 |
# Tickets
|
460 |
399 |
print "Migrating tickets"
|
461 |
400 |
TracTicket.find_each(:batch_size => 200) do |ticket|
|
... | ... | |
463 |
402 |
STDOUT.flush
|
464 |
403 |
i = Issue.new :project => @target_project,
|
465 |
404 |
:subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
|
466 |
|
:description => convert_wiki_text(encode(ticket.description)),
|
|
405 |
:description => encode(ticket.description),
|
467 |
406 |
:priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
|
468 |
407 |
:created_on => ticket.time
|
469 |
408 |
i.author = find_or_create_user(ticket.reporter)
|
... | ... | |
482 |
421 |
Time.fake(ticket.changetime) { i.save }
|
483 |
422 |
end
|
484 |
423 |
|
485 |
|
# Comments and status/resolution changes
|
|
424 |
# Comments and status/resolution/keywords changes
|
486 |
425 |
ticket.changes.group_by(&:time).each do |time, changeset|
|
487 |
426 |
status_change = changeset.select {|change| change.field == 'status'}.first
|
488 |
427 |
resolution_change = changeset.select {|change| change.field == 'resolution'}.first
|
|
428 |
keywords_change = changeset.select {|change| change.field == 'keywords'}.first
|
489 |
429 |
comment_change = changeset.select {|change| change.field == 'comment'}.first
|
490 |
430 |
|
491 |
|
n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
|
|
431 |
n = Journal.new :notes => (comment_change ? encode(comment_change.newvalue) : ''),
|
492 |
432 |
:created_on => time
|
493 |
433 |
n.user = find_or_create_user(changeset.first.author)
|
494 |
434 |
n.journalized = i
|
... | ... | |
507 |
447 |
:old_value => resolution_change.oldvalue,
|
508 |
448 |
:value => resolution_change.newvalue)
|
509 |
449 |
end
|
|
450 |
if keywords_change
|
|
451 |
n.details << JournalDetail.new(:property => 'cf',
|
|
452 |
:prop_key => custom_field_map['keywords'].id,
|
|
453 |
:old_value => keywords_change.oldvalue,
|
|
454 |
:value => keywords_change.newvalue)
|
|
455 |
end
|
510 |
456 |
n.save unless n.details.empty? && n.notes.blank?
|
511 |
457 |
end
|
512 |
458 |
|
... | ... | |
534 |
480 |
if custom_field_map['resolution'] && !ticket.resolution.blank?
|
535 |
481 |
custom_values[custom_field_map['resolution'].id] = ticket.resolution
|
536 |
482 |
end
|
|
483 |
if custom_field_map['keywords'] && !ticket.keywords.blank?
|
|
484 |
custom_values[custom_field_map['keywords'].id] = ticket.keywords
|
|
485 |
end
|
|
486 |
if custom_field_map['tracid']
|
|
487 |
custom_values[custom_field_map['tracid'].id] = ticket.id
|
|
488 |
end
|
537 |
489 |
i.custom_field_values = custom_values
|
538 |
490 |
i.save_custom_field_values
|
539 |
491 |
end
|
... | ... | |
576 |
528 |
end
|
577 |
529 |
end
|
578 |
530 |
|
|
531 |
end
|
|
532 |
puts
|
|
533 |
|
|
534 |
# Now load each wiki page and transform its content into textile format
|
|
535 |
print "Transform texts to textile format:"
|
|
536 |
puts
|
|
537 |
|
|
538 |
print " in Wiki pages..................."
|
579 |
539 |
wiki.reload
|
580 |
540 |
wiki.pages.each do |page|
|
|
541 |
#print '.'
|
581 |
542 |
page.content.text = convert_wiki_text(page.content.text)
|
582 |
543 |
Time.fake(page.content.updated_on) { page.content.save }
|
583 |
544 |
end
|
584 |
|
end
|
585 |
|
puts
|
|
545 |
puts
|
586 |
546 |
|
|
547 |
print " in Issue descriptions..........."
|
|
548 |
TICKET_MAP.each do |newId|
|
|
549 |
|
|
550 |
next if newId.nil?
|
|
551 |
|
|
552 |
#print '.'
|
|
553 |
issue = findIssue(newId)
|
|
554 |
next if issue.nil?
|
|
555 |
|
|
556 |
issue.description = convert_wiki_text(issue.description)
|
|
557 |
issue.save
|
|
558 |
end
|
|
559 |
puts
|
|
560 |
|
|
561 |
print " in Issue journal descriptions..."
|
|
562 |
TICKET_MAP.each do |newId|
|
|
563 |
next if newId.nil?
|
|
564 |
|
|
565 |
#print '.'
|
|
566 |
issue = findIssue(newId)
|
|
567 |
next if issue.nil?
|
|
568 |
|
|
569 |
issue.journals.find(:all).each do |journal|
|
|
570 |
#print '.'
|
|
571 |
journal.notes = convert_wiki_text(journal.notes)
|
|
572 |
journal.save
|
|
573 |
end
|
|
574 |
|
|
575 |
end
|
|
576 |
puts
|
|
577 |
|
|
578 |
print " in Milestone descriptions......."
|
|
579 |
milestone_wiki.each do |name|
|
|
580 |
p = wiki.find_page(name)
|
|
581 |
next if p.nil?
|
|
582 |
|
|
583 |
#print '.'
|
|
584 |
p.content.text = convert_wiki_text(p.content.text)
|
|
585 |
p.content.save
|
|
586 |
end
|
|
587 |
puts
|
|
588 |
|
587 |
589 |
puts
|
588 |
590 |
puts "Components: #{migrated_components}/#{TracComponent.count}"
|
589 |
591 |
puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
|
... | ... | |
593 |
595 |
puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
|
594 |
596 |
puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
|
595 |
597 |
end
|
|
598 |
|
|
599 |
def self.findIssue(id)
|
|
600 |
|
|
601 |
return Issue.find(id)
|
596 |
602 |
|
|
603 |
rescue ActiveRecord::RecordNotFound
|
|
604 |
puts
|
|
605 |
print "[#{id}] not found"
|
|
606 |
|
|
607 |
nil
|
|
608 |
end
|
|
609 |
|
597 |
610 |
def self.limit_for(klass, attribute)
|
598 |
611 |
klass.columns_hash[attribute.to_s].limit
|
599 |
612 |
end
|
... | ... | |
746 |
759 |
DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
|
747 |
760 |
|
748 |
761 |
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}
|
|
762 |
prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
|
750 |
763 |
unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
|
751 |
764 |
prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
|
752 |
765 |
prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
|
... | ... | |
764 |
777 |
|
765 |
778 |
TracMigrate.migrate
|
766 |
779 |
end
|
|
780 |
|
|
781 |
|
|
782 |
desc 'Subversion migration script'
|
|
783 |
task :migrate_from_trac_svn => :environment do
|
|
784 |
|
|
785 |
module SvnMigrate
|
|
786 |
TICKET_MAP = []
|
|
787 |
|
|
788 |
class Commit
|
|
789 |
attr_accessor :revision, :message
|
|
790 |
|
|
791 |
def initialize(attributes={})
|
|
792 |
self.message = attributes[:message] || ""
|
|
793 |
self.revision = attributes[:revision]
|
|
794 |
end
|
|
795 |
end
|
|
796 |
|
|
797 |
class SvnExtendedAdapter < Redmine::Scm::Adapters::SubversionAdapter
|
|
798 |
|
|
799 |
|
|
800 |
|
|
801 |
def set_message(path=nil, revision=nil, msg=nil)
|
|
802 |
path ||= ''
|
|
803 |
|
|
804 |
Tempfile.open('msg') do |tempfile|
|
|
805 |
|
|
806 |
# This is a weird thing. We need to cleanup cr/lf so we have uniform line separators
|
|
807 |
tempfile.print msg.gsub(/\r\n/,'\n')
|
|
808 |
tempfile.flush
|
|
809 |
|
|
810 |
filePath = tempfile.path.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
|
|
811 |
|
|
812 |
cmd = "#{SVN_BIN} propset svn:log --quiet --revprop -r #{revision} -F \"#{filePath}\" "
|
|
813 |
cmd << credentials_string
|
|
814 |
cmd << ' ' + target(URI.escape(path))
|
|
815 |
|
|
816 |
shellout(cmd) do |io|
|
|
817 |
begin
|
|
818 |
loop do
|
|
819 |
line = io.readline
|
|
820 |
puts line
|
|
821 |
end
|
|
822 |
rescue EOFError
|
|
823 |
end
|
|
824 |
end
|
|
825 |
|
|
826 |
raise if $? && $?.exitstatus != 0
|
|
827 |
|
|
828 |
end
|
|
829 |
|
|
830 |
end
|
|
831 |
|
|
832 |
def messages(path=nil)
|
|
833 |
path ||= ''
|
|
834 |
|
|
835 |
commits = Array.new
|
|
836 |
|
|
837 |
cmd = "#{SVN_BIN} log --xml -r 1:HEAD"
|
|
838 |
cmd << credentials_string
|
|
839 |
cmd << ' ' + target(URI.escape(path))
|
|
840 |
|
|
841 |
shellout(cmd) do |io|
|
|
842 |
begin
|
|
843 |
doc = REXML::Document.new(io)
|
|
844 |
doc.elements.each("log/logentry") do |logentry|
|
|
845 |
|
|
846 |
commits << Commit.new(
|
|
847 |
{
|
|
848 |
:revision => logentry.attributes['revision'].to_i,
|
|
849 |
:message => logentry.elements['msg'].text
|
|
850 |
})
|
|
851 |
end
|
|
852 |
rescue => e
|
|
853 |
puts"Error !!!"
|
|
854 |
puts e
|
|
855 |
end
|
|
856 |
end
|
|
857 |
return nil if $? && $?.exitstatus != 0
|
|
858 |
commits
|
|
859 |
end
|
|
860 |
|
|
861 |
end
|
|
862 |
|
|
863 |
def self.migrate
|
|
864 |
|
|
865 |
project = Project.find(@@redmine_project)
|
|
866 |
if !project
|
|
867 |
puts "Could not find project identifier '#{@@redmine_project}'"
|
|
868 |
raise
|
|
869 |
end
|
|
870 |
|
|
871 |
tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
|
|
872 |
if !tid
|
|
873 |
puts "Could not find issue custom field 'TracID'"
|
|
874 |
raise
|
|
875 |
end
|
|
876 |
|
|
877 |
Issue.find( :all, :conditions => { :project_id => project }).each do |issue|
|
|
878 |
val = nil
|
|
879 |
issue.custom_values.each do |value|
|
|
880 |
if value.custom_field.id == tid.id
|
|
881 |
val = value
|
|
882 |
break
|
|
883 |
end
|
|
884 |
end
|
|
885 |
|
|
886 |
TICKET_MAP[val.value.to_i] = issue.id if !val.nil?
|
|
887 |
end
|
|
888 |
|
|
889 |
svn = self.scm
|
|
890 |
msgs = svn.messages(@svn_url)
|
|
891 |
msgs.each do |commit|
|
|
892 |
|
|
893 |
newText = convert_wiki_text(commit.message)
|
|
894 |
|
|
895 |
if newText != commit.message
|
|
896 |
puts "Updating message #{commit.revision}"
|
|
897 |
scm.set_message(@svn_url, commit.revision, newText)
|
|
898 |
end
|
|
899 |
end
|
|
900 |
|
|
901 |
|
|
902 |
end
|
|
903 |
|
|
904 |
# Basic wiki syntax conversion
|
|
905 |
def self.convert_wiki_text(text)
|
|
906 |
convert_wiki_text_mapping(text, TICKET_MAP )
|
|
907 |
end
|
|
908 |
|
|
909 |
def self.set_svn_url(url)
|
|
910 |
@@svn_url = url
|
|
911 |
end
|
|
912 |
|
|
913 |
def self.set_svn_username(username)
|
|
914 |
@@svn_username = username
|
|
915 |
end
|
|
916 |
|
|
917 |
def self.set_svn_password(password)
|
|
918 |
@@svn_password = password
|
|
919 |
end
|
|
920 |
|
|
921 |
def self.set_redmine_project_identifier(identifier)
|
|
922 |
@@redmine_project = identifier
|
|
923 |
end
|
|
924 |
|
|
925 |
def self.scm
|
|
926 |
@scm ||= SvnExtendedAdapter.new @@svn_url, @@svn_url, @@svn_username, @@svn_password, 0, "", nil
|
|
927 |
@scm
|
|
928 |
end
|
|
929 |
end
|
|
930 |
|
|
931 |
def prompt(text, options = {}, &block)
|
|
932 |
default = options[:default] || ''
|
|
933 |
while true
|
|
934 |
print "#{text} [#{default}]: "
|
|
935 |
value = STDIN.gets.chomp!
|
|
936 |
value = default if value.blank?
|
|
937 |
break if yield value
|
|
938 |
end
|
|
939 |
end
|
|
940 |
|
|
941 |
puts
|
|
942 |
if Redmine::DefaultData::Loader.no_data?
|
|
943 |
puts "Redmine configuration need to be loaded before importing data."
|
|
944 |
puts "Please, run this first:"
|
|
945 |
puts
|
|
946 |
puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
|
|
947 |
exit
|
|
948 |
end
|
|
949 |
|
|
950 |
puts "WARNING: all commit messages with references to trac pages will be modified"
|
|
951 |
print "Are you sure you want to continue ? [y/N] "
|
|
952 |
break unless STDIN.gets.match(/^y$/i)
|
|
953 |
puts
|
|
954 |
|
|
955 |
prompt('Subversion repository url') {|repository| SvnMigrate.set_svn_url repository.strip}
|
|
956 |
prompt('Subversion repository username') {|username| SvnMigrate.set_svn_username username}
|
|
957 |
prompt('Subversion repository password') {|password| SvnMigrate.set_svn_password password}
|
|
958 |
prompt('Redmine project identifier') {|identifier| SvnMigrate.set_redmine_project_identifier identifier}
|
|
959 |
puts
|
|
960 |
|
|
961 |
SvnMigrate.migrate
|
|
962 |
|
|
963 |
end
|
|
964 |
|
|
965 |
|
|
966 |
# Basic wiki syntax conversion
|
|
967 |
def convert_wiki_text_mapping(text, ticket_map = [])
|
|
968 |
# New line
|
|
969 |
text = text.gsub(/\[\[[Bb][Rr]\]\]/, "\n") # This has to go before the rules below
|
|
970 |
# Titles (only h1. to h6., and remove #...)
|
|
971 |
text = text.gsub(/(?:^|^\ +)(\={1,6})\ (.+)\ (?:\1)(?:\ *(\ \#.*))?/) {|s| "\nh#{$1.length}. #{$2}#{$3}\n"}
|
|
972 |
|
|
973 |
# External Links:
|
|
974 |
# [http://example.com/]
|
|
975 |
text = text.gsub(/\[((?:https?|s?ftp)\:\S+)\]/, '\1')
|
|
976 |
# [http://example.com/ Example],[http://example.com/ "Example"]
|
|
977 |
# [http://example.com/ "Example for "Example""] -> "Example for 'Example'":http://example.com/
|
|
978 |
text = text.gsub(/\[((?:https?|s?ftp)\:\S+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":#{$1}"}
|
|
979 |
# [mailto:some@example.com],[mailto:"some@example.com"]
|
|
980 |
text = text.gsub(/\[mailto\:([\"']?)(.+?)\1\]/, '\2')
|
|
981 |
|
|
982 |
# Ticket links:
|
|
983 |
# [ticket:234 Text],[ticket:234 This is a test],[ticket:234 "This is a test"]
|
|
984 |
# [ticket:234 "Test "with quotes""] -> "Test 'with quotes'":issues/show/234
|
|
985 |
text = text.gsub(/\[ticket\:(\d+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":/issues/show/#{$1}"}
|
|
986 |
# ticket:1234
|
|
987 |
# excluding ticket:1234:file.txt (used in macros)
|
|
988 |
# #1 - working cause Redmine uses the same syntax.
|
|
989 |
text = text.gsub(/ticket\:(\d+?)([^\:])/, '#\1\2')
|
|
990 |
|
|
991 |
# Source & attachments links:
|
|
992 |
# [source:/trunk/readme.txt Readme File],[source:"/trunk/readme.txt" Readme File],
|
|
993 |
# [source:/trunk/readme.txt],[source:"/trunk/readme.txt"]
|
|
994 |
# The text "Readme File" is not converted,
|
|
995 |
# cause Redmine's wiki does not support this.
|
|
996 |
# Attachments use same syntax.
|
|
997 |
text = text.gsub(/\[(source|attachment)\:([\"']?)([^\"']+?)\2(?:\ +(.+?))?\]/, '\1:"\3"')
|
|
998 |
# source:"/trunk/readme.txt"
|
|
999 |
# source:/trunk/readme.txt - working cause Redmine uses the same syntax.
|
|
1000 |
text = text.gsub(/(source|attachment)\:([\"'])([^\"']+?)\2/, '\1:"\3"')
|
|
1001 |
|
|
1002 |
# Milestone links:
|
|
1003 |
# [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)],
|
|
1004 |
# [milestone:"0.1.0 Mercury"],milestone:"0.1.0 Mercury"
|
|
1005 |
# The text "Milestone 0.1.0 (Mercury)" is not converted,
|
|
1006 |
# cause Redmine's wiki does not support this.
|
|
1007 |
text = text.gsub(/\[milestone\:([\"'])([^\"']+?)\1(?:\ +(.+?))?\]/, 'version:"\2"')
|
|
1008 |
text = text.gsub(/milestone\:([\"'])([^\"']+?)\1/, 'version:"\2"')
|
|
1009 |
# [milestone:0.1.0],milestone:0.1.0
|
|
1010 |
text = text.gsub(/\[milestone\:([^\ ]+?)\]/, 'version:\1')
|
|
1011 |
text = text.gsub(/milestone\:([^\ ]+?)/, 'version:\1')
|
|
1012 |
|
|
1013 |
# Internal Links:
|
|
1014 |
# ["Some Link"]
|
|
1015 |
text = text.gsub(/\[([\"'])(.+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
|
|
1016 |
# [wiki:"Some Link" "Link description"],[wiki:"Some Link" Link description]
|
|
1017 |
text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1[\ \t]+([\"']?)(.+?)\3\]/) {|s| "[[#{$2.delete(',./?;|:')}|#{$4}]]"}
|
|
1018 |
# [wiki:"Some Link"]
|
|
1019 |
text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
|
|
1020 |
# [wiki:SomeLink]
|
|
1021 |
text = text.gsub(/\[wiki\:([^\s\]]+?)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
|
|
1022 |
# [wiki:SomeLink Link description],[wiki:SomeLink "Link description"]
|
|
1023 |
text = text.gsub(/\[wiki\:([^\s\]\"']+?)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$3}]]"}
|
|
1024 |
|
|
1025 |
# Links to CamelCase pages (not work for unicode)
|
|
1026 |
# UsingJustWikiCaps,UsingJustWikiCaps/Subpage
|
|
1027 |
text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+(?:\/[^\s[:punct:]]+)*)/) {|s| "#{$1}#{$2}[[#{$3.delete('/')}]]"}
|
|
1028 |
# Normalize things that were supposed to not be links
|
|
1029 |
# like !NotALink
|
|
1030 |
text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
|
|
1031 |
|
|
1032 |
# Revisions links
|
|
1033 |
text = text.gsub(/\[(\d+)\]/, 'r\1')
|
|
1034 |
# Ticket number re-writing
|
|
1035 |
text = text.gsub(/#(\d+)/) do |s|
|
|
1036 |
if $1.length < 10
|
|
1037 |
# ticket_map[$1.to_i] ||= $1
|
|
1038 |
"\##{ticket_map[$1.to_i] || $1}"
|
|
1039 |
else
|
|
1040 |
s
|
|
1041 |
end
|
|
1042 |
end
|
|
1043 |
|
|
1044 |
# Before convert Code highlighting, need processing inline code
|
|
1045 |
# {{{hello world}}}
|
|
1046 |
text = text.gsub(/\{\{\{(.+?)\}\}\}/, '@\1@')
|
|
1047 |
|
|
1048 |
# We would like to convert the Code highlighting too
|
|
1049 |
# This will go into the next line.
|
|
1050 |
shebang_line = false
|
|
1051 |
# Reguar expression for start of code
|
|
1052 |
pre_re = /\{\{\{/
|
|
1053 |
# Code hightlighing...
|
|
1054 |
shebang_re = /^\#\!([a-z]+)/
|
|
1055 |
# Regular expression for end of code
|
|
1056 |
pre_end_re = /\}\}\}/
|
|
1057 |
|
|
1058 |
# Go through the whole text..extract it line by line
|
|
1059 |
text = text.gsub(/^(.*)$/) do |line|
|
|
1060 |
m_pre = pre_re.match(line)
|
|
1061 |
if m_pre
|
|
1062 |
line = '<pre>'
|
|
1063 |
else
|
|
1064 |
m_sl = shebang_re.match(line)
|
|
1065 |
if m_sl
|
|
1066 |
shebang_line = true
|
|
1067 |
line = '<code class="' + m_sl[1] + '">'
|
|
1068 |
end
|
|
1069 |
m_pre_end = pre_end_re.match(line)
|
|
1070 |
if m_pre_end
|
|
1071 |
line = '</pre>'
|
|
1072 |
if shebang_line
|
|
1073 |
line = '</code>' + line
|
|
1074 |
end
|
|
1075 |
end
|
|
1076 |
end
|
|
1077 |
line
|
|
1078 |
end
|
|
1079 |
|
|
1080 |
# Highlighting
|
|
1081 |
text = text.gsub(/'''''([^\s])/, '_*\1')
|
|
1082 |
text = text.gsub(/([^\s])'''''/, '\1*_')
|
|
1083 |
text = text.gsub(/'''/, '*')
|
|
1084 |
text = text.gsub(/''/, '_')
|
|
1085 |
text = text.gsub(/__/, '+')
|
|
1086 |
text = text.gsub(/~~/, '-')
|
|
1087 |
text = text.gsub(/`/, '@')
|
|
1088 |
text = text.gsub(/,,/, '~')
|
|
1089 |
# Tables
|
|
1090 |
text = text.gsub(/\|\|/, '|')
|
|
1091 |
# Lists:
|
|
1092 |
# bullet
|
|
1093 |
text = text.gsub(/^(\ +)\* /) {|s| '*' * $1.length + " "}
|
|
1094 |
# numbered
|
|
1095 |
text = text.gsub(/^(\ +)\d+\. /) {|s| '#' * $1.length + " "}
|
|
1096 |
# Images (work for only attached in current page [[Image(picture.gif)]])
|
|
1097 |
# need rules for: * [[Image(wiki:WikiFormatting:picture.gif)]] (referring to attachment on another page)
|
|
1098 |
# * [[Image(ticket:1:picture.gif)]] (file attached to a ticket)
|
|
1099 |
# * [[Image(htdocs:picture.gif)]] (referring to a file inside project htdocs)
|
|
1100 |
# * [[Image(source:/trunk/trac/htdocs/trac_logo_mini.png)]] (a file in repository)
|
|
1101 |
text = text.gsub(/\[\[image\((.+?)(?:,.+?)?\)\]\]/i, '!\1!')
|
|
1102 |
# TOC
|
|
1103 |
text = text.gsub(/\[\[TOC(?:\((.*?)\))?\]\]/m) {|s| "{{>toc}}\n"}
|
|
1104 |
|
|
1105 |
text
|
|
1106 |
end
|
767 |
1107 |
end
|
768 |
1108 |
|