--- migrate_from_trac.rake
+++ migrate_from_trac12.rake
@@ -1,25 +1,36 @@
-#-- encoding: UTF-8
-#-- copyright
-# ChiliProject is a project management system.
-#
-# Copyright (C) 2010-2012 the ChiliProject Team
+# redMine - project management software
+# Copyright (C) 2006-2007  Jean-Philippe Lang
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
 # as published by the Free Software Foundation; either version 2
 # of the License, or (at your option) any later version.
 #
-# See doc/COPYRIGHT.rdoc for more details.
-#++
-
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 require 'active_record'
 require 'iconv'
 require 'pp'
 
+
+## Default presets
+$DEFAULT_TRAC_DIRECTORY=ENV['TRAC_ENV']
+$DEFAULT_PROJECT_IDENTIFIER=ENV['TRAC_ENV']).downcase!
+
+# part of the email after the '@'
+$DEFAULT_MAIL_SUFFIX='foo.bar'
+
+
 namespace :redmine do
   desc 'Trac migration script'
-  task :migrate_from_trac => :environment do
+  task :migrate_from_trac12 => :environment do
 
     module TracMigrate
         TICKET_MAP = []
@@ -52,10 +63,17 @@
 
         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
         DEFAULT_TRACKER = TRACKER_BUG
         TRACKER_MAPPING = {'defect' => TRACKER_BUG,
                            'enhancement' => TRACKER_FEATURE,
-                           'task' => TRACKER_FEATURE,
+                           'task' => TRACKER_TASK,
                            'patch' =>TRACKER_FEATURE
                            }
 
@@ -66,10 +84,30 @@
         ROLE_MAPPING = {'admin' => manager_role,
                         'developer' => developer_role
                         }
+        # Add an Hash Table for comments' updatable fields
+        PROP_MAPPING = {'status' => 'status_id',
+                        'owner' => 'assigned_to_id',
+                        'component' => 'category_id',
+                        'milestone' => 'fixed_version_id',
+                        'priority' => 'priority_id',
+                        'summary' => 'subject',
+                        'type' => 'tracker_id'}
+        
+        # Hash table to map completion ratio
+        RATIO_MAPPING = {'' => 0,
+                        'fixed' => 100,
+                        'invalid' => 0,
+                        'wontfix' => 0,
+                        'duplicate' => 100,
+                        'worksforme' => 0}
+
+        @migrate_accounts = false
+        @migrate_accounts_valid_email = true
 
       class ::Time
         class << self
           alias :real_now :now
+          alias :old_at_method :at
           def now
             real_now - @fake_diff.to_i
           end
@@ -78,6 +116,9 @@
             res = yield
             @fake_diff = 0
            res
+          end
+          def at(t)
+            old_at_method(t>1e6? t*1e-6 : t)
           end
         end
       end
@@ -151,8 +192,9 @@
       private
         def trac_fullpath
           attachment_type = read_attribute(:type)
-          trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
-          "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
+          trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*]/n ) {|x| sprintf('%%%02X', x[0]) }
+          trac_dir = id.gsub( /[^a-zA-Z0-9\-_\.!~*\\\/]/n ) {|x| sprintf('%%%02X', x[0]) }
+          "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{trac_dir}/#{trac_file}"
         end
       end
 
@@ -161,12 +203,12 @@
         set_inheritance_column :none
 
         # ticket changes: only migrate status changes and comments
-        has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
-        has_many :attachments, :class_name => "TracAttachment",
+        has_many :changes, :class_name => "TracMigrate::TracTicketChange", :foreign_key => :ticket
+        has_many :attachments, :class_name => "TracMigrate::TracAttachment",
                                :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
                                               " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
                                               ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\''
-        has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
+        has_many :customs, :class_name => "TracMigrate::TracTicketCustom", :foreign_key => :ticket
 
         def ticket_type
           read_attribute(:type)
@@ -187,22 +229,25 @@
       class TracTicketChange < ActiveRecord::Base
         set_table_name :ticket_change
 
-        def time; Time.at(read_attribute(:time)) end
-      end
-
-      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
+        def time
+        	Time.at(read_attribute(:time)) 
+        end
+      end
+
+      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup \
+                           TracBrowser TracCgi TracChangeset TracInstallPlatforms TracMultipleProjects TracModWSGI \
                            TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
                            TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
                            TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
                            TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
                            WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
-                           CamelCase TitleIndex)
-
+                           CamelCase TitleIndex TracNavigation TracFineGrainedPermissions TracWorkflow TimingAndEstimationPluginUserManual \
+                           PageTemplates BadContent TracRepositoryAdmin TracWikiMacros)
       class TracWikiPage < ActiveRecord::Base
         set_table_name :wiki
         set_primary_key :name
 
-        has_many :attachments, :class_name => "TracAttachment",
+        has_many :attachments, :class_name => "TracMigrate::TracAttachment",
                                :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
                                       " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
                                       ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\''
@@ -215,6 +260,12 @@
         def time; Time.at(read_attribute(:time)) end
       end
 
+      class TracAccount < ActiveRecord::Base
+        set_table_name :session
+        named_scope :authenticated, {:conditions => {:authenticated => 1}}
+
+      end
+
       class TracPermission < ActiveRecord::Base
         set_table_name :permission
       end
@@ -225,140 +276,61 @@
 
       def self.find_or_create_user(username, project_member = false)
         return User.anonymous if username.blank?
-
-        u = User.find_by_login(username)
-        if !u
+        return User.anonymous if [
+          'admin', 'anonymous', 'asas', 'remote_user', 'somebody', 'trac', 'foo',
+        ].include? username.downcase
+        
+        ## HACK clean username - if username is an email use first part of the address
+        username = username.split("@")[0] if username.include?("@")
+
+        _u = User.find_by_login(username)
+        if !_u
           # Create a new user if not found
           mail = username[0,limit_for(User, 'mail')]
           if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
             mail = mail_attr.value
-          end
-          mail = "#{mail}@foo.bar" unless mail.include?("@")
+          elsif @migrate_accounts_valid_email
+            return User.find(:first)
+          end
+          mail = "#{mail}@#{$DEFAULT_MAIL_SUFFIX}" unless mail.include?("@")
 
           name = username
           if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
             name = name_attr.value
           end
-          name =~ (/(.*)(\s+\w+)?/)
+          name =~ (/(.+?)(?:[\ \t]+(.+)?|[\ \t]+|)$/)
           fn = $1.strip
+          # Add a dash for lastname or the user is not saved (bugfix)
           ln = ($2 || '-').strip
 
-          u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
+          _u = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option,
+                       :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
                        :firstname => fn[0, limit_for(User, 'firstname')],
-                       :lastname => ln[0, limit_for(User, 'lastname')]
-
-          u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
-          u.password = 'trac'
-          u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
+                       :lastname => ln[0, limit_for(User, 'lastname')])
+
+          _u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
+          _u.password = 'tractrac'
+          _u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
           # finally, a default user is used if the new user is not valid
-          u = User.find(:first) unless u.save
+          _u = User.find(:first) unless _u.save
         end
         # Make sure he is a member of the project
-        if project_member && !u.member_of?(@target_project)
+        if project_member && !_u.member_of?(@target_project)
           role = DEFAULT_ROLE
-          if u.admin
+          if _u.admin
             role = ROLE_MAPPING['admin']
           elsif TracPermission.find_by_username_and_action(username, 'developer')
             role = ROLE_MAPPING['developer']
           end
-          Member.create(:user => u, :project => @target_project, :roles => [role])
-          u.reload
-        end
-        u
+          Member.create(:user => _u, :project => @target_project, :roles => [role])
+          _u.reload
+        end
+        _u
       end
 
       # Basic wiki syntax conversion
       def self.convert_wiki_text(text)
-        # Titles
-        text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
-        # External Links
-        text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
-        # Ticket links:
-        #      [ticket:234 Text],[ticket:234 This is a test]
-        text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
-        #      ticket:1234
-        #      #1 is working cause Redmine uses the same syntax.
-        text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
-        # Milestone links:
-        #      [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
-        #      The text "Milestone 0.1.0 (Mercury)" is not converted,
-        #      cause Redmine's wiki does not support this.
-        text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
-        #      [milestone:"0.1.0 Mercury"]
-        text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
-        text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
-        #      milestone:0.1.0
-        text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
-        text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
-        # Internal Links
-        text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
-        text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
-        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
-        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
-        text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
-        text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
-
-  # Links to pages UsingJustWikiCaps
-  text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
-  # Normalize things that were supposed to not be links
-  # like !NotALink
-  text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
-        # Revisions links
-        text = text.gsub(/\[(\d+)\]/, 'r\1')
-        # Ticket number re-writing
-        text = text.gsub(/#(\d+)/) do |s|
-          if $1.length < 10
-#            TICKET_MAP[$1.to_i] ||= $1
-            "\##{TICKET_MAP[$1.to_i] || $1}"
-          else
-            s
-          end
-        end
-        # We would like to convert the Code highlighting too
-        # This will go into the next line.
-        shebang_line = false
-        # Reguar expression for start of code
-        pre_re = /\{\{\{/
-        # Code hightlighing...
-        shebang_re = /^\#\!([a-z]+)/
-        # Regular expression for end of code
-        pre_end_re = /\}\}\}/
-
-        # Go through the whole text..extract it line by line
-        text = text.gsub(/^(.*)$/) do |line|
-          m_pre = pre_re.match(line)
-          if m_pre
-            line = '<pre>'
-          else
-            m_sl = shebang_re.match(line)
-            if m_sl
-              shebang_line = true
-              line = '<code class="' + m_sl[1] + '">'
-            end
-            m_pre_end = pre_end_re.match(line)
-            if m_pre_end
-              line = '</pre>'
-              if shebang_line
-                line = '</code>' + line
-              end
-            end
-          end
-          line
-        end
-
-        # Highlighting
-        text = text.gsub(/'''''([^\s])/, '_*\1')
-        text = text.gsub(/([^\s])'''''/, '\1*_')
-        text = text.gsub(/'''/, '*')
-        text = text.gsub(/''/, '_')
-        text = text.gsub(/__/, '+')
-        text = text.gsub(/~~/, '-')
-        text = text.gsub(/`/, '@')
-        text = text.gsub(/,,/, '~')
-        # Lists
-        text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
-
-        text
+        convert_wiki_text_mapping(text, TICKET_MAP)
       end
 
       def self.migrate
@@ -367,6 +339,7 @@
         # Quick database test
         TracComponent.count
 
+        migrated_accounts = 0
         migrated_components = 0
         migrated_milestones = 0
         migrated_tickets = 0
@@ -375,39 +348,67 @@
         migrated_wiki_edits = 0
         migrated_wiki_attachments = 0
 
-        #Wiki system initializing...
+        # Wiki system initializing...
         @target_project.wiki.destroy if @target_project.wiki
         @target_project.reload
         wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
         wiki_edit_count = 0
 
+        # Accounts
+        # SELECT sid FROM session WHERE authenticated=1;
+        # SELECT value FROM session_attribute WHERE sid='s0undt3ch' AND name='email';
+        if @migrate_accounts
+          who = "Migrating Accounts"
+          accounts_total = TracAccount.authenticated.find(:all).count
+          TracAccount.authenticated.find(:all).each do |account|
+            if account.authenticated == 1
+              a = find_or_create_user(account.sid, true)
+              next unless a.save!
+              migrated_accounts += 1
+              simplebar(who, migrated_accounts, accounts_total)
+            end
+          end
+        end
+
+        # After this point set migrate_accounts_valid_email to false so that
+        # tickets still have a issuer even though they don't have a valid
+        # email address
+        TracMigrate.set_migrate_accounts_valid_email(false)
+
         # Components
-        print "Migrating components"
+        who = "Migrating components"
         issues_category_map = {}
+        components_total = TracComponent.count
         TracComponent.find(:all).each do |component|
-        print '.'
-        STDOUT.flush
-          c = IssueCategory.new :project => @target_project,
+          	_c = IssueCategory.new :project => @target_project,
                                 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
-        next unless c.save
-        issues_category_map[component.name] = c
-        migrated_components += 1
-        end
-        puts
+			# Owner
+			unless component.owner.blank?
+			  _c.assigned_to = find_or_create_user(component.owner, true)
+			end
+			next unless _c.save
+			issues_category_map[component.name] = _c
+			migrated_components += 1
+			simplebar(who, migrated_components, components_total)
+        end
+        puts if migrated_components < components_total
 
         # Milestones
-        print "Migrating milestones"
+        who = "Migrating milestones"
         version_map = {}
+        milestone_wiki = Array.new
+        milestones_total = TracMilestone.count
         TracMilestone.find(:all).each do |milestone|
-          print '.'
-          STDOUT.flush
           # First we try to find the wiki page...
           p = wiki.find_or_new_page(milestone.name.to_s)
           p.content = WikiContent.new(:page => p) if p.new_record?
-          p.content.text = milestone.description.to_s
+          # be sure that milestone text is not empty
+          p.content.text = milestone.description.nil? || milestone.description.empty? ? milestone.name.to_s : milestone.description.to_s
           p.content.author = find_or_create_user('trac')
           p.content.comments = 'Milestone'
-          p.save
+
+          status_p_save = p.save
+          puts ".status_p_save: " + status_p_save unless status_p_save
 
           v = Version.new :project => @target_project,
                           :name => encode(milestone.name[0, limit_for(Version, 'name')]),
@@ -417,36 +418,45 @@
 
           next unless v.save
           version_map[milestone.name] = v
+          milestone_wiki.push(milestone.name);
           migrated_milestones += 1
-        end
-        puts
+          simplebar(who, migrated_milestones, milestones_total)
+        end
+        puts if migrated_milestones < milestones_total
 
         # Custom fields
         # TODO: read trac.ini instead
-        print "Migrating custom fields"
+        who = "Migrating custom fields"
         custom_field_map = {}
         TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
-          print '.'
-          STDOUT.flush
+          #print '.' # Maybe not needed this out?
+          #STDOUT.flush
           # Redmine custom field name
           field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
           # 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
+          if field_name == 'Billable'
+            format = 'bool'
+          else
+            format = 'string'
+          end
           # Or create a new one
           f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
-                                        :field_format => 'string')
+                                        :field_format => format, :default_value => '')
 
           next if f.new_record?
           f.trackers = Tracker.find(:all)
           f.projects << @target_project
           custom_field_map[field.name] = f
         end
-        puts
+        #puts
 
         # Trac 'resolution' field as a Redmine custom field
         r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
         r = IssueCustomField.new(:name => 'Resolution',
                                  :field_format => 'list',
+                                 :default_value => '',
                                  :is_filter => true) if r.nil?
         r.trackers = Tracker.find(:all)
         r.projects << @target_project
@@ -454,59 +464,241 @@
         r.save!
         custom_field_map['resolution'] = r
 
+        # Trac 'keywords' field as a Redmine custom field
+        k = IssueCustomField.find(:first, :conditions => { :name => "Keywords" })
+        k = IssueCustomField.new(:name => 'Keywords',
+                                 :field_format => 'string',
+                                 :default_value => '',
+                                 :is_filter => true) if k.nil?
+        k.trackers = Tracker.find(:all)
+        k.projects << @target_project
+        k.save!
+        custom_field_map['keywords'] = k
+
+        # Trac ticket id as a Redmine custom field
+        tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
+        tid = IssueCustomField.new(:name => 'TracID',
+                                 :field_format => 'string',
+                                 :default_value => '',
+                                 :is_filter => true) if tid.nil?
+        tid.trackers = Tracker.find(:all)
+        tid.projects << @target_project
+        tid.save!
+        custom_field_map['tracid'] = tid
+  
         # Tickets
-        print "Migrating tickets"
-          TracTicket.find_each(:batch_size => 200) do |ticket|
-          print '.'
-          STDOUT.flush
-          i = Issue.new :project => @target_project,
+        who = "Migrating tickets"
+        tickets_total = TracTicket.count
+        TracTicket.find_each(:batch_size => 200) do |ticket|
+          #puts ".Ticket.time: " + ticket.time.inspect
+          begin
+            ticket_desc = encode(ticket.description)
+          rescue Exception=>e
+            ticket_desc = Iconv.conv("UTF8", "LATIN1", ticket.description)
+          end
+          _i = Issue.new :project => @target_project,
                           :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
-                          :description => convert_wiki_text(encode(ticket.description)),
+                          :description => ticket_desc,
                           :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
                           :created_on => ticket.time
-          i.author = find_or_create_user(ticket.reporter)
-          i.category = issues_category_map[ticket.component] unless ticket.component.blank?
-          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)
-          next unless Time.fake(ticket.changetime) { i.save }
-          TICKET_MAP[ticket.id] = i.id
+
+          # Add the ticket's author to project's reporter list (bugfix)
+          _i.author 	= find_or_create_user(ticket.reporter,true)
+          # Extrapolate done_ratio from ticket's resolution
+          _i.done_ratio = RATIO_MAPPING[ticket.resolution] || 0
+          _i.category 	= issues_category_map[ticket.component] unless ticket.component.blank?
+          _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)
+          
+          # Owner
+          unless ticket.owner.blank?
+            _i.assigned_to = find_or_create_user(ticket.owner, true)
+          end
+          
+          Time.fake(ticket.changetime) { _i.save }
+          
+          TICKET_MAP[ticket.id] = _i.id
           migrated_tickets += 1
-
-          # Owner
-            unless ticket.owner.blank?
-              i.assigned_to = find_or_create_user(ticket.owner, true)
-              Time.fake(ticket.changetime) { i.save }
-            end
-
-          # Comments and status/resolution changes
+          simplebar(who, migrated_tickets, tickets_total)
+          
+          
+          # 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
+            status_w_save = w.save
+          end
+
+          # Necessary to handle direct link to note from timelogs and putting the right start time in issue
+          noteid = 1
+          # Comments and status/resolution/keywords changes
           ticket.changes.group_by(&:time).each do |time, changeset|
-              status_change = changeset.select {|change| change.field == 'status'}.first
+              status_change 	= changeset.select {|change| change.field == 'status'}.first
               resolution_change = changeset.select {|change| change.field == 'resolution'}.first
-              comment_change = changeset.select {|change| change.field == 'comment'}.first
-
-              n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
-                              :created_on => time
-              n.user = find_or_create_user(changeset.first.author)
-              n.journalized = i
+              keywords_change 	= changeset.select {|change| change.field == 'keywords'}.first
+              comment_change 	= changeset.select {|change| change.field == 'comment'}.first
+              # Handle more ticket changes (owner, component, milestone, priority, summary, type, done_ratio and hours)
+              assigned_change 	= changeset.select {|change| change.field == 'owner'}.first
+              category_change 	= changeset.select {|change| change.field == 'component'}.first
+              version_change 	= changeset.select {|change| change.field == 'milestone'}.first
+              priority_change 	= changeset.select {|change| change.field == 'priority'}.first
+              subject_change 	= changeset.select {|change| change.field == 'summary'}.first
+              tracker_change 	= changeset.select {|change| change.field == 'type'}.first
+              time_change 		= changeset.select {|change| change.field == 'hours'}.first
+
+              # If it's the first note then we set the start working time to handle calendar and gantts
+              if noteid == 1
+                _i.start_date = time
+              end
+
+              n = IssueJournal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
+                              :created_at => time,
+                              :version => _i.last_journal.version + 1
+			  
+              n.user = find_or_create_user(changeset.first.author, true)
+              n.journalized = _i
+			  
               if status_change &&
                    STATUS_MAPPING[status_change.oldvalue] &&
                    STATUS_MAPPING[status_change.newvalue] &&
                    (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
-                n.details << JournalDetail.new(:property => 'attr',
-                                               :prop_key => 'status_id',
-                                               :old_value => STATUS_MAPPING[status_change.oldvalue].id,
-                                               :value => STATUS_MAPPING[status_change.newvalue].id)
+                n.changes = n.changes.merge({
+                               'status_id' => [
+                                               STATUS_MAPPING[status_change.oldvalue].id,
+                                               STATUS_MAPPING[status_change.newvalue].id
+                                               ]
+                })
               end
-              if resolution_change
-                n.details << JournalDetail.new(:property => 'cf',
-                                               :prop_key => custom_field_map['resolution'].id,
-                                               :old_value => resolution_change.oldvalue,
-                                               :value => resolution_change.newvalue)
+              
+              # Handle resolution changes
+              if resolution_change              	
+				## BUG not working
+                # n.changes = n.changes.merge({
+                               # custom_field_map['resolution'].id => [
+                                               # resolution_change.oldvalue,
+                                               # resolution_change.newvalue
+                               # ]
+                # })
+
+                # Add a change for the done_ratio
+				n.changes = n.changes.merge({
+								'done_ratio' => [
+												RATIO_MAPPING[resolution_change.oldvalue],
+												RATIO_MAPPING[resolution_change.newvalue]
+												]
+				})
+                
+				# Arbitrary set the due time to the day the ticket was resolved for calendar and gantts
+                case RATIO_MAPPING[resolution_change.newvalue]
+                when 0
+                  _i.due_date = nil
+                when 100
+                  _i.due_date = time
+                end
               end
-              n.save unless n.details.empty? && n.notes.blank?
-          end
+              
+              # Handle keyword changes
+              if keywords_change
+                n.changes = n.changes.merge({
+                				custom_field_map['keywords'].id => [
+                								keywords_change.oldvalue,
+                								keywords_change.newvalue
+                				]
+                })
+              end
+              
+              # Handle assignement/owner changes
+              if assigned_change
+                n.changes = n.changes.merge({
+                 				PROP_MAPPING['owner'] => [
+                							find_or_create_user(assigned_change.oldvalue, true).id,
+                							find_or_create_user(assigned_change.newvalue, true).id
+                				]
+                })
+              end
+              
+              # Handle component/category changes
+              if category_change
+                n.changes = n.changes.merge({
+                				PROP_MAPPING['component'] => [
+                							issues_category_map[category_change.oldvalue].nil? ? nil : issues_category_map[category_change.oldvalue].id,
+                							issues_category_map[category_change.newvalue].nil? ? nil : issues_category_map[category_change.newvalue].id
+                				]
+                })
+              end
+              
+              # Handle version/milestone changes
+              if version_change
+                n.changes = n.changes.merge({
+                				PROP_MAPPING['milestone'] => [
+                							version_map[version_change.oldvalue].nil? ? nil : version_map[version_change.oldvalue].id,
+                							version_map[version_change.newvalue].nil? ? nil : version_map[version_change.newvalue].id
+                				]
+				})
+              end
+              
+              # Handle priority changes
+              if priority_change
+                n.changes = n.changes.merge({
+                				PROP_MAPPING['priority'] => [
+                							PRIORITY_MAPPING[priority_change.oldvalue].id,
+                							PRIORITY_MAPPING[priority_change.newvalue].id
+                				]
+                })
+              end
+              
+              # Handle subject/summary changes
+              if subject_change
+                n.changes = n.changes.merge({
+                				PROP_MAPPING['summary'] => [
+                							encode(subject_change.oldvalue[0, limit_for(Issue, 'subject')]),
+                							encode(subject_change.newvalue[0, limit_for(Issue, 'subject')])
+                				]
+                })
+              end
+              
+              # Handle tracker/type (bug, feature) changes
+              if tracker_change
+                n.changes = n.changes.merge({
+                				PROP_MAPPING['type'] => [
+                							TRACKER_MAPPING[tracker_change.oldvalue].id || DEFAULT_TRACKER.id,
+                							TRACKER_MAPPING[tracker_change.newvalue].id || DEFAULT_TRACKER.id
+                				]
+                })
+              end
+
+              # Add timelog entries for each time changes (from timeandestimation plugin)
+              if time_change && time_change.newvalue != '0' && time_change.newvalue != ''
+                t = TimeEntry.new(:project => @target_project, 
+                                  :issue => _i, 
+                                  :user => n.user,
+                                  :spent_on => time,
+                                  :hours => time_change.newvalue.to_i.abs,		## HACK, hours has to be positiv
+                                  :created_on => time,
+                                  :updated_on => time,
+                                  :activity_id => TimeEntryActivity.find_by_position(2).id,
+                                  :comments => "#{convert_wiki_text(n.notes.each_line.first.chomp)[0,100] unless !n.notes.each_line.first}... \"more\":/issues/#{_i.id}#note-#{noteid}")
+                t.save
+                t.errors.each_full{|msg| puts msg }
+              end
+			  
+			  # saving changes and registering them for the issue
+              n.save unless n.changes.empty? && n.notes.blank?
+			  _i.journals << n unless n.changes.empty? && n.notes.blank?
+			  
+			  # Set correct changetime of the issue
+              ## HACK Unnecessary next unless Time.fake(ticket.changetime) { _i.save }
+              
+              noteid += 1
+          end
+          
+          # saving issue changes to database
+          Time.fake(ticket.changetime) { _i.save }
+		  
+		  #end
 
           # Attachments
           ticket.attachments.each do |attachment|
@@ -515,13 +707,18 @@
                 a = Attachment.new :created_on => attachment.time
                 a.file = attachment
                 a.author = find_or_create_user(attachment.author)
-                a.container = i
+                a.container = _i
                 a.description = attachment.description
+                
+                ## TODO DUMMY HACK be sure, that the attachment exists on the disk - copy real files afterwards
+                ## TODO get attachment file directory from project settings
+                result = system( "touch files/" + a.disk_filename )
+                
                 migrated_ticket_attachments += 1 if a.save
               }
           end
 
-          # Custom fields
+          # Custom fields          
           custom_values = ticket.customs.inject({}) do |h, custom|
             if custom_field = custom_field_map[custom.name]
               h[custom_field.id] = custom.value
@@ -532,32 +729,41 @@
           if custom_field_map['resolution'] && !ticket.resolution.blank?
             custom_values[custom_field_map['resolution'].id] = ticket.resolution
           end
-          i.custom_field_values = custom_values
-          i.save_custom_field_values
+          if custom_field_map['keywords'] && !ticket.keywords.blank?
+            custom_values[custom_field_map['keywords'].id] = ticket.keywords
+          end
+          if custom_field_map['tracid'] 
+            custom_values[custom_field_map['tracid'].id] = ticket.id
+          end
+          _i.custom_field_values = custom_values
+          _i.save_custom_field_values
+          
         end
 
         # update issue id sequence if needed (postgresql)
         Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
-        puts
+        puts if migrated_tickets < tickets_total
 
         # Wiki
-        print "Migrating wiki"
+        who = "Migrating wiki"
         if wiki.save
+          wiki_edits_total = TracWikiPage.count
           TracWikiPage.find(:all, :order => 'name, version').each do |page|
             # Do not migrate Trac manual wiki pages
-            next if TRAC_WIKI_PAGES.include?(page.name)
-            wiki_edit_count += 1
-            print '.'
-            STDOUT.flush
+            if TRAC_WIKI_PAGES.include?(page.name) then
+              wiki_edits_total -= 1
+              next
+            end
             p = wiki.find_or_new_page(page.name)
             p.content = WikiContent.new(:page => p) if p.new_record?
             p.content.text = page.text
             p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
             p.content.comments = page.comment
             Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
+            migrated_wiki_edits += 1
+            simplebar(who, migrated_wiki_edits, wiki_edits_total)
 
             next if p.content.new_record?
-            migrated_wiki_edits += 1
 
             # Attachments
             page.attachments.each do |attachment|
@@ -574,24 +780,81 @@
             end
           end
 
-          wiki.reload
-          wiki.pages.each do |page|
-            page.content.text = convert_wiki_text(page.content.text)
-            Time.fake(page.content.updated_on) { page.content.save }
-          end
-        end
+        end
+        puts if migrated_wiki_edits < wiki_edits_total
+
+        # Now load each wiki page and transform its content into textile format
+        puts "\nTransform texts to textile format:"
+        
+        wiki_pages_count = 0
+        issues_count = 0
+        milestone_wiki_count = 0
+
+        who = "   in Wiki pages"
+        wiki.reload
+        wiki_pages_total = wiki.pages.count
+        wiki.pages.each do |page|
+          page.content.text = convert_wiki_text(page.content.text)
+          Time.fake(page.content.updated_on) { page.content.save }
+          wiki_pages_count += 1
+          simplebar(who, wiki_pages_count, wiki_pages_total)
+        end
+        puts if wiki_pages_count < wiki_pages_total
+        
+	
+        who = "   in Issues"
+        issues_total = TICKET_MAP.count
+        TICKET_MAP.each do |newId|
+          issues_count += 1
+          simplebar(who, issues_count, issues_total)
+          next if newId.nil?
+          issue = findIssue(newId)
+          next if issue.nil?
+          
+          # convert issue description
+          issue.description = convert_wiki_text(issue.description)
+          
+          # Converted issue comments had their last updated time set to the day of the migration (bugfix)
+          next unless Time.fake(issue.updated_on) { issue.save }
+          
+          ## TODO CHECKEN
+          # convert issue journals
+          #issue.journals.find(:all).each do |journal|
+          #  journal.notes = convert_wiki_text(journal.notes)
+          #  journal.save
+          #end
+        end
+        puts if issues_count < issues_total
+
+        who = "   in Milestone descriptions"
+        milestone_wiki_total = milestone_wiki.count
+        milestone_wiki.each do |name|
+          milestone_wiki_count += 1
+          simplebar(who, milestone_wiki_count, milestone_wiki_total)
+          p = wiki.find_page(name)            
+          next if p.nil?
+          p.content.text = convert_wiki_text(p.content.text)
+          p.content.save
+        end
+        puts if milestone_wiki_count < milestone_wiki_total
+
         puts
-
-        puts
-        puts "Components:      #{migrated_components}/#{TracComponent.count}"
-        puts "Milestones:      #{migrated_milestones}/#{TracMilestone.count}"
-        puts "Tickets:         #{migrated_tickets}/#{TracTicket.count}"
+        puts "Components:      #{migrated_components}/#{components_total}"
+        puts "Milestones:      #{migrated_milestones}/#{milestones_total}"
+        puts "Tickets:         #{migrated_tickets}/#{tickets_total}"
         puts "Ticket files:    #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
         puts "Custom values:   #{migrated_custom_values}/#{TracTicketCustom.count}"
-        puts "Wiki edits:      #{migrated_wiki_edits}/#{wiki_edit_count}"
+        puts "Wiki edits:      #{migrated_wiki_edits}/#{wiki_edits_total}"
         puts "Wiki files:      #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
       end
-
+      
+      def self.findIssue(id)
+        return Issue.find(id)
+      rescue ActiveRecord::RecordNotFound
+        puts "[#{id}] not found"
+        nil
+      end
+      
       def self.limit_for(klass, attribute)
         klass.columns_hash[attribute.to_s].limit
       end
@@ -601,6 +864,16 @@
       rescue Iconv::InvalidEncoding
         puts "Invalid encoding!"
         return false
+      end
+
+      def self.set_migrate_accounts(migrate)
+        @migrate_accounts = migrate
+        @migrate_accounts
+      end
+
+      def self.set_migrate_accounts_valid_email(valid_email)
+        @migrate_accounts_valid_email = valid_email
+        @migrate_accounts_valid_email
       end
 
       def self.set_trac_directory(path)
@@ -669,7 +942,10 @@
           project.identifier = identifier
           puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
           # enable issues and wiki for the created project
-          project.enabled_module_names = ['issue_tracking', 'wiki']
+          # Enable all project modules by default
+          #project.enabled_module_names = ['issue_tracking', 'wiki', 'time_tracking', 'news', 'documents', 'files', 'repository', 'boards', 'calendar', 'gantt']
+          # Only enable modules which might have content from the migrated project
+          project.enabled_module_names = ['issue_tracking', 'wiki', 'repository']
         else
           puts
           puts "This project already exists in your Redmine database."
@@ -679,6 +955,8 @@
         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)
         @target_project = project.new_record? ? nil : project
         @target_project.reload
       end
@@ -715,6 +993,7 @@
       end
     end
 
+    
     puts
     if Redmine::DefaultData::Loader.no_data?
       puts "Redmine configuration need to be loaded before importing data."
@@ -730,21 +1009,11 @@
     break unless STDIN.gets.match(/^y$/i)
     puts
 
-    def prompt(text, options = {}, &block)
-      default = options[:default] || ''
-      while true
-        print "#{text} [#{default}]: "
-        STDOUT.flush
-        value = STDIN.gets.chomp!
-        value = default if value.blank?
-        break if yield value
-      end
-    end
-
     DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
 
-    prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
-    prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
+	# Get Trac configuration and migration settings
+	prompt('Trac directory', :default => $DEFAULT_TRAC_DIRECTORY) {|directory| TracMigrate.set_trac_directory directory.strip}
+    prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
     unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
       prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
       prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
@@ -754,13 +1023,250 @@
       prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
     end
     prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
-    prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
+	prompt('Target project identifier', :default => $DEFAULT_PROJECT_IDENTIFIER) {|identifier| TracMigrate.target_project_identifier identifier.downcase}
+    
+	# Ask for user account migration
+    print "Migrate ALL authenticated accounts [y/N]? "
+    STDOUT.flush
+    migrate_accounts = STDIN.gets.match(/^y$/i)
+    TracMigrate.set_migrate_accounts(migrate_accounts)
+
+    if migrate_accounts
+      print "Require authenticated accounts to have an email address set [Y/n]? "
+      STDOUT.flush
+      TracMigrate.set_migrate_accounts_valid_email(STDIN.gets.match(/^y$/i))
+    end
     puts
-
+    
     # Turn off email notifications
     Setting.notified_events = []
-
+    
     TracMigrate.migrate
   end
+
+
+  # Prompt
+  def prompt(text, options = {}, &block)
+    default = options[:default] || ''
+    while true
+      print "#{text} [#{default}]: "
+      STDOUT.flush
+      value = STDIN.gets.chomp!
+      value = default if value.blank?
+      break if yield value
+    end
+  end
+
+  # Basic wiki syntax conversion
+  def convert_wiki_text_mapping(text, ticket_map = [])
+        # Hide links
+        def wiki_links_hide(src)
+          @wiki_links = []
+          @wiki_links_hash = "####WIKILINKS#{src.hash.to_s}####"
+          src.gsub(/(\[\[.+?\|.+?\]\])/) do
+            @wiki_links << $1
+            @wiki_links_hash
+          end
+        end
+        # Restore links
+        def wiki_links_restore(src)
+          @wiki_links.each do |s|
+            src = src.sub("#{@wiki_links_hash}", s.to_s)
+          end
+          src
+        end
+        # Hidding code blocks
+        def code_hide(src)
+          @code = []
+          @code_hash = "####CODEBLOCK#{src.hash.to_s}####"
+          src.gsub(/(\{\{\{.+?\}\}\}|`.+?`)/m) do
+            @code << $1
+            @code_hash
+          end
+        end
+        # Convert code blocks
+        def code_convert(src)
+          @code.each do |s|
+            s = s.to_s
+            if s =~ (/`(.+?)`/m) || s =~ (/\{\{\{(.+?)\}\}\}/) then
+              # inline code
+              s = s.replace("@#{$1}@")
+            else
+              # We would like to convert the Code highlighting too
+              # This will go into the next line.
+              shebang_line = false
+              # Reguar expression for start of code
+              pre_re = /\{\{\{/
+              # Code hightlighing...
+              shebang_re = /^\#\!([a-z]+)/
+              # Regular expression for end of code
+              pre_end_re = /\}\}\}/
+      
+              # Go through the whole text..extract it line by line
+              s = s.gsub(/^(.*)$/) do |line|
+                m_pre = pre_re.match(line)
+                if m_pre
+                  line = '<pre>'
+                else
+                  m_sl = shebang_re.match(line)
+                  if m_sl
+                    shebang_line = true
+                    line = '<code class="' + m_sl[1] + '">'
+                  end
+                  m_pre_end = pre_end_re.match(line)
+                  if m_pre_end
+                    line = '</pre>'
+                    if shebang_line
+                      line = '</code>' + line
+                    end
+                  end
+                end
+                line
+              end
+            end
+            src = src.sub("#{@code_hash}", s)
+          end
+          src
+        end
+
+        # Hide code blocks
+        text = code_hide(text)
+        # New line
+        text = text.gsub(/\[\[[Bb][Rr]\]\]/, "\n") # This has to go before the rules below
+        # Titles (only h1. to h6., and remove #...)
+        text = text.gsub(/(?:^|^\ +)(\={1,6})\ (.+)\ (?:\1)(?:\ *(\ \#.*))?/) {|s| "\nh#{$1.length}. #{$2}#{$3}\n"}
+        
+        # External Links:
+        #      [http://example.com/]
+        text = text.gsub(/\[((?:https?|s?ftp)\:\S+)\]/, '\1')
+        #      [http://example.com/ Example],[http://example.com/ "Example"]
+        #      [http://example.com/ "Example for "Example""] -> "Example for 'Example'":http://example.com/
+        text = text.gsub(/\[((?:https?|s?ftp)\:\S+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":#{$1}"}
+        #      [mailto:some@example.com],[mailto:"some@example.com"]
+        text = text.gsub(/\[mailto\:([\"']?)(.+?)\1\]/, '\2')
+        
+        # Ticket links:
+        #      [ticket:234 Text],[ticket:234 This is a test],[ticket:234 "This is a test"]
+        #      [ticket:234 "Test "with quotes""] -> "Test 'with quotes'":issues/show/234
+        text = text.gsub(/\[ticket\:(\d+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":/issues/show/#{$1}"}
+        #      ticket:1234
+        #      excluding ticket:1234:file.txt (used in macros)
+        #      #1 - working cause Redmine uses the same syntax.
+        text = text.gsub(/ticket\:(\d+?)([^\:])/, '#\1\2')
+
+        # Source & attachments links:
+        #      [source:/trunk/readme.txt Readme File],[source:"/trunk/readme.txt" Readme File],
+        #      [source:/trunk/readme.txt],[source:"/trunk/readme.txt"]
+        #       The text "Readme File" is not converted,
+        #       cause Redmine's wiki does not support this.
+        #      Attachments use same syntax.
+        text = text.gsub(/\[(source|attachment)\:([\"']?)([^\"']+?)\2(?:\ +(.+?))?\]/, '\1:"\3"')
+        #      source:"/trunk/readme.txt"
+        #      source:/trunk/readme.txt - working cause Redmine uses the same syntax.
+        text = text.gsub(/(source|attachment)\:([\"'])([^\"']+?)\2/, '\1:"\3"')
+
+        # Milestone links:
+        #      [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)],
+        #      [milestone:"0.1.0 Mercury"],milestone:"0.1.0 Mercury"
+        #       The text "Milestone 0.1.0 (Mercury)" is not converted,
+        #       cause Redmine's wiki does not support this.
+        text = text.gsub(/\[milestone\:([\"'])([^\"']+?)\1(?:\ +(.+?))?\]/, 'version:"\2"')
+        text = text.gsub(/milestone\:([\"'])([^\"']+?)\1/, 'version:"\2"')
+        #      [milestone:0.1.0],milestone:0.1.0
+        text = text.gsub(/\[milestone\:([^\ ]+?)\]/, 'version:\1')
+        text = text.gsub(/milestone\:([^\ ]+?)/, 'version:\1')
+
+        # Internal Links:
+        #      ["Some Link"]
+        text = text.gsub(/\[([\"'])(.+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
+        #      [wiki:"Some Link" "Link description"],[wiki:"Some Link" Link description]
+        text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1[\ \t]+([\"']?)(.+?)\3\]/) {|s| "[[#{$2.delete(',./?;|:')}|#{$4}]]"}
+        #      [wiki:"Some Link"]
+        text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
+        #      [wiki:SomeLink]
+        text = text.gsub(/\[wiki\:([^\s\]]+?)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
+        #      [wiki:SomeLink Link description],[wiki:SomeLink "Link description"]
+        text = text.gsub(/\[wiki\:([^\s\]\"']+?)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$3}]]"}
+
+        # Before convert CamelCase links, must hide wiki links with description.
+        # Like this: [[http://www.freebsd.org|Hello FreeBSD World]]
+        text = wiki_links_hide(text)
+        # Links to CamelCase pages (not work for unicode)
+        #      UsingJustWikiCaps,UsingJustWikiCaps/Subpage
+        text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+(?:\/[^\s[:punct:]]+)*)/) {|s| "#{$1}#{$2}[[#{$3.delete('/')}]]"}
+        # Normalize things that were supposed to not be links
+        # like !NotALink
+        text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
+        # Now restore hidden links
+        text = wiki_links_restore(text)
+        
+        # Revisions links
+        text = text.gsub(/\[(\d+)\]/, 'r\1')
+        # Ticket number re-writing
+        text = text.gsub(/#(\d+)/) do |s|
+          if $1.length < 10
+            #ticket_map[$1.to_i] ||= $1
+            "\##{ticket_map[$1.to_i] || $1}"
+          else
+            s
+          end
+        end
+        
+        # Highlighting
+        text = text.gsub(/'''''([^\s])/, '_*\1')
+        text = text.gsub(/([^\s])'''''/, '\1*_')
+        text = text.gsub(/'''/, '*')
+        text = text.gsub(/''/, '_')
+        text = text.gsub(/__/, '+')
+        text = text.gsub(/~~/, '-')
+        text = text.gsub(/,,/, '~')
+        # Tables
+        text = text.gsub(/\|\|/, '|')
+        # Lists:
+        #      bullet
+        text = text.gsub(/^(\ +)[\*-] /) {|s| '*' * $1.length + " "}
+        #      numbered
+        text = text.gsub(/^(\ +)\d+\. /) {|s| '#' * $1.length + " "}
+        # Images (work for only attached in current page [[Image(picture.gif)]])
+        # need rules for:  * [[Image(wiki:WikiFormatting:picture.gif)]] (referring to attachment on another page)
+        #                  * [[Image(ticket:1:picture.gif)]] (file attached to a ticket)
+        #                  * [[Image(htdocs:picture.gif)]] (referring to a file inside project htdocs)
+        #                  * [[Image(source:/trunk/trac/htdocs/trac_logo_mini.png)]] (a file in repository) 
+        text = text.gsub(/\[\[image\((.+?)(?:,.+?)?\)\]\]/i, '!\1!')
+        # TOC (is right-aligned, because that in Trac)
+        text = text.gsub(/\[\[TOC(?:\((.*?)\))?\]\]/m) {|s| "{{>toc}}\n"}
+
+        # Restore and convert code blocks
+        text = code_convert(text)
+
+        text
+  end
+  
+  # Simple progress bar
+  def simplebar(title, current, total, out = STDOUT)
+    def get_width
+      default_width = 80
+      begin
+        tiocgwinsz = 0x5413
+        data = [0, 0, 0, 0].pack("SSSS")
+        if out.ioctl(tiocgwinsz, data) >= 0 then
+          rows, cols, xpixels, ypixels = data.unpack("SSSS")
+          if cols >= 0 then cols else default_width end
+        else
+          default_width
+        end
+      rescue Exception
+        default_width
+      end
+    end
+    mark = "*"
+    title_width = 40
+    max = get_width - title_width - 10
+    format = "%-#{title_width}s [%-#{max}s] %3d%%  [%d/%d]%s"
+    bar = current * max / total
+    percentage = bar * 100 / max
+    current == total ? eol = "\n" : eol ="\r"
+    printf(format, title, mark * bar, percentage, current, total, eol)
+    out.flush
+  end
 end
-
