Chris@1494: # Redmine - project management software Chris@1494: # Copyright (C) 2006-2014 Jean-Philippe Lang Chris@1494: # Chris@1494: # This program is free software; you can redistribute it and/or Chris@1494: # modify it under the terms of the GNU General Public License Chris@1494: # as published by the Free Software Foundation; either version 2 Chris@1494: # of the License, or (at your option) any later version. Chris@1494: # Chris@1494: # This program is distributed in the hope that it will be useful, Chris@1494: # but WITHOUT ANY WARRANTY; without even the implied warranty of Chris@1494: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Chris@1494: # GNU General Public License for more details. Chris@1494: # Chris@1494: # You should have received a copy of the GNU General Public License Chris@1494: # along with this program; if not, write to the Free Software Chris@1494: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Chris@1494: Chris@1494: require 'cgi' Chris@1494: require 'redmine/scm/adapters' Chris@1494: Chris@1494: if RUBY_VERSION < '1.9' Chris@1494: require 'iconv' Chris@1494: end Chris@1494: Chris@1494: module Redmine Chris@1494: module Scm Chris@1494: module Adapters Chris@1494: class AbstractAdapter #:nodoc: Chris@1494: Chris@1494: # raised if scm command exited with error, e.g. unknown revision. Chris@1494: class ScmCommandAborted < CommandFailed; end Chris@1494: Chris@1494: class << self Chris@1494: def client_command Chris@1494: "" Chris@1494: end Chris@1494: Chris@1494: def shell_quote_command Chris@1494: if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java' Chris@1494: client_command Chris@1494: else Chris@1494: shell_quote(client_command) Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: # Returns the version of the scm client Chris@1494: # Eg: [1, 5, 0] or [] if unknown Chris@1494: def client_version Chris@1494: [] Chris@1494: end Chris@1494: Chris@1494: # Returns the version string of the scm client Chris@1494: # Eg: '1.5.0' or 'Unknown version' if unknown Chris@1494: def client_version_string Chris@1494: v = client_version || 'Unknown version' Chris@1494: v.is_a?(Array) ? v.join('.') : v.to_s Chris@1494: end Chris@1494: Chris@1494: # Returns true if the current client version is above Chris@1494: # or equals the given one Chris@1494: # If option is :unknown is set to true, it will return Chris@1494: # true if the client version is unknown Chris@1494: def client_version_above?(v, options={}) Chris@1494: ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown]) Chris@1494: end Chris@1494: Chris@1494: def client_available Chris@1494: true Chris@1494: end Chris@1494: Chris@1494: def shell_quote(str) Chris@1494: if Redmine::Platform.mswin? Chris@1494: '"' + str.gsub(/"/, '\\"') + '"' Chris@1494: else Chris@1494: "'" + str.gsub(/'/, "'\"'\"'") + "'" Chris@1494: end Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: def initialize(url, root_url=nil, login=nil, password=nil, Chris@1494: path_encoding=nil) Chris@1494: @url = url Chris@1494: @login = login if login && !login.empty? Chris@1494: @password = (password || "") if @login Chris@1494: @root_url = root_url.blank? ? retrieve_root_url : root_url Chris@1494: end Chris@1494: Chris@1494: def adapter_name Chris@1494: 'Abstract' Chris@1494: end Chris@1494: Chris@1494: def supports_cat? Chris@1494: true Chris@1494: end Chris@1494: Chris@1494: def supports_annotate? Chris@1494: respond_to?('annotate') Chris@1494: end Chris@1494: Chris@1494: def root_url Chris@1494: @root_url Chris@1494: end Chris@1494: Chris@1494: def url Chris@1494: @url Chris@1494: end Chris@1494: Chris@1494: def path_encoding Chris@1494: nil Chris@1494: end Chris@1494: Chris@1494: # get info about the svn repository Chris@1494: def info Chris@1494: return nil Chris@1494: end Chris@1494: Chris@1494: # Returns the entry identified by path and revision identifier Chris@1494: # or nil if entry doesn't exist in the repository Chris@1494: def entry(path=nil, identifier=nil) Chris@1494: parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?} Chris@1494: search_path = parts[0..-2].join('/') Chris@1494: search_name = parts[-1] Chris@1494: if search_path.blank? && search_name.blank? Chris@1494: # Root entry Chris@1494: Entry.new(:path => '', :kind => 'dir') Chris@1494: else Chris@1494: # Search for the entry in the parent directory Chris@1494: es = entries(search_path, identifier) Chris@1494: es ? es.detect {|e| e.name == search_name} : nil Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: # Returns an Entries collection Chris@1494: # or nil if the given path doesn't exist in the repository Chris@1494: def entries(path=nil, identifier=nil, options={}) Chris@1494: return nil Chris@1494: end Chris@1494: Chris@1494: def branches Chris@1494: return nil Chris@1494: end Chris@1494: Chris@1494: def tags Chris@1494: return nil Chris@1494: end Chris@1494: Chris@1494: def default_branch Chris@1494: return nil Chris@1494: end Chris@1494: Chris@1494: def properties(path, identifier=nil) Chris@1494: return nil Chris@1494: end Chris@1494: Chris@1494: def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) Chris@1494: return nil Chris@1494: end Chris@1494: Chris@1494: def diff(path, identifier_from, identifier_to=nil) Chris@1494: return nil Chris@1494: end Chris@1494: Chris@1494: def cat(path, identifier=nil) Chris@1494: return nil Chris@1494: end Chris@1494: Chris@1494: def with_leading_slash(path) Chris@1494: path ||= '' Chris@1494: (path[0,1]!="/") ? "/#{path}" : path Chris@1494: end Chris@1494: Chris@1494: def with_trailling_slash(path) Chris@1494: path ||= '' Chris@1494: (path[-1,1] == "/") ? path : "#{path}/" Chris@1494: end Chris@1494: Chris@1494: def without_leading_slash(path) Chris@1494: path ||= '' Chris@1494: path.gsub(%r{^/+}, '') Chris@1494: end Chris@1494: Chris@1494: def without_trailling_slash(path) Chris@1494: path ||= '' Chris@1494: (path[-1,1] == "/") ? path[0..-2] : path Chris@1494: end Chris@1494: Chris@1494: def shell_quote(str) Chris@1494: self.class.shell_quote(str) Chris@1494: end Chris@1494: Chris@1494: private Chris@1494: def retrieve_root_url Chris@1494: info = self.info Chris@1494: info ? info.root_url : nil Chris@1494: end Chris@1494: Chris@1494: def target(path, sq=true) Chris@1494: path ||= '' Chris@1494: base = path.match(/^\//) ? root_url : url Chris@1494: str = "#{base}/#{path}".gsub(/[?<>\*]/, '') Chris@1494: if sq Chris@1494: str = shell_quote(str) Chris@1494: end Chris@1494: str Chris@1494: end Chris@1494: Chris@1494: def logger Chris@1494: self.class.logger Chris@1494: end Chris@1494: Chris@1494: def shellout(cmd, options = {}, &block) Chris@1494: self.class.shellout(cmd, options, &block) Chris@1494: end Chris@1494: Chris@1494: def self.logger Chris@1494: Rails.logger Chris@1494: end Chris@1494: Chris@1494: # Path to the file where scm stderr output is logged Chris@1494: # Returns nil if the log file is not writable Chris@1494: def self.stderr_log_file Chris@1494: if @stderr_log_file.nil? Chris@1494: writable = false Chris@1494: path = Redmine::Configuration['scm_stderr_log_file'].presence Chris@1494: path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s Chris@1494: if File.exists?(path) Chris@1494: if File.file?(path) && File.writable?(path) Chris@1494: writable = true Chris@1494: else Chris@1494: logger.warn("SCM log file (#{path}) is not writable") Chris@1494: end Chris@1494: else Chris@1494: begin Chris@1494: File.open(path, "w") {} Chris@1494: writable = true Chris@1494: rescue => e Chris@1494: logger.warn("SCM log file (#{path}) cannot be created: #{e.message}") Chris@1494: end Chris@1494: end Chris@1494: @stderr_log_file = writable ? path : false Chris@1494: end Chris@1494: @stderr_log_file || nil Chris@1494: end Chris@1494: Chris@1494: def self.shellout(cmd, options = {}, &block) Chris@1494: if logger && logger.debug? Chris@1494: logger.debug "Shelling out: #{strip_credential(cmd)}" Chris@1494: # Capture stderr in a log file Chris@1494: if stderr_log_file Chris@1494: cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}" Chris@1494: end Chris@1494: end Chris@1494: begin Chris@1494: mode = "r+" Chris@1494: IO.popen(cmd, mode) do |io| Chris@1494: io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding) Chris@1494: io.close_write unless options[:write_stdin] Chris@1494: block.call(io) if block_given? Chris@1494: end Chris@1494: ## If scm command does not exist, Chris@1494: ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException Chris@1494: ## in production environment. Chris@1494: # rescue Errno::ENOENT => e Chris@1494: rescue Exception => e Chris@1494: msg = strip_credential(e.message) Chris@1494: # The command failed, log it and re-raise Chris@1494: logmsg = "SCM command failed, " Chris@1494: logmsg += "make sure that your SCM command (e.g. svn) is " Chris@1494: logmsg += "in PATH (#{ENV['PATH']})\n" Chris@1494: logmsg += "You can configure your scm commands in config/configuration.yml.\n" Chris@1494: logmsg += "#{strip_credential(cmd)}\n" Chris@1494: logmsg += "with: #{msg}" Chris@1494: logger.error(logmsg) Chris@1494: raise CommandFailed.new(msg) Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: # Hides username/password in a given command Chris@1494: def self.strip_credential(cmd) Chris@1494: q = (Redmine::Platform.mswin? ? '"' : "'") Chris@1494: cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx') Chris@1494: end Chris@1494: Chris@1494: def strip_credential(cmd) Chris@1494: self.class.strip_credential(cmd) Chris@1494: end Chris@1494: Chris@1494: def scm_iconv(to, from, str) Chris@1494: return nil if str.nil? Chris@1494: return str if to == from Chris@1494: if str.respond_to?(:force_encoding) Chris@1494: str.force_encoding(from) Chris@1494: begin Chris@1494: str.encode(to) Chris@1494: rescue Exception => err Chris@1494: logger.error("failed to convert from #{from} to #{to}. #{err}") Chris@1494: nil Chris@1494: end Chris@1494: else Chris@1494: begin Chris@1494: Iconv.conv(to, from, str) Chris@1494: rescue Iconv::Failure => err Chris@1494: logger.error("failed to convert from #{from} to #{to}. #{err}") Chris@1494: nil Chris@1494: end Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: def parse_xml(xml) Chris@1494: if RUBY_PLATFORM == 'java' Chris@1494: xml = xml.sub(%r{<\?xml[^>]*\?>}, '') Chris@1494: end Chris@1494: ActiveSupport::XmlMini.parse(xml) Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: class Entries < Array Chris@1494: def sort_by_name Chris@1494: dup.sort! {|x,y| Chris@1494: if x.kind == y.kind Chris@1494: x.name.to_s <=> y.name.to_s Chris@1494: else Chris@1494: x.kind <=> y.kind Chris@1494: end Chris@1494: } Chris@1494: end Chris@1494: Chris@1494: def revisions Chris@1494: revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact) Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: class Info Chris@1494: attr_accessor :root_url, :lastrev Chris@1494: def initialize(attributes={}) Chris@1494: self.root_url = attributes[:root_url] if attributes[:root_url] Chris@1494: self.lastrev = attributes[:lastrev] Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: class Entry Chris@1494: attr_accessor :name, :path, :kind, :size, :lastrev, :changeset Chris@1494: Chris@1494: def initialize(attributes={}) Chris@1494: self.name = attributes[:name] if attributes[:name] Chris@1494: self.path = attributes[:path] if attributes[:path] Chris@1494: self.kind = attributes[:kind] if attributes[:kind] Chris@1494: self.size = attributes[:size].to_i if attributes[:size] Chris@1494: self.lastrev = attributes[:lastrev] Chris@1494: end Chris@1494: Chris@1494: def is_file? Chris@1494: 'file' == self.kind Chris@1494: end Chris@1494: Chris@1494: def is_dir? Chris@1494: 'dir' == self.kind Chris@1494: end Chris@1494: Chris@1494: def is_text? Chris@1494: Redmine::MimeType.is_type?('text', name) Chris@1494: end Chris@1494: Chris@1494: def author Chris@1494: if changeset Chris@1494: changeset.author.to_s Chris@1494: elsif lastrev Chris@1494: Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first) Chris@1494: end Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: class Revisions < Array Chris@1494: def latest Chris@1494: sort {|x,y| Chris@1494: unless x.time.nil? or y.time.nil? Chris@1494: x.time <=> y.time Chris@1494: else Chris@1494: 0 Chris@1494: end Chris@1494: }.last Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: class Revision Chris@1494: attr_accessor :scmid, :name, :author, :time, :message, Chris@1494: :paths, :revision, :branch, :identifier, Chris@1494: :parents Chris@1494: Chris@1494: def initialize(attributes={}) Chris@1494: self.identifier = attributes[:identifier] Chris@1494: self.scmid = attributes[:scmid] Chris@1494: self.name = attributes[:name] || self.identifier Chris@1494: self.author = attributes[:author] Chris@1494: self.time = attributes[:time] Chris@1494: self.message = attributes[:message] || "" Chris@1494: self.paths = attributes[:paths] Chris@1494: self.revision = attributes[:revision] Chris@1494: self.branch = attributes[:branch] Chris@1494: self.parents = attributes[:parents] Chris@1494: end Chris@1494: Chris@1494: # Returns the readable identifier. Chris@1494: def format_identifier Chris@1494: self.identifier.to_s Chris@1494: end Chris@1494: Chris@1494: def ==(other) Chris@1494: if other.nil? Chris@1494: false Chris@1494: elsif scmid.present? Chris@1494: scmid == other.scmid Chris@1494: elsif identifier.present? Chris@1494: identifier == other.identifier Chris@1494: elsif revision.present? Chris@1494: revision == other.revision Chris@1494: end Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: class Annotate Chris@1494: attr_reader :lines, :revisions Chris@1494: Chris@1494: def initialize Chris@1494: @lines = [] Chris@1494: @revisions = [] Chris@1494: end Chris@1494: Chris@1494: def add_line(line, revision) Chris@1494: @lines << line Chris@1494: @revisions << revision Chris@1494: end Chris@1494: Chris@1494: def content Chris@1494: content = lines.join("\n") Chris@1494: end Chris@1494: Chris@1494: def empty? Chris@1494: lines.empty? Chris@1494: end Chris@1494: end Chris@1494: Chris@1494: class Branch < String Chris@1494: attr_accessor :revision, :scmid Chris@1494: end Chris@1494: end Chris@1494: end Chris@1494: end