| 1 | # Redmine - project management software
 | 
  
    | 2 | # Copyright (C) 2006-2010  Jean-Philippe Lang
 | 
  
    | 3 | #
 | 
  
    | 4 | # This program is free software; you can redistribute it and/or
 | 
  
    | 5 | # modify it under the terms of the GNU General Public License
 | 
  
    | 6 | # as published by the Free Software Foundation; either version 2
 | 
  
    | 7 | # of the License, or (at your option) any later version.
 | 
  
    | 8 | # 
 | 
  
    | 9 | # This program is distributed in the hope that it will be useful,
 | 
  
    | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
  
    | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
  
    | 12 | # GNU General Public License for more details.
 | 
  
    | 13 | # 
 | 
  
    | 14 | # You should have received a copy of the GNU General Public License
 | 
  
    | 15 | # along with this program; if not, write to the Free Software
 | 
  
    | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 | 
  
    | 17 | 
 | 
  
    | 18 | require 'redmine/scm/adapters/abstract_adapter'
 | 
  
    | 19 | require 'uri'
 | 
  
    | 20 | 
 | 
  
    | 21 | module Redmine
 | 
  
    | 22 |   module Scm
 | 
  
    | 23 |     module Adapters    
 | 
  
    | 24 |       class SubversionAdapter < AbstractAdapter
 | 
  
    | 25 |       
 | 
  
    | 26 |         # SVN executable name
 | 
  
    | 27 |         SVN_BIN = "svn"
 | 
  
    | 28 |         
 | 
  
    | 29 |         class << self
 | 
  
    | 30 |           def client_version
 | 
  
    | 31 |             @@client_version ||= (svn_binary_version || [])
 | 
  
    | 32 |           end
 | 
  
    | 33 |           
 | 
  
    | 34 |           def svn_binary_version
 | 
  
    | 35 |             cmd = "#{SVN_BIN} --version"
 | 
  
    | 36 |             version = nil
 | 
  
    | 37 |             shellout(cmd) do |io|
 | 
  
    | 38 |               # Read svn version in first returned line
 | 
  
    | 39 |               if m = io.read.to_s.match(%r{\A(.*?)((\d+\.)+\d+)})
 | 
  
    | 40 |                 version = m[2].scan(%r{\d+}).collect(&:to_i)
 | 
  
    | 41 |               end
 | 
  
    | 42 |             end
 | 
  
    | 43 |             return nil if $? && $?.exitstatus != 0
 | 
  
    | 44 |             version
 | 
  
    | 45 |           end
 | 
  
    | 46 |         end
 | 
  
    | 47 |         
 | 
  
    | 48 |         # Get info about the svn repository
 | 
  
    | 49 |         def info
 | 
  
    | 50 |           cmd = "#{SVN_BIN} info --xml #{target}"
 | 
  
    | 51 |           cmd << credentials_string
 | 
  
    | 52 |           info = nil
 | 
  
    | 53 |           shellout(cmd) do |io|
 | 
  
    | 54 |             output = io.read
 | 
  
    | 55 |             begin
 | 
  
    | 56 |               doc = ActiveSupport::XmlMini.parse(output)
 | 
  
    | 57 |               #root_url = doc.elements["info/entry/repository/root"].text          
 | 
  
    | 58 |               info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
 | 
  
    | 59 |                                :lastrev => Revision.new({
 | 
  
    | 60 |                                  :identifier => doc['info']['entry']['commit']['revision'],
 | 
  
    | 61 |                                  :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
 | 
  
    | 62 |                                  :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
 | 
  
    | 63 |                                })
 | 
  
    | 64 |                              })
 | 
  
    | 65 |             rescue
 | 
  
    | 66 |             end
 | 
  
    | 67 |           end
 | 
  
    | 68 |           return nil if $? && $?.exitstatus != 0
 | 
  
    | 69 |           info
 | 
  
    | 70 |         rescue CommandFailed
 | 
  
    | 71 |           return nil
 | 
  
    | 72 |         end
 | 
  
    | 73 |         
 | 
  
    | 74 |         # Returns an Entries collection
 | 
  
    | 75 |         # or nil if the given path doesn't exist in the repository
 | 
  
    | 76 |         def entries(path=nil, identifier=nil)
 | 
  
    | 77 |           path ||= ''
 | 
  
    | 78 |           identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
 | 
  
    | 79 |           entries = Entries.new
 | 
  
    | 80 |           cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
 | 
  
    | 81 |           cmd << credentials_string
 | 
  
    | 82 |           shellout(cmd) do |io|
 | 
  
    | 83 |             output = io.read
 | 
  
    | 84 |             begin
 | 
  
    | 85 |               doc = ActiveSupport::XmlMini.parse(output)
 | 
  
    | 86 |               each_xml_element(doc['lists']['list'], 'entry') do |entry|
 | 
  
    | 87 |                 commit = entry['commit']
 | 
  
    | 88 |                 commit_date = commit['date']
 | 
  
    | 89 |                 # Skip directory if there is no commit date (usually that
 | 
  
    | 90 |                 # means that we don't have read access to it)
 | 
  
    | 91 |                 next if entry['kind'] == 'dir' && commit_date.nil?
 | 
  
    | 92 |                 name = entry['name']['__content__']
 | 
  
    | 93 |                 entries << Entry.new({:name => URI.unescape(name),
 | 
  
    | 94 |                             :path => ((path.empty? ? "" : "#{path}/") + name),
 | 
  
    | 95 |                             :kind => entry['kind'],
 | 
  
    | 96 |                             :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
 | 
  
    | 97 |                             :lastrev => Revision.new({
 | 
  
    | 98 |                               :identifier => commit['revision'],
 | 
  
    | 99 |                               :time => Time.parse(commit_date['__content__'].to_s).localtime,
 | 
  
    | 100 |                               :author => ((a = commit['author']) ? a['__content__'] : nil)
 | 
  
    | 101 |                               })
 | 
  
    | 102 |                             })
 | 
  
    | 103 |               end
 | 
  
    | 104 |             rescue Exception => e
 | 
  
    | 105 |               logger.error("Error parsing svn output: #{e.message}")
 | 
  
    | 106 |               logger.error("Output was:\n #{output}")
 | 
  
    | 107 |             end
 | 
  
    | 108 |           end
 | 
  
    | 109 |           return nil if $? && $?.exitstatus != 0
 | 
  
    | 110 |           logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
 | 
  
    | 111 |           entries.sort_by_name
 | 
  
    | 112 |         end
 | 
  
    | 113 |         
 | 
  
    | 114 |         def properties(path, identifier=nil)
 | 
  
    | 115 |           # proplist xml output supported in svn 1.5.0 and higher
 | 
  
    | 116 |           return nil unless self.class.client_version_above?([1, 5, 0])
 | 
  
    | 117 |           
 | 
  
    | 118 |           identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
 | 
  
    | 119 |           cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
 | 
  
    | 120 |           cmd << credentials_string
 | 
  
    | 121 |           properties = {}
 | 
  
    | 122 |           shellout(cmd) do |io|
 | 
  
    | 123 |             output = io.read
 | 
  
    | 124 |             begin
 | 
  
    | 125 |               doc = ActiveSupport::XmlMini.parse(output)
 | 
  
    | 126 |               each_xml_element(doc['properties']['target'], 'property') do |property|
 | 
  
    | 127 |                 properties[ property['name'] ] = property['__content__'].to_s
 | 
  
    | 128 |               end
 | 
  
    | 129 |             rescue
 | 
  
    | 130 |             end
 | 
  
    | 131 |           end
 | 
  
    | 132 |           return nil if $? && $?.exitstatus != 0
 | 
  
    | 133 |           properties
 | 
  
    | 134 |         end
 | 
  
    | 135 |         
 | 
  
    | 136 |         def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
 | 
  
    | 137 |           path ||= ''
 | 
  
    | 138 |           identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
 | 
  
    | 139 |           identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
 | 
  
    | 140 |           revisions = Revisions.new
 | 
  
    | 141 |           cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
 | 
  
    | 142 |           cmd << credentials_string
 | 
  
    | 143 |           cmd << " --verbose " if  options[:with_paths]
 | 
  
    | 144 |           cmd << " --limit #{options[:limit].to_i}" if options[:limit]
 | 
  
    | 145 |           cmd << ' ' + target(path)
 | 
  
    | 146 |           logger.info 'before svn command'
 | 
  
    | 147 |           shellout(cmd) do |io|
 | 
  
    | 148 |             output = io.read
 | 
  
    | 149 |             begin
 | 
  
    | 150 |               logger.info 'before XML parsing'
 | 
  
    | 151 |               doc = ActiveSupport::XmlMini.with_backend('LibXML').parse(output)
 | 
  
    | 152 |               each_xml_element(doc['log'], 'logentry') do |logentry|
 | 
  
    | 153 | 
 | 
  
    | 154 |                logger.info ('reading revision ' + logentry['revision'])
 | 
  
    | 155 | 
 | 
  
    | 156 |                 paths = []
 | 
  
    | 157 |                 each_xml_element(logentry['paths'], 'path') do |path|
 | 
  
    | 158 | 
 | 
  
    | 159 |                   logger.info ('reading path ' + path['__content__'])
 | 
  
    | 160 | 
 | 
  
    | 161 |                   paths << {:action => path['action'],
 | 
  
    | 162 |                             :path => path['__content__'],
 | 
  
    | 163 |                             :from_path => path['copyfrom-path'],
 | 
  
    | 164 |                             :from_revision => path['copyfrom-rev']
 | 
  
    | 165 |                             }
 | 
  
    | 166 |                 end if logentry['paths'] && logentry['paths']['path']
 | 
  
    | 167 | 
 | 
  
    | 168 |                 logger.info 'before paths array sorting'
 | 
  
    | 169 | 
 | 
  
    | 170 |                 paths.sort! { |x,y| x[:path] <=> y[:path] }
 | 
  
    | 171 |                 
 | 
  
    | 172 |                 revisions << Revision.new({:identifier => logentry['revision'],
 | 
  
    | 173 |                               :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
 | 
  
    | 174 |                               :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
 | 
  
    | 175 |                               :message => logentry['msg']['__content__'],
 | 
  
    | 176 |                               :paths => paths
 | 
  
    | 177 |                             })
 | 
  
    | 178 |               end
 | 
  
    | 179 |             rescue
 | 
  
    | 180 |              logger.info 'exception thrown'
 | 
  
    | 181 |             end
 | 
  
    | 182 |           end
 | 
  
    | 183 |           logger.info 'end of fetching'
 | 
  
    | 184 |           return nil if $? && $?.exitstatus != 0
 | 
  
    | 185 |           revisions
 | 
  
    | 186 |         end
 | 
  
    | 187 |         
 | 
  
    | 188 |         def diff(path, identifier_from, identifier_to=nil, type="inline")
 | 
  
    | 189 |           path ||= ''
 | 
  
    | 190 |           identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
 | 
  
    | 191 |           identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
 | 
  
    | 192 |           
 | 
  
    | 193 |           cmd = "#{SVN_BIN} diff -r "
 | 
  
    | 194 |           cmd << "#{identifier_to}:"
 | 
  
    | 195 |           cmd << "#{identifier_from}"
 | 
  
    | 196 |           cmd << " #{target(path)}@#{identifier_from}"
 | 
  
    | 197 |           cmd << credentials_string
 | 
  
    | 198 |           diff = []
 | 
  
    | 199 |           shellout(cmd) do |io|
 | 
  
    | 200 |             io.each_line do |line|
 | 
  
    | 201 |               diff << line
 | 
  
    | 202 |             end
 | 
  
    | 203 |           end
 | 
  
    | 204 |           return nil if $? && $?.exitstatus != 0
 | 
  
    | 205 |           diff
 | 
  
    | 206 |         end
 | 
  
    | 207 |         
 | 
  
    | 208 |         def cat(path, identifier=nil)
 | 
  
    | 209 |           identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
 | 
  
    | 210 |           cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
 | 
  
    | 211 |           cmd << credentials_string
 | 
  
    | 212 |           cat = nil
 | 
  
    | 213 |           shellout(cmd) do |io|
 | 
  
    | 214 |             io.binmode
 | 
  
    | 215 |             cat = io.read
 | 
  
    | 216 |           end
 | 
  
    | 217 |           return nil if $? && $?.exitstatus != 0
 | 
  
    | 218 |           cat
 | 
  
    | 219 |         end
 | 
  
    | 220 |         
 | 
  
    | 221 |         def annotate(path, identifier=nil)
 | 
  
    | 222 |           identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
 | 
  
    | 223 |           cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
 | 
  
    | 224 |           cmd << credentials_string
 | 
  
    | 225 |           blame = Annotate.new
 | 
  
    | 226 |           shellout(cmd) do |io|
 | 
  
    | 227 |             io.each_line do |line|
 | 
  
    | 228 |               next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
 | 
  
    | 229 |               blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
 | 
  
    | 230 |             end
 | 
  
    | 231 |           end
 | 
  
    | 232 |           return nil if $? && $?.exitstatus != 0
 | 
  
    | 233 |           blame
 | 
  
    | 234 |         end
 | 
  
    | 235 |         
 | 
  
    | 236 |         private
 | 
  
    | 237 |         
 | 
  
    | 238 |         def credentials_string
 | 
  
    | 239 |           str = ''
 | 
  
    | 240 |           str << " --username #{shell_quote(@login)}" unless @login.blank?
 | 
  
    | 241 |           str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
 | 
  
    | 242 |           str << " --no-auth-cache --non-interactive"
 | 
  
    | 243 |           str
 | 
  
    | 244 |         end
 | 
  
    | 245 |         
 | 
  
    | 246 |         # Helper that iterates over the child elements of a xml node
 | 
  
    | 247 |         # MiniXml returns a hash when a single child is found or an array of hashes for multiple children
 | 
  
    | 248 |         def each_xml_element(node, name)
 | 
  
    | 249 |           if node && node[name]
 | 
  
    | 250 |             if node[name].is_a?(Hash)
 | 
  
    | 251 |               yield node[name]
 | 
  
    | 252 |             else
 | 
  
    | 253 |               node[name].each do |element|
 | 
  
    | 254 |                 yield element
 | 
  
    | 255 |               end
 | 
  
    | 256 |             end
 | 
  
    | 257 |           end
 | 
  
    | 258 |         end
 | 
  
    | 259 | 
 | 
  
    | 260 |         def target(path = '')
 | 
  
    | 261 |           base = path.match(/^\//) ? root_url : url
 | 
  
    | 262 |           uri = "#{base}/#{path}"
 | 
  
    | 263 |           uri = URI.escape(URI.escape(uri), '[]')
 | 
  
    | 264 |           shell_quote(uri.gsub(/[?<>\*]/, ''))
 | 
  
    | 265 |         end
 | 
  
    | 266 |       end
 | 
  
    | 267 |     end
 | 
  
    | 268 |   end
 | 
  
    | 269 | end
 |