--- /Users/gilles/Documents/Home/pocafeina/open source/redmine.org/migrate_from_trac.rake.Callegaro 2011-07-02 12:07:49.000000000 +0200 +++ migrate_from_trac.rake 2011-07-02 23:25:13.000000000 +0200 @@ -1,5 +1,9 @@ -# redMine - project management software +# Redmine - project management software # Copyright (C) 2006-2007 Jean-Philippe Lang +# Copyright (C) 2007-2011 Trac/Redmine Community +# References: +# - http://www.redmine.org/boards/1/topics/12273 (Trac Importer Patch Coordination) +# - http://github.com/landy2005/Redmine-migrate-from-Trac # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -52,19 +56,13 @@ 'blocker' => priorities[4] } - TRACKER_BUG = Tracker.find_by_position(1) - TRACKER_FEATURE = Tracker.find_by_position(2) - # Add a fourth issue type for tasks as we use them heavily - t = Tracker.find_by_name('Task') - if !t - t = Tracker.create(:name => 'Task', :is_in_chlog => true, :is_in_roadmap => false, :position => 4) - t.workflows.copy(Tracker.find(1)) - end - TRACKER_TASK = t + TRACKER_BUG = Tracker.find_by_name('Bug') + TRACKER_FEATURE = Tracker.find_by_name('Feature') + TRACKER_SUPPORT = Tracker.find_by_name('Support') DEFAULT_TRACKER = TRACKER_BUG TRACKER_MAPPING = {'defect' => TRACKER_BUG, 'enhancement' => TRACKER_FEATURE, - 'task' => TRACKER_TASK, + 'task' => TRACKER_SUPPORT, 'patch' =>TRACKER_FEATURE } @@ -250,6 +248,22 @@ set_table_name :session_attribute end +# TODO put your Login Mapping in this method and rename method below +# def self.find_or_create_user(username, project_member = false) +# TRAC_REDMINE_LOGIN_MAP = [] +# return TRAC_REDMINE_LOGIN_MAP[username] +# OR more hard-coded: +# if username == 'TracX' +# username = 'RedmineX' +# elsif username == 'gilles' +# username = 'gcornu' +# #elseif ... +# else +# username = 'gcornu' +# end +# return User.find_by_login(username) +# end + def self.find_or_create_user(username, project_member = false) return User.anonymous if username.blank? @@ -356,7 +370,7 @@ :name => encode(milestone.name[0, limit_for(Version, 'name')]), :description => nil, :wiki_page_title => milestone.name.to_s, - :effective_date => milestone.completed + :effective_date => (!milestone.completed.blank? ? milestone.completed : (!milestone.due.blank? ? milestone.due : nil)) next unless v.save version_map[milestone.name] = v @@ -371,10 +385,16 @@ #print "Migrating custom fields" custom_field_map = {} TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field| +# use line below and adapt the WHERE condifiton, if you want to skip some unused custom fields +# TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name} WHERE name NOT IN ('duration', 'software')").each do |field| #print '.' # Maybe not needed this out? #STDOUT.flush # Redmine custom field name field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize + +# # Ugly hack to skip custom field 'Browser', which is in 'list' format... +# next if field_name == 'browser' + # Find if the custom already exists in Redmine f = IssueCustomField.find_by_name(field_name) # Ugly hack to handle billable checkbox. Would require to read the ini file to be cleaner @@ -394,12 +414,23 @@ end #puts +# # Trac custom field 'Browser' field as a Redmine custom field +# b = IssueCustomField.find(:first, :conditions => { :name => "Browser" }) +# b = IssueCustomField.new(:name => 'Browser', +# :field_format => 'list', +# :is_filter => true) if b.nil? +# b.trackers << [TRACKER_BUG, TRACKER_FEATURE, TRACKER_SUPPORT] +# b.projects << @target_project +# b.possible_values = (b.possible_values + %w(IE6 IE7 IE8 IE9 Firefox Chrome Safari Opera)).flatten.compact.uniq +# b.save! +# custom_field_map['browser'] = b + # Trac 'resolution' field as a Redmine custom field r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" }) r = IssueCustomField.new(:name => 'Resolution', :field_format => 'list', :is_filter => true) if r.nil? - r.trackers = Tracker.find(:all) + r.trackers << [TRACKER_BUG, TRACKER_FEATURE, TRACKER_SUPPORT] r.projects << @target_project r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq r.save! @@ -415,12 +446,27 @@ k.save! custom_field_map['keywords'] = k + # Trac 'version' field as a Redmine custom field, taking advantage of feature #2096 (available since Redmine 1.2.0) + v = IssueCustomField.find(:first, :conditions => { :name => "Found in Version" }) + v = IssueCustomField.new(:name => 'Found in Version', + :field_format => 'version', + :is_filter => true) if v.nil? + # Only apply to BUG tracker (?) + v.trackers << TRACKER_BUG + #v.trackers << [TRACKER_BUG, TRACKER_FEATURE] + + # Affect custom field to current Project + v.projects << @target_project + + v.save! + custom_field_map['found_in_version'] = v + # Trac ticket id as a Redmine custom field tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" }) tid = IssueCustomField.new(:name => 'TracID', :field_format => 'string', :is_filter => true) if tid.nil? - tid.trackers = Tracker.find(:all) + tid.trackers << [TRACKER_BUG, TRACKER_FEATURE, TRACKER_SUPPORT] tid.projects << @target_project tid.save! custom_field_map['tracid'] = tid @@ -442,7 +488,8 @@ i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank? i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER - i.id = ticket.id unless Issue.exists?(ticket.id) + # Use the Redmine-genereated new ticket ID anyway (no Ticket ID recycling) + #i.id = ticket.id unless Issue.exists?(ticket.id) next unless Time.fake(ticket.changetime) { i.save } TICKET_MAP[ticket.id] = i.id migrated_tickets += 1 @@ -453,12 +500,13 @@ Time.fake(ticket.changetime) { i.save } end # Handle CC field - ticket.cc.split(',').each do |email| - w = Watcher.new :watchable_type => 'Issue', - :watchable_id => i.id, - :user_id => find_or_create_user(email.strip).id - w.save - end + # Feature disabled (CC field almost never used, No time to validate/test this recent improvments from A. Callegaro) + # ticket.cc.split(',').each do |email| + # w = Watcher.new :watchable_type => 'Issue', + # :watchable_id => i.id, + # :user_id => find_or_create_user(email.strip).id + # w.save + # end # Necessary to handle direct link to note from timelogs and putting the right start time in issue noteid = 1 @@ -611,6 +659,19 @@ if custom_field_map['tracid'] custom_values[custom_field_map['tracid'].id] = ticket.id end + + if !ticket.version.blank? && custom_field_map['found_in_version'] + found_in = version_map[ticket.version] + if !found_in.nil? + puts "Issue #{i.id} found in #{found_in.name.to_s} (#{found_in.id.to_s}) - trac: #{ticket.version}" + else + #TODO: add better error management here... + puts "Issue #{i.id} : ouch... - trac: #{ticket.version}" + end + custom_values[custom_field_map['found_in_version'].id] = found_in.id.to_s + STDOUT.flush + end + i.custom_field_values = custom_values i.save_custom_field_values end @@ -677,7 +738,8 @@ puts if wiki_pages_count < wiki_pages_total who = " in Issues" - issues_total = TICKET_MAP.count + #issues_total = TICKET_MAP.length #works with Ruby <= 1.8.6 + issues_total = TICKET_MAP.count #works with Ruby >= 1.8.7 TICKET_MAP.each do |newId| issues_count += 1 simplebar(who, issues_count, issues_total) @@ -697,7 +759,8 @@ puts if issues_count < issues_total who = " in Milestone descriptions" - milestone_wiki_total = milestone_wiki.count + #milestone_wiki_total = milestone_wiki.length #works with Ruby <= 1.8.6 + milestone_wiki_total = milestone_wiki.count #works with Ruby >= 1.8.7 milestone_wiki.each do |name| milestone_wiki_count += 1 simplebar(who, milestone_wiki_count, milestone_wiki_total) @@ -802,8 +865,8 @@ project.identifier = identifier puts "Unable to create a project with identifier '#{identifier}'!" unless project.save # enable issues and wiki for the created project - # Enable all project modules by default - project.enabled_module_names = ['issue_tracking', 'wiki', 'time_tracking', 'news', 'documents', 'files', 'repository', 'boards', 'calendar', 'gantt'] + # Enable only a minimal set of modules by default + project.enabled_module_names = ['issue_tracking', 'wiki'] else puts puts "This project already exists in your Redmine database." @@ -813,8 +876,7 @@ end project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG) project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE) - # Add Task type to the project - project.trackers << TRACKER_TASK unless project.trackers.include?(TRACKER_TASK) + project.trackers << TRACKER_SUPPORT unless project.trackers.include?(TRACKER_SUPPORT) @target_project = project.new_record? ? nil : project @target_project.reload end @@ -890,7 +952,7 @@ desc 'Subversion migration script' - task :migrate_from_trac_svn => :environment do + task :migrate_svn_commit_properties => :environment do require 'redmine/scm/adapters/abstract_adapter' require 'redmine/scm/adapters/subversion_adapter' @@ -902,15 +964,37 @@ TICKET_MAP = [] class Commit - attr_accessor :revision, :message + attr_accessor :revision, :message, :author def initialize(attributes={}) + self.author = attributes[:author] || "" self.message = attributes[:message] || "" self.revision = attributes[:revision] end end class SvnExtendedAdapter < Redmine::Scm::Adapters::SubversionAdapter + + def set_author(path=nil, revision=nil, author=nil) + path ||= '' + + cmd = "#{SVN_BIN} propset svn:author --quiet --revprop -r #{revision} \"#{author}\" " + cmd << credentials_string + cmd << ' ' + target(URI.escape(path)) + + shellout(cmd) do |io| + begin + loop do + line = io.readline + puts line + end + rescue EOFError + end + end + + raise if $? && $?.exitstatus != 0 + + end def set_message(path=nil, revision=nil, msg=nil) path ||= '' @@ -960,7 +1044,8 @@ commits << Commit.new( { :revision => logentry.attributes['revision'].to_i, - :message => logentry.elements['msg'].text + :message => logentry.elements['msg'].text, + :author => logentry.elements['author'].text }) end rescue => e @@ -974,7 +1059,34 @@ end - def self.migrate + def self.migrate_authors + svn = self.scm + commits = svn.messages(@svn_url) + commits.each do |commit| + orig_author_name = commit.author + new_author_name = orig_author_name + + # TODO put your Trac/SVN/Redmine username mapping here: + if (commit.author == 'TracX') + new_author_name = 'RedmineY' + elsif (commit.author == 'gilles') + new_author_name = 'gcornu' + #elsif (commit.author == 'seco') + #... + else + new_author_name = 'RedmineY' + end + + if (new_author_name != orig_author_name) + scm.set_author(@svn_url, commit.revision, new_author_name) + puts "r#{commit.revision} - Author replaced: #{orig_author_name} -> #{new_author_name}" + else + puts "r#{commit.revision} - Author kept: #{orig_author_name} unchanged " + end + end + end + + def self.migrate_messages project = Project.find(@@redmine_project) if !project @@ -1008,6 +1120,11 @@ if newText != commit.message puts "Updating message #{commit.revision}" + + # Marcel Nadje enhancement, see http://www.redmine.org/issues/2748#note-3 + # Hint: enable charset conversion if needed... + #newText = Iconv.conv('CP1252', 'UTF-8', newText) + scm.set_message(@svn_url, commit.revision, newText) end end @@ -1037,34 +1154,52 @@ end def self.scm - # Bugfix, with redmine 1.0.1 (Debian's) it wasn't working anymore + # Thomas Recloux fix, see http://www.redmine.org/issues/2748#note-1 + # The constructor of the SvnExtendedAdapter has ony got four parameters, + # => parameters 5,6 and 7 removed @scm ||= SvnExtendedAdapter.new @@svn_url, @@svn_url, @@svn_username, @@svn_password + #@scm ||= SvnExtendedAdapter.new @@svn_url, @@svn_url, @@svn_username, @@svn_password, 0, "", nil @scm end end puts - if Redmine::DefaultData::Loader.no_data? - puts "Redmine configuration need to be loaded before importing data." - puts "Please, run this first:" - puts - puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\"" - exit - end - - puts "WARNING: all commit messages with references to trac pages will be modified" - print "Are you sure you want to continue ? [y/N] " - break unless STDIN.gets.match(/^y$/i) - puts - prompt('Subversion repository url') {|repository| SvnMigrate.set_svn_url repository.strip} prompt('Subversion repository username') {|username| SvnMigrate.set_svn_username username} prompt('Subversion repository password') {|password| SvnMigrate.set_svn_password password} - prompt('Redmine project identifier') {|identifier| SvnMigrate.set_redmine_project_identifier identifier} puts + + author_migration_enabled = unsafe_prompt('1) Start Migration of SVN Commit Authors (y,n)?', {:default => 'n'}) == 'y' + puts + if author_migration_enabled + puts "WARNING: Some (maybe all) commit authors will be replaced" + print "Are you sure you want to continue ? [y/N] " + break unless STDIN.gets.match(/^y$/i) + + SvnMigrate.migrate_authors + end - SvnMigrate.migrate - + message_migration_enabled = unsafe_prompt('2) Start Migration of SVN Commit Messages (y,n)?', {:default => 'n'}) == 'y' + puts + if message_migration_enabled + if Redmine::DefaultData::Loader.no_data? + puts "Redmine configuration need to be loaded before importing data." + puts "Please, run this first:" + puts + puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\"" + exit + end + + puts "WARNING: all commit messages with references to trac pages will be modified" + print "Are you sure you want to continue ? [y/N] " + break unless STDIN.gets.match(/^y$/i) + puts + + prompt('Redmine project identifier') {|identifier| SvnMigrate.set_redmine_project_identifier identifier} + puts + + SvnMigrate.migrate_messages + end end # Prompt @@ -1079,6 +1214,15 @@ end end + # Sorry, I had troubles to intagrate 'prompt' and quickly went this way... + def unsafe_prompt(text, options = {}) + default = options[:default] || '' + print "#{text} [#{default}]: " + value = STDIN.gets.chomp! + value = default if value.blank? + value + end + # Basic wiki syntax conversion def convert_wiki_text_mapping(text, ticket_map = []) # Hide links @@ -1241,6 +1385,7 @@ text = text.gsub(/''/, '_') text = text.gsub(/__/, '+') text = text.gsub(/~~/, '-') + text = text.gsub(/`/, '@') text = text.gsub(/,,/, '~') # Tables text = text.gsub(/\|\|/, '|') @@ -1258,6 +1403,24 @@ # TOC (is right-aligned, because that in Trac) text = text.gsub(/\[\[TOC(?:\((.*?)\))?\]\]/m) {|s| "{{>toc}}\n"} + # Thomas Recloux enhancements, see http://www.redmine.org/issues/2748#note-1 + # Redmine needs a space between keywords "refs,ref,fix" and the issue number (#1234) in subversion commit messages. + # TODO: rewrite it in a more regex-style way + + text = text.gsub("refs#", "refs #") + text = text.gsub("Refs#", "refs #") + text = text.gsub("REFS#", "refs #") + text = text.gsub("ref#", "refs #") + text = text.gsub("Ref#", "refs #") + text = text.gsub("REF#", "refs #") + + text = text.gsub("fix#", "fixes #") + text = text.gsub("Fix#", "fixes #") + text = text.gsub("FIX#", "fixes #") + text = text.gsub("fixes#", "fixes #") + text = text.gsub("Fixes#", "fixes #") + text = text.gsub("FIXES#", "fixes #") + # Restore and convert code blocks text = code_convert(text)