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