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