annotate lib/redmine/scm/adapters/abstract_adapter.rb @ 922:ad295b270cd4 live

FIx #446: "non-utf8 paths in repositories blow up repo viewer and reposman" by ensuring the iconv conversion always happens even if source and dest are intended to be the same encoding
author Chris Cannam
date Tue, 13 Mar 2012 16:33:49 +0000
parents 851510f1b535
children 18beae6cb226
rev   line source
Chris@441 1 # Redmine - project management software
Chris@441 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
Chris@0 3 #
Chris@0 4 # This program is free software; you can redistribute it and/or
Chris@0 5 # modify it under the terms of the GNU General Public License
Chris@0 6 # as published by the Free Software Foundation; either version 2
Chris@0 7 # of the License, or (at your option) any later version.
Chris@441 8 #
Chris@0 9 # This program is distributed in the hope that it will be useful,
Chris@0 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 12 # GNU General Public License for more details.
Chris@441 13 #
Chris@0 14 # You should have received a copy of the GNU General Public License
Chris@0 15 # along with this program; if not, write to the Free Software
Chris@0 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 17
Chris@0 18 require 'cgi'
Chris@0 19
Chris@0 20 module Redmine
Chris@0 21 module Scm
Chris@245 22 module Adapters
Chris@0 23 class CommandFailed < StandardError #:nodoc:
Chris@0 24 end
Chris@245 25
Chris@0 26 class AbstractAdapter #:nodoc:
Chris@0 27 class << self
Chris@245 28 def client_command
Chris@245 29 ""
Chris@245 30 end
Chris@245 31
Chris@0 32 # Returns the version of the scm client
Chris@0 33 # Eg: [1, 5, 0] or [] if unknown
Chris@0 34 def client_version
Chris@0 35 []
Chris@0 36 end
Chris@245 37
Chris@0 38 # Returns the version string of the scm client
Chris@0 39 # Eg: '1.5.0' or 'Unknown version' if unknown
Chris@0 40 def client_version_string
Chris@0 41 v = client_version || 'Unknown version'
Chris@0 42 v.is_a?(Array) ? v.join('.') : v.to_s
Chris@0 43 end
Chris@245 44
Chris@0 45 # Returns true if the current client version is above
Chris@0 46 # or equals the given one
Chris@0 47 # If option is :unknown is set to true, it will return
Chris@0 48 # true if the client version is unknown
Chris@0 49 def client_version_above?(v, options={})
Chris@0 50 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
Chris@0 51 end
Chris@245 52
Chris@245 53 def client_available
Chris@245 54 true
Chris@245 55 end
Chris@245 56
Chris@245 57 def shell_quote(str)
Chris@245 58 if Redmine::Platform.mswin?
Chris@245 59 '"' + str.gsub(/"/, '\\"') + '"'
Chris@245 60 else
Chris@245 61 "'" + str.gsub(/'/, "'\"'\"'") + "'"
Chris@245 62 end
Chris@245 63 end
Chris@0 64 end
Chris@245 65
Chris@245 66 def initialize(url, root_url=nil, login=nil, password=nil,
Chris@245 67 path_encoding=nil)
Chris@0 68 @url = url
Chris@0 69 @login = login if login && !login.empty?
Chris@0 70 @password = (password || "") if @login
Chris@0 71 @root_url = root_url.blank? ? retrieve_root_url : root_url
Chris@0 72 end
Chris@245 73
Chris@0 74 def adapter_name
Chris@0 75 'Abstract'
Chris@0 76 end
Chris@245 77
Chris@0 78 def supports_cat?
Chris@0 79 true
Chris@0 80 end
Chris@0 81
Chris@0 82 def supports_annotate?
Chris@0 83 respond_to?('annotate')
Chris@0 84 end
Chris@245 85
Chris@0 86 def root_url
Chris@0 87 @root_url
Chris@0 88 end
Chris@245 89
Chris@0 90 def url
Chris@0 91 @url
Chris@0 92 end
Chris@441 93
Chris@441 94 def path_encoding
Chris@441 95 nil
Chris@441 96 end
Chris@441 97
Chris@0 98 # get info about the svn repository
Chris@0 99 def info
Chris@0 100 return nil
Chris@0 101 end
Chris@441 102
Chris@0 103 # Returns the entry identified by path and revision identifier
Chris@0 104 # or nil if entry doesn't exist in the repository
Chris@0 105 def entry(path=nil, identifier=nil)
Chris@0 106 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
Chris@0 107 search_path = parts[0..-2].join('/')
Chris@0 108 search_name = parts[-1]
Chris@0 109 if search_path.blank? && search_name.blank?
Chris@0 110 # Root entry
Chris@0 111 Entry.new(:path => '', :kind => 'dir')
Chris@0 112 else
Chris@0 113 # Search for the entry in the parent directory
Chris@0 114 es = entries(search_path, identifier)
Chris@0 115 es ? es.detect {|e| e.name == search_name} : nil
Chris@0 116 end
Chris@0 117 end
Chris@441 118
Chris@0 119 # Returns an Entries collection
Chris@0 120 # or nil if the given path doesn't exist in the repository
Chris@441 121 def entries(path=nil, identifier=nil, options={})
Chris@0 122 return nil
Chris@0 123 end
Chris@0 124
Chris@0 125 def branches
Chris@0 126 return nil
Chris@0 127 end
Chris@0 128
Chris@441 129 def tags
Chris@0 130 return nil
Chris@0 131 end
Chris@0 132
Chris@0 133 def default_branch
Chris@0 134 return nil
Chris@0 135 end
Chris@441 136
Chris@0 137 def properties(path, identifier=nil)
Chris@0 138 return nil
Chris@0 139 end
Chris@441 140
Chris@0 141 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
Chris@0 142 return nil
Chris@0 143 end
Chris@441 144
Chris@0 145 def diff(path, identifier_from, identifier_to=nil)
Chris@0 146 return nil
Chris@0 147 end
Chris@441 148
Chris@0 149 def cat(path, identifier=nil)
Chris@0 150 return nil
Chris@0 151 end
Chris@441 152
Chris@0 153 def with_leading_slash(path)
Chris@0 154 path ||= ''
Chris@0 155 (path[0,1]!="/") ? "/#{path}" : path
Chris@0 156 end
Chris@0 157
Chris@0 158 def with_trailling_slash(path)
Chris@0 159 path ||= ''
Chris@0 160 (path[-1,1] == "/") ? path : "#{path}/"
Chris@0 161 end
Chris@245 162
Chris@0 163 def without_leading_slash(path)
Chris@0 164 path ||= ''
Chris@0 165 path.gsub(%r{^/+}, '')
Chris@0 166 end
Chris@0 167
Chris@0 168 def without_trailling_slash(path)
Chris@0 169 path ||= ''
Chris@0 170 (path[-1,1] == "/") ? path[0..-2] : path
Chris@0 171 end
Chris@245 172
Chris@0 173 def shell_quote(str)
Chris@245 174 self.class.shell_quote(str)
Chris@0 175 end
Chris@0 176
Chris@0 177 private
Chris@0 178 def retrieve_root_url
Chris@0 179 info = self.info
Chris@0 180 info ? info.root_url : nil
Chris@0 181 end
Chris@441 182
Chris@0 183 def target(path)
Chris@0 184 path ||= ''
Chris@0 185 base = path.match(/^\//) ? root_url : url
Chris@0 186 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
Chris@0 187 end
Chris@245 188
Chris@0 189 def logger
Chris@0 190 self.class.logger
Chris@0 191 end
Chris@245 192
Chris@0 193 def shellout(cmd, &block)
Chris@0 194 self.class.shellout(cmd, &block)
Chris@0 195 end
Chris@245 196
Chris@0 197 def self.logger
Chris@0 198 RAILS_DEFAULT_LOGGER
Chris@0 199 end
Chris@245 200
Chris@0 201 def self.shellout(cmd, &block)
Chris@507 202 if logger && logger.debug?
Chris@507 203 logger.debug "Shelling out: #{strip_credential(cmd)}"
Chris@507 204 end
Chris@0 205 if Rails.env == 'development'
Chris@0 206 # Capture stderr when running in dev environment
Chris@0 207 cmd = "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log"
Chris@0 208 end
Chris@0 209 begin
Chris@245 210 if RUBY_VERSION < '1.9'
Chris@245 211 mode = "r+"
Chris@245 212 else
Chris@245 213 mode = "r+:ASCII-8BIT"
Chris@245 214 end
Chris@245 215 IO.popen(cmd, mode) do |io|
Chris@0 216 io.close_write
Chris@0 217 block.call(io) if block_given?
Chris@0 218 end
Chris@0 219 rescue Errno::ENOENT => e
Chris@0 220 msg = strip_credential(e.message)
Chris@0 221 # The command failed, log it and re-raise
Chris@507 222 logmsg = "SCM command failed, "
Chris@507 223 logmsg += "make sure that your SCM command (e.g. svn) is "
Chris@507 224 logmsg += "in PATH (#{ENV['PATH']})\n"
Chris@507 225 logmsg += "You can configure your scm commands in config/configuration.yml.\n"
Chris@507 226 logmsg += "#{strip_credential(cmd)}\n"
Chris@507 227 logmsg += "with: #{msg}"
Chris@507 228 logger.error(logmsg)
Chris@0 229 raise CommandFailed.new(msg)
Chris@0 230 end
Chris@245 231 end
Chris@245 232
Chris@0 233 # Hides username/password in a given command
Chris@0 234 def self.strip_credential(cmd)
Chris@0 235 q = (Redmine::Platform.mswin? ? '"' : "'")
Chris@0 236 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
Chris@0 237 end
Chris@441 238
Chris@0 239 def strip_credential(cmd)
Chris@0 240 self.class.strip_credential(cmd)
Chris@0 241 end
Chris@245 242
Chris@245 243 def scm_iconv(to, from, str)
Chris@245 244 return nil if str.nil?
Chris@922 245 # bug 446: non-utf8 paths in repositories blow up repo viewer and reposman
Chris@922 246 # -- Remove this short-circuit: we want the conversion to
Chris@922 247 # happen always, so we can trap the error here if the
Chris@922 248 # source text happens not to be in the advertised
Chris@922 249 # encoding (instead of having the database blow up later)
Chris@922 250 # return str if to == from
Chris@245 251 begin
Chris@245 252 Iconv.conv(to, from, str)
Chris@245 253 rescue Iconv::Failure => err
Chris@245 254 logger.error("failed to convert from #{from} to #{to}. #{err}")
Chris@245 255 nil
Chris@245 256 end
Chris@245 257 end
Chris@0 258 end
Chris@245 259
Chris@0 260 class Entries < Array
Chris@0 261 def sort_by_name
Chris@441 262 sort {|x,y|
Chris@0 263 if x.kind == y.kind
Chris@0 264 x.name.to_s <=> y.name.to_s
Chris@0 265 else
Chris@0 266 x.kind <=> y.kind
Chris@0 267 end
Chris@245 268 }
Chris@0 269 end
Chris@441 270
Chris@0 271 def revisions
Chris@0 272 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
Chris@0 273 end
Chris@0 274 end
Chris@441 275
Chris@0 276 class Info
Chris@0 277 attr_accessor :root_url, :lastrev
Chris@0 278 def initialize(attributes={})
Chris@0 279 self.root_url = attributes[:root_url] if attributes[:root_url]
Chris@0 280 self.lastrev = attributes[:lastrev]
Chris@0 281 end
Chris@0 282 end
Chris@441 283
Chris@0 284 class Entry
Chris@0 285 attr_accessor :name, :path, :kind, :size, :lastrev
Chris@0 286 def initialize(attributes={})
Chris@0 287 self.name = attributes[:name] if attributes[:name]
Chris@0 288 self.path = attributes[:path] if attributes[:path]
Chris@0 289 self.kind = attributes[:kind] if attributes[:kind]
Chris@0 290 self.size = attributes[:size].to_i if attributes[:size]
Chris@0 291 self.lastrev = attributes[:lastrev]
Chris@0 292 end
Chris@441 293
Chris@0 294 def is_file?
Chris@0 295 'file' == self.kind
Chris@0 296 end
Chris@441 297
Chris@0 298 def is_dir?
Chris@0 299 'dir' == self.kind
Chris@0 300 end
Chris@441 301
Chris@0 302 def is_text?
Chris@0 303 Redmine::MimeType.is_type?('text', name)
Chris@0 304 end
Chris@0 305 end
Chris@441 306
Chris@0 307 class Revisions < Array
Chris@0 308 def latest
Chris@0 309 sort {|x,y|
Chris@0 310 unless x.time.nil? or y.time.nil?
Chris@0 311 x.time <=> y.time
Chris@0 312 else
Chris@0 313 0
Chris@0 314 end
Chris@0 315 }.last
Chris@441 316 end
Chris@0 317 end
Chris@441 318
Chris@0 319 class Revision
Chris@441 320 attr_accessor :scmid, :name, :author, :time, :message,
Chris@441 321 :paths, :revision, :branch, :identifier
Chris@0 322
Chris@0 323 def initialize(attributes={})
Chris@0 324 self.identifier = attributes[:identifier]
Chris@441 325 self.scmid = attributes[:scmid]
Chris@441 326 self.name = attributes[:name] || self.identifier
Chris@441 327 self.author = attributes[:author]
Chris@441 328 self.time = attributes[:time]
Chris@441 329 self.message = attributes[:message] || ""
Chris@441 330 self.paths = attributes[:paths]
Chris@441 331 self.revision = attributes[:revision]
Chris@441 332 self.branch = attributes[:branch]
Chris@117 333 end
Chris@117 334
Chris@117 335 # Returns the readable identifier.
Chris@117 336 def format_identifier
Chris@441 337 self.identifier.to_s
Chris@117 338 end
Chris@245 339 end
Chris@117 340
Chris@0 341 class Annotate
Chris@0 342 attr_reader :lines, :revisions
Chris@441 343
Chris@0 344 def initialize
Chris@0 345 @lines = []
Chris@0 346 @revisions = []
Chris@0 347 end
Chris@441 348
Chris@0 349 def add_line(line, revision)
Chris@0 350 @lines << line
Chris@0 351 @revisions << revision
Chris@0 352 end
Chris@441 353
Chris@0 354 def content
Chris@0 355 content = lines.join("\n")
Chris@0 356 end
Chris@441 357
Chris@0 358 def empty?
Chris@0 359 lines.empty?
Chris@0 360 end
Chris@0 361 end
Chris@0 362 end
Chris@0 363 end
Chris@0 364 end