PageRenderTime 325ms CodeModel.GetById 91ms app.highlight 123ms RepoModel.GetById 104ms app.codeStats 0ms

/lib/tasks/migrate_from_trac.rake

https://bitbucket.org/eimajenthat/redmine
Ruby | 772 lines | 719 code | 28 blank | 25 comment | 8 complexity | 52aff35e2d8db95690bee74bcfe3e935 MD5 | raw file
  1# Redmine - project management software
  2# Copyright (C) 2006-2013  Jean-Philippe Lang
  3#
  4# This program is free software; you can redistribute it and/or
  5# modify it under the terms of the GNU General Public License
  6# as published by the Free Software Foundation; either version 2
  7# of the License, or (at your option) any later version.
  8#
  9# This program is distributed in the hope that it will be useful,
 10# but WITHOUT ANY WARRANTY; without even the implied warranty of
 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 12# GNU General Public License for more details.
 13#
 14# You should have received a copy of the GNU General Public License
 15# along with this program; if not, write to the Free Software
 16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 17
 18require 'active_record'
 19require 'iconv'
 20require 'pp'
 21
 22namespace :redmine do
 23  desc 'Trac migration script'
 24  task :migrate_from_trac => :environment do
 25
 26    module TracMigrate
 27        TICKET_MAP = []
 28
 29        DEFAULT_STATUS = IssueStatus.default
 30        assigned_status = IssueStatus.find_by_position(2)
 31        resolved_status = IssueStatus.find_by_position(3)
 32        feedback_status = IssueStatus.find_by_position(4)
 33        closed_status = IssueStatus.where(:is_closed => true).first
 34        STATUS_MAPPING = {'new' => DEFAULT_STATUS,
 35                          'reopened' => feedback_status,
 36                          'assigned' => assigned_status,
 37                          'closed' => closed_status
 38                          }
 39
 40        priorities = IssuePriority.all
 41        DEFAULT_PRIORITY = priorities[0]
 42        PRIORITY_MAPPING = {'lowest' => priorities[0],
 43                            'low' => priorities[0],
 44                            'normal' => priorities[1],
 45                            'high' => priorities[2],
 46                            'highest' => priorities[3],
 47                            # ---
 48                            'trivial' => priorities[0],
 49                            'minor' => priorities[1],
 50                            'major' => priorities[2],
 51                            'critical' => priorities[3],
 52                            'blocker' => priorities[4]
 53                            }
 54
 55        TRACKER_BUG = Tracker.find_by_position(1)
 56        TRACKER_FEATURE = Tracker.find_by_position(2)
 57        DEFAULT_TRACKER = TRACKER_BUG
 58        TRACKER_MAPPING = {'defect' => TRACKER_BUG,
 59                           'enhancement' => TRACKER_FEATURE,
 60                           'task' => TRACKER_FEATURE,
 61                           'patch' =>TRACKER_FEATURE
 62                           }
 63
 64        roles = Role.where(:builtin => 0).order('position ASC').all
 65        manager_role = roles[0]
 66        developer_role = roles[1]
 67        DEFAULT_ROLE = roles.last
 68        ROLE_MAPPING = {'admin' => manager_role,
 69                        'developer' => developer_role
 70                        }
 71
 72      class ::Time
 73        class << self
 74          alias :real_now :now
 75          def now
 76            real_now - @fake_diff.to_i
 77          end
 78          def fake(time)
 79            @fake_diff = real_now - time
 80            res = yield
 81            @fake_diff = 0
 82           res
 83          end
 84        end
 85      end
 86
 87      class TracComponent < ActiveRecord::Base
 88        self.table_name = :component
 89      end
 90
 91      class TracMilestone < ActiveRecord::Base
 92        self.table_name = :milestone
 93        # If this attribute is set a milestone has a defined target timepoint
 94        def due
 95          if read_attribute(:due) && read_attribute(:due) > 0
 96            Time.at(read_attribute(:due)).to_date
 97          else
 98            nil
 99          end
100        end
101        # This is the real timepoint at which the milestone has finished.
102        def completed
103          if read_attribute(:completed) && read_attribute(:completed) > 0
104            Time.at(read_attribute(:completed)).to_date
105          else
106            nil
107          end
108        end
109
110        def description
111          # Attribute is named descr in Trac v0.8.x
112          has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
113        end
114      end
115
116      class TracTicketCustom < ActiveRecord::Base
117        self.table_name = :ticket_custom
118      end
119
120      class TracAttachment < ActiveRecord::Base
121        self.table_name = :attachment
122        set_inheritance_column :none
123
124        def time; Time.at(read_attribute(:time)) end
125
126        def original_filename
127          filename
128        end
129
130        def content_type
131          ''
132        end
133
134        def exist?
135          File.file? trac_fullpath
136        end
137
138        def open
139          File.open("#{trac_fullpath}", 'rb') {|f|
140            @file = f
141            yield self
142          }
143        end
144
145        def read(*args)
146          @file.read(*args)
147        end
148
149        def description
150          read_attribute(:description).to_s.slice(0,255)
151        end
152
153      private
154        def trac_fullpath
155          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}"
158        end
159      end
160
161      class TracTicket < ActiveRecord::Base
162        self.table_name = :ticket
163        set_inheritance_column :none
164
165        # ticket changes: only migrate status changes and comments
166        has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
167        has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
168
169        def attachments
170          TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
171        end
172
173        def ticket_type
174          read_attribute(:type)
175        end
176
177        def summary
178          read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
179        end
180
181        def description
182          read_attribute(:description).blank? ? summary : read_attribute(:description)
183        end
184
185        def time; Time.at(read_attribute(:time)) end
186        def changetime; Time.at(read_attribute(:changetime)) end
187      end
188
189      class TracTicketChange < ActiveRecord::Base
190        self.table_name = :ticket_change
191
192        def self.columns
193          # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
194          super.select {|column| column.name.to_s != 'field'}
195        end
196
197        def time; Time.at(read_attribute(:time)) end
198      end
199
200      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
201                           TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
202                           TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
203                           TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
204                           TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
205                           WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
206                           CamelCase TitleIndex)
207
208      class TracWikiPage < ActiveRecord::Base
209        self.table_name = :wiki
210        set_primary_key :name
211
212        def self.columns
213          # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
214          super.select {|column| column.name.to_s != 'readonly'}
215        end
216
217        def attachments
218          TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
219        end
220
221        def time; Time.at(read_attribute(:time)) end
222      end
223
224      class TracPermission < ActiveRecord::Base
225        self.table_name = :permission
226      end
227
228      class TracSessionAttribute < ActiveRecord::Base
229        self.table_name = :session_attribute
230      end
231
232      def self.find_or_create_user(username, project_member = false)
233        return User.anonymous if username.blank?
234
235        u = User.find_by_login(username)
236        if !u
237          # Create a new user if not found
238          mail = username[0, User::MAIL_LENGTH_LIMIT]
239          if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
240            mail = mail_attr.value
241          end
242          mail = "#{mail}@foo.bar" unless mail.include?("@")
243
244          name = username
245          if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
246            name = name_attr.value
247          end
248          name =~ (/(.*)(\s+\w+)?/)
249          fn = $1.strip
250          ln = ($2 || '-').strip
251
252          u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
253                       :firstname => fn[0, limit_for(User, 'firstname')],
254                       :lastname => ln[0, limit_for(User, 'lastname')]
255
256          u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
257          u.password = 'trac'
258          u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
259          # finally, a default user is used if the new user is not valid
260          u = User.first unless u.save
261        end
262        # Make sure he is a member of the project
263        if project_member && !u.member_of?(@target_project)
264          role = DEFAULT_ROLE
265          if u.admin
266            role = ROLE_MAPPING['admin']
267          elsif TracPermission.find_by_username_and_action(username, 'developer')
268            role = ROLE_MAPPING['developer']
269          end
270          Member.create(:user => u, :project => @target_project, :roles => [role])
271          u.reload
272        end
273        u
274      end
275
276      # Basic wiki syntax conversion
277      def self.convert_wiki_text(text)
278        # Titles
279        text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
280        # External Links
281        text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
282        # Ticket links:
283        #      [ticket:234 Text],[ticket:234 This is a test]
284        text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
285        #      ticket:1234
286        #      #1 is working cause Redmine uses the same syntax.
287        text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
288        # Milestone links:
289        #      [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
290        #      The text "Milestone 0.1.0 (Mercury)" is not converted,
291        #      cause Redmine's wiki does not support this.
292        text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
293        #      [milestone:"0.1.0 Mercury"]
294        text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
295        text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
296        #      milestone:0.1.0
297        text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
298        text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
299        # Internal Links
300        text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
301        text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
302        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
303        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
304        text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
305        text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
306
307  # Links to pages UsingJustWikiCaps
308  text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
309  # Normalize things that were supposed to not be links
310  # like !NotALink
311  text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
312        # Revisions links
313        text = text.gsub(/\[(\d+)\]/, 'r\1')
314        # Ticket number re-writing
315        text = text.gsub(/#(\d+)/) do |s|
316          if $1.length < 10
317#            TICKET_MAP[$1.to_i] ||= $1
318            "\##{TICKET_MAP[$1.to_i] || $1}"
319          else
320            s
321          end
322        end
323        # We would like to convert the Code highlighting too
324        # This will go into the next line.
325        shebang_line = false
326        # Reguar expression for start of code
327        pre_re = /\{\{\{/
328        # Code hightlighing...
329        shebang_re = /^\#\!([a-z]+)/
330        # Regular expression for end of code
331        pre_end_re = /\}\}\}/
332
333        # Go through the whole text..extract it line by line
334        text = text.gsub(/^(.*)$/) do |line|
335          m_pre = pre_re.match(line)
336          if m_pre
337            line = '<pre>'
338          else
339            m_sl = shebang_re.match(line)
340            if m_sl
341              shebang_line = true
342              line = '<code class="' + m_sl[1] + '">'
343            end
344            m_pre_end = pre_end_re.match(line)
345            if m_pre_end
346              line = '</pre>'
347              if shebang_line
348                line = '</code>' + line
349              end
350            end
351          end
352          line
353        end
354
355        # Highlighting
356        text = text.gsub(/'''''([^\s])/, '_*\1')
357        text = text.gsub(/([^\s])'''''/, '\1*_')
358        text = text.gsub(/'''/, '*')
359        text = text.gsub(/''/, '_')
360        text = text.gsub(/__/, '+')
361        text = text.gsub(/~~/, '-')
362        text = text.gsub(/`/, '@')
363        text = text.gsub(/,,/, '~')
364        # Lists
365        text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
366
367        text
368      end
369
370      def self.migrate
371        establish_connection
372
373        # Quick database test
374        TracComponent.count
375
376        migrated_components = 0
377        migrated_milestones = 0
378        migrated_tickets = 0
379        migrated_custom_values = 0
380        migrated_ticket_attachments = 0
381        migrated_wiki_edits = 0
382        migrated_wiki_attachments = 0
383
384        #Wiki system initializing...
385        @target_project.wiki.destroy if @target_project.wiki
386        @target_project.reload
387        wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
388        wiki_edit_count = 0
389
390        # Components
391        print "Migrating components"
392        issues_category_map = {}
393        TracComponent.all.each do |component|
394        print '.'
395        STDOUT.flush
396          c = IssueCategory.new :project => @target_project,
397                                :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
398        next unless c.save
399        issues_category_map[component.name] = c
400        migrated_components += 1
401        end
402        puts
403
404        # Milestones
405        print "Migrating milestones"
406        version_map = {}
407        TracMilestone.all.each do |milestone|
408          print '.'
409          STDOUT.flush
410          # First we try to find the wiki page...
411          p = wiki.find_or_new_page(milestone.name.to_s)
412          p.content = WikiContent.new(:page => p) if p.new_record?
413          p.content.text = milestone.description.to_s
414          p.content.author = find_or_create_user('trac')
415          p.content.comments = 'Milestone'
416          p.save
417
418          v = Version.new :project => @target_project,
419                          :name => encode(milestone.name[0, limit_for(Version, 'name')]),
420                          :description => nil,
421                          :wiki_page_title => milestone.name.to_s,
422                          :effective_date => milestone.completed
423
424          next unless v.save
425          version_map[milestone.name] = v
426          migrated_milestones += 1
427        end
428        puts
429
430        # Custom fields
431        # TODO: read trac.ini instead
432        print "Migrating custom fields"
433        custom_field_map = {}
434        TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
435          print '.'
436          STDOUT.flush
437          # Redmine custom field name
438          field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
439          # Find if the custom already exists in Redmine
440          f = IssueCustomField.find_by_name(field_name)
441          # Or create a new one
442          f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
443                                        :field_format => 'string')
444
445          next if f.new_record?
446          f.trackers = Tracker.all
447          f.projects << @target_project
448          custom_field_map[field.name] = f
449        end
450        puts
451
452        # Trac 'resolution' field as a Redmine custom field
453        r = IssueCustomField.where(:name => "Resolution").first
454        r = IssueCustomField.new(:name => 'Resolution',
455                                 :field_format => 'list',
456                                 :is_filter => true) if r.nil?
457        r.trackers = Tracker.all
458        r.projects << @target_project
459        r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
460        r.save!
461        custom_field_map['resolution'] = r
462
463        # Tickets
464        print "Migrating tickets"
465          TracTicket.find_each(:batch_size => 200) do |ticket|
466          print '.'
467          STDOUT.flush
468          i = Issue.new :project => @target_project,
469                          :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
470                          :description => convert_wiki_text(encode(ticket.description)),
471                          :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
472                          :created_on => ticket.time
473          i.author = find_or_create_user(ticket.reporter)
474          i.category = issues_category_map[ticket.component] unless ticket.component.blank?
475          i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
476          i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
477          i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
478          i.id = ticket.id unless Issue.exists?(ticket.id)
479          next unless Time.fake(ticket.changetime) { i.save }
480          TICKET_MAP[ticket.id] = i.id
481          migrated_tickets += 1
482
483          # Owner
484            unless ticket.owner.blank?
485              i.assigned_to = find_or_create_user(ticket.owner, true)
486              Time.fake(ticket.changetime) { i.save }
487            end
488
489          # Comments and status/resolution changes
490          ticket.ticket_changes.group_by(&:time).each do |time, changeset|
491              status_change = changeset.select {|change| change.field == 'status'}.first
492              resolution_change = changeset.select {|change| change.field == 'resolution'}.first
493              comment_change = changeset.select {|change| change.field == 'comment'}.first
494
495              n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
496                              :created_on => time
497              n.user = find_or_create_user(changeset.first.author)
498              n.journalized = i
499              if status_change &&
500                   STATUS_MAPPING[status_change.oldvalue] &&
501                   STATUS_MAPPING[status_change.newvalue] &&
502                   (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
503                n.details << JournalDetail.new(:property => 'attr',
504                                               :prop_key => 'status_id',
505                                               :old_value => STATUS_MAPPING[status_change.oldvalue].id,
506                                               :value => STATUS_MAPPING[status_change.newvalue].id)
507              end
508              if resolution_change
509                n.details << JournalDetail.new(:property => 'cf',
510                                               :prop_key => custom_field_map['resolution'].id,
511                                               :old_value => resolution_change.oldvalue,
512                                               :value => resolution_change.newvalue)
513              end
514              n.save unless n.details.empty? && n.notes.blank?
515          end
516
517          # Attachments
518          ticket.attachments.each do |attachment|
519            next unless attachment.exist?
520              attachment.open {
521                a = Attachment.new :created_on => attachment.time
522                a.file = attachment
523                a.author = find_or_create_user(attachment.author)
524                a.container = i
525                a.description = attachment.description
526                migrated_ticket_attachments += 1 if a.save
527              }
528          end
529
530          # Custom fields
531          custom_values = ticket.customs.inject({}) do |h, custom|
532            if custom_field = custom_field_map[custom.name]
533              h[custom_field.id] = custom.value
534              migrated_custom_values += 1
535            end
536            h
537          end
538          if custom_field_map['resolution'] && !ticket.resolution.blank?
539            custom_values[custom_field_map['resolution'].id] = ticket.resolution
540          end
541          i.custom_field_values = custom_values
542          i.save_custom_field_values
543        end
544
545        # update issue id sequence if needed (postgresql)
546        Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
547        puts
548
549        # Wiki
550        print "Migrating wiki"
551        if wiki.save
552          TracWikiPage.order('name, version').all.each do |page|
553            # Do not migrate Trac manual wiki pages
554            next if TRAC_WIKI_PAGES.include?(page.name)
555            wiki_edit_count += 1
556            print '.'
557            STDOUT.flush
558            p = wiki.find_or_new_page(page.name)
559            p.content = WikiContent.new(:page => p) if p.new_record?
560            p.content.text = page.text
561            p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
562            p.content.comments = page.comment
563            Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
564
565            next if p.content.new_record?
566            migrated_wiki_edits += 1
567
568            # Attachments
569            page.attachments.each do |attachment|
570              next unless attachment.exist?
571              next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
572              attachment.open {
573                a = Attachment.new :created_on => attachment.time
574                a.file = attachment
575                a.author = find_or_create_user(attachment.author)
576                a.description = attachment.description
577                a.container = p
578                migrated_wiki_attachments += 1 if a.save
579              }
580            end
581          end
582
583          wiki.reload
584          wiki.pages.each do |page|
585            page.content.text = convert_wiki_text(page.content.text)
586            Time.fake(page.content.updated_on) { page.content.save }
587          end
588        end
589        puts
590
591        puts
592        puts "Components:      #{migrated_components}/#{TracComponent.count}"
593        puts "Milestones:      #{migrated_milestones}/#{TracMilestone.count}"
594        puts "Tickets:         #{migrated_tickets}/#{TracTicket.count}"
595        puts "Ticket files:    #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
596        puts "Custom values:   #{migrated_custom_values}/#{TracTicketCustom.count}"
597        puts "Wiki edits:      #{migrated_wiki_edits}/#{wiki_edit_count}"
598        puts "Wiki files:      #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
599      end
600
601      def self.limit_for(klass, attribute)
602        klass.columns_hash[attribute.to_s].limit
603      end
604
605      def self.encoding(charset)
606        @ic = Iconv.new('UTF-8', charset)
607      rescue Iconv::InvalidEncoding
608        puts "Invalid encoding!"
609        return false
610      end
611
612      def self.set_trac_directory(path)
613        @@trac_directory = path
614        raise "This directory doesn't exist!" unless File.directory?(path)
615        raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
616        @@trac_directory
617      rescue Exception => e
618        puts e
619        return false
620      end
621
622      def self.trac_directory
623        @@trac_directory
624      end
625
626      def self.set_trac_adapter(adapter)
627        return false if adapter.blank?
628        raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
629        # If adapter is sqlite or sqlite3, make sure that trac.db exists
630        raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
631        @@trac_adapter = adapter
632      rescue Exception => e
633        puts e
634        return false
635      end
636
637      def self.set_trac_db_host(host)
638        return nil if host.blank?
639        @@trac_db_host = host
640      end
641
642      def self.set_trac_db_port(port)
643        return nil if port.to_i == 0
644        @@trac_db_port = port.to_i
645      end
646
647      def self.set_trac_db_name(name)
648        return nil if name.blank?
649        @@trac_db_name = name
650      end
651
652      def self.set_trac_db_username(username)
653        @@trac_db_username = username
654      end
655
656      def self.set_trac_db_password(password)
657        @@trac_db_password = password
658      end
659
660      def self.set_trac_db_schema(schema)
661        @@trac_db_schema = schema
662      end
663
664      mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
665
666      def self.trac_db_path; "#{trac_directory}/db/trac.db" end
667      def self.trac_attachments_directory; "#{trac_directory}/attachments" end
668
669      def self.target_project_identifier(identifier)
670        project = Project.find_by_identifier(identifier)
671        if !project
672          # create the target project
673          project = Project.new :name => identifier.humanize,
674                                :description => ''
675          project.identifier = identifier
676          puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
677          # enable issues and wiki for the created project
678          project.enabled_module_names = ['issue_tracking', 'wiki']
679        else
680          puts
681          puts "This project already exists in your Redmine database."
682          print "Are you sure you want to append data to this project ? [Y/n] "
683          STDOUT.flush
684          exit if STDIN.gets.match(/^n$/i)
685        end
686        project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
687        project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
688        @target_project = project.new_record? ? nil : project
689        @target_project.reload
690      end
691
692      def self.connection_params
693        if trac_adapter == 'sqlite3'
694          {:adapter => 'sqlite3',
695           :database => trac_db_path}
696        else
697          {:adapter => trac_adapter,
698           :database => trac_db_name,
699           :host => trac_db_host,
700           :port => trac_db_port,
701           :username => trac_db_username,
702           :password => trac_db_password,
703           :schema_search_path => trac_db_schema
704          }
705        end
706      end
707
708      def self.establish_connection
709        constants.each do |const|
710          klass = const_get(const)
711          next unless klass.respond_to? 'establish_connection'
712          klass.establish_connection connection_params
713        end
714      end
715
716    private
717      def self.encode(text)
718        @ic.iconv text
719      rescue
720        text
721      end
722    end
723
724    puts
725    if Redmine::DefaultData::Loader.no_data?
726      puts "Redmine configuration need to be loaded before importing data."
727      puts "Please, run this first:"
728      puts
729      puts "  rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
730      exit
731    end
732
733    puts "WARNING: a new project will be added to Redmine during this process."
734    print "Are you sure you want to continue ? [y/N] "
735    STDOUT.flush
736    break unless STDIN.gets.match(/^y$/i)
737    puts
738
739    def prompt(text, options = {}, &block)
740      default = options[:default] || ''
741      while true
742        print "#{text} [#{default}]: "
743        STDOUT.flush
744        value = STDIN.gets.chomp!
745        value = default if value.blank?
746        break if yield value
747      end
748    end
749
750    DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
751
752    prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
753    prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
754    unless %w(sqlite3).include?(TracMigrate.trac_adapter)
755      prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
756      prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
757      prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
758      prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
759      prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
760      prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
761    end
762    prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
763    prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
764    puts
765
766    # Turn off email notifications
767    Setting.notified_events = []
768
769    TracMigrate.migrate
770  end
771end
772