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