mirror of
				https://github.com/redmine/redmine.git
				synced 2025-11-03 20:06:24 +01:00 
			
		
		
		
	Contributed by Pavel Rosický. git-svn-id: http://svn.redmine.org/redmine/trunk@17988 e93f8b46-1217-0410-a6f0-8f06a7374b81
		
			
				
	
	
		
			439 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			439 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
# Redmine - project management software
 | 
						|
# Copyright (C) 2006-2017  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.
 | 
						|
#
 | 
						|
# 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 'cgi'
 | 
						|
require 'redmine/scm/adapters'
 | 
						|
 | 
						|
module Redmine
 | 
						|
  module Scm
 | 
						|
    module Adapters
 | 
						|
      # @private
 | 
						|
      class AbstractAdapter
 | 
						|
        include Redmine::Utils::Shell
 | 
						|
 | 
						|
        # raised if scm command exited with error, e.g. unknown revision.
 | 
						|
        class ScmCommandAborted < ::Redmine::Scm::Adapters::CommandFailed; end
 | 
						|
 | 
						|
        class << self
 | 
						|
 | 
						|
          def client_command
 | 
						|
            ""
 | 
						|
          end
 | 
						|
 | 
						|
          def shell_quote(str)
 | 
						|
            Redmine::Utils::Shell.shell_quote str
 | 
						|
          end
 | 
						|
 | 
						|
          def shell_quote_command
 | 
						|
            Redmine::Utils::Shell.shell_quote_command 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
 | 
						|
 | 
						|
        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
 | 
						|
 | 
						|
        def path_encoding
 | 
						|
          nil
 | 
						|
        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, options={})
 | 
						|
          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
 | 
						|
 | 
						|
      private
 | 
						|
        def retrieve_root_url
 | 
						|
          info = self.info
 | 
						|
          info ? info.root_url : nil
 | 
						|
        end
 | 
						|
 | 
						|
        def target(path, sq=true)
 | 
						|
          path ||= ''
 | 
						|
          base = path.match(/^\//) ? root_url : url
 | 
						|
          str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
 | 
						|
          if sq
 | 
						|
            str = shell_quote(str)
 | 
						|
          end
 | 
						|
          str
 | 
						|
        end
 | 
						|
 | 
						|
        def logger
 | 
						|
          self.class.logger
 | 
						|
        end
 | 
						|
 | 
						|
        def shellout(cmd, options = {}, &block)
 | 
						|
          self.class.shellout(cmd, options, &block)
 | 
						|
        end
 | 
						|
 | 
						|
        def self.logger
 | 
						|
          Rails.logger
 | 
						|
        end
 | 
						|
 | 
						|
        # Path to the file where scm stderr output is logged
 | 
						|
        # Returns nil if the log file is not writable
 | 
						|
        def self.stderr_log_file
 | 
						|
          if @stderr_log_file.nil?
 | 
						|
            writable = false
 | 
						|
            path = Redmine::Configuration['scm_stderr_log_file'].presence
 | 
						|
            path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s
 | 
						|
            if File.exists?(path)
 | 
						|
              if File.file?(path) && File.writable?(path)
 | 
						|
                writable = true
 | 
						|
              else
 | 
						|
                logger.warn("SCM log file (#{path}) is not writable")
 | 
						|
              end
 | 
						|
            else
 | 
						|
              begin
 | 
						|
                File.open(path, "w") {}
 | 
						|
                writable = true
 | 
						|
              rescue => e
 | 
						|
                logger.warn("SCM log file (#{path}) cannot be created: #{e.message}")
 | 
						|
              end
 | 
						|
            end
 | 
						|
            @stderr_log_file = writable ? path : false
 | 
						|
          end
 | 
						|
          @stderr_log_file || nil
 | 
						|
        end
 | 
						|
 | 
						|
        def self.shellout(cmd, options = {}, &block)
 | 
						|
          if logger && logger.debug?
 | 
						|
            logger.debug "Shelling out: #{strip_credential(cmd)}"
 | 
						|
            # Capture stderr in a log file
 | 
						|
            if stderr_log_file
 | 
						|
              cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
 | 
						|
            end
 | 
						|
          end
 | 
						|
          begin
 | 
						|
            mode = "r+"
 | 
						|
            IO.popen(cmd, mode) do |io|
 | 
						|
              io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
 | 
						|
              io.close_write unless options[:write_stdin]
 | 
						|
              block.call(io) if block_given?
 | 
						|
            end
 | 
						|
          ## If scm command does not exist,
 | 
						|
          ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
 | 
						|
          ## in production environment.
 | 
						|
          # rescue Errno::ENOENT => e
 | 
						|
          rescue Exception => e
 | 
						|
            msg = strip_credential(e.message)
 | 
						|
            # The command failed, log it and re-raise
 | 
						|
            logmsg = "SCM command failed, "
 | 
						|
            logmsg += "make sure that your SCM command (e.g. svn) is "
 | 
						|
            logmsg += "in PATH (#{ENV['PATH']})\n"
 | 
						|
            logmsg += "You can configure your scm commands in config/configuration.yml.\n"
 | 
						|
            logmsg += "#{strip_credential(cmd)}\n"
 | 
						|
            logmsg += "with: #{msg}"
 | 
						|
            logger.error(logmsg)
 | 
						|
            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 if str.nil?
 | 
						|
          return str if to == from && str.encoding.to_s == from
 | 
						|
          str = str.dup
 | 
						|
          str.force_encoding(from)
 | 
						|
          begin
 | 
						|
            str.encode(to)
 | 
						|
          rescue Exception => err
 | 
						|
            logger.error("failed to convert from #{from} to #{to}. #{err}")
 | 
						|
            nil
 | 
						|
          end
 | 
						|
        end
 | 
						|
 | 
						|
        def parse_xml(xml)
 | 
						|
          if RUBY_PLATFORM == 'java'
 | 
						|
            xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
 | 
						|
          end
 | 
						|
          ActiveSupport::XmlMini.parse(xml)
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      class Entries < Array
 | 
						|
        def sort_by_name
 | 
						|
          dup.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
 | 
						|
      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, :changeset
 | 
						|
 | 
						|
        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
 | 
						|
 | 
						|
        def author
 | 
						|
          if changeset
 | 
						|
            changeset.author.to_s
 | 
						|
          elsif lastrev
 | 
						|
            Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first)
 | 
						|
          end
 | 
						|
        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, :identifier,
 | 
						|
                      :parents
 | 
						|
 | 
						|
        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]
 | 
						|
          self.parents    = attributes[:parents]
 | 
						|
        end
 | 
						|
 | 
						|
        # Returns the readable identifier.
 | 
						|
        def format_identifier
 | 
						|
          self.identifier.to_s
 | 
						|
        end
 | 
						|
 | 
						|
        def ==(other)
 | 
						|
          if other.nil?
 | 
						|
            false
 | 
						|
          elsif scmid.present?
 | 
						|
            scmid == other.scmid
 | 
						|
          elsif identifier.present?
 | 
						|
            identifier == other.identifier
 | 
						|
          elsif revision.present?
 | 
						|
            revision == other.revision
 | 
						|
          end
 | 
						|
        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
 | 
						|
 | 
						|
      class Branch < String
 | 
						|
        attr_accessor :revision, :scmid
 | 
						|
      end
 | 
						|
 | 
						|
      module ScmData
 | 
						|
        def self.binary?(data)
 | 
						|
          unless data.empty?
 | 
						|
            data.count( "^ -~", "^\r\n" ).fdiv(data.size) > 0.3 || data.index( "\x00" )
 | 
						|
          end
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |