annotate lib/redmine/scm/adapters/subversion_adapter.rb @ 252:adc8466df404 cannam

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