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