annotate lib/redmine/scm/adapters/subversion_adapter.rb @ 8:0c83d98252d9 yuya

* Add custom repo prefix and proper auth realm, remove auth cache (seems like an unwise feature), pass DB handle around, various other bits of tidying
author Chris Cannam
date Thu, 12 Aug 2010 15:31:37 +0100
parents 513646585e45
children af80e5618e9b
rev   line source
Chris@0 1 # Redmine - project management software
Chris@0 2 # Copyright (C) 2006-2010 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@0 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@0 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 'redmine/scm/adapters/abstract_adapter'
Chris@0 19 require 'uri'
Chris@0 20
Chris@0 21 module Redmine
Chris@0 22 module Scm
Chris@0 23 module Adapters
Chris@0 24 class SubversionAdapter < AbstractAdapter
Chris@0 25
Chris@0 26 # SVN executable name
Chris@0 27 SVN_BIN = "svn"
Chris@0 28
Chris@0 29 class << self
Chris@0 30 def client_version
Chris@0 31 @@client_version ||= (svn_binary_version || [])
Chris@0 32 end
Chris@0 33
Chris@0 34 def svn_binary_version
Chris@0 35 cmd = "#{SVN_BIN} --version"
Chris@0 36 version = nil
Chris@0 37 shellout(cmd) do |io|
Chris@0 38 # Read svn version in first returned line
Chris@0 39 if m = io.gets.to_s.match(%r{((\d+\.)+\d+)})
Chris@0 40 version = m[0].scan(%r{\d+}).collect(&:to_i)
Chris@0 41 end
Chris@0 42 end
Chris@0 43 return nil if $? && $?.exitstatus != 0
Chris@0 44 version
Chris@0 45 end
Chris@0 46 end
Chris@0 47
Chris@0 48 # Get info about the svn repository
Chris@0 49 def info
Chris@0 50 cmd = "#{SVN_BIN} info --xml #{target}"
Chris@0 51 cmd << credentials_string
Chris@0 52 info = nil
Chris@0 53 shellout(cmd) do |io|
Chris@0 54 output = io.read
Chris@0 55 begin
Chris@0 56 doc = ActiveSupport::XmlMini.parse(output)
Chris@0 57 #root_url = doc.elements["info/entry/repository/root"].text
Chris@0 58 info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
Chris@0 59 :lastrev => Revision.new({
Chris@0 60 :identifier => doc['info']['entry']['commit']['revision'],
Chris@0 61 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
Chris@0 62 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
Chris@0 63 })
Chris@0 64 })
Chris@0 65 rescue
Chris@0 66 end
Chris@0 67 end
Chris@0 68 return nil if $? && $?.exitstatus != 0
Chris@0 69 info
Chris@0 70 rescue CommandFailed
Chris@0 71 return nil
Chris@0 72 end
Chris@0 73
Chris@0 74 # Returns an Entries collection
Chris@0 75 # or nil if the given path doesn't exist in the repository
Chris@0 76 def entries(path=nil, identifier=nil)
Chris@0 77 path ||= ''
Chris@0 78 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
Chris@0 79 entries = Entries.new
Chris@0 80 cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
Chris@0 81 cmd << credentials_string
Chris@0 82 shellout(cmd) do |io|
Chris@0 83 output = io.read
Chris@0 84 begin
Chris@0 85 doc = ActiveSupport::XmlMini.parse(output)
Chris@0 86 each_xml_element(doc['lists']['list'], 'entry') do |entry|
Chris@0 87 commit = entry['commit']
Chris@0 88 commit_date = commit['date']
Chris@0 89 # Skip directory if there is no commit date (usually that
Chris@0 90 # means that we don't have read access to it)
Chris@0 91 next if entry['kind'] == 'dir' && commit_date.nil?
Chris@0 92 name = entry['name']['__content__']
Chris@0 93 entries << Entry.new({:name => URI.unescape(name),
Chris@0 94 :path => ((path.empty? ? "" : "#{path}/") + name),
Chris@0 95 :kind => entry['kind'],
Chris@0 96 :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
Chris@0 97 :lastrev => Revision.new({
Chris@0 98 :identifier => commit['revision'],
Chris@0 99 :time => Time.parse(commit_date['__content__'].to_s).localtime,
Chris@0 100 :author => ((a = commit['author']) ? a['__content__'] : nil)
Chris@0 101 })
Chris@0 102 })
Chris@0 103 end
Chris@0 104 rescue Exception => e
Chris@0 105 logger.error("Error parsing svn output: #{e.message}")
Chris@0 106 logger.error("Output was:\n #{output}")
Chris@0 107 end
Chris@0 108 end
Chris@0 109 return nil if $? && $?.exitstatus != 0
Chris@0 110 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
Chris@0 111 entries.sort_by_name
Chris@0 112 end
Chris@0 113
Chris@0 114 def properties(path, identifier=nil)
Chris@0 115 # proplist xml output supported in svn 1.5.0 and higher
Chris@0 116 return nil unless self.class.client_version_above?([1, 5, 0])
Chris@0 117
Chris@0 118 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
Chris@0 119 cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
Chris@0 120 cmd << credentials_string
Chris@0 121 properties = {}
Chris@0 122 shellout(cmd) do |io|
Chris@0 123 output = io.read
Chris@0 124 begin
Chris@0 125 doc = ActiveSupport::XmlMini.parse(output)
Chris@0 126 each_xml_element(doc['properties']['target'], 'property') do |property|
Chris@0 127 properties[ property['name'] ] = property['__content__'].to_s
Chris@0 128 end
Chris@0 129 rescue
Chris@0 130 end
Chris@0 131 end
Chris@0 132 return nil if $? && $?.exitstatus != 0
Chris@0 133 properties
Chris@0 134 end
Chris@0 135
Chris@0 136 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
Chris@0 137 path ||= ''
Chris@0 138 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
Chris@0 139 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
Chris@0 140 revisions = Revisions.new
Chris@0 141 cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
Chris@0 142 cmd << credentials_string
Chris@0 143 cmd << " --verbose " if options[:with_paths]
Chris@0 144 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
Chris@0 145 cmd << ' ' + target(path)
Chris@0 146 shellout(cmd) do |io|
Chris@0 147 output = io.read
Chris@0 148 begin
Chris@0 149 doc = ActiveSupport::XmlMini.parse(output)
Chris@0 150 each_xml_element(doc['log'], 'logentry') do |logentry|
Chris@0 151 paths = []
Chris@0 152 each_xml_element(logentry['paths'], 'path') do |path|
Chris@0 153 paths << {:action => path['action'],
Chris@0 154 :path => path['__content__'],
Chris@0 155 :from_path => path['copyfrom-path'],
Chris@0 156 :from_revision => path['copyfrom-rev']
Chris@0 157 }
Chris@0 158 end if logentry['paths'] && logentry['paths']['path']
Chris@0 159 paths.sort! { |x,y| x[:path] <=> y[:path] }
Chris@0 160
Chris@0 161 revisions << Revision.new({:identifier => logentry['revision'],
Chris@0 162 :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
Chris@0 163 :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
Chris@0 164 :message => logentry['msg']['__content__'],
Chris@0 165 :paths => paths
Chris@0 166 })
Chris@0 167 end
Chris@0 168 rescue
Chris@0 169 end
Chris@0 170 end
Chris@0 171 return nil if $? && $?.exitstatus != 0
Chris@0 172 revisions
Chris@0 173 end
Chris@0 174
Chris@0 175 def diff(path, identifier_from, identifier_to=nil, type="inline")
Chris@0 176 path ||= ''
Chris@0 177 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
Chris@0 178 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
Chris@0 179
Chris@0 180 cmd = "#{SVN_BIN} diff -r "
Chris@0 181 cmd << "#{identifier_to}:"
Chris@0 182 cmd << "#{identifier_from}"
Chris@0 183 cmd << " #{target(path)}@#{identifier_from}"
Chris@0 184 cmd << credentials_string
Chris@0 185 diff = []
Chris@0 186 shellout(cmd) do |io|
Chris@0 187 io.each_line do |line|
Chris@0 188 diff << line
Chris@0 189 end
Chris@0 190 end
Chris@0 191 return nil if $? && $?.exitstatus != 0
Chris@0 192 diff
Chris@0 193 end
Chris@0 194
Chris@0 195 def cat(path, identifier=nil)
Chris@0 196 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
Chris@0 197 cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
Chris@0 198 cmd << credentials_string
Chris@0 199 cat = nil
Chris@0 200 shellout(cmd) do |io|
Chris@0 201 io.binmode
Chris@0 202 cat = io.read
Chris@0 203 end
Chris@0 204 return nil if $? && $?.exitstatus != 0
Chris@0 205 cat
Chris@0 206 end
Chris@0 207
Chris@0 208 def annotate(path, identifier=nil)
Chris@0 209 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
Chris@0 210 cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
Chris@0 211 cmd << credentials_string
Chris@0 212 blame = Annotate.new
Chris@0 213 shellout(cmd) do |io|
Chris@0 214 io.each_line do |line|
Chris@0 215 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
Chris@0 216 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
Chris@0 217 end
Chris@0 218 end
Chris@0 219 return nil if $? && $?.exitstatus != 0
Chris@0 220 blame
Chris@0 221 end
Chris@0 222
Chris@0 223 private
Chris@0 224
Chris@0 225 def credentials_string
Chris@0 226 str = ''
Chris@0 227 str << " --username #{shell_quote(@login)}" unless @login.blank?
Chris@0 228 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
Chris@0 229 str << " --no-auth-cache --non-interactive"
Chris@0 230 str
Chris@0 231 end
Chris@0 232
Chris@0 233 # Helper that iterates over the child elements of a xml node
Chris@0 234 # MiniXml returns a hash when a single child is found or an array of hashes for multiple children
Chris@0 235 def each_xml_element(node, name)
Chris@0 236 if node && node[name]
Chris@0 237 if node[name].is_a?(Hash)
Chris@0 238 yield node[name]
Chris@0 239 else
Chris@0 240 node[name].each do |element|
Chris@0 241 yield element
Chris@0 242 end
Chris@0 243 end
Chris@0 244 end
Chris@0 245 end
Chris@0 246
Chris@0 247 def target(path = '')
Chris@0 248 base = path.match(/^\//) ? root_url : url
Chris@0 249 uri = "#{base}/#{path}"
Chris@0 250 uri = URI.escape(URI.escape(uri), '[]')
Chris@0 251 shell_quote(uri.gsub(/[?<>\*]/, ''))
Chris@0 252 end
Chris@0 253 end
Chris@0 254 end
Chris@0 255 end
Chris@0 256 end