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