Project

General

Profile

RE: Trac Importer Patch Coordination ยป migrate_from_trac.rake.Callegaro-Cornu.patch

Changes proposed after patch from Anthony Callegaro (diff-errors in the 2 precedent posts should be fixed) - Gilles Cornu, 2011-07-03 01:50

View differences:

migrate_from_trac.rake 2011-07-02 23:25:13.000000000 +0200
1
# redMine - project management software
1
# Redmine - project management software
2 2
# Copyright (C) 2006-2007  Jean-Philippe Lang
3
# Copyright (C) 2007-2011  Trac/Redmine Community
4
# References:
5
#  - http://www.redmine.org/boards/1/topics/12273 (Trac Importer Patch Coordination)
6
#  - http://github.com/landy2005/Redmine-migrate-from-Trac
3 7
#
4 8
# This program is free software; you can redistribute it and/or
5 9
# modify it under the terms of the GNU General Public License
......
52 56
                            'blocker' => priorities[4]
53 57
                            }
54 58

  
55
        TRACKER_BUG = Tracker.find_by_position(1)
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
59
        TRACKER_BUG = Tracker.find_by_name('Bug')
60
        TRACKER_FEATURE = Tracker.find_by_name('Feature')
61
        TRACKER_SUPPORT = Tracker.find_by_name('Support')
64 62
        DEFAULT_TRACKER = TRACKER_BUG
65 63
        TRACKER_MAPPING = {'defect' => TRACKER_BUG,
66 64
                           'enhancement' => TRACKER_FEATURE,
67
                           'task' => TRACKER_TASK,
65
                           'task' => TRACKER_SUPPORT,
68 66
                           'patch' =>TRACKER_FEATURE
69 67
                           }
70 68

  
......
250 248
        set_table_name :session_attribute
251 249
      end
252 250

  
251
# TODO put your Login Mapping in this method and rename method below
252
#      def self.find_or_create_user(username, project_member = false)
253
#        TRAC_REDMINE_LOGIN_MAP = []
254
#        return TRAC_REDMINE_LOGIN_MAP[username]
255
# OR more hard-coded:
256
#        if username == 'TracX'
257
#          username = 'RedmineX'
258
#        elsif username == 'gilles'
259
#          username = 'gcornu'
260
#        #elseif ...
261
#        else
262
#          username = 'gcornu'
263
#        end
264
#        return User.find_by_login(username)  
265
#      end
266

  
253 267
      def self.find_or_create_user(username, project_member = false)
254 268
        return User.anonymous if username.blank?
255 269

  
......
356 370
                          :name => encode(milestone.name[0, limit_for(Version, 'name')]),
357 371
                          :description => nil,
358 372
                          :wiki_page_title => milestone.name.to_s,
359
                          :effective_date => milestone.completed
373
                          :effective_date => (!milestone.completed.blank? ? milestone.completed : (!milestone.due.blank? ? milestone.due : nil))
360 374

  
361 375
          next unless v.save
362 376
          version_map[milestone.name] = v
......
371 385
        #print "Migrating custom fields"
372 386
        custom_field_map = {}
373 387
        TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
388
# use line below and adapt the WHERE condifiton, if you want to skip some unused custom fields
389
#        TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name} WHERE name NOT IN ('duration', 'software')").each do |field|
374 390
          #print '.' # Maybe not needed this out?
375 391
          #STDOUT.flush
376 392
          # Redmine custom field name
377 393
          field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
394

  
395
#          # Ugly hack to skip custom field 'Browser', which is in 'list' format...
396
#          next if field_name == 'browser'
397

  
378 398
          # Find if the custom already exists in Redmine
379 399
          f = IssueCustomField.find_by_name(field_name)
380 400
          # Ugly hack to handle billable checkbox. Would require to read the ini file to be cleaner
......
394 414
        end
395 415
        #puts
396 416

  
417
#        # Trac custom field 'Browser' field as a Redmine custom field
418
#        b = IssueCustomField.find(:first, :conditions => { :name => "Browser" })
419
#        b = IssueCustomField.new(:name => 'Browser',
420
#                                 :field_format => 'list',
421
#                                 :is_filter => true) if b.nil?
422
#        b.trackers << [TRACKER_BUG, TRACKER_FEATURE, TRACKER_SUPPORT]
423
#        b.projects << @target_project
424
#        b.possible_values = (b.possible_values + %w(IE6 IE7 IE8 IE9 Firefox Chrome Safari Opera)).flatten.compact.uniq
425
#        b.save!
426
#        custom_field_map['browser'] = b
427

  
397 428
        # Trac 'resolution' field as a Redmine custom field
398 429
        r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
399 430
        r = IssueCustomField.new(:name => 'Resolution',
400 431
                                 :field_format => 'list',
401 432
                                 :is_filter => true) if r.nil?
402
        r.trackers = Tracker.find(:all)
433
        r.trackers << [TRACKER_BUG, TRACKER_FEATURE, TRACKER_SUPPORT] 
403 434
        r.projects << @target_project
404 435
        r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
405 436
        r.save!
......
415 446
        k.save!
416 447
        custom_field_map['keywords'] = k
417 448

  
449
        # Trac 'version' field as a Redmine custom field, taking advantage of feature #2096 (available since Redmine 1.2.0)
450
        v = IssueCustomField.find(:first, :conditions => { :name => "Found in Version" })
451
        v = IssueCustomField.new(:name => 'Found in Version',
452
                                 :field_format => 'version',
453
                                 :is_filter => true) if v.nil?
454
        # Only apply to BUG tracker (?)
455
        v.trackers << TRACKER_BUG
456
        #v.trackers << [TRACKER_BUG, TRACKER_FEATURE]
457

  
458
        # Affect custom field to current Project
459
        v.projects << @target_project
460

  
461
        v.save!
462
        custom_field_map['found_in_version'] = v
463

  
418 464
        # Trac ticket id as a Redmine custom field
419 465
        tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
420 466
        tid = IssueCustomField.new(:name => 'TracID',
421 467
                                 :field_format => 'string',
422 468
                                 :is_filter => true) if tid.nil?
423
        tid.trackers = Tracker.find(:all)
469
        tid.trackers << [TRACKER_BUG, TRACKER_FEATURE, TRACKER_SUPPORT] 
424 470
        tid.projects << @target_project
425 471
        tid.save!
426 472
        custom_field_map['tracid'] = tid
......
442 488
          i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
443 489
          i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
444 490
          i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
445
          i.id = ticket.id unless Issue.exists?(ticket.id)
491
          # Use the Redmine-genereated new ticket ID anyway (no Ticket ID recycling)
492
          #i.id = ticket.id unless Issue.exists?(ticket.id)
446 493
          next unless Time.fake(ticket.changetime) { i.save }
447 494
          TICKET_MAP[ticket.id] = i.id
448 495
          migrated_tickets += 1
......
453 500
              Time.fake(ticket.changetime) { i.save }
454 501
            end
455 502
          # 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
503
   # Feature disabled (CC field almost never used, No time to validate/test this recent improvments from A. Callegaro)
504
   #       ticket.cc.split(',').each do |email|
505
   #         w = Watcher.new :watchable_type => 'Issue',
506
   #                         :watchable_id => i.id,
507
   #                         :user_id => find_or_create_user(email.strip).id 
508
   #         w.save
509
   #       end
462 510

  
463 511
          # Necessary to handle direct link to note from timelogs and putting the right start time in issue
464 512
          noteid = 1
......
611 659
          if custom_field_map['tracid'] 
612 660
            custom_values[custom_field_map['tracid'].id] = ticket.id
613 661
          end
662

  
663
          if !ticket.version.blank? && custom_field_map['found_in_version']
664
            found_in = version_map[ticket.version]
665
            if !found_in.nil?
666
              puts "Issue #{i.id} found in #{found_in.name.to_s} (#{found_in.id.to_s}) - trac: #{ticket.version}"
667
            else
668
              #TODO: add better error management here...
669
              puts "Issue #{i.id} : ouch...  - trac: #{ticket.version}"  
670
            end 
671
            custom_values[custom_field_map['found_in_version'].id] = found_in.id.to_s
672
            STDOUT.flush
673
          end
674

  
614 675
          i.custom_field_values = custom_values
615 676
          i.save_custom_field_values
616 677
        end
......
677 738
        puts if wiki_pages_count < wiki_pages_total
678 739
        
679 740
        who = "   in Issues"
680
        issues_total = TICKET_MAP.count
741
        #issues_total = TICKET_MAP.length #works with Ruby <= 1.8.6
742
        issues_total = TICKET_MAP.count #works with Ruby >= 1.8.7
681 743
        TICKET_MAP.each do |newId|
682 744
          issues_count += 1
683 745
          simplebar(who, issues_count, issues_total)
......
697 759
        puts if issues_count < issues_total
698 760

  
699 761
        who = "   in Milestone descriptions"
700
        milestone_wiki_total = milestone_wiki.count
762
        #milestone_wiki_total = milestone_wiki.length #works with Ruby <= 1.8.6
763
        milestone_wiki_total = milestone_wiki.count #works with Ruby >= 1.8.7
701 764
        milestone_wiki.each do |name|
702 765
          milestone_wiki_count += 1
703 766
          simplebar(who, milestone_wiki_count, milestone_wiki_total)
......
802 865
          project.identifier = identifier
803 866
          puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
804 867
          # enable issues and wiki for the created project
805
          # Enable all project modules by default
806
          project.enabled_module_names = ['issue_tracking', 'wiki', 'time_tracking', 'news', 'documents', 'files', 'repository', 'boards', 'calendar', 'gantt']
868
          # Enable only a minimal set of modules by default
869
          project.enabled_module_names = ['issue_tracking', 'wiki']
807 870
        else
808 871
          puts
809 872
          puts "This project already exists in your Redmine database."
......
813 876
        end
814 877
        project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
815 878
        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)
879
        project.trackers << TRACKER_SUPPORT unless project.trackers.include?(TRACKER_SUPPORT)
818 880
        @target_project = project.new_record? ? nil : project
819 881
        @target_project.reload
820 882
      end
......
890 952

  
891 953

  
892 954
  desc 'Subversion migration script'
893
  task :migrate_from_trac_svn => :environment do
955
  task :migrate_svn_commit_properties => :environment do
894 956

  
895 957
    require 'redmine/scm/adapters/abstract_adapter'
896 958
    require 'redmine/scm/adapters/subversion_adapter'
......
902 964
        TICKET_MAP = []
903 965

  
904 966
        class Commit
905
          attr_accessor :revision, :message
967
          attr_accessor :revision, :message, :author
906 968
          
907 969
          def initialize(attributes={})
970
            self.author = attributes[:author] || ""
908 971
            self.message = attributes[:message] || ""
909 972
            self.revision = attributes[:revision]
910 973
          end
911 974
        end
912 975
        
913 976
        class SvnExtendedAdapter < Redmine::Scm::Adapters::SubversionAdapter
977
        
978
            def set_author(path=nil, revision=nil, author=nil)
979
              path ||= ''
980

  
981
              cmd = "#{SVN_BIN} propset svn:author --quiet --revprop -r #{revision}  \"#{author}\" "
982
              cmd << credentials_string
983
              cmd << ' ' + target(URI.escape(path))
984

  
985
              shellout(cmd) do |io|
986
                begin
987
                  loop do 
988
                    line = io.readline
989
                    puts line
990
                  end
991
                rescue EOFError
992
                end  
993
              end
994

  
995
              raise if $? && $?.exitstatus != 0
996

  
997
            end
914 998

  
915 999
            def set_message(path=nil, revision=nil, msg=nil)
916 1000
              path ||= ''
......
960 1044
                    commits << Commit.new(
961 1045
                                                {
962 1046
                                                  :revision => logentry.attributes['revision'].to_i,
963
                                                  :message => logentry.elements['msg'].text
1047
                                                  :message => logentry.elements['msg'].text,
1048
                                                  :author => logentry.elements['author'].text
964 1049
                                                })
965 1050
                  end
966 1051
                rescue => e
......
974 1059
          
975 1060
        end
976 1061
        
977
        def self.migrate
1062
        def self.migrate_authors
1063
          svn = self.scm          
1064
          commits = svn.messages(@svn_url)
1065
          commits.each do |commit| 
1066
            orig_author_name = commit.author
1067
            new_author_name = orig_author_name
1068
            
1069
            # TODO put your Trac/SVN/Redmine username mapping here:
1070
            if (commit.author == 'TracX')
1071
               new_author_name = 'RedmineY'
1072
            elsif (commit.author == 'gilles')
1073
               new_author_name = 'gcornu'
1074
            #elsif (commit.author == 'seco')
1075
            #...
1076
            else
1077
               new_author_name = 'RedmineY'
1078
            end
1079
            
1080
            if (new_author_name != orig_author_name)
1081
              scm.set_author(@svn_url, commit.revision, new_author_name)
1082
              puts "r#{commit.revision} - Author replaced: #{orig_author_name} -> #{new_author_name}"
1083
            else
1084
              puts "r#{commit.revision} - Author kept: #{orig_author_name} unchanged "
1085
            end
1086
          end
1087
        end
1088
        
1089
        def self.migrate_messages
978 1090

  
979 1091
          project = Project.find(@@redmine_project)
980 1092
          if !project
......
1008 1120
            
1009 1121
            if newText != commit.message             
1010 1122
              puts "Updating message #{commit.revision}"
1123
              
1124
              # Marcel Nadje enhancement, see http://www.redmine.org/issues/2748#note-3
1125
              # Hint: enable charset conversion if needed...
1126
              #newText = Iconv.conv('CP1252', 'UTF-8', newText)
1127
              
1011 1128
              scm.set_message(@svn_url, commit.revision, newText)
1012 1129
            end
1013 1130
          end
......
1037 1154
        end
1038 1155
      
1039 1156
        def self.scm
1040
          # Bugfix, with redmine 1.0.1 (Debian's) it wasn't working anymore
1157
          # Thomas Recloux fix, see http://www.redmine.org/issues/2748#note-1
1158
          # The constructor of the SvnExtendedAdapter has ony got four parameters, 
1159
          # => parameters 5,6 and 7 removed
1041 1160
          @scm ||= SvnExtendedAdapter.new @@svn_url, @@svn_url, @@svn_username, @@svn_password
1161
          #@scm ||= SvnExtendedAdapter.new @@svn_url, @@svn_url, @@svn_username, @@svn_password, 0, "", nil
1042 1162
          @scm
1043 1163
        end
1044 1164
    end
1045 1165

  
1046 1166
    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 1167
    prompt('Subversion repository url') {|repository| SvnMigrate.set_svn_url repository.strip}
1061 1168
    prompt('Subversion repository username') {|username| SvnMigrate.set_svn_username username}
1062 1169
    prompt('Subversion repository password') {|password| SvnMigrate.set_svn_password password}
1063
    prompt('Redmine project identifier') {|identifier| SvnMigrate.set_redmine_project_identifier identifier}
1064 1170
    puts
1171
        
1172
    author_migration_enabled = unsafe_prompt('1) Start Migration of SVN Commit Authors (y,n)?', {:default => 'n'}) == 'y'
1173
    puts
1174
    if author_migration_enabled
1175
      puts "WARNING: Some (maybe all) commit authors will be replaced"
1176
      print "Are you sure you want to continue ? [y/N] "
1177
      break unless STDIN.gets.match(/^y$/i)
1178
      
1179
      SvnMigrate.migrate_authors
1180
    end
1065 1181

  
1066
    SvnMigrate.migrate
1067
    
1182
    message_migration_enabled = unsafe_prompt('2) Start Migration of SVN Commit Messages (y,n)?', {:default => 'n'}) == 'y'
1183
    puts
1184
    if message_migration_enabled
1185
      if Redmine::DefaultData::Loader.no_data?
1186
        puts "Redmine configuration need to be loaded before importing data."
1187
        puts "Please, run this first:"
1188
        puts
1189
        puts "  rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
1190
        exit
1191
      end
1192
  
1193
      puts "WARNING: all commit messages with references to trac pages will be modified"
1194
      print "Are you sure you want to continue ? [y/N] "
1195
      break unless STDIN.gets.match(/^y$/i)
1196
      puts
1197
  
1198
      prompt('Redmine project identifier') {|identifier| SvnMigrate.set_redmine_project_identifier identifier}
1199
      puts
1200
  
1201
      SvnMigrate.migrate_messages
1202
    end
1068 1203
  end
1069 1204

  
1070 1205
  # Prompt
......
1079 1214
    end
1080 1215
  end
1081 1216

  
1217
  # Sorry, I had troubles to intagrate 'prompt' and quickly went this way...
1218
  def unsafe_prompt(text, options = {})
1219
    default = options[:default] || ''
1220
    print "#{text} [#{default}]: "
1221
    value = STDIN.gets.chomp!
1222
    value = default if value.blank?
1223
    value
1224
  end
1225

  
1082 1226
  # Basic wiki syntax conversion
1083 1227
  def convert_wiki_text_mapping(text, ticket_map = [])
1084 1228
        # Hide links
......
1241 1385
        text = text.gsub(/''/, '_')
1242 1386
        text = text.gsub(/__/, '+')
1243 1387
        text = text.gsub(/~~/, '-')
1388
        text = text.gsub(/`/, '@')
1244 1389
        text = text.gsub(/,,/, '~')
1245 1390
        # Tables
1246 1391
        text = text.gsub(/\|\|/, '|')
......
1258 1403
        # TOC (is right-aligned, because that in Trac)
1259 1404
        text = text.gsub(/\[\[TOC(?:\((.*?)\))?\]\]/m) {|s| "{{>toc}}\n"}
1260 1405

  
1406
        # Thomas Recloux enhancements, see http://www.redmine.org/issues/2748#note-1
1407
        # Redmine needs a space between keywords "refs,ref,fix" and the issue number (#1234) in subversion commit messages.
1408
        # TODO: rewrite it in a more regex-style way
1409

  
1410
        text = text.gsub("refs#", "refs #")
1411
        text = text.gsub("Refs#", "refs #")
1412
        text = text.gsub("REFS#", "refs #")
1413
        text = text.gsub("ref#", "refs #")
1414
        text = text.gsub("Ref#", "refs #")
1415
        text = text.gsub("REF#", "refs #")
1416

  
1417
        text = text.gsub("fix#", "fixes #")
1418
        text = text.gsub("Fix#", "fixes #")
1419
        text = text.gsub("FIX#", "fixes #")
1420
        text = text.gsub("fixes#", "fixes #")
1421
        text = text.gsub("Fixes#", "fixes #")
1422
        text = text.gsub("FIXES#", "fixes #")
1423
        
1261 1424
        # Restore and convert code blocks
1262 1425
        text = code_convert(text)
1263 1426

  
    (1-1/1)