PageRenderTime 32ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/redmine/scm/adapters/mercurial_adapter.rb

https://bitbucket.org/sharjeelaslam/redmine-tivilon-dev
Ruby | 341 lines | 274 code | 35 blank | 32 comment | 19 complexity | 51094ee36fd54935d5c2cd286c74b44e MD5 | raw file
  1. # Redmine - project management software
  2. # Copyright (C) 2006-2013 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. require 'redmine/scm/adapters/abstract_adapter'
  18. require 'cgi'
  19. module Redmine
  20. module Scm
  21. module Adapters
  22. class MercurialAdapter < AbstractAdapter
  23. # Mercurial executable name
  24. HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
  25. HELPERS_DIR = File.dirname(__FILE__) + "/mercurial"
  26. HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py"
  27. TEMPLATE_NAME = "hg-template"
  28. TEMPLATE_EXTENSION = "tmpl"
  29. # raised if hg command exited with error, e.g. unknown revision.
  30. class HgCommandAborted < CommandFailed; end
  31. class << self
  32. def client_command
  33. @@bin ||= HG_BIN
  34. end
  35. def sq_bin
  36. @@sq_bin ||= shell_quote_command
  37. end
  38. def client_version
  39. @@client_version ||= (hgversion || [])
  40. end
  41. def client_available
  42. client_version_above?([1, 2])
  43. end
  44. def hgversion
  45. # The hg version is expressed either as a
  46. # release number (eg 0.9.5 or 1.0) or as a revision
  47. # id composed of 12 hexa characters.
  48. theversion = hgversion_from_command_line.dup
  49. if theversion.respond_to?(:force_encoding)
  50. theversion.force_encoding('ASCII-8BIT')
  51. end
  52. if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
  53. m[2].scan(%r{\d+}).collect(&:to_i)
  54. end
  55. end
  56. def hgversion_from_command_line
  57. shellout("#{sq_bin} --version") { |io| io.read }.to_s
  58. end
  59. def template_path
  60. @@template_path ||= template_path_for(client_version)
  61. end
  62. def template_path_for(version)
  63. "#{HELPERS_DIR}/#{TEMPLATE_NAME}-1.0.#{TEMPLATE_EXTENSION}"
  64. end
  65. end
  66. def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
  67. super
  68. @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
  69. end
  70. def path_encoding
  71. @path_encoding
  72. end
  73. def info
  74. tip = summary['repository']['tip']
  75. Info.new(:root_url => CGI.unescape(summary['repository']['root']),
  76. :lastrev => Revision.new(:revision => tip['revision'],
  77. :scmid => tip['node']))
  78. # rescue HgCommandAborted
  79. rescue Exception => e
  80. logger.error "hg: error during getting info: #{e.message}"
  81. nil
  82. end
  83. def tags
  84. as_ary(summary['repository']['tag']).map { |e| e['name'] }
  85. end
  86. # Returns map of {'tag' => 'nodeid', ...}
  87. def tagmap
  88. alist = as_ary(summary['repository']['tag']).map do |e|
  89. e.values_at('name', 'node')
  90. end
  91. Hash[*alist.flatten]
  92. end
  93. def branches
  94. brs = []
  95. as_ary(summary['repository']['branch']).each do |e|
  96. br = Branch.new(e['name'])
  97. br.revision = e['revision']
  98. br.scmid = e['node']
  99. brs << br
  100. end
  101. brs
  102. end
  103. # Returns map of {'branch' => 'nodeid', ...}
  104. def branchmap
  105. alist = as_ary(summary['repository']['branch']).map do |e|
  106. e.values_at('name', 'node')
  107. end
  108. Hash[*alist.flatten]
  109. end
  110. def summary
  111. return @summary if @summary
  112. hg 'rhsummary' do |io|
  113. output = io.read
  114. if output.respond_to?(:force_encoding)
  115. output.force_encoding('UTF-8')
  116. end
  117. begin
  118. @summary = parse_xml(output)['rhsummary']
  119. rescue
  120. end
  121. end
  122. end
  123. private :summary
  124. def entries(path=nil, identifier=nil, options={})
  125. p1 = scm_iconv(@path_encoding, 'UTF-8', path)
  126. manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)),
  127. CGI.escape(without_leading_slash(p1.to_s))) do |io|
  128. output = io.read
  129. if output.respond_to?(:force_encoding)
  130. output.force_encoding('UTF-8')
  131. end
  132. begin
  133. parse_xml(output)['rhmanifest']['repository']['manifest']
  134. rescue
  135. end
  136. end
  137. path_prefix = path.blank? ? '' : with_trailling_slash(path)
  138. entries = Entries.new
  139. as_ary(manifest['dir']).each do |e|
  140. n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
  141. p = "#{path_prefix}#{n}"
  142. entries << Entry.new(:name => n, :path => p, :kind => 'dir')
  143. end
  144. as_ary(manifest['file']).each do |e|
  145. n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
  146. p = "#{path_prefix}#{n}"
  147. lr = Revision.new(:revision => e['revision'], :scmid => e['node'],
  148. :identifier => e['node'],
  149. :time => Time.at(e['time'].to_i))
  150. entries << Entry.new(:name => n, :path => p, :kind => 'file',
  151. :size => e['size'].to_i, :lastrev => lr)
  152. end
  153. entries
  154. rescue HgCommandAborted
  155. nil # means not found
  156. end
  157. def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
  158. revs = Revisions.new
  159. each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
  160. revs
  161. end
  162. # Iterates the revisions by using a template file that
  163. # makes Mercurial produce a xml output.
  164. def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
  165. hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
  166. hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
  167. hg_args << '--limit' << options[:limit] if options[:limit]
  168. hg_args << hgtarget(path) unless path.blank?
  169. log = hg(*hg_args) do |io|
  170. output = io.read
  171. if output.respond_to?(:force_encoding)
  172. output.force_encoding('UTF-8')
  173. end
  174. begin
  175. # Mercurial < 1.5 does not support footer template for '</log>'
  176. parse_xml("#{output}</log>")['log']
  177. rescue
  178. end
  179. end
  180. as_ary(log['logentry']).each do |le|
  181. cpalist = as_ary(le['paths']['path-copied']).map do |e|
  182. [e['__content__'], e['copyfrom-path']].map do |s|
  183. scm_iconv('UTF-8', @path_encoding, CGI.unescape(s))
  184. end
  185. end
  186. cpmap = Hash[*cpalist.flatten]
  187. paths = as_ary(le['paths']['path']).map do |e|
  188. p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
  189. {:action => e['action'],
  190. :path => with_leading_slash(p),
  191. :from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
  192. :from_revision => (cpmap.member?(p) ? le['node'] : nil)}
  193. end.sort { |a, b| a[:path] <=> b[:path] }
  194. parents_ary = []
  195. as_ary(le['parents']['parent']).map do |par|
  196. parents_ary << par['__content__'] if par['__content__'] != "000000000000"
  197. end
  198. yield Revision.new(:revision => le['revision'],
  199. :scmid => le['node'],
  200. :author => (le['author']['__content__'] rescue ''),
  201. :time => Time.parse(le['date']['__content__']),
  202. :message => le['msg']['__content__'],
  203. :paths => paths,
  204. :parents => parents_ary)
  205. end
  206. self
  207. end
  208. # Returns list of nodes in the specified branch
  209. def nodes_in_branch(branch, options={})
  210. hg_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)]
  211. hg_args << '--from' << CGI.escape(branch)
  212. hg_args << '--to' << '0'
  213. hg_args << '--limit' << options[:limit] if options[:limit]
  214. hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
  215. end
  216. def diff(path, identifier_from, identifier_to=nil)
  217. hg_args = %w|rhdiff|
  218. if identifier_to
  219. hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
  220. else
  221. hg_args << '-c' << hgrev(identifier_from)
  222. end
  223. unless path.blank?
  224. p = scm_iconv(@path_encoding, 'UTF-8', path)
  225. hg_args << CGI.escape(hgtarget(p))
  226. end
  227. diff = []
  228. hg *hg_args do |io|
  229. io.each_line do |line|
  230. diff << line
  231. end
  232. end
  233. diff
  234. rescue HgCommandAborted
  235. nil # means not found
  236. end
  237. def cat(path, identifier=nil)
  238. p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
  239. hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
  240. io.binmode
  241. io.read
  242. end
  243. rescue HgCommandAborted
  244. nil # means not found
  245. end
  246. def annotate(path, identifier=nil)
  247. p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
  248. blame = Annotate.new
  249. hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
  250. io.each_line do |line|
  251. line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding)
  252. next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
  253. r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
  254. :identifier => $3)
  255. blame.add_line($4.rstrip, r)
  256. end
  257. end
  258. blame
  259. rescue HgCommandAborted
  260. # means not found or cannot be annotated
  261. Annotate.new
  262. end
  263. class Revision < Redmine::Scm::Adapters::Revision
  264. # Returns the readable identifier
  265. def format_identifier
  266. "#{revision}:#{scmid}"
  267. end
  268. end
  269. # Runs 'hg' command with the given args
  270. def hg(*args, &block)
  271. repo_path = root_url || url
  272. full_args = ['-R', repo_path, '--encoding', 'utf-8']
  273. full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
  274. full_args << '--config' << 'diff.git=false'
  275. full_args += args
  276. ret = shellout(
  277. self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
  278. &block
  279. )
  280. if $? && $?.exitstatus != 0
  281. raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
  282. end
  283. ret
  284. end
  285. private :hg
  286. # Returns correct revision identifier
  287. def hgrev(identifier, sq=false)
  288. rev = identifier.blank? ? 'tip' : identifier.to_s
  289. rev = shell_quote(rev) if sq
  290. rev
  291. end
  292. private :hgrev
  293. def hgtarget(path)
  294. path ||= ''
  295. root_url + '/' + without_leading_slash(path)
  296. end
  297. private :hgtarget
  298. def as_ary(o)
  299. return [] unless o
  300. o.is_a?(Array) ? o : Array[o]
  301. end
  302. private :as_ary
  303. end
  304. end
  305. end
  306. end