Project

General

Profile

Patch #5035 » migrate_from_trac_v5.patch

Mike Stupalov, 2010-03-23 15:44

View differences:

lib/tasks/migrate_from_trac.rake (working copy)
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
......
153 159
      private
154 160
        def trac_fullpath
155 161
          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}"
162
          trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02X', x[0]) }
163
          trac_dir = id.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02X', x[0]) }
164
          puts
165
          puts filename + " : " + "#{attachment_type}/#{trac_dir}/#{trac_file}"
166
          "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{trac_dir}/#{trac_file}"
158 167
        end
159 168
      end
160 169

  
......
192 201
        def time; Time.at(read_attribute(:time)) end
193 202
      end
194 203

  
195
      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
204
      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup \
205
                           TracBrowser TracCgi TracChangeset TracInstallPlatforms TracMultipleProjects TracModWSGI \
196 206
                           TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
197 207
                           TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
198 208
                           TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
199 209
                           TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
200 210
                           WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
201 211
                           CamelCase TitleIndex)
202

  
203 212
      class TracWikiPage < ActiveRecord::Base
204 213
        set_table_name :wiki
205 214
        set_primary_key :name
......
241 250
          if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
242 251
            name = name_attr.value
243 252
          end
244
          name =~ (/(.*)(\s+\w+)?/)
253
          name =~ (/(.+?)(?:[\ \t]+(.+)?|[\ \t]+|)$/)
245 254
          fn = $1.strip
246 255
          ln = ($2 || '-').strip
247 256

  
......
271 280

  
272 281
      # Basic wiki syntax conversion
273 282
      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
283
        convert_wiki_text_mapping(text, TICKET_MAP)
364 284
      end
365 285

  
366 286
      def self.migrate
......
391 311
        STDOUT.flush
392 312
          c = IssueCategory.new :project => @target_project,
393 313
                                :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
314
        #Owner
315
        unless component.owner.blank?
316
          c.assigned_to = find_or_create_user(component.owner, true)
317
        end
394 318
        next unless c.save
395 319
        issues_category_map[component.name] = c
396 320
        migrated_components += 1
......
400 324
        # Milestones
401 325
        print "Migrating milestones"
402 326
        version_map = {}
327
        milestone_wiki = Array.new
403 328
        TracMilestone.find(:all).each do |milestone|
404 329
          print '.'
405 330
          STDOUT.flush
......
419 344

  
420 345
          next unless v.save
421 346
          version_map[milestone.name] = v
347
          milestone_wiki.push(milestone.name);
422 348
          migrated_milestones += 1
423 349
        end
424 350
        puts
......
456 382
        r.save!
457 383
        custom_field_map['resolution'] = r
458 384

  
385
        # Trac 'keywords' field as a Redmine custom field
386
        k = IssueCustomField.find(:first, :conditions => { :name => "Keywords" })
387
        k = IssueCustomField.new(:name => 'Keywords',
388
                                 :field_format => 'string',
389
                                 :is_filter => true) if k.nil?
390
        k.trackers = Tracker.find(:all)
391
        k.projects << @target_project
392
        k.save!
393
        custom_field_map['keywords'] = k
394

  
395
        # Trac ticket id as a Redmine custom field
396
        tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
397
        tid = IssueCustomField.new(:name => 'TracID',
398
                                 :field_format => 'string',
399
                                 :is_filter => true) if tid.nil?
400
        tid.trackers = Tracker.find(:all)
401
        tid.projects << @target_project
402
        tid.save!
403
        custom_field_map['tracid'] = tid
404
  
459 405
        # Tickets
460 406
        print "Migrating tickets"
461 407
          TracTicket.find_each(:batch_size => 200) do |ticket|
......
463 409
          STDOUT.flush
464 410
          i = Issue.new :project => @target_project,
465 411
                          :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
466
                          :description => convert_wiki_text(encode(ticket.description)),
412
                          :description => encode(ticket.description),
467 413
                          :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
468 414
                          :created_on => ticket.time
469 415
          i.author = find_or_create_user(ticket.reporter)
......
482 428
              Time.fake(ticket.changetime) { i.save }
483 429
            end
484 430

  
485
          # Comments and status/resolution changes
431
          # Comments and status/resolution/keywords changes
486 432
          ticket.changes.group_by(&:time).each do |time, changeset|
487 433
              status_change = changeset.select {|change| change.field == 'status'}.first
488 434
              resolution_change = changeset.select {|change| change.field == 'resolution'}.first
435
              keywords_change = changeset.select {|change| change.field == 'keywords'}.first
489 436
              comment_change = changeset.select {|change| change.field == 'comment'}.first
490 437

  
491
              n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
438
              n = Journal.new :notes => (comment_change ? encode(comment_change.newvalue) : ''),
492 439
                              :created_on => time
493 440
              n.user = find_or_create_user(changeset.first.author)
494 441
              n.journalized = i
......
507 454
                                               :old_value => resolution_change.oldvalue,
508 455
                                               :value => resolution_change.newvalue)
509 456
              end
457
              if keywords_change
458
                n.details << JournalDetail.new(:property => 'cf',
459
                                               :prop_key => custom_field_map['keywords'].id,
460
                                               :old_value => keywords_change.oldvalue,
461
                                               :value => keywords_change.newvalue)
462
              end
510 463
              n.save unless n.details.empty? && n.notes.blank?
511 464
          end
512 465

  
......
534 487
          if custom_field_map['resolution'] && !ticket.resolution.blank?
535 488
            custom_values[custom_field_map['resolution'].id] = ticket.resolution
536 489
          end
490
          if custom_field_map['keywords'] && !ticket.keywords.blank?
491
            custom_values[custom_field_map['keywords'].id] = ticket.keywords
492
          end
493
          if custom_field_map['tracid'] 
494
            custom_values[custom_field_map['tracid'].id] = ticket.id
495
          end
537 496
          i.custom_field_values = custom_values
538 497
          i.save_custom_field_values
539 498
        end
......
576 535
            end
577 536
          end
578 537

  
538
        end
539
        puts
540

  
541
    # Now load each wiki page and transform its content into textile format
542
    print "Transform texts to textile format:"
543
    puts
544

  
545
    print "   in Wiki pages..................."
579 546
          wiki.reload
580 547
          wiki.pages.each do |page|
548
            #print '.'
581 549
            page.content.text = convert_wiki_text(page.content.text)
582 550
            Time.fake(page.content.updated_on) { page.content.save }
583 551
          end
584
        end
585
        puts
552
    puts
586 553

  
554
    print "   in Issue descriptions..........."
555
          TICKET_MAP.each do |newId|
556

  
557
            next if newId.nil?
558
            
559
            #print '.'
560
            issue = findIssue(newId)
561
            next if issue.nil?
562

  
563
            issue.description = convert_wiki_text(issue.description)
564
      issue.save            
565
          end
566
    puts
567

  
568
    print "   in Issue journal descriptions..."
569
          TICKET_MAP.each do |newId|
570
            next if newId.nil?
571
            
572
            #print '.'
573
            issue = findIssue(newId)
574
            next if issue.nil?
575
            
576
            issue.journals.find(:all).each do |journal|
577
              #print '.'
578
              journal.notes = convert_wiki_text(journal.notes)
579
              journal.save
580
            end
581
  
582
          end
583
    puts
584

  
585
    print "   in Milestone descriptions......."
586
          milestone_wiki.each do |name|
587
            p = wiki.find_page(name)            
588
            next if p.nil?
589
                  
590
            #print '.'            
591
            p.content.text = convert_wiki_text(p.content.text)
592
            p.content.save
593
    end
594
    puts
595

  
587 596
        puts
588 597
        puts "Components:      #{migrated_components}/#{TracComponent.count}"
589 598
        puts "Milestones:      #{migrated_milestones}/#{TracMilestone.count}"
......
593 602
        puts "Wiki edits:      #{migrated_wiki_edits}/#{wiki_edit_count}"
594 603
        puts "Wiki files:      #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
595 604
      end
605
      
606
      def self.findIssue(id)
607
        
608
        return Issue.find(id)
596 609

  
610
      rescue ActiveRecord::RecordNotFound
611
  puts
612
        print "[#{id}] not found"
613

  
614
        nil      
615
      end
616
      
597 617
      def self.limit_for(klass, attribute)
598 618
        klass.columns_hash[attribute.to_s].limit
599 619
      end
......
746 766
    DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
747 767

  
748 768
    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}
769
    prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
750 770
    unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
751 771
      prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
752 772
      prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
......
756 776
      prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
757 777
    end
758 778
    prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
759
    prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
779
    prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier.downcase}
760 780
    puts
761 781
    
762 782
    # Turn off email notifications
......
764 784
    
765 785
    TracMigrate.migrate
766 786
  end
787

  
788

  
789
  desc 'Subversion migration script'
790
  task :migrate_from_trac_svn => :environment do
791
  
792
    module SvnMigrate 
793
        TICKET_MAP = []
794

  
795
        class Commit
796
          attr_accessor :revision, :message
797
          
798
          def initialize(attributes={})
799
            self.message = attributes[:message] || ""
800
            self.revision = attributes[:revision]
801
          end
802
        end
803
        
804
        class SvnExtendedAdapter < Redmine::Scm::Adapters::SubversionAdapter
805
        
806

  
807

  
808
            def set_message(path=nil, revision=nil, msg=nil)
809
              path ||= ''
810

  
811
              Tempfile.open('msg') do |tempfile|
812

  
813
                # This is a weird thing. We need to cleanup cr/lf so we have uniform line separators              
814
                tempfile.print msg.gsub(/\r\n/,'\n')
815
                tempfile.flush
816

  
817
                filePath = tempfile.path.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
818

  
819
                cmd = "#{SVN_BIN} propset svn:log --quiet --revprop -r #{revision}  -F \"#{filePath}\" "
820
                cmd << credentials_string
821
                cmd << ' ' + target(URI.escape(path))
822

  
823
                shellout(cmd) do |io|
824
                  begin
825
                    loop do 
826
                      line = io.readline
827
                      puts line
828
                    end
829
                  rescue EOFError
830
                  end  
831
                end
832

  
833
                raise if $? && $?.exitstatus != 0
834

  
835
              end
836
              
837
            end
838
        
839
            def messages(path=nil)
840
              path ||= ''
841

  
842
              commits = Array.new
843

  
844
              cmd = "#{SVN_BIN} log --xml -r 1:HEAD"
845
              cmd << credentials_string
846
              cmd << ' ' + target(URI.escape(path))
847
                            
848
              shellout(cmd) do |io|
849
                begin
850
                  doc = REXML::Document.new(io)
851
                  doc.elements.each("log/logentry") do |logentry|
852

  
853
                    commits << Commit.new(
854
                                                {
855
                                                  :revision => logentry.attributes['revision'].to_i,
856
                                                  :message => logentry.elements['msg'].text
857
                                                })
858
                  end
859
                rescue => e
860
                  puts"Error !!!"
861
                  puts e
862
                end
863
              end
864
              return nil if $? && $?.exitstatus != 0
865
              commits
866
            end
867
          
868
        end
869
        
870
        def self.migrate
871

  
872
          project = Project.find(@@redmine_project)
873
          if !project
874
            puts "Could not find project identifier '#{@@redmine_project}'"
875
            raise 
876
          end
877
                    
878
          tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
879
          if !tid
880
            puts "Could not find issue custom field 'TracID'"
881
            raise 
882
          end
883
          
884
          Issue.find( :all, :conditions => { :project_id => project }).each do |issue|
885
            val = nil
886
            issue.custom_values.each do |value|
887
              if value.custom_field.id == tid.id
888
                val = value
889
                break
890
              end
891
            end
892
            
893
            TICKET_MAP[val.value.to_i] = issue.id if !val.nil?            
894
          end
895
          
896
          svn = self.scm          
897
          msgs = svn.messages(@svn_url)
898
          msgs.each do |commit| 
899
          
900
            newText = convert_wiki_text(commit.message)
901
            
902
            if newText != commit.message             
903
              puts "Updating message #{commit.revision}"
904
              scm.set_message(@svn_url, commit.revision, newText)
905
            end
906
          end
907
          
908
          
909
        end
910
        
911
        # Basic wiki syntax conversion
912
        def self.convert_wiki_text(text)
913
          convert_wiki_text_mapping(text, TICKET_MAP )
914
        end
915
        
916
        def self.set_svn_url(url)
917
          @@svn_url = url
918
        end
919

  
920
        def self.set_svn_username(username)
921
          @@svn_username = username
922
        end
923

  
924
        def self.set_svn_password(password)
925
          @@svn_password = password
926
        end
927

  
928
        def self.set_redmine_project_identifier(identifier)
929
          @@redmine_project = identifier
930
        end
931
      
932
        def self.scm
933
          @scm ||= SvnExtendedAdapter.new @@svn_url, @@svn_url, @@svn_username, @@svn_password, 0, "", nil
934
          @scm
935
        end
936
    end
937
    
938
    def prompt(text, options = {}, &block)
939
      default = options[:default] || ''
940
      while true
941
        print "#{text} [#{default}]: "
942
        value = STDIN.gets.chomp!
943
        value = default if value.blank?
944
        break if yield value
945
      end
946
    end
947

  
948
    puts
949
    if Redmine::DefaultData::Loader.no_data?
950
      puts "Redmine configuration need to be loaded before importing data."
951
      puts "Please, run this first:"
952
      puts
953
      puts "  rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
954
      exit
955
    end
956

  
957
    puts "WARNING: all commit messages with references to trac pages will be modified"
958
    print "Are you sure you want to continue ? [y/N] "
959
    break unless STDIN.gets.match(/^y$/i)
960
    puts
961

  
962
    prompt('Subversion repository url') {|repository| SvnMigrate.set_svn_url repository.strip}
963
    prompt('Subversion repository username') {|username| SvnMigrate.set_svn_username username}
964
    prompt('Subversion repository password') {|password| SvnMigrate.set_svn_password password}
965
    prompt('Redmine project identifier') {|identifier| SvnMigrate.set_redmine_project_identifier identifier}
966
    puts
967

  
968
    SvnMigrate.migrate
969
    
970
  end
971

  
972

  
973
  # Basic wiki syntax conversion
974
  def convert_wiki_text_mapping(text, ticket_map = [])
975
        # New line
976
        text = text.gsub(/\[\[[Bb][Rr]\]\]/, "\n") # This has to go before the rules below
977
        # Titles (only h1. to h6., and remove #...)
978
        text = text.gsub(/(?:^|^\ +)(\={1,6})\ (.+)\ (?:\1)(?:\ *(\ \#.*))?/) {|s| "\nh#{$1.length}. #{$2}#{$3}\n"}
979
        
980
        # External Links:
981
        #      [http://example.com/]
982
        text = text.gsub(/\[((?:https?|s?ftp)\:\S+)\]/, '\1')
983
        #      [http://example.com/ Example],[http://example.com/ "Example"]
984
        #      [http://example.com/ "Example for "Example""] -> "Example for 'Example'":http://example.com/
985
        text = text.gsub(/\[((?:https?|s?ftp)\:\S+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":#{$1}"}
986
        #      [mailto:some@example.com],[mailto:"some@example.com"]
987
        text = text.gsub(/\[mailto\:([\"']?)(.+?)\1\]/, '\2')
988
        
989
        # Ticket links:
990
        #      [ticket:234 Text],[ticket:234 This is a test],[ticket:234 "This is a test"]
991
        #      [ticket:234 "Test "with quotes""] -> "Test 'with quotes'":issues/show/234
992
        text = text.gsub(/\[ticket\:(\d+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":/issues/show/#{$1}"}
993
        #      ticket:1234
994
        #      excluding ticket:1234:file.txt (used in macros)
995
        #      #1 - working cause Redmine uses the same syntax.
996
        text = text.gsub(/ticket\:(\d+?)([^\:])/, '#\1\2')
997

  
998
        # Source & attachments links:
999
        #      [source:/trunk/readme.txt Readme File],[source:"/trunk/readme.txt" Readme File],
1000
        #      [source:/trunk/readme.txt],[source:"/trunk/readme.txt"]
1001
        #       The text "Readme File" is not converted,
1002
        #       cause Redmine's wiki does not support this.
1003
        #      Attachments use same syntax.
1004
        text = text.gsub(/\[(source|attachment)\:([\"']?)([^\"']+?)\2(?:\ +(.+?))?\]/, '\1:"\3"')
1005
        #      source:"/trunk/readme.txt"
1006
        #      source:/trunk/readme.txt - working cause Redmine uses the same syntax.
1007
        text = text.gsub(/(source|attachment)\:([\"'])([^\"']+?)\2/, '\1:"\3"')
1008

  
1009
        # Milestone links:
1010
        #      [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)],
1011
        #      [milestone:"0.1.0 Mercury"],milestone:"0.1.0 Mercury"
1012
        #       The text "Milestone 0.1.0 (Mercury)" is not converted,
1013
        #       cause Redmine's wiki does not support this.
1014
        text = text.gsub(/\[milestone\:([\"'])([^\"']+?)\1(?:\ +(.+?))?\]/, 'version:"\2"')
1015
        text = text.gsub(/milestone\:([\"'])([^\"']+?)\1/, 'version:"\2"')
1016
        #      [milestone:0.1.0],milestone:0.1.0
1017
        text = text.gsub(/\[milestone\:([^\ ]+?)\]/, 'version:\1')
1018
        text = text.gsub(/milestone\:([^\ ]+?)/, 'version:\1')
1019

  
1020
        # Internal Links:
1021
        #      ["Some Link"]
1022
        text = text.gsub(/\[([\"'])(.+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
1023
        #      [wiki:"Some Link" "Link description"],[wiki:"Some Link" Link description]
1024
        text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1[\ \t]+([\"']?)(.+?)\3\]/) {|s| "[[#{$2.delete(',./?;|:')}|#{$4}]]"}
1025
        #      [wiki:"Some Link"]
1026
        text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
1027
        #      [wiki:SomeLink]
1028
        text = text.gsub(/\[wiki\:([^\s\]]+?)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
1029
        #      [wiki:SomeLink Link description],[wiki:SomeLink "Link description"]
1030
        text = text.gsub(/\[wiki\:([^\s\]\"']+?)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$3}]]"}
1031

  
1032
        # Links to CamelCase pages (not work for unicode)
1033
        #      UsingJustWikiCaps,UsingJustWikiCaps/Subpage
1034
        text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+(?:\/[^\s[:punct:]]+)*)/) {|s| "#{$1}#{$2}[[#{$3.delete('/')}]]"}
1035
        # Normalize things that were supposed to not be links
1036
        # like !NotALink
1037
        text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
1038

  
1039
        # Revisions links
1040
        text = text.gsub(/\[(\d+)\]/, 'r\1')
1041
        # Ticket number re-writing
1042
        text = text.gsub(/#(\d+)/) do |s|
1043
          if $1.length < 10
1044
#            ticket_map[$1.to_i] ||= $1
1045
            "\##{ticket_map[$1.to_i] || $1}"
1046
          else
1047
            s
1048
          end
1049
        end
1050
        
1051
        # Before convert Code highlighting, need processing inline code
1052
        #      {{{hello world}}}
1053
        text = text.gsub(/\{\{\{(.+?)\}\}\}/, '@\1@')
1054
        
1055
        # We would like to convert the Code highlighting too
1056
        # This will go into the next line.
1057
        shebang_line = false
1058
        # Reguar expression for start of code
1059
        pre_re = /\{\{\{/
1060
        # Code hightlighing...
1061
        shebang_re = /^\#\!([a-z]+)/
1062
        # Regular expression for end of code
1063
        pre_end_re = /\}\}\}/
1064

  
1065
        # Go through the whole text..extract it line by line
1066
        text = text.gsub(/^(.*)$/) do |line|
1067
          m_pre = pre_re.match(line)
1068
          if m_pre
1069
            line = '<pre>'
1070
          else
1071
            m_sl = shebang_re.match(line)
1072
            if m_sl
1073
              shebang_line = true
1074
              line = '<code class="' + m_sl[1] + '">'
1075
            end
1076
            m_pre_end = pre_end_re.match(line)
1077
            if m_pre_end
1078
              line = '</pre>'
1079
              if shebang_line
1080
                line = '</code>' + line
1081
              end
1082
            end
1083
          end
1084
          line
1085
        end
1086

  
1087
        # Highlighting
1088
        text = text.gsub(/'''''([^\s])/, '_*\1')
1089
        text = text.gsub(/([^\s])'''''/, '\1*_')
1090
        text = text.gsub(/'''/, '*')
1091
        text = text.gsub(/''/, '_')
1092
        text = text.gsub(/__/, '+')
1093
        text = text.gsub(/~~/, '-')
1094
        text = text.gsub(/`/, '@')
1095
        text = text.gsub(/,,/, '~')
1096
        # Tables
1097
        text = text.gsub(/\|\|/, '|')
1098
        # Lists:
1099
        #      bullet
1100
        text = text.gsub(/^(\ +)\* /) {|s| '*' * $1.length + " "}
1101
        #      numbered
1102
        text = text.gsub(/^(\ +)\d+\. /) {|s| '#' * $1.length + " "}
1103
        # Images (work for only attached in current page [[Image(picture.gif)]])
1104
        # need rules for:  * [[Image(wiki:WikiFormatting:picture.gif)]] (referring to attachment on another page)
1105
        #                  * [[Image(ticket:1:picture.gif)]] (file attached to a ticket)
1106
        #                  * [[Image(htdocs:picture.gif)]] (referring to a file inside project htdocs)
1107
        #                  * [[Image(source:/trunk/trac/htdocs/trac_logo_mini.png)]] (a file in repository) 
1108
        text = text.gsub(/\[\[image\((.+?)(?:,.+?)?\)\]\]/i, '!\1!')
1109
        # TOC
1110
        text = text.gsub(/\[\[TOC(?:\((.*?)\))?\]\]/m) {|s| "{{>toc}}\n"}
1111
        
1112
        text
1113
  end
767 1114
end
768 1115

  
(6-6/7)