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