comparison lib/redmine/scm/adapters/mercurial_adapter.rb @ 245:051f544170fe

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