Mercurial > hg > soundsoftware-site
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 |