PageRenderTime 26ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/open_project/scm/adapters/subversion.rb

https://gitlab.com/MichelZuniga/openproject
Ruby | 333 lines | 219 code | 51 blank | 63 comment | 21 complexity | 2b403fc049cfbbc47821e06be24be1d8 MD5 | raw file
  1. #-- encoding: UTF-8
  2. #-- copyright
  3. # OpenProject is a project management system.
  4. # Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License version 3.
  8. #
  9. # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
  10. # Copyright (C) 2006-2013 Jean-Philippe Lang
  11. # Copyright (C) 2010-2013 the ChiliProject Team
  12. #
  13. # This program is free software; you can redistribute it and/or
  14. # modify it under the terms of the GNU General Public License
  15. # as published by the Free Software Foundation; either version 2
  16. # of the License, or (at your option) any later version.
  17. #
  18. # This program is distributed in the hope that it will be useful,
  19. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. # GNU General Public License for more details.
  22. #
  23. # You should have received a copy of the GNU General Public License
  24. # along with this program; if not, write to the Free Software
  25. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  26. #
  27. # See doc/COPYRIGHT.rdoc for more details.
  28. #++
  29. require 'uri'
  30. module OpenProject
  31. module Scm
  32. module Adapters
  33. class Subversion < Base
  34. include LocalClient
  35. def client_command
  36. @client_command ||= self.class.config[:client_command] || 'svn'
  37. end
  38. def svnadmin_command
  39. @svnadmin_command ||= (self.class.config[:svnadmin_command] || 'svnadmin')
  40. end
  41. def client_version
  42. @client_version ||= (svn_binary_version || [])
  43. end
  44. def svn_binary_version
  45. scm_version = scm_version_from_command_line.dup
  46. m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
  47. if m
  48. m[2].scan(%r{\d+}).map(&:to_i)
  49. end
  50. end
  51. def scm_version_from_command_line
  52. capture_out('--version')
  53. end
  54. ##
  55. # Subversion may be local or remote,
  56. # for now determine it by the URL type.
  57. def local?
  58. url.start_with?('file://')
  59. end
  60. ##
  61. # Returns the local repository path
  62. # (if applicable).
  63. def local_repository_path
  64. root_url.sub('file://', '')
  65. end
  66. def initialize(url, root_url = nil, login = nil, password = nil, _path_encoding = nil)
  67. super(url, root_url)
  68. @login = login
  69. @password = password
  70. end
  71. ##
  72. # Checks the status of this repository and throws unless it can be accessed
  73. # correctly by the adapter.
  74. #
  75. # @raise [ScmUnavailable] raised when repository is unavailable.
  76. def check_availability!
  77. # Check whether we can access svn repository uuid
  78. popen3(['info', '--xml', target]) do |stdout, stderr|
  79. doc = Nokogiri::XML(stdout.read)
  80. raise Exceptions::ScmEmpty if doc.at_xpath('/info/entry/commit[@revision="0"]')
  81. return if doc.at_xpath('/info/entry/repository/uuid')
  82. raise Exceptions::ScmUnauthorized.new if io_include?(stderr,
  83. 'E215004: Authentication failed')
  84. end
  85. raise Exceptions::ScmUnavailable
  86. end
  87. ##
  88. # Creates an empty repository using svnadmin
  89. #
  90. def create_empty_svn
  91. _, err, code = Open3.capture3(svnadmin_command, 'create', root_url)
  92. if code != 0
  93. msg = "Failed to create empty subversion repository with `#{svnadmin_command} create`"
  94. logger.error(msg)
  95. logger.debug("Error output is #{err}")
  96. raise Exceptions::CommandFailed.new(client_command, msg)
  97. end
  98. end
  99. # Get info about the svn repository
  100. def info
  101. cmd = build_svn_cmd(['info', '--xml', target])
  102. xml_capture(cmd, force_encoding: true) do |doc|
  103. Info.new(
  104. root_url: doc.xpath('/info/entry/repository/root').text,
  105. lastrev: extract_revision(doc.at_xpath('/info/entry/commit'))
  106. )
  107. end
  108. end
  109. def entries(path = nil, identifier = nil)
  110. path ||= ''
  111. identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
  112. entries = Entries.new
  113. cmd = ['list', '--xml', "#{target(path)}@#{identifier}"]
  114. xml_capture(cmd, force_encoding: true) do |doc|
  115. doc.xpath('/lists/list/entry').each { |list| entries << extract_entry(list, path) }
  116. end
  117. entries.sort_by_name
  118. end
  119. def properties(path, identifier = nil)
  120. # proplist xml output supported in svn 1.5.0 and higher
  121. return nil unless client_version_above?([1, 5, 0])
  122. identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
  123. cmd = ['proplist', '--verbose', '--xml', "#{target(path)}@#{identifier}"]
  124. properties = {}
  125. xml_capture(cmd, force_encoding: true) do |doc|
  126. doc.xpath('/properties/target/property').each do |prop|
  127. properties[prop['name']] = prop.text
  128. end
  129. end
  130. properties
  131. end
  132. def revisions(path = nil, identifier_from = nil, identifier_to = nil, options = {})
  133. revisions = Revisions.new
  134. fetch_revision_entries(identifier_from, identifier_to, options, path) do |logentry|
  135. paths = logentry.xpath('paths/path').map { |entry| build_path(entry) }
  136. paths.sort! { |x, y| x[:path] <=> y[:path] }
  137. r = extract_revision(logentry)
  138. r.paths = paths
  139. revisions << r
  140. end
  141. revisions
  142. end
  143. def diff(path, identifier_from, identifier_to = nil, _type = 'inline')
  144. path ||= ''
  145. identifier_from = numeric_identifier(identifier_from)
  146. identifier_to = numeric_identifier(identifier_to, identifier_from - 1)
  147. cmd = ['diff', '-r', "#{identifier_to}:#{identifier_from}",
  148. "#{target(path)}@#{identifier_from}"]
  149. capture_svn(cmd).lines.map(&:chomp)
  150. end
  151. def numeric_identifier(identifier, default = '')
  152. if identifier && identifier.to_i > 0
  153. identifier.to_i
  154. else
  155. default
  156. end
  157. end
  158. def cat(path, identifier = nil)
  159. identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
  160. cmd = ['cat', "#{target(path)}@#{identifier}"]
  161. capture_svn(cmd)
  162. end
  163. def annotate(path, identifier = nil)
  164. identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
  165. cmd = ['blame', "#{target(path)}@#{identifier}"]
  166. blame = Annotate.new
  167. popen3(cmd) do |io, _|
  168. io.each_line do |line|
  169. next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
  170. blame.add_line($3.rstrip, Revision.new(identifier: $1.to_i, author: $2.strip))
  171. end
  172. end
  173. blame
  174. end
  175. private
  176. ##
  177. # Builds the SVM command arguments around the given parameters
  178. # Appends to the parameter:
  179. # --username, --password if specified for this repository
  180. # --no-auth-cache force re-authentication
  181. # --non-interactive avoid prompts
  182. def build_svn_cmd(args)
  183. if @login.present?
  184. args.push('--username', shell_quote(@login))
  185. args.push('--password', shell_quote(@password)) if @password.present?
  186. end
  187. args.push('--no-auth-cache', '--non-interactive')
  188. end
  189. def xml_capture(cmd, opts = {})
  190. output = capture_svn(cmd, opts)
  191. doc = Nokogiri::XML(output)
  192. # Yield helper methods instead of doc
  193. yield doc
  194. end
  195. def extract_entry(entry, path)
  196. revision = extract_revision(entry.at_xpath('commit'))
  197. kind, size, name = parse_entry(entry)
  198. # Skip directory if there is no commit date (usually that
  199. # means that we don't have read access to it)
  200. return if kind == 'dir' && revision.time.nil?
  201. Entry.new(
  202. name: URI.unescape(name),
  203. path: ((path.empty? ? '' : "#{path}/") + name),
  204. kind: kind,
  205. size: size.empty? ? nil : size.to_i,
  206. lastrev: revision
  207. )
  208. end
  209. def parse_entry(entry)
  210. kind = entry['kind']
  211. size = entry.xpath('size').text
  212. name = entry.xpath('name').text
  213. [kind, size, name]
  214. end
  215. def build_path(entry)
  216. {
  217. action: entry['action'],
  218. path: entry.text,
  219. from_path: entry['copyfrom-path'],
  220. from_revision: entry['copyfrom-rev']
  221. }
  222. end
  223. def extract_revision(commit_node)
  224. # We may be unauthorized to read the commit date
  225. date =
  226. begin
  227. Time.parse(commit_node.xpath('date').text).localtime
  228. rescue ArgumentError
  229. nil
  230. end
  231. Revision.new(
  232. identifier: commit_node['revision'],
  233. time: date,
  234. message: commit_node.xpath('msg').text,
  235. author: commit_node.xpath('author').text
  236. )
  237. end
  238. def fetch_revision_entries(identifier_from, identifier_to, options, path, &block)
  239. path ||= ''
  240. identifier_from = numeric_identifier(identifier_from, 'HEAD')
  241. identifier_to = numeric_identifier(identifier_to, 1)
  242. cmd = ['log', '--xml', '-r', "#{identifier_from}:#{identifier_to}"]
  243. cmd << '--verbose' if options[:with_paths]
  244. cmd << '--limit' << options[:limit].to_s if options[:limit]
  245. cmd << target(path)
  246. xml_capture(cmd, force_encoding: true) do |doc|
  247. doc.xpath('/log/logentry').each &block
  248. end
  249. end
  250. def target(path = '')
  251. base = path.match(/\A\//) ? root_url : url
  252. uri = "#{base}/#{path}"
  253. URI.escape(URI.escape(uri), '[]')
  254. # shell_quote(uri.gsub(/[?<>\*]/, ''))
  255. end
  256. ##
  257. # Builds the full git arguments from the parameters
  258. # and return the executed stdout as a string
  259. def capture_svn(args, opt = {})
  260. cmd = build_svn_cmd(args)
  261. output = capture_out(cmd)
  262. if opt[:force_encoding] && output.respond_to?(:force_encoding)
  263. output.force_encoding('UTF-8')
  264. end
  265. output
  266. end
  267. ##
  268. # Builds the full git arguments from the parameters
  269. # and calls the given block with in, out, err, thread
  270. # from +Open3#popen3+.
  271. def popen3(args, &block)
  272. cmd = build_svn_cmd(args)
  273. super(cmd) do |_stdin, stdout, stderr, wait_thr|
  274. block.call(stdout, stderr)
  275. process = wait_thr.value
  276. return process.exitstatus == 0
  277. end
  278. end
  279. end
  280. end
  281. end
  282. end