2019-03-17 16:36:34 +00:00
# frozen_string_literal: true
2019-03-15 01:32:57 +00:00
2011-05-06 13:48:36 +00:00
# Redmine - project management software
2022-01-02 05:29:10 +00:00
# Copyright (C) 2006-2022 Jean-Philippe Lang
2007-06-12 20:12:05 +00:00
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
2011-05-06 13:48:36 +00:00
#
2007-06-12 20:12:05 +00:00
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
2011-05-06 13:48:36 +00:00
#
2007-06-12 20:12:05 +00:00
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'cgi'
2013-12-23 17:07:51 +00:00
require 'redmine/scm/adapters'
2007-06-12 20:12:05 +00:00
module Redmine
module Scm
2011-02-18 00:57:43 +00:00
module Adapters
2018-06-08 00:55:41 +00:00
# @private
class AbstractAdapter
2017-06-17 07:42:54 +00:00
include Redmine :: Utils :: Shell
2011-05-25 16:14:15 +00:00
# raised if scm command exited with error, e.g. unknown revision.
2014-10-22 17:37:16 +00:00
class ScmCommandAborted < :: Redmine :: Scm :: Adapters :: CommandFailed ; end
2011-05-25 16:14:15 +00:00
2008-07-05 08:59:04 +00:00
class << self
2011-02-14 06:14:34 +00:00
def client_command
" "
end
2017-06-17 07:42:54 +00:00
def shell_quote ( str )
Redmine :: Utils :: Shell . shell_quote str
end
2011-07-15 11:43:53 +00:00
def shell_quote_command
2017-06-17 07:42:54 +00:00
Redmine :: Utils :: Shell . shell_quote_command client_command
2011-07-15 11:43:53 +00:00
end
2008-07-05 08:59:04 +00:00
# Returns the version of the scm client
2008-07-12 09:06:19 +00:00
# Eg: [1, 5, 0] or [] if unknown
2008-07-05 08:59:04 +00:00
def client_version
2008-07-12 09:06:19 +00:00
[ ]
2008-07-05 08:59:04 +00:00
end
2011-02-24 05:58:59 +00:00
2008-07-05 08:59:04 +00:00
# Returns the version string of the scm client
2008-07-12 09:06:19 +00:00
# Eg: '1.5.0' or 'Unknown version' if unknown
2008-07-05 08:59:04 +00:00
def client_version_string
2008-07-12 09:06:19 +00:00
v = client_version || 'Unknown version'
v . is_a? ( Array ) ? v . join ( '.' ) : v . to_s
end
2011-02-24 05:58:59 +00:00
2008-07-12 09:06:19 +00:00
# Returns true if the current client version is above
# or equals the given one
# If option is :unknown is set to true, it will return
# true if the client version is unknown
def client_version_above? ( v , options = { } )
( ( client_version < = > v ) > = 0 ) || ( client_version . empty? && options [ :unknown ] )
2008-07-05 08:59:04 +00:00
end
2011-02-14 06:14:34 +00:00
def client_available
true
end
2008-07-05 08:59:04 +00:00
end
2011-02-14 06:14:34 +00:00
2011-02-24 05:58:59 +00:00
def initialize ( url , root_url = nil , login = nil , password = nil ,
path_encoding = nil )
2007-06-12 20:12:05 +00:00
@url = url
@login = login if login && ! login . empty?
@password = ( password || " " ) if @login
@root_url = root_url . blank? ? retrieve_root_url : root_url
end
2011-02-24 05:58:59 +00:00
2007-06-12 20:12:05 +00:00
def adapter_name
'Abstract'
end
2011-02-24 05:58:59 +00:00
2007-06-24 19:30:38 +00:00
def supports_cat?
true
end
2007-12-02 20:58:02 +00:00
def supports_annotate?
2021-12-29 06:12:27 +00:00
respond_to? ( :annotate )
2007-12-02 20:58:02 +00:00
end
2011-02-24 05:58:59 +00:00
2007-06-12 20:12:05 +00:00
def root_url
@root_url
end
2011-02-24 05:58:59 +00:00
2007-06-12 20:12:05 +00:00
def url
@url
end
2011-05-04 13:16:10 +00:00
2011-05-21 02:16:51 +00:00
def path_encoding
nil
end
2007-06-12 20:12:05 +00:00
# get info about the svn repository
def info
return nil
end
2011-05-04 13:16:10 +00:00
2007-06-12 20:12:05 +00:00
# Returns the entry identified by path and revision identifier
# or nil if entry doesn't exist in the repository
def entry ( path = nil , identifier = nil )
2008-04-27 10:12:15 +00:00
parts = path . to_s . split ( %r{ [ \ / \\ ] } ) . select { | n | ! n . blank? }
search_path = parts [ 0 .. - 2 ] . join ( '/' )
search_name = parts [ - 1 ]
if search_path . blank? && search_name . blank?
# Root entry
Entry . new ( :path = > '' , :kind = > 'dir' )
else
# Search for the entry in the parent directory
es = entries ( search_path , identifier )
es ? es . detect { | e | e . name == search_name } : nil
end
2007-06-12 20:12:05 +00:00
end
2011-05-06 13:48:36 +00:00
2007-06-12 20:12:05 +00:00
# Returns an Entries collection
# or nil if the given path doesn't exist in the repository
2011-05-04 14:11:25 +00:00
def entries ( path = nil , identifier = nil , options = { } )
2007-06-12 20:12:05 +00:00
return nil
end
2009-08-15 22:41:40 +00:00
def branches
return nil
end
2011-05-06 13:48:36 +00:00
def tags
2009-08-15 22:41:40 +00:00
return nil
end
def default_branch
return nil
end
2011-05-06 13:48:36 +00:00
2008-07-05 08:59:04 +00:00
def properties ( path , identifier = nil )
return nil
end
2011-05-06 13:48:36 +00:00
2007-06-12 20:12:05 +00:00
def revisions ( path = nil , identifier_from = nil , identifier_to = nil , options = { } )
return nil
end
2011-05-06 13:48:36 +00:00
2008-06-08 16:28:42 +00:00
def diff ( path , identifier_from , identifier_to = nil )
2007-06-12 20:12:05 +00:00
return nil
end
2011-05-06 13:48:36 +00:00
2007-06-12 20:12:05 +00:00
def cat ( path , identifier = nil )
return nil
end
2011-04-10 08:11:18 +00:00
2007-06-12 20:12:05 +00:00
def with_leading_slash ( path )
path || = ''
2022-08-24 13:45:20 +00:00
path . start_with? ( '/' ) ? path : " / #{ path } "
2007-06-12 20:12:05 +00:00
end
2008-06-07 09:19:50 +00:00
2022-08-28 07:32:24 +00:00
def with_trailing_slash ( path )
2008-06-07 09:19:50 +00:00
path || = ''
2022-08-24 13:45:20 +00:00
path . end_with? ( '/' ) ? path : " #{ path } / "
2008-06-07 09:19:50 +00:00
end
2011-02-14 06:14:34 +00:00
2022-08-28 07:32:24 +00:00
def with_trailling_slash ( path )
ActiveSupport :: Deprecation . warn 'Redmine::Scm::Adapters::AbstractAdapter#with_trailling_slash is deprecated and will be removed in Redmine 6.0. Please use #with_trailing_slash instead.'
with_trailing_slash ( path )
end
2008-06-08 15:40:24 +00:00
def without_leading_slash ( path )
path || = ''
path . gsub ( %r{ ^/+ } , '' )
end
2022-08-28 07:32:24 +00:00
def without_trailing_slash ( path )
2008-06-08 15:40:24 +00:00
path || = ''
2022-08-24 13:45:20 +00:00
path . end_with? ( '/' ) ? path [ 0 .. - 2 ] : path
2014-01-31 09:38:43 +00:00
end
2011-02-14 06:14:34 +00:00
2022-08-28 07:32:24 +00:00
def without_trailling_slash ( path )
ActiveSupport :: Deprecation . warn 'Redmine::Scm::Adapters::AbstractAdapter#without_trailling_slash is deprecated and will be removed in Redmine 6.0. Please use #without_trailing_slash instead.'
without_trailing_slash ( path )
end
2021-04-23 00:46:45 +00:00
def valid_name? ( name )
return true if name . nil?
return true if name . is_a? ( Integer ) && name > 0
return true if name . is_a? ( String ) && name =~ / \ A[0-9]* \ z /
false
end
2019-07-29 05:38:30 +00:00
private
2007-06-12 20:12:05 +00:00
def retrieve_root_url
info = self . info
info ? info . root_url : nil
end
2011-04-10 08:11:18 +00:00
2011-05-25 23:50:59 +00:00
def target ( path , sq = true )
2008-03-12 23:00:11 +00:00
path || = ''
2019-03-27 02:15:24 +00:00
base = / ^ \/ / . match? ( path ) ? root_url : url
2011-05-25 23:50:59 +00:00
str = " #{ base } / #{ path } " . gsub ( / [?<> \ *] / , '' )
if sq
str = shell_quote ( str )
end
str
2007-06-12 20:12:05 +00:00
end
2011-02-14 06:14:34 +00:00
2007-06-12 20:12:05 +00:00
def logger
2008-07-05 08:59:04 +00:00
self . class . logger
2007-06-12 20:12:05 +00:00
end
2011-02-14 06:14:34 +00:00
2012-03-28 15:40:37 +00:00
def shellout ( cmd , options = { } , & block )
self . class . shellout ( cmd , options , & block )
2008-07-05 08:59:04 +00:00
end
2011-02-18 00:57:43 +00:00
2013-01-20 12:04:23 +00:00
# Path to the file where scm stderr output is logged
2013-04-03 20:01:41 +00:00
# Returns nil if the log file is not writable
2013-01-20 12:04:23 +00:00
def self . stderr_log_file
2013-04-03 20:01:41 +00:00
if @stderr_log_file . nil?
writable = false
path = Redmine :: Configuration [ 'scm_stderr_log_file' ] . presence
path || = Rails . root . join ( " log/ #{ Rails . env } .scm.stderr.log " ) . to_s
2021-12-27 03:20:31 +00:00
if File . exist? ( path )
2019-03-17 16:36:34 +00:00
if File . file? ( path ) && File . writable? ( path )
2013-04-03 20:01:41 +00:00
writable = true
else
logger . warn ( " SCM log file ( #{ path } ) is not writable " )
end
else
begin
File . open ( path , " w " ) { }
writable = true
rescue = > e
logger . warn ( " SCM log file ( #{ path } ) cannot be created: #{ e . message } " )
end
end
@stderr_log_file = writable ? path : false
end
@stderr_log_file || nil
2013-01-20 12:04:23 +00:00
end
2019-10-19 11:15:30 +00:00
private_class_method :stderr_log_file
2013-01-20 12:04:23 +00:00
2019-10-19 13:33:10 +00:00
# Singleton class method is public
class << self
def logger
Rails . logger
2011-06-08 11:19:13 +00:00
end
2019-10-19 13:33:10 +00:00
def shellout ( cmd , options = { } , & block )
if logger && logger . debug?
logger . debug " Shelling out: #{ strip_credential ( cmd ) } "
# Capture stderr in a log file
if stderr_log_file
cmd = " #{ cmd } 2>> #{ shell_quote ( stderr_log_file ) } "
end
end
begin
mode = " r+ "
IO . popen ( cmd , mode ) do | io |
io . set_encoding ( " ASCII-8BIT " ) if io . respond_to? ( :set_encoding )
io . close_write unless options [ :write_stdin ]
2019-10-22 12:41:09 +00:00
yield ( io ) if block_given?
2019-10-19 13:33:10 +00:00
end
rescue = > e
msg = strip_credential ( e . message )
# The command failed, log it and re-raise
logmsg = " SCM command failed, "
logmsg += " make sure that your SCM command (e.g. svn) is "
logmsg += " in PATH ( #{ ENV [ 'PATH' ] } ) \n "
logmsg += " You can configure your scm commands in config/configuration.yml. \n "
logmsg += " #{ strip_credential ( cmd ) } \n "
logmsg += " with: #{ msg } "
logger . error ( logmsg )
raise CommandFailed . new ( msg )
2007-12-15 12:14:40 +00:00
end
2007-06-12 20:12:05 +00:00
end
2011-02-18 00:57:43 +00:00
end
2008-06-06 14:37:49 +00:00
# Hides username/password in a given command
2008-08-25 12:27:15 +00:00
def self . strip_credential ( cmd )
2008-08-25 11:01:37 +00:00
q = ( Redmine :: Platform . mswin? ? '"' : " ' " )
2008-06-06 14:37:49 +00:00
cmd . to_s . gsub ( / ( \ - \ -(password|username)) \ s+( #{ q } [^ #{ q } ]+ #{ q } |[^ #{ q } ] \ S+) / , '\\1 xxxx' )
end
2019-10-19 11:15:30 +00:00
private_class_method :strip_credential
2011-04-10 08:11:18 +00:00
2008-06-06 14:37:49 +00:00
def strip_credential ( cmd )
2008-08-25 12:27:15 +00:00
self . class . strip_credential ( cmd )
2008-06-06 14:37:49 +00:00
end
2011-02-21 12:10:16 +00:00
def scm_iconv ( to , from , str )
2019-03-17 16:36:34 +00:00
return if str . nil?
2015-01-18 16:16:23 +00:00
return str if to == from && str . encoding . to_s == from
2020-08-09 15:55:59 +00:00
2019-03-17 16:36:34 +00:00
str = str . dup
2014-10-22 17:37:16 +00:00
str . force_encoding ( from )
begin
str . encode ( to )
2019-05-25 06:50:25 +00:00
rescue = > err
2014-10-22 17:37:16 +00:00
logger . error ( " failed to convert from #{ from } to #{ to } . #{ err } " )
nil
2011-02-21 12:10:16 +00:00
end
end
2012-04-26 17:57:49 +00:00
def parse_xml ( xml )
if RUBY_PLATFORM == 'java'
xml = xml . sub ( %r{ < \ ?xml[^>]* \ ?> } , '' )
end
ActiveSupport :: XmlMini . parse ( xml )
end
2007-06-12 20:12:05 +00:00
end
2011-02-21 12:10:16 +00:00
2007-06-12 20:12:05 +00:00
class Entries < Array
def sort_by_name
2020-08-09 15:57:28 +00:00
dup . sort! do | x , y |
2007-06-12 20:12:05 +00:00
if x . kind == y . kind
2009-10-24 11:08:09 +00:00
x . name . to_s < = > y . name . to_s
2007-06-12 20:12:05 +00:00
else
x . kind < = > y . kind
end
2020-08-09 15:57:28 +00:00
end
2007-06-12 20:12:05 +00:00
end
2011-05-04 13:16:10 +00:00
2007-06-12 20:12:05 +00:00
def revisions
revisions || = Revisions . new ( collect { | entry | entry . lastrev } . compact )
end
end
2011-05-04 13:16:10 +00:00
2007-06-12 20:12:05 +00:00
class Info
attr_accessor :root_url , :lastrev
def initialize ( attributes = { } )
self . root_url = attributes [ :root_url ] if attributes [ :root_url ]
self . lastrev = attributes [ :lastrev ]
end
end
2011-05-04 13:16:10 +00:00
2007-06-12 20:12:05 +00:00
class Entry
2012-06-10 18:32:09 +00:00
attr_accessor :name , :path , :kind , :size , :lastrev , :changeset
2007-06-12 20:12:05 +00:00
def initialize ( attributes = { } )
self . name = attributes [ :name ] if attributes [ :name ]
self . path = attributes [ :path ] if attributes [ :path ]
self . kind = attributes [ :kind ] if attributes [ :kind ]
self . size = attributes [ :size ] . to_i if attributes [ :size ]
self . lastrev = attributes [ :lastrev ]
end
2011-05-04 13:16:10 +00:00
2007-06-12 20:12:05 +00:00
def is_file?
2020-10-27 15:19:05 +00:00
self . kind == 'file'
2007-06-12 20:12:05 +00:00
end
2011-05-04 13:16:10 +00:00
2007-06-12 20:12:05 +00:00
def is_dir?
2020-10-27 15:19:05 +00:00
self . kind == 'dir'
2007-06-12 20:12:05 +00:00
end
2011-05-04 13:16:10 +00:00
2007-06-12 20:12:05 +00:00
def is_text?
Redmine :: MimeType . is_type? ( 'text' , name )
end
2012-06-10 19:19:22 +00:00
def author
if changeset
changeset . author . to_s
elsif lastrev
Redmine :: CodesetUtil . replace_invalid_utf8 ( lastrev . author . to_s . split ( '<' ) . first )
end
end
2007-06-12 20:12:05 +00:00
end
2011-05-06 13:48:36 +00:00
2007-06-12 20:12:05 +00:00
class Revisions < Array
def latest
2020-08-09 15:57:28 +00:00
sort do | x , y |
2007-06-12 20:12:05 +00:00
unless x . time . nil? or y . time . nil?
x . time < = > y . time
else
0
end
2020-08-09 15:57:28 +00:00
end . last
2011-05-06 13:48:36 +00:00
end
2007-06-12 20:12:05 +00:00
end
2011-05-06 13:48:36 +00:00
2007-06-12 20:12:05 +00:00
class Revision
2011-04-11 10:24:44 +00:00
attr_accessor :scmid , :name , :author , :time , :message ,
2011-10-28 04:55:54 +00:00
:paths , :revision , :branch , :identifier ,
:parents
2009-08-15 22:41:40 +00:00
2007-06-12 20:12:05 +00:00
def initialize ( attributes = { } )
self . identifier = attributes [ :identifier ]
2011-04-11 10:25:38 +00:00
self . scmid = attributes [ :scmid ]
self . name = attributes [ :name ] || self . identifier
self . author = attributes [ :author ]
self . time = attributes [ :time ]
self . message = attributes [ :message ] || " "
self . paths = attributes [ :paths ]
self . revision = attributes [ :revision ]
self . branch = attributes [ :branch ]
2011-10-28 04:55:54 +00:00
self . parents = attributes [ :parents ]
2007-06-12 20:12:05 +00:00
end
2009-08-15 22:41:40 +00:00
2011-01-02 09:45:05 +00:00
# Returns the readable identifier.
def format_identifier
2011-04-11 10:24:44 +00:00
self . identifier . to_s
2011-01-02 09:45:05 +00:00
end
2012-07-28 18:11:03 +00:00
def == ( other )
if other . nil?
false
elsif scmid . present?
scmid == other . scmid
elsif identifier . present?
identifier == other . identifier
elsif revision . present?
revision == other . revision
end
end
2007-06-12 20:12:05 +00:00
end
2011-02-18 00:57:43 +00:00
2007-12-02 20:58:02 +00:00
class Annotate
attr_reader :lines , :revisions
2011-05-06 13:48:36 +00:00
2007-12-02 20:58:02 +00:00
def initialize
@lines = [ ]
@revisions = [ ]
end
2011-05-06 13:48:36 +00:00
2007-12-02 20:58:02 +00:00
def add_line ( line , revision )
@lines << line
@revisions << revision
end
2011-05-06 13:48:36 +00:00
2007-12-02 20:58:02 +00:00
def content
content = lines . join ( " \n " )
end
2011-05-06 13:48:36 +00:00
2007-12-02 20:58:02 +00:00
def empty?
lines . empty?
end
end
2011-10-28 05:17:13 +00:00
class Branch < String
attr_accessor :revision , :scmid
end
2017-06-10 04:29:15 +00:00
module ScmData
def self . binary? ( data )
unless data . empty?
2020-10-02 12:58:35 +00:00
data . count ( " ^ -~ " , " ^ \r \n " ) . fdiv ( data . size ) > 0 . 3 || data . index ( " \x00 " )
2017-06-10 04:29:15 +00:00
end
end
end
2007-06-12 20:12:05 +00:00
end
end
end