text: #-- encoding: UTF-8
  #-- copyright
  # ChiliProject is a project management system.
  #
  # Copyright (C) 2010-2013 the ChiliProject Team
  #
  # 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.
  #++
  
  require 'cgi'
  
  module Redmine
    module Scm
      module Adapters
        class CommandFailed < StandardError #:nodoc:
        end
  
        class AbstractAdapter #:nodoc:
          class << self
            def client_command
              ""
            end
  
            # Returns the version of the scm client
            # Eg: [1, 5, 0] or [] if unknown
            def client_version
              []
            end
  
            # Returns the version string of the scm client
            # Eg: '1.5.0' or 'Unknown version' if unknown
            def client_version_string
              v = client_version || 'Unknown version'
              v.is_a?(Array) ? v.join('.') : v.to_s
            end
  
            # Returns true if the current client version is above
            # or equals the given one
            # If option is :unknown is set to true, it will return
            # true if the client version is unknown
            def client_version_above?(v, options={})
              ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
            end
  
            def client_available
              true
            end
  
            def shell_quote(str)
              if Redmine::Platform.mswin?
                '"' + str.gsub(/"/, '\\"') + '"'
              else
                "'" + str.gsub(/'/, "'\"'\"'") + "'"
              end
            end
          end
  
          def initialize(url, root_url=nil, login=nil, password=nil,
                         path_encoding=nil)
            @url = url
            @login = login if login && !login.empty?
            @password = (password || "") if @login
            @root_url = root_url.blank? ? retrieve_root_url : root_url
          end
  
          def adapter_name
            'Abstract'
          end
  
          def supports_cat?
            true
          end
  
          def supports_annotate?
            respond_to?('annotate')
          end
  
          def root_url
            @root_url
          end
  
          def url
            @url
          end
  
          # get info about the svn repository
          def info
            return nil
          end
  
          # Returns the entry identified by path and revision identifier
          # or nil if entry doesn't exist in the repository
          def entry(path=nil, identifier=nil)
            parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
            search_path = parts[0..-2].join('/')
            search_name = parts[-1]
            if search_path.blank? && search_name.blank?
              # Root entry
              Entry.new(:path => '', :kind => 'dir')
            else
              # Search for the entry in the parent directory
              es = entries(search_path, identifier)
              es ? es.detect {|e| e.name == search_name} : nil
            end
          end
  
          # Returns an Entries collection
          # or nil if the given path doesn't exist in the repository
          def entries(path=nil, identifier=nil)
            return nil
          end
  
          def branches
            return nil
          end
  
          def tags
            return nil
          end
  
          def default_branch
            return nil
          end
  
          def properties(path, identifier=nil)
            return nil
          end
  
          def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
            return nil
          end
  
          def diff(path, identifier_from, identifier_to=nil)
            return nil
          end
  
          def cat(path, identifier=nil)
            return nil
          end
  
          def with_leading_slash(path)
            path ||= ''
            (path[0,1]!="/") ? "/#{path}" : path
          end
  
          def with_trailling_slash(path)
            path ||= ''
            (path[-1,1] == "/") ? path : "#{path}/"
          end
  
          def without_leading_slash(path)
            path ||= ''
            path.gsub(%r{^/+}, '')
          end
  
          def without_trailling_slash(path)
            path ||= ''
            (path[-1,1] == "/") ? path[0..-2] : path
           end
  
          def shell_quote(str)
            self.class.shell_quote(str)
          end
  
        private
          def retrieve_root_url
            info = self.info
            info ? info.root_url : nil
          end
  
          def target(path)
            path ||= ''
            base = path.match(/^\//) ? root_url : url
            shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
          end
  
          def logger
            self.class.logger
          end
  
          def shellout(cmd, &block)
            self.class.shellout(cmd, &block)
          end
  
          def self.logger
            RAILS_DEFAULT_LOGGER
          end
  
          def self.shellout(cmd, &block)
            logger.debug "Shelling out: #{strip_credential(cmd)}" if logger && logger.debug?
            if Rails.env == 'development'
              # Capture stderr when running in dev environment
              cmd = "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log"
            end
            begin
              if RUBY_VERSION < '1.9'
                mode = "r+"
              else
                mode = "r+:ASCII-8BIT"
              end
              IO.popen(cmd, mode) do |io|
                io.close_write
                block.call(io) if block_given?
              end
            rescue Errno::ENOENT => e
              msg = strip_credential(e.message)
              # The command failed, log it and re-raise
              logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n  with: #{msg}")
              raise CommandFailed.new(msg)
            end
          end
  
          # Hides username/password in a given command
          def self.strip_credential(cmd)
            q = (Redmine::Platform.mswin? ? '"' : "'")
            cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
          end
  
          def strip_credential(cmd)
            self.class.strip_credential(cmd)
          end
  
          def scm_iconv(to, from, str)
            return nil if str.nil?
            return str if to == from
            begin
              Iconv.conv(to, from, str)
            rescue Iconv::Failure => err
              logger.error("failed to convert from #{from} to #{to}. #{err}")
              nil
            end
          end
        end
  
        class Entries < Array
          def sort_by_name
            sort {|x,y|
              if x.kind == y.kind
                x.name.to_s <=> y.name.to_s
              else
                x.kind <=> y.kind
              end
            }
          end
  
          def revisions
            revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
          end
  
          # Required since Ruby 1.9.3 as the core compact always returns an
          # instance of Array. This method follows the spec for Array#compact
          def compact
            ary = self.dup
            ary.compact!
            ary
          end
        end
  
        class Info
          attr_accessor :root_url, :lastrev
          def initialize(attributes={})
            self.root_url = attributes[:root_url] if attributes[:root_url]
            self.lastrev = attributes[:lastrev]
          end
        end
  
        class Entry
          attr_accessor :name, :path, :kind, :size, :lastrev
          def initialize(attributes={})
            self.name = attributes[:name] if attributes[:name]
            self.path = attributes[:path] if attributes[:path]
            self.kind = attributes[:kind] if attributes[:kind]
            self.size = attributes[:size].to_i if attributes[:size]
            self.lastrev = attributes[:lastrev]
          end
  
          def is_file?
            'file' == self.kind
          end
  
          def is_dir?
            'dir' == self.kind
          end
  
          def is_text?
            Redmine::MimeType.is_type?('text', name)
          end
        end
  
        class Revisions < Array
          def latest
            sort {|x,y|
              unless x.time.nil? or y.time.nil?
                x.time <=> y.time
              else
                0
              end
            }.last
          end
        end
  
        class Revision
          attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch
          attr_writer :identifier
  
          def initialize(attributes={})
            self.identifier = attributes[:identifier]
            self.scmid = attributes[:scmid]
            self.name = attributes[:name] || self.identifier
            self.author = attributes[:author]
            self.time = attributes[:time]
            self.message = attributes[:message] || ""
            self.paths = attributes[:paths]
            self.revision = attributes[:revision]
            self.branch = attributes[:branch]
          end
  
          # Returns the identifier of this revision; see also Changeset model
          def identifier
            (@identifier || revision).to_s
          end
  
          # Returns the readable identifier.
          def format_identifier
            identifier
          end
        end
  
        class Annotate
          attr_reader :lines, :revisions
  
          def initialize
            @lines = []
            @revisions = []
          end
  
          def add_line(line, revision)
            @lines << line
            @revisions << revision
          end
  
          def content
            content = lines.join("\n")
          end
  
          def empty?
            lines.empty?
          end
        end
      end
    end
  end
  