##// END OF EJS Templates
templatekw: add public function to wrap a dict by _hybrid object
Yuya Nishihara -
r31925:5b2241e8 default
parent child Browse files
Show More
@@ -1,428 +1,428 b''
1 1 # formatter.py - generic output formatting for mercurial
2 2 #
3 3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Generic output formatting for Mercurial
9 9
10 10 The formatter provides API to show data in various ways. The following
11 11 functions should be used in place of ui.write():
12 12
13 13 - fm.write() for unconditional output
14 14 - fm.condwrite() to show some extra data conditionally in plain output
15 15 - fm.context() to provide changectx to template output
16 16 - fm.data() to provide extra data to JSON or template output
17 17 - fm.plain() to show raw text that isn't provided to JSON or template output
18 18
19 19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 20 beforehand so the data is converted to the appropriate data type. Use
21 21 fm.isplain() if you need to convert or format data conditionally which isn't
22 22 supported by the formatter API.
23 23
24 24 To build nested structure (i.e. a list of dicts), use fm.nested().
25 25
26 26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27 27
28 28 fm.condwrite() vs 'if cond:':
29 29
30 30 In most cases, use fm.condwrite() so users can selectively show the data
31 31 in template output. If it's costly to build data, use plain 'if cond:' with
32 32 fm.write().
33 33
34 34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35 35
36 36 fm.nested() should be used to form a tree structure (a list of dicts of
37 37 lists of dicts...) which can be accessed through template keywords, e.g.
38 38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 39 exports a dict-type object to template, which can be accessed by e.g.
40 40 "{get(foo, key)}" function.
41 41
42 42 Doctest helper:
43 43
44 44 >>> def show(fn, verbose=False, **opts):
45 45 ... import sys
46 46 ... from . import ui as uimod
47 47 ... ui = uimod.ui()
48 48 ... ui.fout = sys.stdout # redirect to doctest
49 49 ... ui.verbose = verbose
50 50 ... return fn(ui, ui.formatter(fn.__name__, opts))
51 51
52 52 Basic example:
53 53
54 54 >>> def files(ui, fm):
55 55 ... files = [('foo', 123, (0, 0)), ('bar', 456, (1, 0))]
56 56 ... for f in files:
57 57 ... fm.startitem()
58 58 ... fm.write('path', '%s', f[0])
59 59 ... fm.condwrite(ui.verbose, 'date', ' %s',
60 60 ... fm.formatdate(f[2], '%Y-%m-%d %H:%M:%S'))
61 61 ... fm.data(size=f[1])
62 62 ... fm.plain('\\n')
63 63 ... fm.end()
64 64 >>> show(files)
65 65 foo
66 66 bar
67 67 >>> show(files, verbose=True)
68 68 foo 1970-01-01 00:00:00
69 69 bar 1970-01-01 00:00:01
70 70 >>> show(files, template='json')
71 71 [
72 72 {
73 73 "date": [0, 0],
74 74 "path": "foo",
75 75 "size": 123
76 76 },
77 77 {
78 78 "date": [1, 0],
79 79 "path": "bar",
80 80 "size": 456
81 81 }
82 82 ]
83 83 >>> show(files, template='path: {path}\\ndate: {date|rfc3339date}\\n')
84 84 path: foo
85 85 date: 1970-01-01T00:00:00+00:00
86 86 path: bar
87 87 date: 1970-01-01T00:00:01+00:00
88 88
89 89 Nested example:
90 90
91 91 >>> def subrepos(ui, fm):
92 92 ... fm.startitem()
93 93 ... fm.write('repo', '[%s]\\n', 'baz')
94 94 ... files(ui, fm.nested('files'))
95 95 ... fm.end()
96 96 >>> show(subrepos)
97 97 [baz]
98 98 foo
99 99 bar
100 100 >>> show(subrepos, template='{repo}: {join(files % "{path}", ", ")}\\n')
101 101 baz: foo, bar
102 102 """
103 103
104 104 from __future__ import absolute_import
105 105
106 106 import itertools
107 107 import os
108 108
109 109 from .i18n import _
110 110 from .node import (
111 111 hex,
112 112 short,
113 113 )
114 114
115 115 from . import (
116 116 error,
117 117 templatefilters,
118 118 templatekw,
119 119 templater,
120 120 util,
121 121 )
122 122
123 123 pickle = util.pickle
124 124
125 125 class _nullconverter(object):
126 126 '''convert non-primitive data types to be processed by formatter'''
127 127 @staticmethod
128 128 def formatdate(date, fmt):
129 129 '''convert date tuple to appropriate format'''
130 130 return date
131 131 @staticmethod
132 132 def formatdict(data, key, value, fmt, sep):
133 133 '''convert dict or key-value pairs to appropriate dict format'''
134 134 # use plain dict instead of util.sortdict so that data can be
135 135 # serialized as a builtin dict in pickle output
136 136 return dict(data)
137 137 @staticmethod
138 138 def formatlist(data, name, fmt, sep):
139 139 '''convert iterable to appropriate list format'''
140 140 return list(data)
141 141
142 142 class baseformatter(object):
143 143 def __init__(self, ui, topic, opts, converter):
144 144 self._ui = ui
145 145 self._topic = topic
146 146 self._style = opts.get("style")
147 147 self._template = opts.get("template")
148 148 self._converter = converter
149 149 self._item = None
150 150 # function to convert node to string suitable for this output
151 151 self.hexfunc = hex
152 152 def __enter__(self):
153 153 return self
154 154 def __exit__(self, exctype, excvalue, traceback):
155 155 if exctype is None:
156 156 self.end()
157 157 def _showitem(self):
158 158 '''show a formatted item once all data is collected'''
159 159 pass
160 160 def startitem(self):
161 161 '''begin an item in the format list'''
162 162 if self._item is not None:
163 163 self._showitem()
164 164 self._item = {}
165 165 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
166 166 '''convert date tuple to appropriate format'''
167 167 return self._converter.formatdate(date, fmt)
168 168 def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
169 169 '''convert dict or key-value pairs to appropriate dict format'''
170 170 return self._converter.formatdict(data, key, value, fmt, sep)
171 171 def formatlist(self, data, name, fmt='%s', sep=' '):
172 172 '''convert iterable to appropriate list format'''
173 173 # name is mandatory argument for now, but it could be optional if
174 174 # we have default template keyword, e.g. {item}
175 175 return self._converter.formatlist(data, name, fmt, sep)
176 176 def context(self, **ctxs):
177 177 '''insert context objects to be used to render template keywords'''
178 178 pass
179 179 def data(self, **data):
180 180 '''insert data into item that's not shown in default output'''
181 181 self._item.update(data)
182 182 def write(self, fields, deftext, *fielddata, **opts):
183 183 '''do default text output while assigning data to item'''
184 184 fieldkeys = fields.split()
185 185 assert len(fieldkeys) == len(fielddata)
186 186 self._item.update(zip(fieldkeys, fielddata))
187 187 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
188 188 '''do conditional write (primarily for plain formatter)'''
189 189 fieldkeys = fields.split()
190 190 assert len(fieldkeys) == len(fielddata)
191 191 self._item.update(zip(fieldkeys, fielddata))
192 192 def plain(self, text, **opts):
193 193 '''show raw text for non-templated mode'''
194 194 pass
195 195 def isplain(self):
196 196 '''check for plain formatter usage'''
197 197 return False
198 198 def nested(self, field):
199 199 '''sub formatter to store nested data in the specified field'''
200 200 self._item[field] = data = []
201 201 return _nestedformatter(self._ui, self._converter, data)
202 202 def end(self):
203 203 '''end output for the formatter'''
204 204 if self._item is not None:
205 205 self._showitem()
206 206
207 207 class _nestedformatter(baseformatter):
208 208 '''build sub items and store them in the parent formatter'''
209 209 def __init__(self, ui, converter, data):
210 210 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
211 211 self._data = data
212 212 def _showitem(self):
213 213 self._data.append(self._item)
214 214
215 215 def _iteritems(data):
216 216 '''iterate key-value pairs in stable order'''
217 217 if isinstance(data, dict):
218 218 return sorted(data.iteritems())
219 219 return data
220 220
221 221 class _plainconverter(object):
222 222 '''convert non-primitive data types to text'''
223 223 @staticmethod
224 224 def formatdate(date, fmt):
225 225 '''stringify date tuple in the given format'''
226 226 return util.datestr(date, fmt)
227 227 @staticmethod
228 228 def formatdict(data, key, value, fmt, sep):
229 229 '''stringify key-value pairs separated by sep'''
230 230 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
231 231 @staticmethod
232 232 def formatlist(data, name, fmt, sep):
233 233 '''stringify iterable separated by sep'''
234 234 return sep.join(fmt % e for e in data)
235 235
236 236 class plainformatter(baseformatter):
237 237 '''the default text output scheme'''
238 238 def __init__(self, ui, topic, opts):
239 239 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
240 240 if ui.debugflag:
241 241 self.hexfunc = hex
242 242 else:
243 243 self.hexfunc = short
244 244 def startitem(self):
245 245 pass
246 246 def data(self, **data):
247 247 pass
248 248 def write(self, fields, deftext, *fielddata, **opts):
249 249 self._ui.write(deftext % fielddata, **opts)
250 250 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
251 251 '''do conditional write'''
252 252 if cond:
253 253 self._ui.write(deftext % fielddata, **opts)
254 254 def plain(self, text, **opts):
255 255 self._ui.write(text, **opts)
256 256 def isplain(self):
257 257 return True
258 258 def nested(self, field):
259 259 # nested data will be directly written to ui
260 260 return self
261 261 def end(self):
262 262 pass
263 263
264 264 class debugformatter(baseformatter):
265 265 def __init__(self, ui, out, topic, opts):
266 266 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
267 267 self._out = out
268 268 self._out.write("%s = [\n" % self._topic)
269 269 def _showitem(self):
270 270 self._out.write(" " + repr(self._item) + ",\n")
271 271 def end(self):
272 272 baseformatter.end(self)
273 273 self._out.write("]\n")
274 274
275 275 class pickleformatter(baseformatter):
276 276 def __init__(self, ui, out, topic, opts):
277 277 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
278 278 self._out = out
279 279 self._data = []
280 280 def _showitem(self):
281 281 self._data.append(self._item)
282 282 def end(self):
283 283 baseformatter.end(self)
284 284 self._out.write(pickle.dumps(self._data))
285 285
286 286 class jsonformatter(baseformatter):
287 287 def __init__(self, ui, out, topic, opts):
288 288 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
289 289 self._out = out
290 290 self._out.write("[")
291 291 self._first = True
292 292 def _showitem(self):
293 293 if self._first:
294 294 self._first = False
295 295 else:
296 296 self._out.write(",")
297 297
298 298 self._out.write("\n {\n")
299 299 first = True
300 300 for k, v in sorted(self._item.items()):
301 301 if first:
302 302 first = False
303 303 else:
304 304 self._out.write(",\n")
305 305 u = templatefilters.json(v, paranoid=False)
306 306 self._out.write(' "%s": %s' % (k, u))
307 307 self._out.write("\n }")
308 308 def end(self):
309 309 baseformatter.end(self)
310 310 self._out.write("\n]\n")
311 311
312 312 class _templateconverter(object):
313 313 '''convert non-primitive data types to be processed by templater'''
314 314 @staticmethod
315 315 def formatdate(date, fmt):
316 316 '''return date tuple'''
317 317 return date
318 318 @staticmethod
319 319 def formatdict(data, key, value, fmt, sep):
320 320 '''build object that can be evaluated as either plain string or dict'''
321 321 data = util.sortdict(_iteritems(data))
322 322 def f():
323 323 yield _plainconverter.formatdict(data, key, value, fmt, sep)
324 return templatekw._hybrid(f(), data, lambda k: {key: k, value: data[k]},
325 lambda d: fmt % (d[key], d[value]))
324 return templatekw.hybriddict(data, key=key, value=value, fmt=fmt,
325 gen=f())
326 326 @staticmethod
327 327 def formatlist(data, name, fmt, sep):
328 328 '''build object that can be evaluated as either plain string or list'''
329 329 data = list(data)
330 330 def f():
331 331 yield _plainconverter.formatlist(data, name, fmt, sep)
332 332 return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f())
333 333
334 334 class templateformatter(baseformatter):
335 335 def __init__(self, ui, out, topic, opts):
336 336 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
337 337 self._out = out
338 338 self._topic = topic
339 339 self._t = gettemplater(ui, topic, opts.get('template', ''),
340 340 cache=templatekw.defaulttempl)
341 341 self._counter = itertools.count()
342 342 self._cache = {} # for templatekw/funcs to store reusable data
343 343 def context(self, **ctxs):
344 344 '''insert context objects to be used to render template keywords'''
345 345 assert all(k == 'ctx' for k in ctxs)
346 346 self._item.update(ctxs)
347 347 def _showitem(self):
348 348 # TODO: add support for filectx. probably each template keyword or
349 349 # function will have to declare dependent resources. e.g.
350 350 # @templatekeyword(..., requires=('ctx',))
351 351 props = {}
352 352 if 'ctx' in self._item:
353 353 props.update(templatekw.keywords)
354 354 props['index'] = next(self._counter)
355 355 # explicitly-defined fields precede templatekw
356 356 props.update(self._item)
357 357 if 'ctx' in self._item:
358 358 # but template resources must be always available
359 359 props['templ'] = self._t
360 360 props['repo'] = props['ctx'].repo()
361 361 props['revcache'] = {}
362 362 g = self._t(self._topic, ui=self._ui, cache=self._cache, **props)
363 363 self._out.write(templater.stringify(g))
364 364
365 365 def lookuptemplate(ui, topic, tmpl):
366 366 # looks like a literal template?
367 367 if '{' in tmpl:
368 368 return tmpl, None
369 369
370 370 # perhaps a stock style?
371 371 if not os.path.split(tmpl)[0]:
372 372 mapname = (templater.templatepath('map-cmdline.' + tmpl)
373 373 or templater.templatepath(tmpl))
374 374 if mapname and os.path.isfile(mapname):
375 375 return None, mapname
376 376
377 377 # perhaps it's a reference to [templates]
378 378 t = ui.config('templates', tmpl)
379 379 if t:
380 380 return templater.unquotestring(t), None
381 381
382 382 if tmpl == 'list':
383 383 ui.write(_("available styles: %s\n") % templater.stylelist())
384 384 raise error.Abort(_("specify a template"))
385 385
386 386 # perhaps it's a path to a map or a template
387 387 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
388 388 # is it a mapfile for a style?
389 389 if os.path.basename(tmpl).startswith("map-"):
390 390 return None, os.path.realpath(tmpl)
391 391 tmpl = open(tmpl).read()
392 392 return tmpl, None
393 393
394 394 # constant string?
395 395 return tmpl, None
396 396
397 397 def gettemplater(ui, topic, spec, cache=None):
398 398 tmpl, mapfile = lookuptemplate(ui, topic, spec)
399 399 assert not (tmpl and mapfile)
400 400 if mapfile:
401 401 return templater.templater.frommapfile(mapfile, cache=cache)
402 402 return maketemplater(ui, topic, tmpl, cache=cache)
403 403
404 404 def maketemplater(ui, topic, tmpl, cache=None):
405 405 """Create a templater from a string template 'tmpl'"""
406 406 aliases = ui.configitems('templatealias')
407 407 t = templater.templater(cache=cache, aliases=aliases)
408 408 if tmpl:
409 409 t.cache[topic] = tmpl
410 410 return t
411 411
412 412 def formatter(ui, topic, opts):
413 413 template = opts.get("template", "")
414 414 if template == "json":
415 415 return jsonformatter(ui, ui, topic, opts)
416 416 elif template == "pickle":
417 417 return pickleformatter(ui, ui, topic, opts)
418 418 elif template == "debug":
419 419 return debugformatter(ui, ui, topic, opts)
420 420 elif template != "":
421 421 return templateformatter(ui, ui, topic, opts)
422 422 # developer config: ui.formatdebug
423 423 elif ui.configbool('ui', 'formatdebug'):
424 424 return debugformatter(ui, ui, topic, opts)
425 425 # deprecated config: ui.formatjson
426 426 elif ui.configbool('ui', 'formatjson'):
427 427 return jsonformatter(ui, ui, topic, opts)
428 428 return plainformatter(ui, topic, opts)
@@ -1,672 +1,677 b''
1 1 # templatekw.py - common changeset template keywords
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from .i18n import _
11 11 from .node import hex, nullid
12 12 from . import (
13 13 encoding,
14 14 error,
15 15 hbisect,
16 16 patch,
17 17 registrar,
18 18 scmutil,
19 19 util,
20 20 )
21 21
22 22 class _hybrid(object):
23 23 """Wrapper for list or dict to support legacy template
24 24
25 25 This class allows us to handle both:
26 26 - "{files}" (legacy command-line-specific list hack) and
27 27 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
28 28 and to access raw values:
29 29 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
30 30 - "{get(extras, key)}"
31 31 - "{files|json}"
32 32 """
33 33
34 34 def __init__(self, gen, values, makemap, joinfmt):
35 35 if gen is not None:
36 36 self.gen = gen
37 37 self._values = values
38 38 self._makemap = makemap
39 39 self.joinfmt = joinfmt
40 40 @util.propertycache
41 41 def gen(self):
42 42 return self._defaultgen()
43 43 def _defaultgen(self):
44 44 """Generator to stringify this as {join(self, ' ')}"""
45 45 for i, d in enumerate(self.itermaps()):
46 46 if i > 0:
47 47 yield ' '
48 48 yield self.joinfmt(d)
49 49 def itermaps(self):
50 50 makemap = self._makemap
51 51 for x in self._values:
52 52 yield makemap(x)
53 53 def __contains__(self, x):
54 54 return x in self._values
55 55 def __len__(self):
56 56 return len(self._values)
57 57 def __iter__(self):
58 58 return iter(self._values)
59 59 def __getattr__(self, name):
60 60 if name not in ('get', 'items', 'iteritems', 'iterkeys', 'itervalues',
61 61 'keys', 'values'):
62 62 raise AttributeError(name)
63 63 return getattr(self._values, name)
64 64
65 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
66 """Wrap data to support both dict-like and string-like operations"""
67 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
68 lambda d: fmt % (d[key], d[value]))
69
65 70 def hybridlist(data, name, fmt='%s', gen=None):
66 71 """Wrap data to support both list-like and string-like operations"""
67 72 return _hybrid(gen, data, lambda x: {name: x}, lambda d: fmt % d[name])
68 73
69 74 def unwraphybrid(thing):
70 75 """Return an object which can be stringified possibly by using a legacy
71 76 template"""
72 77 if not util.safehasattr(thing, 'gen'):
73 78 return thing
74 79 return thing.gen
75 80
76 81 def showlist(name, values, plural=None, element=None, separator=' ', **args):
77 82 if not element:
78 83 element = name
79 84 f = _showlist(name, values, plural, separator, **args)
80 85 return hybridlist(values, name=element, gen=f)
81 86
82 87 def _showlist(name, values, plural=None, separator=' ', **args):
83 88 '''expand set of values.
84 89 name is name of key in template map.
85 90 values is list of strings or dicts.
86 91 plural is plural of name, if not simply name + 's'.
87 92 separator is used to join values as a string
88 93
89 94 expansion works like this, given name 'foo'.
90 95
91 96 if values is empty, expand 'no_foos'.
92 97
93 98 if 'foo' not in template map, return values as a string,
94 99 joined by 'separator'.
95 100
96 101 expand 'start_foos'.
97 102
98 103 for each value, expand 'foo'. if 'last_foo' in template
99 104 map, expand it instead of 'foo' for last key.
100 105
101 106 expand 'end_foos'.
102 107 '''
103 108 templ = args['templ']
104 109 if plural:
105 110 names = plural
106 111 else: names = name + 's'
107 112 if not values:
108 113 noname = 'no_' + names
109 114 if noname in templ:
110 115 yield templ(noname, **args)
111 116 return
112 117 if name not in templ:
113 118 if isinstance(values[0], str):
114 119 yield separator.join(values)
115 120 else:
116 121 for v in values:
117 122 yield dict(v, **args)
118 123 return
119 124 startname = 'start_' + names
120 125 if startname in templ:
121 126 yield templ(startname, **args)
122 127 vargs = args.copy()
123 128 def one(v, tag=name):
124 129 try:
125 130 vargs.update(v)
126 131 except (AttributeError, ValueError):
127 132 try:
128 133 for a, b in v:
129 134 vargs[a] = b
130 135 except ValueError:
131 136 vargs[name] = v
132 137 return templ(tag, **vargs)
133 138 lastname = 'last_' + name
134 139 if lastname in templ:
135 140 last = values.pop()
136 141 else:
137 142 last = None
138 143 for v in values:
139 144 yield one(v)
140 145 if last is not None:
141 146 yield one(last, tag=lastname)
142 147 endname = 'end_' + names
143 148 if endname in templ:
144 149 yield templ(endname, **args)
145 150
146 151 def _formatrevnode(ctx):
147 152 """Format changeset as '{rev}:{node|formatnode}', which is the default
148 153 template provided by cmdutil.changeset_templater"""
149 154 repo = ctx.repo()
150 155 if repo.ui.debugflag:
151 156 hexnode = ctx.hex()
152 157 else:
153 158 hexnode = ctx.hex()[:12]
154 159 return '%d:%s' % (scmutil.intrev(ctx.rev()), hexnode)
155 160
156 161 def getfiles(repo, ctx, revcache):
157 162 if 'files' not in revcache:
158 163 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
159 164 return revcache['files']
160 165
161 166 def getlatesttags(repo, ctx, cache, pattern=None):
162 167 '''return date, distance and name for the latest tag of rev'''
163 168
164 169 cachename = 'latesttags'
165 170 if pattern is not None:
166 171 cachename += '-' + pattern
167 172 match = util.stringmatcher(pattern)[2]
168 173 else:
169 174 match = util.always
170 175
171 176 if cachename not in cache:
172 177 # Cache mapping from rev to a tuple with tag date, tag
173 178 # distance and tag name
174 179 cache[cachename] = {-1: (0, 0, ['null'])}
175 180 latesttags = cache[cachename]
176 181
177 182 rev = ctx.rev()
178 183 todo = [rev]
179 184 while todo:
180 185 rev = todo.pop()
181 186 if rev in latesttags:
182 187 continue
183 188 ctx = repo[rev]
184 189 tags = [t for t in ctx.tags()
185 190 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
186 191 and match(t))]
187 192 if tags:
188 193 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
189 194 continue
190 195 try:
191 196 # The tuples are laid out so the right one can be found by
192 197 # comparison.
193 198 pdate, pdist, ptag = max(
194 199 latesttags[p.rev()] for p in ctx.parents())
195 200 except KeyError:
196 201 # Cache miss - recurse
197 202 todo.append(rev)
198 203 todo.extend(p.rev() for p in ctx.parents())
199 204 continue
200 205 latesttags[rev] = pdate, pdist + 1, ptag
201 206 return latesttags[rev]
202 207
203 208 def getrenamedfn(repo, endrev=None):
204 209 rcache = {}
205 210 if endrev is None:
206 211 endrev = len(repo)
207 212
208 213 def getrenamed(fn, rev):
209 214 '''looks up all renames for a file (up to endrev) the first
210 215 time the file is given. It indexes on the changerev and only
211 216 parses the manifest if linkrev != changerev.
212 217 Returns rename info for fn at changerev rev.'''
213 218 if fn not in rcache:
214 219 rcache[fn] = {}
215 220 fl = repo.file(fn)
216 221 for i in fl:
217 222 lr = fl.linkrev(i)
218 223 renamed = fl.renamed(fl.node(i))
219 224 rcache[fn][lr] = renamed
220 225 if lr >= endrev:
221 226 break
222 227 if rev in rcache[fn]:
223 228 return rcache[fn][rev]
224 229
225 230 # If linkrev != rev (i.e. rev not found in rcache) fallback to
226 231 # filectx logic.
227 232 try:
228 233 return repo[rev][fn].renamed()
229 234 except error.LookupError:
230 235 return None
231 236
232 237 return getrenamed
233 238
234 239 # default templates internally used for rendering of lists
235 240 defaulttempl = {
236 241 'parent': '{rev}:{node|formatnode} ',
237 242 'manifest': '{rev}:{node|formatnode}',
238 243 'file_copy': '{name} ({source})',
239 244 'envvar': '{key}={value}',
240 245 'extra': '{key}={value|stringescape}'
241 246 }
242 247 # filecopy is preserved for compatibility reasons
243 248 defaulttempl['filecopy'] = defaulttempl['file_copy']
244 249
245 250 # keywords are callables like:
246 251 # fn(repo, ctx, templ, cache, revcache, **args)
247 252 # with:
248 253 # repo - current repository instance
249 254 # ctx - the changectx being displayed
250 255 # templ - the templater instance
251 256 # cache - a cache dictionary for the whole templater run
252 257 # revcache - a cache dictionary for the current revision
253 258 keywords = {}
254 259
255 260 templatekeyword = registrar.templatekeyword(keywords)
256 261
257 262 @templatekeyword('author')
258 263 def showauthor(repo, ctx, templ, **args):
259 264 """String. The unmodified author of the changeset."""
260 265 return ctx.user()
261 266
262 267 @templatekeyword('bisect')
263 268 def showbisect(repo, ctx, templ, **args):
264 269 """String. The changeset bisection status."""
265 270 return hbisect.label(repo, ctx.node())
266 271
267 272 @templatekeyword('branch')
268 273 def showbranch(**args):
269 274 """String. The name of the branch on which the changeset was
270 275 committed.
271 276 """
272 277 return args['ctx'].branch()
273 278
274 279 @templatekeyword('branches')
275 280 def showbranches(**args):
276 281 """List of strings. The name of the branch on which the
277 282 changeset was committed. Will be empty if the branch name was
278 283 default. (DEPRECATED)
279 284 """
280 285 branch = args['ctx'].branch()
281 286 if branch != 'default':
282 287 return showlist('branch', [branch], plural='branches', **args)
283 288 return showlist('branch', [], plural='branches', **args)
284 289
285 290 @templatekeyword('bookmarks')
286 291 def showbookmarks(**args):
287 292 """List of strings. Any bookmarks associated with the
288 293 changeset. Also sets 'active', the name of the active bookmark.
289 294 """
290 295 repo = args['ctx']._repo
291 296 bookmarks = args['ctx'].bookmarks()
292 297 active = repo._activebookmark
293 298 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
294 299 f = _showlist('bookmark', bookmarks, **args)
295 300 return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark'])
296 301
297 302 @templatekeyword('children')
298 303 def showchildren(**args):
299 304 """List of strings. The children of the changeset."""
300 305 ctx = args['ctx']
301 306 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
302 307 return showlist('children', childrevs, element='child', **args)
303 308
304 309 # Deprecated, but kept alive for help generation a purpose.
305 310 @templatekeyword('currentbookmark')
306 311 def showcurrentbookmark(**args):
307 312 """String. The active bookmark, if it is
308 313 associated with the changeset (DEPRECATED)"""
309 314 return showactivebookmark(**args)
310 315
311 316 @templatekeyword('activebookmark')
312 317 def showactivebookmark(**args):
313 318 """String. The active bookmark, if it is
314 319 associated with the changeset"""
315 320 active = args['repo']._activebookmark
316 321 if active and active in args['ctx'].bookmarks():
317 322 return active
318 323 return ''
319 324
320 325 @templatekeyword('date')
321 326 def showdate(repo, ctx, templ, **args):
322 327 """Date information. The date when the changeset was committed."""
323 328 return ctx.date()
324 329
325 330 @templatekeyword('desc')
326 331 def showdescription(repo, ctx, templ, **args):
327 332 """String. The text of the changeset description."""
328 333 s = ctx.description()
329 334 if isinstance(s, encoding.localstr):
330 335 # try hard to preserve utf-8 bytes
331 336 return encoding.tolocal(encoding.fromlocal(s).strip())
332 337 else:
333 338 return s.strip()
334 339
335 340 @templatekeyword('diffstat')
336 341 def showdiffstat(repo, ctx, templ, **args):
337 342 """String. Statistics of changes with the following format:
338 343 "modified files: +added/-removed lines"
339 344 """
340 345 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
341 346 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
342 347 return '%s: +%s/-%s' % (len(stats), adds, removes)
343 348
344 349 @templatekeyword('envvars')
345 350 def showenvvars(repo, **args):
346 351 """A dictionary of environment variables. (EXPERIMENTAL)"""
347 352
348 353 env = repo.ui.exportableenviron()
349 354 env = util.sortdict((k, env[k]) for k in sorted(env))
350 355 makemap = lambda k: {'key': k, 'value': env[k]}
351 356 c = [makemap(k) for k in env]
352 357 f = _showlist('envvar', c, plural='envvars', **args)
353 358 return _hybrid(f, env, makemap,
354 359 lambda x: '%s=%s' % (x['key'], x['value']))
355 360
356 361 @templatekeyword('extras')
357 362 def showextras(**args):
358 363 """List of dicts with key, value entries of the 'extras'
359 364 field of this changeset."""
360 365 extras = args['ctx'].extra()
361 366 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
362 367 makemap = lambda k: {'key': k, 'value': extras[k]}
363 368 c = [makemap(k) for k in extras]
364 369 f = _showlist('extra', c, plural='extras', **args)
365 370 return _hybrid(f, extras, makemap,
366 371 lambda x: '%s=%s' % (x['key'], util.escapestr(x['value'])))
367 372
368 373 @templatekeyword('file_adds')
369 374 def showfileadds(**args):
370 375 """List of strings. Files added by this changeset."""
371 376 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
372 377 return showlist('file_add', getfiles(repo, ctx, revcache)[1],
373 378 element='file', **args)
374 379
375 380 @templatekeyword('file_copies')
376 381 def showfilecopies(**args):
377 382 """List of strings. Files copied in this changeset with
378 383 their sources.
379 384 """
380 385 cache, ctx = args['cache'], args['ctx']
381 386 copies = args['revcache'].get('copies')
382 387 if copies is None:
383 388 if 'getrenamed' not in cache:
384 389 cache['getrenamed'] = getrenamedfn(args['repo'])
385 390 copies = []
386 391 getrenamed = cache['getrenamed']
387 392 for fn in ctx.files():
388 393 rename = getrenamed(fn, ctx.rev())
389 394 if rename:
390 395 copies.append((fn, rename[0]))
391 396
392 397 copies = util.sortdict(copies)
393 398 makemap = lambda k: {'name': k, 'source': copies[k]}
394 399 c = [makemap(k) for k in copies]
395 400 f = _showlist('file_copy', c, plural='file_copies', **args)
396 401 return _hybrid(f, copies, makemap,
397 402 lambda x: '%s (%s)' % (x['name'], x['source']))
398 403
399 404 # showfilecopiesswitch() displays file copies only if copy records are
400 405 # provided before calling the templater, usually with a --copies
401 406 # command line switch.
402 407 @templatekeyword('file_copies_switch')
403 408 def showfilecopiesswitch(**args):
404 409 """List of strings. Like "file_copies" but displayed
405 410 only if the --copied switch is set.
406 411 """
407 412 copies = args['revcache'].get('copies') or []
408 413 copies = util.sortdict(copies)
409 414 makemap = lambda k: {'name': k, 'source': copies[k]}
410 415 c = [makemap(k) for k in copies]
411 416 f = _showlist('file_copy', c, plural='file_copies', **args)
412 417 return _hybrid(f, copies, makemap,
413 418 lambda x: '%s (%s)' % (x['name'], x['source']))
414 419
415 420 @templatekeyword('file_dels')
416 421 def showfiledels(**args):
417 422 """List of strings. Files removed by this changeset."""
418 423 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
419 424 return showlist('file_del', getfiles(repo, ctx, revcache)[2],
420 425 element='file', **args)
421 426
422 427 @templatekeyword('file_mods')
423 428 def showfilemods(**args):
424 429 """List of strings. Files modified by this changeset."""
425 430 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
426 431 return showlist('file_mod', getfiles(repo, ctx, revcache)[0],
427 432 element='file', **args)
428 433
429 434 @templatekeyword('files')
430 435 def showfiles(**args):
431 436 """List of strings. All files modified, added, or removed by this
432 437 changeset.
433 438 """
434 439 return showlist('file', args['ctx'].files(), **args)
435 440
436 441 @templatekeyword('graphnode')
437 442 def showgraphnode(repo, ctx, **args):
438 443 """String. The character representing the changeset node in
439 444 an ASCII revision graph"""
440 445 wpnodes = repo.dirstate.parents()
441 446 if wpnodes[1] == nullid:
442 447 wpnodes = wpnodes[:1]
443 448 if ctx.node() in wpnodes:
444 449 return '@'
445 450 elif ctx.obsolete():
446 451 return 'x'
447 452 elif ctx.closesbranch():
448 453 return '_'
449 454 else:
450 455 return 'o'
451 456
452 457 @templatekeyword('index')
453 458 def showindex(**args):
454 459 """Integer. The current iteration of the loop. (0 indexed)"""
455 460 # just hosts documentation; should be overridden by template mapping
456 461 raise error.Abort(_("can't use index in this context"))
457 462
458 463 @templatekeyword('latesttag')
459 464 def showlatesttag(**args):
460 465 """List of strings. The global tags on the most recent globally
461 466 tagged ancestor of this changeset. If no such tags exist, the list
462 467 consists of the single string "null".
463 468 """
464 469 return showlatesttags(None, **args)
465 470
466 471 def showlatesttags(pattern, **args):
467 472 """helper method for the latesttag keyword and function"""
468 473 repo, ctx = args['repo'], args['ctx']
469 474 cache = args['cache']
470 475 latesttags = getlatesttags(repo, ctx, cache, pattern)
471 476
472 477 # latesttag[0] is an implementation detail for sorting csets on different
473 478 # branches in a stable manner- it is the date the tagged cset was created,
474 479 # not the date the tag was created. Therefore it isn't made visible here.
475 480 makemap = lambda v: {
476 481 'changes': _showchangessincetag,
477 482 'distance': latesttags[1],
478 483 'latesttag': v, # BC with {latesttag % '{latesttag}'}
479 484 'tag': v
480 485 }
481 486
482 487 tags = latesttags[2]
483 488 f = _showlist('latesttag', tags, separator=':', **args)
484 489 return _hybrid(f, tags, makemap, lambda x: x['latesttag'])
485 490
486 491 @templatekeyword('latesttagdistance')
487 492 def showlatesttagdistance(repo, ctx, templ, cache, **args):
488 493 """Integer. Longest path to the latest tag."""
489 494 return getlatesttags(repo, ctx, cache)[1]
490 495
491 496 @templatekeyword('changessincelatesttag')
492 497 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
493 498 """Integer. All ancestors not in the latest tag."""
494 499 latesttag = getlatesttags(repo, ctx, cache)[2][0]
495 500
496 501 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
497 502
498 503 def _showchangessincetag(repo, ctx, **args):
499 504 offset = 0
500 505 revs = [ctx.rev()]
501 506 tag = args['tag']
502 507
503 508 # The only() revset doesn't currently support wdir()
504 509 if ctx.rev() is None:
505 510 offset = 1
506 511 revs = [p.rev() for p in ctx.parents()]
507 512
508 513 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
509 514
510 515 @templatekeyword('manifest')
511 516 def showmanifest(**args):
512 517 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
513 518 mnode = ctx.manifestnode()
514 519 if mnode is None:
515 520 # just avoid crash, we might want to use the 'ff...' hash in future
516 521 return
517 522 args = args.copy()
518 523 args.update({'rev': repo.manifestlog._revlog.rev(mnode),
519 524 'node': hex(mnode)})
520 525 return templ('manifest', **args)
521 526
522 527 def shownames(namespace, **args):
523 528 """helper method to generate a template keyword for a namespace"""
524 529 ctx = args['ctx']
525 530 repo = ctx.repo()
526 531 ns = repo.names[namespace]
527 532 names = ns.names(repo, ctx.node())
528 533 return showlist(ns.templatename, names, plural=namespace, **args)
529 534
530 535 @templatekeyword('namespaces')
531 536 def shownamespaces(**args):
532 537 """Dict of lists. Names attached to this changeset per
533 538 namespace."""
534 539 ctx = args['ctx']
535 540 repo = ctx.repo()
536 541 namespaces = util.sortdict((k, showlist('name', ns.names(repo, ctx.node()),
537 542 **args))
538 543 for k, ns in repo.names.iteritems())
539 544 f = _showlist('namespace', list(namespaces), **args)
540 545 return _hybrid(f, namespaces,
541 546 lambda k: {'namespace': k, 'names': namespaces[k]},
542 547 lambda x: x['namespace'])
543 548
544 549 @templatekeyword('node')
545 550 def shownode(repo, ctx, templ, **args):
546 551 """String. The changeset identification hash, as a 40 hexadecimal
547 552 digit string.
548 553 """
549 554 return ctx.hex()
550 555
551 556 @templatekeyword('obsolete')
552 557 def showobsolete(repo, ctx, templ, **args):
553 558 """String. Whether the changeset is obsolete.
554 559 """
555 560 if ctx.obsolete():
556 561 return 'obsolete'
557 562 return ''
558 563
559 564 @templatekeyword('p1rev')
560 565 def showp1rev(repo, ctx, templ, **args):
561 566 """Integer. The repository-local revision number of the changeset's
562 567 first parent, or -1 if the changeset has no parents."""
563 568 return ctx.p1().rev()
564 569
565 570 @templatekeyword('p2rev')
566 571 def showp2rev(repo, ctx, templ, **args):
567 572 """Integer. The repository-local revision number of the changeset's
568 573 second parent, or -1 if the changeset has no second parent."""
569 574 return ctx.p2().rev()
570 575
571 576 @templatekeyword('p1node')
572 577 def showp1node(repo, ctx, templ, **args):
573 578 """String. The identification hash of the changeset's first parent,
574 579 as a 40 digit hexadecimal string. If the changeset has no parents, all
575 580 digits are 0."""
576 581 return ctx.p1().hex()
577 582
578 583 @templatekeyword('p2node')
579 584 def showp2node(repo, ctx, templ, **args):
580 585 """String. The identification hash of the changeset's second
581 586 parent, as a 40 digit hexadecimal string. If the changeset has no second
582 587 parent, all digits are 0."""
583 588 return ctx.p2().hex()
584 589
585 590 @templatekeyword('parents')
586 591 def showparents(**args):
587 592 """List of strings. The parents of the changeset in "rev:node"
588 593 format. If the changeset has only one "natural" parent (the predecessor
589 594 revision) nothing is shown."""
590 595 repo = args['repo']
591 596 ctx = args['ctx']
592 597 pctxs = scmutil.meaningfulparents(repo, ctx)
593 598 prevs = [str(p.rev()) for p in pctxs] # ifcontains() needs a list of str
594 599 parents = [[('rev', p.rev()),
595 600 ('node', p.hex()),
596 601 ('phase', p.phasestr())]
597 602 for p in pctxs]
598 603 f = _showlist('parent', parents, **args)
599 604 return _hybrid(f, prevs, lambda x: {'ctx': repo[int(x)], 'revcache': {}},
600 605 lambda d: _formatrevnode(d['ctx']))
601 606
602 607 @templatekeyword('phase')
603 608 def showphase(repo, ctx, templ, **args):
604 609 """String. The changeset phase name."""
605 610 return ctx.phasestr()
606 611
607 612 @templatekeyword('phaseidx')
608 613 def showphaseidx(repo, ctx, templ, **args):
609 614 """Integer. The changeset phase index."""
610 615 return ctx.phase()
611 616
612 617 @templatekeyword('rev')
613 618 def showrev(repo, ctx, templ, **args):
614 619 """Integer. The repository-local changeset revision number."""
615 620 return scmutil.intrev(ctx.rev())
616 621
617 622 def showrevslist(name, revs, **args):
618 623 """helper to generate a list of revisions in which a mapped template will
619 624 be evaluated"""
620 625 repo = args['ctx'].repo()
621 626 revs = [str(r) for r in revs] # ifcontains() needs a list of str
622 627 f = _showlist(name, revs, **args)
623 628 return _hybrid(f, revs,
624 629 lambda x: {name: x, 'ctx': repo[int(x)], 'revcache': {}},
625 630 lambda d: d[name])
626 631
627 632 @templatekeyword('subrepos')
628 633 def showsubrepos(**args):
629 634 """List of strings. Updated subrepositories in the changeset."""
630 635 ctx = args['ctx']
631 636 substate = ctx.substate
632 637 if not substate:
633 638 return showlist('subrepo', [], **args)
634 639 psubstate = ctx.parents()[0].substate or {}
635 640 subrepos = []
636 641 for sub in substate:
637 642 if sub not in psubstate or substate[sub] != psubstate[sub]:
638 643 subrepos.append(sub) # modified or newly added in ctx
639 644 for sub in psubstate:
640 645 if sub not in substate:
641 646 subrepos.append(sub) # removed in ctx
642 647 return showlist('subrepo', sorted(subrepos), **args)
643 648
644 649 # don't remove "showtags" definition, even though namespaces will put
645 650 # a helper function for "tags" keyword into "keywords" map automatically,
646 651 # because online help text is built without namespaces initialization
647 652 @templatekeyword('tags')
648 653 def showtags(**args):
649 654 """List of strings. Any tags associated with the changeset."""
650 655 return shownames('tags', **args)
651 656
652 657 def loadkeyword(ui, extname, registrarobj):
653 658 """Load template keyword from specified registrarobj
654 659 """
655 660 for name, func in registrarobj._table.iteritems():
656 661 keywords[name] = func
657 662
658 663 @templatekeyword('termwidth')
659 664 def termwidth(repo, ctx, templ, **args):
660 665 """Integer. The width of the current terminal."""
661 666 return repo.ui.termwidth()
662 667
663 668 @templatekeyword('troubles')
664 669 def showtroubles(**args):
665 670 """List of strings. Evolution troubles affecting the changeset.
666 671
667 672 (EXPERIMENTAL)
668 673 """
669 674 return showlist('trouble', args['ctx'].troubles(), **args)
670 675
671 676 # tell hggettext to extract docstrings from these functions:
672 677 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now