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