##// END OF EJS Templates
scm: mercurial: wrap revison, tag and branch with URL encoding for entries (#4455, #1981, #7246)....
Toshi MARUYAMA -
r4869:c3e8fc5f1a2d
parent child
Show More
@@ -1,183 +1,183
1 1 # redminehelper: Redmine helper extension for Mercurial
2 2 #
3 3 # Copyright 2010 Alessio Franceschelli (alefranz.net)
4 4 # Copyright 2010-2011 Yuya Nishihara <yuya@tcha.org>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8 """helper commands for Redmine to reduce the number of hg calls
9 9
10 10 To test this extension, please try::
11 11
12 12 $ hg --config extensions.redminehelper=redminehelper.py rhsummary
13 13
14 14 I/O encoding:
15 15
16 16 :file path: urlencoded, raw string
17 17 :tag name: utf-8
18 18 :branch name: utf-8
19 19 :node: 12-digits (short) hex string
20 20
21 21 Output example of rhsummary::
22 22
23 23 <?xml version="1.0"?>
24 24 <rhsummary>
25 25 <repository root="/foo/bar">
26 26 <tip revision="1234" node="abcdef0123..."/>
27 27 <tag revision="123" node="34567abc..." name="1.1.1"/>
28 28 <branch .../>
29 29 ...
30 30 </repository>
31 31 </rhsummary>
32 32
33 33 Output example of rhmanifest::
34 34
35 35 <?xml version="1.0"?>
36 36 <rhmanifest>
37 37 <repository root="/foo/bar">
38 38 <manifest revision="1234" path="lib">
39 39 <file name="diff.rb" revision="123" node="34567abc..." time="12345"
40 40 size="100"/>
41 41 ...
42 42 <dir name="redmine"/>
43 43 ...
44 44 </manifest>
45 45 </repository>
46 46 </rhmanifest>
47 47 """
48 48 import re, time, cgi, urllib
49 49 from mercurial import cmdutil, commands, node, error
50 50
51 51 _x = cgi.escape
52 52 _u = lambda s: cgi.escape(urllib.quote(s))
53 53
54 54 def _tip(ui, repo):
55 55 # see mercurial/commands.py:tip
56 56 def tiprev():
57 57 try:
58 58 return len(repo) - 1
59 59 except TypeError: # Mercurial < 1.1
60 60 return repo.changelog.count() - 1
61 61 tipctx = repo.changectx(tiprev())
62 62 ui.write('<tip revision="%d" node="%s"/>\n'
63 63 % (tipctx.rev(), _x(node.short(tipctx.node()))))
64 64
65 65 _SPECIAL_TAGS = ('tip',)
66 66
67 67 def _tags(ui, repo):
68 68 # see mercurial/commands.py:tags
69 69 for t, n in reversed(repo.tagslist()):
70 70 if t in _SPECIAL_TAGS:
71 71 continue
72 72 try:
73 73 r = repo.changelog.rev(n)
74 74 except error.LookupError:
75 75 continue
76 76 ui.write('<tag revision="%d" node="%s" name="%s"/>\n'
77 77 % (r, _x(node.short(n)), _x(t)))
78 78
79 79 def _branches(ui, repo):
80 80 # see mercurial/commands.py:branches
81 81 def iterbranches():
82 82 for t, n in repo.branchtags().iteritems():
83 83 yield t, n, repo.changelog.rev(n)
84 84 def branchheads(branch):
85 85 try:
86 86 return repo.branchheads(branch, closed=False)
87 87 except TypeError: # Mercurial < 1.2
88 88 return repo.branchheads(branch)
89 89 for t, n, r in sorted(iterbranches(), key=lambda e: e[2], reverse=True):
90 90 if repo.lookup(r) in branchheads(t):
91 91 ui.write('<branch revision="%d" node="%s" name="%s"/>\n'
92 92 % (r, _x(node.short(n)), _x(t)))
93 93
94 94 def _manifest(ui, repo, path, rev):
95 95 ctx = repo.changectx(rev)
96 96 ui.write('<manifest revision="%d" path="%s">\n'
97 97 % (ctx.rev(), _u(path)))
98 98
99 99 known = set()
100 100 pathprefix = (path.rstrip('/') + '/').lstrip('/')
101 101 for f, n in sorted(ctx.manifest().iteritems(), key=lambda e: e[0]):
102 102 if not f.startswith(pathprefix):
103 103 continue
104 104 name = re.sub(r'/.*', '/', f[len(pathprefix):])
105 105 if name in known:
106 106 continue
107 107 known.add(name)
108 108
109 109 if name.endswith('/'):
110 110 ui.write('<dir name="%s"/>\n'
111 111 % _x(urllib.quote(name[:-1])))
112 112 else:
113 113 fctx = repo.filectx(f, fileid=n)
114 114 tm, tzoffset = fctx.date()
115 115 ui.write('<file name="%s" revision="%d" node="%s" '
116 116 'time="%d" size="%d"/>\n'
117 117 % (_u(name), fctx.rev(), _x(node.short(fctx.node())),
118 118 tm, fctx.size(), ))
119 119
120 120 ui.write('</manifest>\n')
121 121
122 122 def rhannotate(ui, repo, *pats, **opts):
123 123 return commands.annotate(ui, repo, *map(urllib.unquote_plus, pats), **opts)
124 124
125 125 def rhcat(ui, repo, file1, *pats, **opts):
126 126 return commands.cat(ui, repo, urllib.unquote_plus(file1), *map(urllib.unquote_plus, pats), **opts)
127 127
128 128 def rhdiff(ui, repo, *pats, **opts):
129 129 """diff repository (or selected files)"""
130 130 change = opts.pop('change', None)
131 131 if change: # add -c option for Mercurial<1.1
132 132 base = repo.changectx(change).parents()[0].rev()
133 133 opts['rev'] = [str(base), change]
134 134 opts['nodates'] = True
135 135 return commands.diff(ui, repo, *map(urllib.unquote_plus, pats), **opts)
136 136
137 137 def rhmanifest(ui, repo, path='', **opts):
138 138 """output the sub-manifest of the specified directory"""
139 139 ui.write('<?xml version="1.0"?>\n')
140 140 ui.write('<rhmanifest>\n')
141 141 ui.write('<repository root="%s">\n' % _u(repo.root))
142 142 try:
143 _manifest(ui, repo, urllib.unquote_plus(path), opts.get('rev'))
143 _manifest(ui, repo, urllib.unquote_plus(path), urllib.unquote_plus(opts.get('rev')))
144 144 finally:
145 145 ui.write('</repository>\n')
146 146 ui.write('</rhmanifest>\n')
147 147
148 148 def rhsummary(ui, repo, **opts):
149 149 """output the summary of the repository"""
150 150 ui.write('<?xml version="1.0"?>\n')
151 151 ui.write('<rhsummary>\n')
152 152 ui.write('<repository root="%s">\n' % _u(repo.root))
153 153 try:
154 154 _tip(ui, repo)
155 155 _tags(ui, repo)
156 156 _branches(ui, repo)
157 157 # TODO: bookmarks in core (Mercurial>=1.8)
158 158 finally:
159 159 ui.write('</repository>\n')
160 160 ui.write('</rhsummary>\n')
161 161
162 162 # This extension should be compatible with Mercurial 0.9.5.
163 163 # Note that Mercurial 0.9.5 doesn't have extensions.wrapfunction().
164 164 cmdtable = {
165 165 'rhannotate': (rhannotate,
166 166 [('r', 'rev', '', 'revision'),
167 167 ('u', 'user', None, 'list the author (long with -v)'),
168 168 ('n', 'number', None, 'list the revision number (default)'),
169 169 ('c', 'changeset', None, 'list the changeset'),
170 170 ],
171 171 'hg rhannotate [-r REV] [-u] [-n] [-c] FILE...'),
172 172 'rhcat': (rhcat,
173 173 [('r', 'rev', '', 'revision')],
174 174 'hg rhcat ([-r REV] ...) FILE...'),
175 175 'rhdiff': (rhdiff,
176 176 [('r', 'rev', [], 'revision'),
177 177 ('c', 'change', '', 'change made by revision')],
178 178 'hg rhdiff ([-c REV] | [-r REV] ...) [FILE]...'),
179 179 'rhmanifest': (rhmanifest,
180 180 [('r', 'rev', '', 'show the specified revision')],
181 181 'hg rhmanifest [-r REV] [PATH]'),
182 182 'rhsummary': (rhsummary, [], 'hg rhsummary'),
183 183 }
@@ -1,301 +1,301
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/scm/adapters/abstract_adapter'
19 19 require 'cgi'
20 20
21 21 module Redmine
22 22 module Scm
23 23 module Adapters
24 24 class MercurialAdapter < AbstractAdapter
25 25
26 26 # Mercurial executable name
27 27 HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
28 28 HELPERS_DIR = File.dirname(__FILE__) + "/mercurial"
29 29 HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py"
30 30 TEMPLATE_NAME = "hg-template"
31 31 TEMPLATE_EXTENSION = "tmpl"
32 32
33 33 # raised if hg command exited with error, e.g. unknown revision.
34 34 class HgCommandAborted < CommandFailed; end
35 35
36 36 class << self
37 37 def client_command
38 38 @@bin ||= HG_BIN
39 39 end
40 40
41 41 def sq_bin
42 42 @@sq_bin ||= shell_quote(HG_BIN)
43 43 end
44 44
45 45 def client_version
46 46 @@client_version ||= (hgversion || [])
47 47 end
48 48
49 49 def client_available
50 50 !client_version.empty?
51 51 end
52 52
53 53 def hgversion
54 54 # The hg version is expressed either as a
55 55 # release number (eg 0.9.5 or 1.0) or as a revision
56 56 # id composed of 12 hexa characters.
57 57 theversion = hgversion_from_command_line
58 58 if theversion.respond_to?(:force_encoding)
59 59 theversion.force_encoding('ASCII-8BIT')
60 60 end
61 61 if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
62 62 m[2].scan(%r{\d+}).collect(&:to_i)
63 63 end
64 64 end
65 65
66 66 def hgversion_from_command_line
67 67 shellout("#{sq_bin} --version") { |io| io.read }.to_s
68 68 end
69 69
70 70 def template_path
71 71 @@template_path ||= template_path_for(client_version)
72 72 end
73 73
74 74 def template_path_for(version)
75 75 if ((version <=> [0,9,5]) > 0) || version.empty?
76 76 ver = "1.0"
77 77 else
78 78 ver = "0.9.5"
79 79 end
80 80 "#{HELPERS_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
81 81 end
82 82 end
83 83
84 84 def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
85 85 super
86 86 @path_encoding = path_encoding || 'UTF-8'
87 87 end
88 88
89 89 def info
90 90 tip = summary['repository']['tip']
91 91 Info.new(:root_url => CGI.unescape(summary['repository']['root']),
92 92 :lastrev => Revision.new(:revision => tip['revision'],
93 93 :scmid => tip['node']))
94 94 end
95 95
96 96 def tags
97 97 as_ary(summary['repository']['tag']).map { |e| e['name'] }
98 98 end
99 99
100 100 # Returns map of {'tag' => 'nodeid', ...}
101 101 def tagmap
102 102 alist = as_ary(summary['repository']['tag']).map do |e|
103 103 e.values_at('name', 'node')
104 104 end
105 105 Hash[*alist.flatten]
106 106 end
107 107
108 108 def branches
109 109 as_ary(summary['repository']['branch']).map { |e| e['name'] }
110 110 end
111 111
112 112 # Returns map of {'branch' => 'nodeid', ...}
113 113 def branchmap
114 114 alist = as_ary(summary['repository']['branch']).map do |e|
115 115 e.values_at('name', 'node')
116 116 end
117 117 Hash[*alist.flatten]
118 118 end
119 119
120 120 def summary
121 121 return @summary if @summary
122 122 hg 'rhsummary' do |io|
123 123 begin
124 124 @summary = ActiveSupport::XmlMini.parse(io.read)['rhsummary']
125 125 rescue
126 126 end
127 127 end
128 128 end
129 129 private :summary
130 130
131 131 def entries(path=nil, identifier=nil)
132 132 p1 = scm_iconv(@path_encoding, 'UTF-8', path)
133 manifest = hg('rhmanifest', '-r', hgrev(identifier),
133 manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)),
134 134 CGI.escape(without_leading_slash(p1.to_s))) do |io|
135 135 begin
136 136 ActiveSupport::XmlMini.parse(io.read)['rhmanifest']['repository']['manifest']
137 137 rescue
138 138 end
139 139 end
140 140 path_prefix = path.blank? ? '' : with_trailling_slash(path)
141 141
142 142 entries = Entries.new
143 143 as_ary(manifest['dir']).each do |e|
144 144 n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
145 145 p = "#{path_prefix}#{n}"
146 146 entries << Entry.new(:name => n, :path => p, :kind => 'dir')
147 147 end
148 148
149 149 as_ary(manifest['file']).each do |e|
150 150 n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
151 151 p = "#{path_prefix}#{n}"
152 152 lr = Revision.new(:revision => e['revision'], :scmid => e['node'],
153 153 :identifier => e['node'],
154 154 :time => Time.at(e['time'].to_i))
155 155 entries << Entry.new(:name => n, :path => p, :kind => 'file',
156 156 :size => e['size'].to_i, :lastrev => lr)
157 157 end
158 158
159 159 entries
160 160 rescue HgCommandAborted
161 161 nil # means not found
162 162 end
163 163
164 164 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
165 165 revs = Revisions.new
166 166 each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
167 167 revs
168 168 end
169 169
170 170 # Iterates the revisions by using a template file that
171 171 # makes Mercurial produce a xml output.
172 172 def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
173 173 hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
174 174 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
175 175 hg_args << '--limit' << options[:limit] if options[:limit]
176 176 hg_args << hgtarget(path) unless path.blank?
177 177 log = hg(*hg_args) do |io|
178 178 begin
179 179 # Mercurial < 1.5 does not support footer template for '</log>'
180 180 ActiveSupport::XmlMini.parse("#{io.read}</log>")['log']
181 181 rescue
182 182 end
183 183 end
184 184
185 185 as_ary(log['logentry']).each do |le|
186 186 cpalist = as_ary(le['paths']['path-copied']).map do |e|
187 187 [e['__content__'], e['copyfrom-path']].map { |s| CGI.unescape(s) }
188 188 end
189 189 cpmap = Hash[*cpalist.flatten]
190 190
191 191 paths = as_ary(le['paths']['path']).map do |e|
192 192 p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
193 193 {:action => e['action'], :path => with_leading_slash(p),
194 194 :from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
195 195 :from_revision => (cpmap.member?(p) ? le['revision'] : nil)}
196 196 end.sort { |a, b| a[:path] <=> b[:path] }
197 197
198 198 yield Revision.new(:revision => le['revision'],
199 199 :scmid => le['node'],
200 200 :author => (le['author']['__content__'] rescue ''),
201 201 :time => Time.parse(le['date']['__content__']).localtime,
202 202 :message => le['msg']['__content__'],
203 203 :paths => paths)
204 204 end
205 205 self
206 206 end
207 207
208 208 def diff(path, identifier_from, identifier_to=nil)
209 209 hg_args = %w|rhdiff|
210 210 if identifier_to
211 211 hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
212 212 else
213 213 hg_args << '-c' << hgrev(identifier_from)
214 214 end
215 215 unless path.blank?
216 216 p = scm_iconv(@path_encoding, 'UTF-8', path)
217 217 hg_args << CGI.escape(hgtarget(p))
218 218 end
219 219 diff = []
220 220 hg *hg_args do |io|
221 221 io.each_line do |line|
222 222 diff << line
223 223 end
224 224 end
225 225 diff
226 226 rescue HgCommandAborted
227 227 nil # means not found
228 228 end
229 229
230 230 def cat(path, identifier=nil)
231 231 p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
232 232 hg 'rhcat', '-r', hgrev(identifier), hgtarget(p) do |io|
233 233 io.binmode
234 234 io.read
235 235 end
236 236 rescue HgCommandAborted
237 237 nil # means not found
238 238 end
239 239
240 240 def annotate(path, identifier=nil)
241 241 p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
242 242 blame = Annotate.new
243 243 hg 'rhannotate', '-ncu', '-r', hgrev(identifier), hgtarget(p) do |io|
244 244 io.each_line do |line|
245 245 line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding)
246 246 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
247 247 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
248 248 :identifier => $3)
249 249 blame.add_line($4.rstrip, r)
250 250 end
251 251 end
252 252 blame
253 253 rescue HgCommandAborted
254 254 nil # means not found or cannot be annotated
255 255 end
256 256
257 257 class Revision < Redmine::Scm::Adapters::Revision
258 258 # Returns the readable identifier
259 259 def format_identifier
260 260 "#{revision}:#{scmid}"
261 261 end
262 262 end
263 263
264 264 # Runs 'hg' command with the given args
265 265 def hg(*args, &block)
266 266 repo_path = root_url || url
267 267 full_args = [HG_BIN, '-R', repo_path, '--encoding', 'utf-8']
268 268 full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
269 269 full_args << '--config' << 'diff.git=false'
270 270 full_args += args
271 271 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
272 272 if $? && $?.exitstatus != 0
273 273 raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
274 274 end
275 275 ret
276 276 end
277 277 private :hg
278 278
279 279 # Returns correct revision identifier
280 280 def hgrev(identifier, sq=false)
281 281 rev = identifier.blank? ? 'tip' : identifier.to_s
282 282 rev = shell_quote(rev) if sq
283 283 rev
284 284 end
285 285 private :hgrev
286 286
287 287 def hgtarget(path)
288 288 path ||= ''
289 289 root_url + '/' + without_leading_slash(path)
290 290 end
291 291 private :hgtarget
292 292
293 293 def as_ary(o)
294 294 return [] unless o
295 295 o.is_a?(Array) ? o : Array[o]
296 296 end
297 297 private :as_ary
298 298 end
299 299 end
300 300 end
301 301 end
General Comments 0
You need to be logged in to leave comments. Login now