Mercurial > hg > soundsoftware-site
comparison lib/redmine/scm/adapters/mercurial_adapter.rb @ 511:107d36338b70 live
Merge from branch "cannam"
author | Chris Cannam |
---|---|
date | Thu, 14 Jul 2011 10:43:07 +0100 |
parents | 851510f1b535 |
children | 5e80956cc792 |
comparison
equal
deleted
inserted
replaced
451:a9f6345cb43d | 511:107d36338b70 |
---|---|
1 # redMine - project management software | 1 # Redmine - project management software |
2 # Copyright (C) 2006-2007 Jean-Philippe Lang | 2 # Copyright (C) 2006-2011 Jean-Philippe Lang |
3 # | 3 # |
4 # This program is free software; you can redistribute it and/or | 4 # This program is free software; you can redistribute it and/or |
5 # modify it under the terms of the GNU General Public License | 5 # modify it under the terms of the GNU General Public License |
6 # as published by the Free Software Foundation; either version 2 | 6 # as published by the Free Software Foundation; either version 2 |
7 # of the License, or (at your option) any later version. | 7 # of the License, or (at your option) any later version. |
8 # | 8 # |
9 # This program is distributed in the hope that it will be useful, | 9 # This program is distributed in the hope that it will be useful, |
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 # GNU General Public License for more details. | 12 # GNU General Public License for more details. |
13 # | 13 # |
14 # You should have received a copy of the GNU General Public License | 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 | 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. | 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | 17 |
18 require 'redmine/scm/adapters/abstract_adapter' | 18 require 'redmine/scm/adapters/abstract_adapter' |
19 require 'rexml/document' | 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 = "hg" | 27 HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg" |
28 HG_HELPER_EXT = "#{RAILS_ROOT}/extra/mercurial/redminehelper.py" | 28 HELPERS_DIR = File.dirname(__FILE__) + "/mercurial" |
29 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial" | 29 HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py" |
30 TEMPLATE_NAME = "hg-template" | 30 TEMPLATE_NAME = "hg-template" |
31 TEMPLATE_EXTENSION = "tmpl" | 31 TEMPLATE_EXTENSION = "tmpl" |
32 | 32 |
33 # raised if hg command exited with error, e.g. unknown revision. | 33 # raised if hg command exited with error, e.g. unknown revision. |
34 class HgCommandAborted < CommandFailed; end | 34 class HgCommandAborted < CommandFailed; end |
35 | 35 |
36 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 | |
37 def client_version | 45 def client_version |
38 @client_version ||= hgversion | 46 @@client_version ||= (hgversion || []) |
39 end | 47 end |
40 | 48 |
41 def hgversion | 49 def client_available |
50 client_version_above?([0, 9, 5]) | |
51 end | |
52 | |
53 def hgversion | |
42 # The hg version is expressed either as a | 54 # The hg version is expressed either as a |
43 # 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 |
44 # id composed of 12 hexa characters. | 56 # id composed of 12 hexa characters. |
45 hgversion_str.to_s.split('.').map { |e| e.to_i } | 57 theversion = hgversion_from_command_line.dup |
46 end | 58 if theversion.respond_to?(:force_encoding) |
47 private :hgversion | 59 theversion.force_encoding('ASCII-8BIT') |
48 | 60 end |
49 def hgversion_str | 61 if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)}) |
50 shellout("#{HG_BIN} --version") { |io| io.gets }.to_s[/\d+(\.\d+)+/] | 62 m[2].scan(%r{\d+}).collect(&:to_i) |
51 end | 63 end |
52 private :hgversion_str | 64 end |
53 | 65 |
66 def hgversion_from_command_line | |
67 shellout("#{sq_bin} --version") { |io| io.read }.to_s | |
68 end | |
69 | |
54 def template_path | 70 def template_path |
55 template_path_for(client_version) | 71 @@template_path ||= template_path_for(client_version) |
56 end | 72 end |
57 | 73 |
58 def template_path_for(version) | 74 def template_path_for(version) |
59 if ((version <=> [0,9,5]) > 0) || version.empty? | 75 if ((version <=> [0,9,5]) > 0) || version.empty? |
60 ver = "1.0" | 76 ver = "1.0" |
61 else | 77 else |
62 ver = "0.9.5" | 78 ver = "0.9.5" |
63 end | 79 end |
64 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" | 80 "#{HELPERS_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" |
65 end | 81 end |
66 private :template_path_for | 82 end |
67 end | 83 |
68 | 84 def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil) |
85 super | |
86 @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding | |
87 end | |
88 | |
89 def path_encoding | |
90 @path_encoding | |
91 end | |
92 | |
69 def info | 93 def info |
70 tip = summary['tip'].first | 94 tip = summary['repository']['tip'] |
71 Info.new(:root_url => summary['root'].first['path'], | 95 Info.new(:root_url => CGI.unescape(summary['repository']['root']), |
72 :lastrev => Revision.new(:identifier => tip['rev'].to_i, | 96 :lastrev => Revision.new(:revision => tip['revision'], |
73 :revision => tip['rev'], | |
74 :scmid => tip['node'])) | 97 :scmid => tip['node'])) |
98 # rescue HgCommandAborted | |
99 rescue Exception => e | |
100 logger.error "hg: error during getting info: #{e.message}" | |
101 nil | |
75 end | 102 end |
76 | 103 |
77 def tags | 104 def tags |
78 summary['tags'].map { |e| e['name'] } | 105 as_ary(summary['repository']['tag']).map { |e| e['name'] } |
79 end | 106 end |
80 | 107 |
81 # Returns map of {'tag' => 'nodeid', ...} | 108 # Returns map of {'tag' => 'nodeid', ...} |
82 def tagmap | 109 def tagmap |
83 alist = summary['tags'].map { |e| e.values_at('name', 'node') } | 110 alist = as_ary(summary['repository']['tag']).map do |e| |
111 e.values_at('name', 'node') | |
112 end | |
84 Hash[*alist.flatten] | 113 Hash[*alist.flatten] |
85 end | 114 end |
86 | 115 |
87 def branches | 116 def branches |
88 summary['branches'].map { |e| e['name'] } | 117 as_ary(summary['repository']['branch']).map { |e| e['name'] } |
89 end | 118 end |
90 | 119 |
91 # Returns map of {'branch' => 'nodeid', ...} | 120 # Returns map of {'branch' => 'nodeid', ...} |
92 def branchmap | 121 def branchmap |
93 alist = summary['branches'].map { |e| e.values_at('name', 'node') } | 122 alist = as_ary(summary['repository']['branch']).map do |e| |
123 e.values_at('name', 'node') | |
124 end | |
94 Hash[*alist.flatten] | 125 Hash[*alist.flatten] |
95 end | 126 end |
96 | 127 |
97 # NOTE: DO NOT IMPLEMENT default_branch !! | |
98 # It's used as the default revision by RepositoriesController. | |
99 | |
100 def summary | 128 def summary |
101 @summary ||= fetchg 'rhsummary' | 129 return @summary if @summary |
130 hg 'rhsummary' do |io| | |
131 output = io.read | |
132 if output.respond_to?(:force_encoding) | |
133 output.force_encoding('UTF-8') | |
134 end | |
135 begin | |
136 @summary = ActiveSupport::XmlMini.parse(output)['rhsummary'] | |
137 rescue | |
138 end | |
139 end | |
102 end | 140 end |
103 private :summary | 141 private :summary |
104 | 142 |
105 def entries(path=nil, identifier=nil) | 143 def entries(path=nil, identifier=nil, options={}) |
144 p1 = scm_iconv(@path_encoding, 'UTF-8', path) | |
145 manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)), | |
146 CGI.escape(without_leading_slash(p1.to_s))) do |io| | |
147 output = io.read | |
148 if output.respond_to?(:force_encoding) | |
149 output.force_encoding('UTF-8') | |
150 end | |
151 begin | |
152 ActiveSupport::XmlMini.parse(output)['rhmanifest']['repository']['manifest'] | |
153 rescue | |
154 end | |
155 end | |
156 path_prefix = path.blank? ? '' : with_trailling_slash(path) | |
157 | |
106 entries = Entries.new | 158 entries = Entries.new |
107 fetched_entries = fetchg('rhentries', '-r', hgrev(identifier), | 159 as_ary(manifest['dir']).each do |e| |
108 without_leading_slash(path.to_s)) | 160 n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name'])) |
109 | 161 p = "#{path_prefix}#{n}" |
110 fetched_entries['dirs'].each do |e| | 162 entries << Entry.new(:name => n, :path => p, :kind => 'dir') |
111 entries << Entry.new(:name => e['name'], | 163 end |
112 :path => "#{with_trailling_slash(path)}#{e['name']}", | 164 |
113 :kind => 'dir') | 165 as_ary(manifest['file']).each do |e| |
114 end | 166 n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name'])) |
115 | 167 p = "#{path_prefix}#{n}" |
116 fetched_entries['files'].each do |e| | 168 lr = Revision.new(:revision => e['revision'], :scmid => e['node'], |
117 entries << Entry.new(:name => e['name'], | 169 :identifier => e['node'], |
118 :path => "#{with_trailling_slash(path)}#{e['name']}", | 170 :time => Time.at(e['time'].to_i)) |
119 :kind => 'file', | 171 entries << Entry.new(:name => n, :path => p, :kind => 'file', |
120 :size => e['size'].to_i, | 172 :size => e['size'].to_i, :lastrev => lr) |
121 :lastrev => Revision.new(:identifier => e['rev'].to_i, | |
122 :time => Time.at(e['time'].to_i))) | |
123 end | 173 end |
124 | 174 |
125 entries | 175 entries |
126 rescue HgCommandAborted | 176 rescue HgCommandAborted |
127 nil # means not found | 177 nil # means not found |
128 end | 178 end |
129 | 179 |
130 # TODO: is this api necessary? | |
131 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) | 180 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) |
132 revisions = Revisions.new | 181 revs = Revisions.new |
133 each_revision { |e| revisions << e } | 182 each_revision(path, identifier_from, identifier_to, options) { |e| revs << e } |
134 revisions | 183 revs |
135 end | 184 end |
136 | 185 |
137 # Iterates the revisions by using a template file that | 186 # Iterates the revisions by using a template file that |
138 # makes Mercurial produce a xml output. | 187 # makes Mercurial produce a xml output. |
139 def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={}) | 188 def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={}) |
140 hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] | 189 hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] |
141 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" | 190 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" |
142 hg_args << '--limit' << options[:limit] if options[:limit] | 191 hg_args << '--limit' << options[:limit] if options[:limit] |
143 hg_args << without_leading_slash(path) unless path.blank? | 192 hg_args << hgtarget(path) unless path.blank? |
144 doc = hg(*hg_args) { |io| REXML::Document.new(io.read) } | 193 log = hg(*hg_args) do |io| |
145 # TODO: ??? HG doesn't close the XML Document... | 194 output = io.read |
146 | 195 if output.respond_to?(:force_encoding) |
147 doc.each_element('log/logentry') do |le| | 196 output.force_encoding('UTF-8') |
148 cpalist = le.get_elements('paths/path-copied').map do |e| | 197 end |
149 [e.text, e.attributes['copyfrom-path']] | 198 begin |
199 ActiveSupport::XmlMini.parse("#{output}")['log'] | |
200 rescue | |
201 end | |
202 end | |
203 as_ary(log['logentry']).each do |le| | |
204 cpalist = as_ary(le['paths']['path-copied']).map do |e| | |
205 [e['__content__'], e['copyfrom-path']].map do |s| | |
206 scm_iconv('UTF-8', @path_encoding, CGI.unescape(s)) | |
207 end | |
150 end | 208 end |
151 cpmap = Hash[*cpalist.flatten] | 209 cpmap = Hash[*cpalist.flatten] |
152 | 210 paths = as_ary(le['paths']['path']).map do |e| |
153 paths = le.get_elements('paths/path').map do |e| | 211 p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) ) |
154 {:action => e.attributes['action'], :path => with_leading_slash(e.text), | 212 {:action => e['action'], |
155 :from_path => (cpmap.member?(e.text) ? with_leading_slash(cpmap[e.text]) : nil), | 213 :path => with_leading_slash(p), |
156 :from_revision => (cpmap.member?(e.text) ? le.attributes['revision'] : nil)} | 214 :from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil), |
215 :from_revision => (cpmap.member?(p) ? le['node'] : nil)} | |
157 end.sort { |a, b| a[:path] <=> b[:path] } | 216 end.sort { |a, b| a[:path] <=> b[:path] } |
158 | 217 yield Revision.new(:revision => le['revision'], |
159 yield Revision.new(:identifier => le.attributes['revision'], | 218 :scmid => le['node'], |
160 :revision => le.attributes['revision'], | 219 :author => (le['author']['__content__'] rescue ''), |
161 :scmid => le.attributes['node'], | 220 :time => Time.parse(le['date']['__content__']), |
162 :author => (le.elements['author'].text rescue ''), | 221 :message => le['msg']['__content__'], |
163 :time => Time.parse(le.elements['date'].text).localtime, | 222 :paths => paths) |
164 :message => le.elements['msg'].text, | |
165 :paths => paths) | |
166 end | 223 end |
167 self | 224 self |
168 end | 225 end |
169 | 226 |
170 # Returns list of nodes in the specified branch | 227 # Returns list of nodes in the specified branch |
171 def nodes_in_branch(branch, path=nil, identifier_from=nil, identifier_to=nil, options={}) | 228 def nodes_in_branch(branch, options={}) |
172 hg_args = ['log', '--template', '{node|short}\n', '-b', branch] | 229 hg_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)] |
173 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" | 230 hg_args << '--from' << CGI.escape(branch) |
231 hg_args << '--to' << '0' | |
174 hg_args << '--limit' << options[:limit] if options[:limit] | 232 hg_args << '--limit' << options[:limit] if options[:limit] |
175 hg_args << without_leading_slash(path) unless path.blank? | |
176 hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } } | 233 hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } } |
177 end | 234 end |
178 | 235 |
179 def diff(path, identifier_from, identifier_to=nil) | 236 def diff(path, identifier_from, identifier_to=nil) |
180 hg_args = ['diff', '--nodates'] | 237 hg_args = %w|rhdiff| |
181 if identifier_to | 238 if identifier_to |
182 hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) | 239 hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) |
183 else | 240 else |
184 hg_args << '-c' << hgrev(identifier_from) | 241 hg_args << '-c' << hgrev(identifier_from) |
185 end | 242 end |
186 hg_args << without_leading_slash(path) unless path.blank? | 243 unless path.blank? |
187 | 244 p = scm_iconv(@path_encoding, 'UTF-8', path) |
245 hg_args << CGI.escape(hgtarget(p)) | |
246 end | |
247 diff = [] | |
188 hg *hg_args do |io| | 248 hg *hg_args do |io| |
189 io.collect | 249 io.each_line do |line| |
190 end | 250 diff << line |
251 end | |
252 end | |
253 diff | |
191 rescue HgCommandAborted | 254 rescue HgCommandAborted |
192 nil # means not found | 255 nil # means not found |
193 end | 256 end |
194 | 257 |
195 def cat(path, identifier=nil) | 258 def cat(path, identifier=nil) |
196 hg 'cat', '-r', hgrev(identifier), without_leading_slash(path) do |io| | 259 p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) |
260 hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| | |
197 io.binmode | 261 io.binmode |
198 io.read | 262 io.read |
199 end | 263 end |
200 rescue HgCommandAborted | 264 rescue HgCommandAborted |
201 nil # means not found | 265 nil # means not found |
202 end | 266 end |
203 | 267 |
204 def annotate(path, identifier=nil) | 268 def annotate(path, identifier=nil) |
269 p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) | |
205 blame = Annotate.new | 270 blame = Annotate.new |
206 hg 'annotate', '-ncu', '-r', hgrev(identifier), without_leading_slash(path) do |io| | 271 hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| |
207 io.each do |line| | 272 io.each_line do |line| |
208 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):(.*)$} | 273 line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding) |
209 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3) | 274 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$} |
275 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3, | |
276 :identifier => $3) | |
210 blame.add_line($4.rstrip, r) | 277 blame.add_line($4.rstrip, r) |
211 end | 278 end |
212 end | 279 end |
213 blame | 280 blame |
214 rescue HgCommandAborted | 281 rescue HgCommandAborted |
215 nil # means not found or cannot be annotated | 282 # means not found or cannot be annotated |
283 Annotate.new | |
284 end | |
285 | |
286 class Revision < Redmine::Scm::Adapters::Revision | |
287 # Returns the readable identifier | |
288 def format_identifier | |
289 "#{revision}:#{scmid}" | |
290 end | |
216 end | 291 end |
217 | 292 |
218 # Runs 'hg' command with the given args | 293 # Runs 'hg' command with the given args |
219 def hg(*args, &block) | 294 def hg(*args, &block) |
220 full_args = [HG_BIN, '--cwd', url] | 295 repo_path = root_url || url |
296 full_args = [HG_BIN, '-R', repo_path, '--encoding', 'utf-8'] | |
221 full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}" | 297 full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}" |
298 full_args << '--config' << 'diff.git=false' | |
222 full_args += args | 299 full_args += args |
223 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block) | 300 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block) |
224 if $? && $?.exitstatus != 0 | 301 if $? && $?.exitstatus != 0 |
225 raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}" | 302 raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}" |
226 end | 303 end |
227 ret | 304 ret |
228 end | 305 end |
229 private :hg | 306 private :hg |
230 | 307 |
231 # Runs 'hg' helper, then parses output to return | |
232 def fetchg(*args) | |
233 # command output example: | |
234 # :tip: rev node | |
235 # 100 abcdef012345 | |
236 # :tags: rev node name | |
237 # 100 abcdef012345 tip | |
238 # ... | |
239 data = Hash.new { |h, k| h[k] = [] } | |
240 hg(*args) do |io| | |
241 key, attrs = nil, nil | |
242 io.each do |line| | |
243 next if line.chomp.empty? | |
244 if /^:(\w+): ([\w ]+)/ =~ line | |
245 key = $1 | |
246 attrs = $2.split(/ /) | |
247 elsif key | |
248 alist = attrs.zip(line.chomp.split(/ /, attrs.size)) | |
249 data[key] << Hash[*alist.flatten] | |
250 end | |
251 end | |
252 end | |
253 data | |
254 end | |
255 private :fetchg | |
256 | |
257 # Returns correct revision identifier | 308 # Returns correct revision identifier |
258 def hgrev(identifier) | 309 def hgrev(identifier, sq=false) |
259 identifier.blank? ? 'tip' : identifier.to_s | 310 rev = identifier.blank? ? 'tip' : identifier.to_s |
311 rev = shell_quote(rev) if sq | |
312 rev | |
260 end | 313 end |
261 private :hgrev | 314 private :hgrev |
315 | |
316 def hgtarget(path) | |
317 path ||= '' | |
318 root_url + '/' + without_leading_slash(path) | |
319 end | |
320 private :hgtarget | |
321 | |
322 def as_ary(o) | |
323 return [] unless o | |
324 o.is_a?(Array) ? o : Array[o] | |
325 end | |
326 private :as_ary | |
262 end | 327 end |
263 end | 328 end |
264 end | 329 end |
265 end | 330 end |