##// END OF EJS Templates
templater: allow dynamically switching the default dict/list formatting...
Yuya Nishihara -
r36651:034a07e6 default
parent child Browse files
Show More
@@ -1,543 +1,547 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.verbose = verbose
49 49 ... ui.pushbuffer()
50 50 ... try:
51 51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 52 ... pycompat.byteskwargs(opts)))
53 53 ... finally:
54 54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55 55
56 56 Basic example:
57 57
58 58 >>> def files(ui, fm):
59 59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 60 ... for f in files:
61 61 ... fm.startitem()
62 62 ... fm.write(b'path', b'%s', f[0])
63 63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 65 ... fm.data(size=f[1])
66 66 ... fm.plain(b'\\n')
67 67 ... fm.end()
68 68 >>> show(files)
69 69 foo
70 70 bar
71 71 >>> show(files, verbose=True)
72 72 foo 1970-01-01 00:00:00
73 73 bar 1970-01-01 00:00:01
74 74 >>> show(files, template=b'json')
75 75 [
76 76 {
77 77 "date": [0, 0],
78 78 "path": "foo",
79 79 "size": 123
80 80 },
81 81 {
82 82 "date": [1, 0],
83 83 "path": "bar",
84 84 "size": 456
85 85 }
86 86 ]
87 87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 88 path: foo
89 89 date: 1970-01-01T00:00:00+00:00
90 90 path: bar
91 91 date: 1970-01-01T00:00:01+00:00
92 92
93 93 Nested example:
94 94
95 95 >>> def subrepos(ui, fm):
96 96 ... fm.startitem()
97 97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 98 ... files(ui, fm.nested(b'files'))
99 99 ... fm.end()
100 100 >>> show(subrepos)
101 101 [baz]
102 102 foo
103 103 bar
104 104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 105 baz: foo, bar
106 106 """
107 107
108 108 from __future__ import absolute_import, print_function
109 109
110 110 import collections
111 111 import contextlib
112 112 import itertools
113 113 import os
114 114
115 115 from .i18n import _
116 116 from .node import (
117 117 hex,
118 118 short,
119 119 )
120 120
121 121 from . import (
122 122 error,
123 123 pycompat,
124 124 templatefilters,
125 125 templatekw,
126 126 templater,
127 127 util,
128 128 )
129 129 from .utils import dateutil
130 130
131 131 pickle = util.pickle
132 132
133 133 class _nullconverter(object):
134 134 '''convert non-primitive data types to be processed by formatter'''
135 135
136 136 # set to True if context object should be stored as item
137 137 storecontext = False
138 138
139 139 @staticmethod
140 140 def formatdate(date, fmt):
141 141 '''convert date tuple to appropriate format'''
142 142 return date
143 143 @staticmethod
144 144 def formatdict(data, key, value, fmt, sep):
145 145 '''convert dict or key-value pairs to appropriate dict format'''
146 146 # use plain dict instead of util.sortdict so that data can be
147 147 # serialized as a builtin dict in pickle output
148 148 return dict(data)
149 149 @staticmethod
150 150 def formatlist(data, name, fmt, sep):
151 151 '''convert iterable to appropriate list format'''
152 152 return list(data)
153 153
154 154 class baseformatter(object):
155 155 def __init__(self, ui, topic, opts, converter):
156 156 self._ui = ui
157 157 self._topic = topic
158 158 self._style = opts.get("style")
159 159 self._template = opts.get("template")
160 160 self._converter = converter
161 161 self._item = None
162 162 # function to convert node to string suitable for this output
163 163 self.hexfunc = hex
164 164 def __enter__(self):
165 165 return self
166 166 def __exit__(self, exctype, excvalue, traceback):
167 167 if exctype is None:
168 168 self.end()
169 169 def _showitem(self):
170 170 '''show a formatted item once all data is collected'''
171 171 def startitem(self):
172 172 '''begin an item in the format list'''
173 173 if self._item is not None:
174 174 self._showitem()
175 175 self._item = {}
176 176 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
177 177 '''convert date tuple to appropriate format'''
178 178 return self._converter.formatdate(date, fmt)
179 def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
179 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
180 180 '''convert dict or key-value pairs to appropriate dict format'''
181 181 return self._converter.formatdict(data, key, value, fmt, sep)
182 def formatlist(self, data, name, fmt='%s', sep=' '):
182 def formatlist(self, data, name, fmt=None, sep=' '):
183 183 '''convert iterable to appropriate list format'''
184 184 # name is mandatory argument for now, but it could be optional if
185 185 # we have default template keyword, e.g. {item}
186 186 return self._converter.formatlist(data, name, fmt, sep)
187 187 def context(self, **ctxs):
188 188 '''insert context objects to be used to render template keywords'''
189 189 ctxs = pycompat.byteskwargs(ctxs)
190 190 assert all(k == 'ctx' for k in ctxs)
191 191 if self._converter.storecontext:
192 192 self._item.update(ctxs)
193 193 def data(self, **data):
194 194 '''insert data into item that's not shown in default output'''
195 195 data = pycompat.byteskwargs(data)
196 196 self._item.update(data)
197 197 def write(self, fields, deftext, *fielddata, **opts):
198 198 '''do default text output while assigning data to item'''
199 199 fieldkeys = fields.split()
200 200 assert len(fieldkeys) == len(fielddata)
201 201 self._item.update(zip(fieldkeys, fielddata))
202 202 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
203 203 '''do conditional write (primarily for plain formatter)'''
204 204 fieldkeys = fields.split()
205 205 assert len(fieldkeys) == len(fielddata)
206 206 self._item.update(zip(fieldkeys, fielddata))
207 207 def plain(self, text, **opts):
208 208 '''show raw text for non-templated mode'''
209 209 def isplain(self):
210 210 '''check for plain formatter usage'''
211 211 return False
212 212 def nested(self, field):
213 213 '''sub formatter to store nested data in the specified field'''
214 214 self._item[field] = data = []
215 215 return _nestedformatter(self._ui, self._converter, data)
216 216 def end(self):
217 217 '''end output for the formatter'''
218 218 if self._item is not None:
219 219 self._showitem()
220 220
221 221 def nullformatter(ui, topic):
222 222 '''formatter that prints nothing'''
223 223 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
224 224
225 225 class _nestedformatter(baseformatter):
226 226 '''build sub items and store them in the parent formatter'''
227 227 def __init__(self, ui, converter, data):
228 228 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
229 229 self._data = data
230 230 def _showitem(self):
231 231 self._data.append(self._item)
232 232
233 233 def _iteritems(data):
234 234 '''iterate key-value pairs in stable order'''
235 235 if isinstance(data, dict):
236 236 return sorted(data.iteritems())
237 237 return data
238 238
239 239 class _plainconverter(object):
240 240 '''convert non-primitive data types to text'''
241 241
242 242 storecontext = False
243 243
244 244 @staticmethod
245 245 def formatdate(date, fmt):
246 246 '''stringify date tuple in the given format'''
247 247 return dateutil.datestr(date, fmt)
248 248 @staticmethod
249 249 def formatdict(data, key, value, fmt, sep):
250 250 '''stringify key-value pairs separated by sep'''
251 if fmt is None:
252 fmt = '%s=%s'
251 253 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
252 254 @staticmethod
253 255 def formatlist(data, name, fmt, sep):
254 256 '''stringify iterable separated by sep'''
257 if fmt is None:
258 fmt = '%s'
255 259 return sep.join(fmt % e for e in data)
256 260
257 261 class plainformatter(baseformatter):
258 262 '''the default text output scheme'''
259 263 def __init__(self, ui, out, topic, opts):
260 264 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
261 265 if ui.debugflag:
262 266 self.hexfunc = hex
263 267 else:
264 268 self.hexfunc = short
265 269 if ui is out:
266 270 self._write = ui.write
267 271 else:
268 272 self._write = lambda s, **opts: out.write(s)
269 273 def startitem(self):
270 274 pass
271 275 def data(self, **data):
272 276 pass
273 277 def write(self, fields, deftext, *fielddata, **opts):
274 278 self._write(deftext % fielddata, **opts)
275 279 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
276 280 '''do conditional write'''
277 281 if cond:
278 282 self._write(deftext % fielddata, **opts)
279 283 def plain(self, text, **opts):
280 284 self._write(text, **opts)
281 285 def isplain(self):
282 286 return True
283 287 def nested(self, field):
284 288 # nested data will be directly written to ui
285 289 return self
286 290 def end(self):
287 291 pass
288 292
289 293 class debugformatter(baseformatter):
290 294 def __init__(self, ui, out, topic, opts):
291 295 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
292 296 self._out = out
293 297 self._out.write("%s = [\n" % self._topic)
294 298 def _showitem(self):
295 299 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
296 300 def end(self):
297 301 baseformatter.end(self)
298 302 self._out.write("]\n")
299 303
300 304 class pickleformatter(baseformatter):
301 305 def __init__(self, ui, out, topic, opts):
302 306 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
303 307 self._out = out
304 308 self._data = []
305 309 def _showitem(self):
306 310 self._data.append(self._item)
307 311 def end(self):
308 312 baseformatter.end(self)
309 313 self._out.write(pickle.dumps(self._data))
310 314
311 315 class jsonformatter(baseformatter):
312 316 def __init__(self, ui, out, topic, opts):
313 317 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
314 318 self._out = out
315 319 self._out.write("[")
316 320 self._first = True
317 321 def _showitem(self):
318 322 if self._first:
319 323 self._first = False
320 324 else:
321 325 self._out.write(",")
322 326
323 327 self._out.write("\n {\n")
324 328 first = True
325 329 for k, v in sorted(self._item.items()):
326 330 if first:
327 331 first = False
328 332 else:
329 333 self._out.write(",\n")
330 334 u = templatefilters.json(v, paranoid=False)
331 335 self._out.write(' "%s": %s' % (k, u))
332 336 self._out.write("\n }")
333 337 def end(self):
334 338 baseformatter.end(self)
335 339 self._out.write("\n]\n")
336 340
337 341 class _templateconverter(object):
338 342 '''convert non-primitive data types to be processed by templater'''
339 343
340 344 storecontext = True
341 345
342 346 @staticmethod
343 347 def formatdate(date, fmt):
344 348 '''return date tuple'''
345 349 return date
346 350 @staticmethod
347 351 def formatdict(data, key, value, fmt, sep):
348 352 '''build object that can be evaluated as either plain string or dict'''
349 353 data = util.sortdict(_iteritems(data))
350 354 def f():
351 355 yield _plainconverter.formatdict(data, key, value, fmt, sep)
352 356 return templatekw.hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
353 357 @staticmethod
354 358 def formatlist(data, name, fmt, sep):
355 359 '''build object that can be evaluated as either plain string or list'''
356 360 data = list(data)
357 361 def f():
358 362 yield _plainconverter.formatlist(data, name, fmt, sep)
359 363 return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f)
360 364
361 365 class templateformatter(baseformatter):
362 366 def __init__(self, ui, out, topic, opts):
363 367 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
364 368 self._out = out
365 369 spec = lookuptemplate(ui, topic, opts.get('template', ''))
366 370 self._tref = spec.ref
367 371 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
368 372 resources=templateresources(ui),
369 373 cache=templatekw.defaulttempl)
370 374 self._parts = templatepartsmap(spec, self._t,
371 375 ['docheader', 'docfooter', 'separator'])
372 376 self._counter = itertools.count()
373 377 self._renderitem('docheader', {})
374 378
375 379 def _showitem(self):
376 380 item = self._item.copy()
377 381 item['index'] = index = next(self._counter)
378 382 if index > 0:
379 383 self._renderitem('separator', {})
380 384 self._renderitem(self._tref, item)
381 385
382 386 def _renderitem(self, part, item):
383 387 if part not in self._parts:
384 388 return
385 389 ref = self._parts[part]
386 390
387 391 # TODO: add support for filectx
388 392 props = {}
389 393 # explicitly-defined fields precede templatekw
390 394 props.update(item)
391 395 if 'ctx' in item:
392 396 # but template resources must be always available
393 397 props['repo'] = props['ctx'].repo()
394 398 props['revcache'] = {}
395 399 props = pycompat.strkwargs(props)
396 400 g = self._t(ref, **props)
397 401 self._out.write(templater.stringify(g))
398 402
399 403 def end(self):
400 404 baseformatter.end(self)
401 405 self._renderitem('docfooter', {})
402 406
403 407 templatespec = collections.namedtuple(r'templatespec',
404 408 r'ref tmpl mapfile')
405 409
406 410 def lookuptemplate(ui, topic, tmpl):
407 411 """Find the template matching the given -T/--template spec 'tmpl'
408 412
409 413 'tmpl' can be any of the following:
410 414
411 415 - a literal template (e.g. '{rev}')
412 416 - a map-file name or path (e.g. 'changelog')
413 417 - a reference to [templates] in config file
414 418 - a path to raw template file
415 419
416 420 A map file defines a stand-alone template environment. If a map file
417 421 selected, all templates defined in the file will be loaded, and the
418 422 template matching the given topic will be rendered. Aliases won't be
419 423 loaded from user config, but from the map file.
420 424
421 425 If no map file selected, all templates in [templates] section will be
422 426 available as well as aliases in [templatealias].
423 427 """
424 428
425 429 # looks like a literal template?
426 430 if '{' in tmpl:
427 431 return templatespec('', tmpl, None)
428 432
429 433 # perhaps a stock style?
430 434 if not os.path.split(tmpl)[0]:
431 435 mapname = (templater.templatepath('map-cmdline.' + tmpl)
432 436 or templater.templatepath(tmpl))
433 437 if mapname and os.path.isfile(mapname):
434 438 return templatespec(topic, None, mapname)
435 439
436 440 # perhaps it's a reference to [templates]
437 441 if ui.config('templates', tmpl):
438 442 return templatespec(tmpl, None, None)
439 443
440 444 if tmpl == 'list':
441 445 ui.write(_("available styles: %s\n") % templater.stylelist())
442 446 raise error.Abort(_("specify a template"))
443 447
444 448 # perhaps it's a path to a map or a template
445 449 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
446 450 # is it a mapfile for a style?
447 451 if os.path.basename(tmpl).startswith("map-"):
448 452 return templatespec(topic, None, os.path.realpath(tmpl))
449 453 with util.posixfile(tmpl, 'rb') as f:
450 454 tmpl = f.read()
451 455 return templatespec('', tmpl, None)
452 456
453 457 # constant string?
454 458 return templatespec('', tmpl, None)
455 459
456 460 def templatepartsmap(spec, t, partnames):
457 461 """Create a mapping of {part: ref}"""
458 462 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
459 463 if spec.mapfile:
460 464 partsmap.update((p, p) for p in partnames if p in t)
461 465 elif spec.ref:
462 466 for part in partnames:
463 467 ref = '%s:%s' % (spec.ref, part) # select config sub-section
464 468 if ref in t:
465 469 partsmap[part] = ref
466 470 return partsmap
467 471
468 472 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
469 473 """Create a templater from either a literal template or loading from
470 474 a map file"""
471 475 assert not (spec.tmpl and spec.mapfile)
472 476 if spec.mapfile:
473 477 frommapfile = templater.templater.frommapfile
474 478 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
475 479 cache=cache)
476 480 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
477 481 cache=cache)
478 482
479 483 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
480 484 """Create a templater from a string template 'tmpl'"""
481 485 aliases = ui.configitems('templatealias')
482 486 t = templater.templater(defaults=defaults, resources=resources,
483 487 cache=cache, aliases=aliases)
484 488 t.cache.update((k, templater.unquotestring(v))
485 489 for k, v in ui.configitems('templates'))
486 490 if tmpl:
487 491 t.cache[''] = tmpl
488 492 return t
489 493
490 494 def templateresources(ui, repo=None):
491 495 """Create a dict of template resources designed for the default templatekw
492 496 and function"""
493 497 return {
494 498 'cache': {}, # for templatekw/funcs to store reusable data
495 499 'ctx': None,
496 500 'repo': repo,
497 501 'revcache': None, # per-ctx cache; set later
498 502 'ui': ui,
499 503 }
500 504
501 505 def formatter(ui, out, topic, opts):
502 506 template = opts.get("template", "")
503 507 if template == "json":
504 508 return jsonformatter(ui, out, topic, opts)
505 509 elif template == "pickle":
506 510 return pickleformatter(ui, out, topic, opts)
507 511 elif template == "debug":
508 512 return debugformatter(ui, out, topic, opts)
509 513 elif template != "":
510 514 return templateformatter(ui, out, topic, opts)
511 515 # developer config: ui.formatdebug
512 516 elif ui.configbool('ui', 'formatdebug'):
513 517 return debugformatter(ui, out, topic, opts)
514 518 # deprecated config: ui.formatjson
515 519 elif ui.configbool('ui', 'formatjson'):
516 520 return jsonformatter(ui, out, topic, opts)
517 521 return plainformatter(ui, out, topic, opts)
518 522
519 523 @contextlib.contextmanager
520 524 def openformatter(ui, filename, topic, opts):
521 525 """Create a formatter that writes outputs to the specified file
522 526
523 527 Must be invoked using the 'with' statement.
524 528 """
525 529 with util.posixfile(filename, 'wb') as out:
526 530 with formatter(ui, out, topic, opts) as fm:
527 531 yield fm
528 532
529 533 @contextlib.contextmanager
530 534 def _neverending(fm):
531 535 yield fm
532 536
533 537 def maybereopen(fm, filename, opts):
534 538 """Create a formatter backed by file if filename specified, else return
535 539 the given formatter
536 540
537 541 Must be invoked using the 'with' statement. This will never call fm.end()
538 542 of the given formatter.
539 543 """
540 544 if filename:
541 545 return openformatter(fm._ui, filename, fm._topic, opts)
542 546 else:
543 547 return _neverending(fm)
@@ -1,995 +1,999 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 (
12 12 hex,
13 13 nullid,
14 14 )
15 15
16 16 from . import (
17 17 encoding,
18 18 error,
19 19 hbisect,
20 20 i18n,
21 21 obsutil,
22 22 patch,
23 23 pycompat,
24 24 registrar,
25 25 scmutil,
26 26 util,
27 27 )
28 28
29 29 class _hybrid(object):
30 30 """Wrapper for list or dict to support legacy template
31 31
32 32 This class allows us to handle both:
33 33 - "{files}" (legacy command-line-specific list hack) and
34 34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
35 35 and to access raw values:
36 36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
37 37 - "{get(extras, key)}"
38 38 - "{files|json}"
39 39 """
40 40
41 41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
42 42 if gen is not None:
43 43 self.gen = gen # generator or function returning generator
44 44 self._values = values
45 45 self._makemap = makemap
46 46 self.joinfmt = joinfmt
47 47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
48 48 def gen(self):
49 49 """Default generator to stringify this as {join(self, ' ')}"""
50 50 for i, x in enumerate(self._values):
51 51 if i > 0:
52 52 yield ' '
53 53 yield self.joinfmt(x)
54 54 def itermaps(self):
55 55 makemap = self._makemap
56 56 for x in self._values:
57 57 yield makemap(x)
58 58 def __contains__(self, x):
59 59 return x in self._values
60 60 def __getitem__(self, key):
61 61 return self._values[key]
62 62 def __len__(self):
63 63 return len(self._values)
64 64 def __iter__(self):
65 65 return iter(self._values)
66 66 def __getattr__(self, name):
67 67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
68 68 r'itervalues', r'keys', r'values'):
69 69 raise AttributeError(name)
70 70 return getattr(self._values, name)
71 71
72 72 class _mappable(object):
73 73 """Wrapper for non-list/dict object to support map operation
74 74
75 75 This class allows us to handle both:
76 76 - "{manifest}"
77 77 - "{manifest % '{rev}:{node}'}"
78 78 - "{manifest.rev}"
79 79
80 80 Unlike a _hybrid, this does not simulate the behavior of the underling
81 81 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
82 82 """
83 83
84 84 def __init__(self, gen, key, value, makemap):
85 85 if gen is not None:
86 86 self.gen = gen # generator or function returning generator
87 87 self._key = key
88 88 self._value = value # may be generator of strings
89 89 self._makemap = makemap
90 90
91 91 def gen(self):
92 92 yield pycompat.bytestr(self._value)
93 93
94 94 def tomap(self):
95 95 return self._makemap(self._key)
96 96
97 97 def itermaps(self):
98 98 yield self.tomap()
99 99
100 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
100 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
101 101 """Wrap data to support both dict-like and string-like operations"""
102 if fmt is None:
103 fmt = '%s=%s'
102 104 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
103 105 lambda k: fmt % (k, data[k]))
104 106
105 def hybridlist(data, name, fmt='%s', gen=None):
107 def hybridlist(data, name, fmt=None, gen=None):
106 108 """Wrap data to support both list-like and string-like operations"""
109 if fmt is None:
110 fmt = '%s'
107 111 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
108 112
109 113 def unwraphybrid(thing):
110 114 """Return an object which can be stringified possibly by using a legacy
111 115 template"""
112 116 gen = getattr(thing, 'gen', None)
113 117 if gen is None:
114 118 return thing
115 119 if callable(gen):
116 120 return gen()
117 121 return gen
118 122
119 123 def unwrapvalue(thing):
120 124 """Move the inner value object out of the wrapper"""
121 125 if not util.safehasattr(thing, '_value'):
122 126 return thing
123 127 return thing._value
124 128
125 129 def wraphybridvalue(container, key, value):
126 130 """Wrap an element of hybrid container to be mappable
127 131
128 132 The key is passed to the makemap function of the given container, which
129 133 should be an item generated by iter(container).
130 134 """
131 135 makemap = getattr(container, '_makemap', None)
132 136 if makemap is None:
133 137 return value
134 138 if util.safehasattr(value, '_makemap'):
135 139 # a nested hybrid list/dict, which has its own way of map operation
136 140 return value
137 141 return _mappable(None, key, value, makemap)
138 142
139 143 def compatdict(context, mapping, name, data, key='key', value='value',
140 fmt='%s=%s', plural=None, separator=' '):
144 fmt=None, plural=None, separator=' '):
141 145 """Wrap data like hybriddict(), but also supports old-style list template
142 146
143 147 This exists for backward compatibility with the old-style template. Use
144 148 hybriddict() for new template keywords.
145 149 """
146 150 c = [{key: k, value: v} for k, v in data.iteritems()]
147 151 t = context.resource(mapping, 'templ')
148 152 f = _showlist(name, c, t, mapping, plural, separator)
149 153 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
150 154
151 def compatlist(context, mapping, name, data, element=None, fmt='%s',
155 def compatlist(context, mapping, name, data, element=None, fmt=None,
152 156 plural=None, separator=' '):
153 157 """Wrap data like hybridlist(), but also supports old-style list template
154 158
155 159 This exists for backward compatibility with the old-style template. Use
156 160 hybridlist() for new template keywords.
157 161 """
158 162 t = context.resource(mapping, 'templ')
159 163 f = _showlist(name, data, t, mapping, plural, separator)
160 164 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
161 165
162 166 def showdict(name, data, mapping, plural=None, key='key', value='value',
163 fmt='%s=%s', separator=' '):
167 fmt=None, separator=' '):
164 168 ui = mapping.get('ui')
165 169 if ui:
166 170 ui.deprecwarn("templatekw.showdict() is deprecated, use compatdict()",
167 171 '4.6')
168 172 c = [{key: k, value: v} for k, v in data.iteritems()]
169 173 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
170 174 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
171 175
172 176 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
173 177 ui = mapping.get('ui')
174 178 if ui:
175 179 ui.deprecwarn("templatekw.showlist() is deprecated, use compatlist()",
176 180 '4.6')
177 181 if not element:
178 182 element = name
179 183 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
180 184 return hybridlist(values, name=element, gen=f)
181 185
182 186 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
183 187 '''expand set of values.
184 188 name is name of key in template map.
185 189 values is list of strings or dicts.
186 190 plural is plural of name, if not simply name + 's'.
187 191 separator is used to join values as a string
188 192
189 193 expansion works like this, given name 'foo'.
190 194
191 195 if values is empty, expand 'no_foos'.
192 196
193 197 if 'foo' not in template map, return values as a string,
194 198 joined by 'separator'.
195 199
196 200 expand 'start_foos'.
197 201
198 202 for each value, expand 'foo'. if 'last_foo' in template
199 203 map, expand it instead of 'foo' for last key.
200 204
201 205 expand 'end_foos'.
202 206 '''
203 207 strmapping = pycompat.strkwargs(mapping)
204 208 if not plural:
205 209 plural = name + 's'
206 210 if not values:
207 211 noname = 'no_' + plural
208 212 if noname in templ:
209 213 yield templ(noname, **strmapping)
210 214 return
211 215 if name not in templ:
212 216 if isinstance(values[0], bytes):
213 217 yield separator.join(values)
214 218 else:
215 219 for v in values:
216 220 r = dict(v)
217 221 r.update(mapping)
218 222 yield r
219 223 return
220 224 startname = 'start_' + plural
221 225 if startname in templ:
222 226 yield templ(startname, **strmapping)
223 227 vmapping = mapping.copy()
224 228 def one(v, tag=name):
225 229 try:
226 230 vmapping.update(v)
227 231 # Python 2 raises ValueError if the type of v is wrong. Python
228 232 # 3 raises TypeError.
229 233 except (AttributeError, TypeError, ValueError):
230 234 try:
231 235 # Python 2 raises ValueError trying to destructure an e.g.
232 236 # bytes. Python 3 raises TypeError.
233 237 for a, b in v:
234 238 vmapping[a] = b
235 239 except (TypeError, ValueError):
236 240 vmapping[name] = v
237 241 return templ(tag, **pycompat.strkwargs(vmapping))
238 242 lastname = 'last_' + name
239 243 if lastname in templ:
240 244 last = values.pop()
241 245 else:
242 246 last = None
243 247 for v in values:
244 248 yield one(v)
245 249 if last is not None:
246 250 yield one(last, tag=lastname)
247 251 endname = 'end_' + plural
248 252 if endname in templ:
249 253 yield templ(endname, **strmapping)
250 254
251 255 def getlatesttags(context, mapping, pattern=None):
252 256 '''return date, distance and name for the latest tag of rev'''
253 257 repo = context.resource(mapping, 'repo')
254 258 ctx = context.resource(mapping, 'ctx')
255 259 cache = context.resource(mapping, 'cache')
256 260
257 261 cachename = 'latesttags'
258 262 if pattern is not None:
259 263 cachename += '-' + pattern
260 264 match = util.stringmatcher(pattern)[2]
261 265 else:
262 266 match = util.always
263 267
264 268 if cachename not in cache:
265 269 # Cache mapping from rev to a tuple with tag date, tag
266 270 # distance and tag name
267 271 cache[cachename] = {-1: (0, 0, ['null'])}
268 272 latesttags = cache[cachename]
269 273
270 274 rev = ctx.rev()
271 275 todo = [rev]
272 276 while todo:
273 277 rev = todo.pop()
274 278 if rev in latesttags:
275 279 continue
276 280 ctx = repo[rev]
277 281 tags = [t for t in ctx.tags()
278 282 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
279 283 and match(t))]
280 284 if tags:
281 285 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
282 286 continue
283 287 try:
284 288 ptags = [latesttags[p.rev()] for p in ctx.parents()]
285 289 if len(ptags) > 1:
286 290 if ptags[0][2] == ptags[1][2]:
287 291 # The tuples are laid out so the right one can be found by
288 292 # comparison in this case.
289 293 pdate, pdist, ptag = max(ptags)
290 294 else:
291 295 def key(x):
292 296 changessincetag = len(repo.revs('only(%d, %s)',
293 297 ctx.rev(), x[2][0]))
294 298 # Smallest number of changes since tag wins. Date is
295 299 # used as tiebreaker.
296 300 return [-changessincetag, x[0]]
297 301 pdate, pdist, ptag = max(ptags, key=key)
298 302 else:
299 303 pdate, pdist, ptag = ptags[0]
300 304 except KeyError:
301 305 # Cache miss - recurse
302 306 todo.append(rev)
303 307 todo.extend(p.rev() for p in ctx.parents())
304 308 continue
305 309 latesttags[rev] = pdate, pdist + 1, ptag
306 310 return latesttags[rev]
307 311
308 312 def getrenamedfn(repo, endrev=None):
309 313 rcache = {}
310 314 if endrev is None:
311 315 endrev = len(repo)
312 316
313 317 def getrenamed(fn, rev):
314 318 '''looks up all renames for a file (up to endrev) the first
315 319 time the file is given. It indexes on the changerev and only
316 320 parses the manifest if linkrev != changerev.
317 321 Returns rename info for fn at changerev rev.'''
318 322 if fn not in rcache:
319 323 rcache[fn] = {}
320 324 fl = repo.file(fn)
321 325 for i in fl:
322 326 lr = fl.linkrev(i)
323 327 renamed = fl.renamed(fl.node(i))
324 328 rcache[fn][lr] = renamed
325 329 if lr >= endrev:
326 330 break
327 331 if rev in rcache[fn]:
328 332 return rcache[fn][rev]
329 333
330 334 # If linkrev != rev (i.e. rev not found in rcache) fallback to
331 335 # filectx logic.
332 336 try:
333 337 return repo[rev][fn].renamed()
334 338 except error.LookupError:
335 339 return None
336 340
337 341 return getrenamed
338 342
339 343 def getlogcolumns():
340 344 """Return a dict of log column labels"""
341 345 _ = pycompat.identity # temporarily disable gettext
342 346 # i18n: column positioning for "hg log"
343 347 columns = _('bookmark: %s\n'
344 348 'branch: %s\n'
345 349 'changeset: %s\n'
346 350 'copies: %s\n'
347 351 'date: %s\n'
348 352 'extra: %s=%s\n'
349 353 'files+: %s\n'
350 354 'files-: %s\n'
351 355 'files: %s\n'
352 356 'instability: %s\n'
353 357 'manifest: %s\n'
354 358 'obsolete: %s\n'
355 359 'parent: %s\n'
356 360 'phase: %s\n'
357 361 'summary: %s\n'
358 362 'tag: %s\n'
359 363 'user: %s\n')
360 364 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
361 365 i18n._(columns).splitlines(True)))
362 366
363 367 # default templates internally used for rendering of lists
364 368 defaulttempl = {
365 369 'parent': '{rev}:{node|formatnode} ',
366 370 'manifest': '{rev}:{node|formatnode}',
367 371 'file_copy': '{name} ({source})',
368 372 'envvar': '{key}={value}',
369 373 'extra': '{key}={value|stringescape}'
370 374 }
371 375 # filecopy is preserved for compatibility reasons
372 376 defaulttempl['filecopy'] = defaulttempl['file_copy']
373 377
374 378 # keywords are callables (see registrar.templatekeyword for details)
375 379 keywords = {}
376 380 templatekeyword = registrar.templatekeyword(keywords)
377 381
378 382 @templatekeyword('author', requires={'ctx'})
379 383 def showauthor(context, mapping):
380 384 """String. The unmodified author of the changeset."""
381 385 ctx = context.resource(mapping, 'ctx')
382 386 return ctx.user()
383 387
384 388 @templatekeyword('bisect', requires={'repo', 'ctx'})
385 389 def showbisect(context, mapping):
386 390 """String. The changeset bisection status."""
387 391 repo = context.resource(mapping, 'repo')
388 392 ctx = context.resource(mapping, 'ctx')
389 393 return hbisect.label(repo, ctx.node())
390 394
391 395 @templatekeyword('branch', requires={'ctx'})
392 396 def showbranch(context, mapping):
393 397 """String. The name of the branch on which the changeset was
394 398 committed.
395 399 """
396 400 ctx = context.resource(mapping, 'ctx')
397 401 return ctx.branch()
398 402
399 403 @templatekeyword('branches', requires={'ctx', 'templ'})
400 404 def showbranches(context, mapping):
401 405 """List of strings. The name of the branch on which the
402 406 changeset was committed. Will be empty if the branch name was
403 407 default. (DEPRECATED)
404 408 """
405 409 ctx = context.resource(mapping, 'ctx')
406 410 branch = ctx.branch()
407 411 if branch != 'default':
408 412 return compatlist(context, mapping, 'branch', [branch],
409 413 plural='branches')
410 414 return compatlist(context, mapping, 'branch', [], plural='branches')
411 415
412 416 @templatekeyword('bookmarks', requires={'repo', 'ctx', 'templ'})
413 417 def showbookmarks(context, mapping):
414 418 """List of strings. Any bookmarks associated with the
415 419 changeset. Also sets 'active', the name of the active bookmark.
416 420 """
417 421 repo = context.resource(mapping, 'repo')
418 422 ctx = context.resource(mapping, 'ctx')
419 423 templ = context.resource(mapping, 'templ')
420 424 bookmarks = ctx.bookmarks()
421 425 active = repo._activebookmark
422 426 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
423 427 f = _showlist('bookmark', bookmarks, templ, mapping)
424 428 return _hybrid(f, bookmarks, makemap, pycompat.identity)
425 429
426 430 @templatekeyword('children', requires={'ctx', 'templ'})
427 431 def showchildren(context, mapping):
428 432 """List of strings. The children of the changeset."""
429 433 ctx = context.resource(mapping, 'ctx')
430 434 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
431 435 return compatlist(context, mapping, 'children', childrevs, element='child')
432 436
433 437 # Deprecated, but kept alive for help generation a purpose.
434 438 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
435 439 def showcurrentbookmark(context, mapping):
436 440 """String. The active bookmark, if it is associated with the changeset.
437 441 (DEPRECATED)"""
438 442 return showactivebookmark(context, mapping)
439 443
440 444 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
441 445 def showactivebookmark(context, mapping):
442 446 """String. The active bookmark, if it is associated with the changeset."""
443 447 repo = context.resource(mapping, 'repo')
444 448 ctx = context.resource(mapping, 'ctx')
445 449 active = repo._activebookmark
446 450 if active and active in ctx.bookmarks():
447 451 return active
448 452 return ''
449 453
450 454 @templatekeyword('date', requires={'ctx'})
451 455 def showdate(context, mapping):
452 456 """Date information. The date when the changeset was committed."""
453 457 ctx = context.resource(mapping, 'ctx')
454 458 return ctx.date()
455 459
456 460 @templatekeyword('desc', requires={'ctx'})
457 461 def showdescription(context, mapping):
458 462 """String. The text of the changeset description."""
459 463 ctx = context.resource(mapping, 'ctx')
460 464 s = ctx.description()
461 465 if isinstance(s, encoding.localstr):
462 466 # try hard to preserve utf-8 bytes
463 467 return encoding.tolocal(encoding.fromlocal(s).strip())
464 468 else:
465 469 return s.strip()
466 470
467 471 @templatekeyword('diffstat', requires={'ctx'})
468 472 def showdiffstat(context, mapping):
469 473 """String. Statistics of changes with the following format:
470 474 "modified files: +added/-removed lines"
471 475 """
472 476 ctx = context.resource(mapping, 'ctx')
473 477 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
474 478 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
475 479 return '%d: +%d/-%d' % (len(stats), adds, removes)
476 480
477 481 @templatekeyword('envvars', requires={'ui', 'templ'})
478 482 def showenvvars(context, mapping):
479 483 """A dictionary of environment variables. (EXPERIMENTAL)"""
480 484 ui = context.resource(mapping, 'ui')
481 485 env = ui.exportableenviron()
482 486 env = util.sortdict((k, env[k]) for k in sorted(env))
483 487 return compatdict(context, mapping, 'envvar', env, plural='envvars')
484 488
485 489 @templatekeyword('extras', requires={'ctx', 'templ'})
486 490 def showextras(context, mapping):
487 491 """List of dicts with key, value entries of the 'extras'
488 492 field of this changeset."""
489 493 ctx = context.resource(mapping, 'ctx')
490 494 templ = context.resource(mapping, 'templ')
491 495 extras = ctx.extra()
492 496 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
493 497 makemap = lambda k: {'key': k, 'value': extras[k]}
494 498 c = [makemap(k) for k in extras]
495 499 f = _showlist('extra', c, templ, mapping, plural='extras')
496 500 return _hybrid(f, extras, makemap,
497 501 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
498 502
499 503 def _showfilesbystat(context, mapping, name, index):
500 504 repo = context.resource(mapping, 'repo')
501 505 ctx = context.resource(mapping, 'ctx')
502 506 revcache = context.resource(mapping, 'revcache')
503 507 if 'files' not in revcache:
504 508 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
505 509 files = revcache['files'][index]
506 510 return compatlist(context, mapping, name, files, element='file')
507 511
508 512 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
509 513 def showfileadds(context, mapping):
510 514 """List of strings. Files added by this changeset."""
511 515 return _showfilesbystat(context, mapping, 'file_add', 1)
512 516
513 517 @templatekeyword('file_copies',
514 518 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
515 519 def showfilecopies(context, mapping):
516 520 """List of strings. Files copied in this changeset with
517 521 their sources.
518 522 """
519 523 repo = context.resource(mapping, 'repo')
520 524 ctx = context.resource(mapping, 'ctx')
521 525 cache = context.resource(mapping, 'cache')
522 526 copies = context.resource(mapping, 'revcache').get('copies')
523 527 if copies is None:
524 528 if 'getrenamed' not in cache:
525 529 cache['getrenamed'] = getrenamedfn(repo)
526 530 copies = []
527 531 getrenamed = cache['getrenamed']
528 532 for fn in ctx.files():
529 533 rename = getrenamed(fn, ctx.rev())
530 534 if rename:
531 535 copies.append((fn, rename[0]))
532 536
533 537 copies = util.sortdict(copies)
534 538 return compatdict(context, mapping, 'file_copy', copies,
535 539 key='name', value='source', fmt='%s (%s)',
536 540 plural='file_copies')
537 541
538 542 # showfilecopiesswitch() displays file copies only if copy records are
539 543 # provided before calling the templater, usually with a --copies
540 544 # command line switch.
541 545 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
542 546 def showfilecopiesswitch(context, mapping):
543 547 """List of strings. Like "file_copies" but displayed
544 548 only if the --copied switch is set.
545 549 """
546 550 copies = context.resource(mapping, 'revcache').get('copies') or []
547 551 copies = util.sortdict(copies)
548 552 return compatdict(context, mapping, 'file_copy', copies,
549 553 key='name', value='source', fmt='%s (%s)',
550 554 plural='file_copies')
551 555
552 556 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
553 557 def showfiledels(context, mapping):
554 558 """List of strings. Files removed by this changeset."""
555 559 return _showfilesbystat(context, mapping, 'file_del', 2)
556 560
557 561 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
558 562 def showfilemods(context, mapping):
559 563 """List of strings. Files modified by this changeset."""
560 564 return _showfilesbystat(context, mapping, 'file_mod', 0)
561 565
562 566 @templatekeyword('files', requires={'ctx', 'templ'})
563 567 def showfiles(context, mapping):
564 568 """List of strings. All files modified, added, or removed by this
565 569 changeset.
566 570 """
567 571 ctx = context.resource(mapping, 'ctx')
568 572 return compatlist(context, mapping, 'file', ctx.files())
569 573
570 574 @templatekeyword('graphnode', requires={'repo', 'ctx'})
571 575 def showgraphnode(context, mapping):
572 576 """String. The character representing the changeset node in an ASCII
573 577 revision graph."""
574 578 repo = context.resource(mapping, 'repo')
575 579 ctx = context.resource(mapping, 'ctx')
576 580 return getgraphnode(repo, ctx)
577 581
578 582 def getgraphnode(repo, ctx):
579 583 wpnodes = repo.dirstate.parents()
580 584 if wpnodes[1] == nullid:
581 585 wpnodes = wpnodes[:1]
582 586 if ctx.node() in wpnodes:
583 587 return '@'
584 588 elif ctx.obsolete():
585 589 return 'x'
586 590 elif ctx.isunstable():
587 591 return '*'
588 592 elif ctx.closesbranch():
589 593 return '_'
590 594 else:
591 595 return 'o'
592 596
593 597 @templatekeyword('graphwidth', requires=())
594 598 def showgraphwidth(context, mapping):
595 599 """Integer. The width of the graph drawn by 'log --graph' or zero."""
596 600 # just hosts documentation; should be overridden by template mapping
597 601 return 0
598 602
599 603 @templatekeyword('index', requires=())
600 604 def showindex(context, mapping):
601 605 """Integer. The current iteration of the loop. (0 indexed)"""
602 606 # just hosts documentation; should be overridden by template mapping
603 607 raise error.Abort(_("can't use index in this context"))
604 608
605 609 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache', 'templ'})
606 610 def showlatesttag(context, mapping):
607 611 """List of strings. The global tags on the most recent globally
608 612 tagged ancestor of this changeset. If no such tags exist, the list
609 613 consists of the single string "null".
610 614 """
611 615 return showlatesttags(context, mapping, None)
612 616
613 617 def showlatesttags(context, mapping, pattern):
614 618 """helper method for the latesttag keyword and function"""
615 619 latesttags = getlatesttags(context, mapping, pattern)
616 620
617 621 # latesttag[0] is an implementation detail for sorting csets on different
618 622 # branches in a stable manner- it is the date the tagged cset was created,
619 623 # not the date the tag was created. Therefore it isn't made visible here.
620 624 makemap = lambda v: {
621 625 'changes': _showchangessincetag,
622 626 'distance': latesttags[1],
623 627 'latesttag': v, # BC with {latesttag % '{latesttag}'}
624 628 'tag': v
625 629 }
626 630
627 631 tags = latesttags[2]
628 632 templ = context.resource(mapping, 'templ')
629 633 f = _showlist('latesttag', tags, templ, mapping, separator=':')
630 634 return _hybrid(f, tags, makemap, pycompat.identity)
631 635
632 636 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
633 637 def showlatesttagdistance(context, mapping):
634 638 """Integer. Longest path to the latest tag."""
635 639 return getlatesttags(context, mapping)[1]
636 640
637 641 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
638 642 def showchangessincelatesttag(context, mapping):
639 643 """Integer. All ancestors not in the latest tag."""
640 644 mapping = mapping.copy()
641 645 mapping['tag'] = getlatesttags(context, mapping)[2][0]
642 646 return _showchangessincetag(context, mapping)
643 647
644 648 def _showchangessincetag(context, mapping):
645 649 repo = context.resource(mapping, 'repo')
646 650 ctx = context.resource(mapping, 'ctx')
647 651 offset = 0
648 652 revs = [ctx.rev()]
649 653 tag = context.symbol(mapping, 'tag')
650 654
651 655 # The only() revset doesn't currently support wdir()
652 656 if ctx.rev() is None:
653 657 offset = 1
654 658 revs = [p.rev() for p in ctx.parents()]
655 659
656 660 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
657 661
658 662 # teach templater latesttags.changes is switched to (context, mapping) API
659 663 _showchangessincetag._requires = {'repo', 'ctx'}
660 664
661 665 @templatekeyword('manifest', requires={'repo', 'ctx', 'templ'})
662 666 def showmanifest(context, mapping):
663 667 repo = context.resource(mapping, 'repo')
664 668 ctx = context.resource(mapping, 'ctx')
665 669 templ = context.resource(mapping, 'templ')
666 670 mnode = ctx.manifestnode()
667 671 if mnode is None:
668 672 # just avoid crash, we might want to use the 'ff...' hash in future
669 673 return
670 674 mrev = repo.manifestlog._revlog.rev(mnode)
671 675 mhex = hex(mnode)
672 676 mapping = mapping.copy()
673 677 mapping.update({'rev': mrev, 'node': mhex})
674 678 f = templ('manifest', **pycompat.strkwargs(mapping))
675 679 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
676 680 # rev and node are completely different from changeset's.
677 681 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
678 682
679 683 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
680 684 def showobsfate(context, mapping):
681 685 # this function returns a list containing pre-formatted obsfate strings.
682 686 #
683 687 # This function will be replaced by templates fragments when we will have
684 688 # the verbosity templatekw available.
685 689 succsandmarkers = showsuccsandmarkers(context, mapping)
686 690
687 691 ui = context.resource(mapping, 'ui')
688 692 values = []
689 693
690 694 for x in succsandmarkers:
691 695 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
692 696
693 697 return compatlist(context, mapping, "fate", values)
694 698
695 699 def shownames(context, mapping, namespace):
696 700 """helper method to generate a template keyword for a namespace"""
697 701 repo = context.resource(mapping, 'repo')
698 702 ctx = context.resource(mapping, 'ctx')
699 703 ns = repo.names[namespace]
700 704 names = ns.names(repo, ctx.node())
701 705 return compatlist(context, mapping, ns.templatename, names,
702 706 plural=namespace)
703 707
704 708 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
705 709 def shownamespaces(context, mapping):
706 710 """Dict of lists. Names attached to this changeset per
707 711 namespace."""
708 712 repo = context.resource(mapping, 'repo')
709 713 ctx = context.resource(mapping, 'ctx')
710 714 templ = context.resource(mapping, 'templ')
711 715
712 716 namespaces = util.sortdict()
713 717 def makensmapfn(ns):
714 718 # 'name' for iterating over namespaces, templatename for local reference
715 719 return lambda v: {'name': v, ns.templatename: v}
716 720
717 721 for k, ns in repo.names.iteritems():
718 722 names = ns.names(repo, ctx.node())
719 723 f = _showlist('name', names, templ, mapping)
720 724 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
721 725
722 726 f = _showlist('namespace', list(namespaces), templ, mapping)
723 727
724 728 def makemap(ns):
725 729 return {
726 730 'namespace': ns,
727 731 'names': namespaces[ns],
728 732 'builtin': repo.names[ns].builtin,
729 733 'colorname': repo.names[ns].colorname,
730 734 }
731 735
732 736 return _hybrid(f, namespaces, makemap, pycompat.identity)
733 737
734 738 @templatekeyword('node', requires={'ctx'})
735 739 def shownode(context, mapping):
736 740 """String. The changeset identification hash, as a 40 hexadecimal
737 741 digit string.
738 742 """
739 743 ctx = context.resource(mapping, 'ctx')
740 744 return ctx.hex()
741 745
742 746 @templatekeyword('obsolete', requires={'ctx'})
743 747 def showobsolete(context, mapping):
744 748 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
745 749 ctx = context.resource(mapping, 'ctx')
746 750 if ctx.obsolete():
747 751 return 'obsolete'
748 752 return ''
749 753
750 754 @templatekeyword('peerurls', requires={'repo'})
751 755 def showpeerurls(context, mapping):
752 756 """A dictionary of repository locations defined in the [paths] section
753 757 of your configuration file."""
754 758 repo = context.resource(mapping, 'repo')
755 759 # see commands.paths() for naming of dictionary keys
756 760 paths = repo.ui.paths
757 761 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
758 762 def makemap(k):
759 763 p = paths[k]
760 764 d = {'name': k, 'url': p.rawloc}
761 765 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
762 766 return d
763 767 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
764 768
765 769 @templatekeyword("predecessors", requires={'repo', 'ctx'})
766 770 def showpredecessors(context, mapping):
767 771 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
768 772 repo = context.resource(mapping, 'repo')
769 773 ctx = context.resource(mapping, 'ctx')
770 774 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
771 775 predecessors = map(hex, predecessors)
772 776
773 777 return _hybrid(None, predecessors,
774 778 lambda x: {'ctx': repo[x], 'revcache': {}},
775 779 lambda x: scmutil.formatchangeid(repo[x]))
776 780
777 781 @templatekeyword('reporoot', requires={'repo'})
778 782 def showreporoot(context, mapping):
779 783 """String. The root directory of the current repository."""
780 784 repo = context.resource(mapping, 'repo')
781 785 return repo.root
782 786
783 787 @templatekeyword("successorssets", requires={'repo', 'ctx'})
784 788 def showsuccessorssets(context, mapping):
785 789 """Returns a string of sets of successors for a changectx. Format used
786 790 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
787 791 while also diverged into ctx3. (EXPERIMENTAL)"""
788 792 repo = context.resource(mapping, 'repo')
789 793 ctx = context.resource(mapping, 'ctx')
790 794 if not ctx.obsolete():
791 795 return ''
792 796
793 797 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
794 798 ssets = [[hex(n) for n in ss] for ss in ssets]
795 799
796 800 data = []
797 801 for ss in ssets:
798 802 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
799 803 lambda x: scmutil.formatchangeid(repo[x]))
800 804 data.append(h)
801 805
802 806 # Format the successorssets
803 807 def render(d):
804 808 t = []
805 809 for i in d.gen():
806 810 t.append(i)
807 811 return "".join(t)
808 812
809 813 def gen(data):
810 814 yield "; ".join(render(d) for d in data)
811 815
812 816 return _hybrid(gen(data), data, lambda x: {'successorset': x},
813 817 pycompat.identity)
814 818
815 819 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
816 820 def showsuccsandmarkers(context, mapping):
817 821 """Returns a list of dict for each final successor of ctx. The dict
818 822 contains successors node id in "successors" keys and the list of
819 823 obs-markers from ctx to the set of successors in "markers".
820 824 (EXPERIMENTAL)
821 825 """
822 826 repo = context.resource(mapping, 'repo')
823 827 ctx = context.resource(mapping, 'ctx')
824 828 templ = context.resource(mapping, 'templ')
825 829
826 830 values = obsutil.successorsandmarkers(repo, ctx)
827 831
828 832 if values is None:
829 833 values = []
830 834
831 835 # Format successors and markers to avoid exposing binary to templates
832 836 data = []
833 837 for i in values:
834 838 # Format successors
835 839 successors = i['successors']
836 840
837 841 successors = [hex(n) for n in successors]
838 842 successors = _hybrid(None, successors,
839 843 lambda x: {'ctx': repo[x], 'revcache': {}},
840 844 lambda x: scmutil.formatchangeid(repo[x]))
841 845
842 846 # Format markers
843 847 finalmarkers = []
844 848 for m in i['markers']:
845 849 hexprec = hex(m[0])
846 850 hexsucs = tuple(hex(n) for n in m[1])
847 851 hexparents = None
848 852 if m[5] is not None:
849 853 hexparents = tuple(hex(n) for n in m[5])
850 854 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
851 855 finalmarkers.append(newmarker)
852 856
853 857 data.append({'successors': successors, 'markers': finalmarkers})
854 858
855 859 f = _showlist('succsandmarkers', data, templ, mapping)
856 860 return _hybrid(f, data, lambda x: x, pycompat.identity)
857 861
858 862 @templatekeyword('p1rev', requires={'ctx'})
859 863 def showp1rev(context, mapping):
860 864 """Integer. The repository-local revision number of the changeset's
861 865 first parent, or -1 if the changeset has no parents."""
862 866 ctx = context.resource(mapping, 'ctx')
863 867 return ctx.p1().rev()
864 868
865 869 @templatekeyword('p2rev', requires={'ctx'})
866 870 def showp2rev(context, mapping):
867 871 """Integer. The repository-local revision number of the changeset's
868 872 second parent, or -1 if the changeset has no second parent."""
869 873 ctx = context.resource(mapping, 'ctx')
870 874 return ctx.p2().rev()
871 875
872 876 @templatekeyword('p1node', requires={'ctx'})
873 877 def showp1node(context, mapping):
874 878 """String. The identification hash of the changeset's first parent,
875 879 as a 40 digit hexadecimal string. If the changeset has no parents, all
876 880 digits are 0."""
877 881 ctx = context.resource(mapping, 'ctx')
878 882 return ctx.p1().hex()
879 883
880 884 @templatekeyword('p2node', requires={'ctx'})
881 885 def showp2node(context, mapping):
882 886 """String. The identification hash of the changeset's second
883 887 parent, as a 40 digit hexadecimal string. If the changeset has no second
884 888 parent, all digits are 0."""
885 889 ctx = context.resource(mapping, 'ctx')
886 890 return ctx.p2().hex()
887 891
888 892 @templatekeyword('parents', requires={'repo', 'ctx', 'templ'})
889 893 def showparents(context, mapping):
890 894 """List of strings. The parents of the changeset in "rev:node"
891 895 format. If the changeset has only one "natural" parent (the predecessor
892 896 revision) nothing is shown."""
893 897 repo = context.resource(mapping, 'repo')
894 898 ctx = context.resource(mapping, 'ctx')
895 899 templ = context.resource(mapping, 'templ')
896 900 pctxs = scmutil.meaningfulparents(repo, ctx)
897 901 prevs = [p.rev() for p in pctxs]
898 902 parents = [[('rev', p.rev()),
899 903 ('node', p.hex()),
900 904 ('phase', p.phasestr())]
901 905 for p in pctxs]
902 906 f = _showlist('parent', parents, templ, mapping)
903 907 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
904 908 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
905 909
906 910 @templatekeyword('phase', requires={'ctx'})
907 911 def showphase(context, mapping):
908 912 """String. The changeset phase name."""
909 913 ctx = context.resource(mapping, 'ctx')
910 914 return ctx.phasestr()
911 915
912 916 @templatekeyword('phaseidx', requires={'ctx'})
913 917 def showphaseidx(context, mapping):
914 918 """Integer. The changeset phase index. (ADVANCED)"""
915 919 ctx = context.resource(mapping, 'ctx')
916 920 return ctx.phase()
917 921
918 922 @templatekeyword('rev', requires={'ctx'})
919 923 def showrev(context, mapping):
920 924 """Integer. The repository-local changeset revision number."""
921 925 ctx = context.resource(mapping, 'ctx')
922 926 return scmutil.intrev(ctx)
923 927
924 928 def showrevslist(context, mapping, name, revs):
925 929 """helper to generate a list of revisions in which a mapped template will
926 930 be evaluated"""
927 931 repo = context.resource(mapping, 'repo')
928 932 templ = context.resource(mapping, 'templ')
929 933 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
930 934 return _hybrid(f, revs,
931 935 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
932 936 pycompat.identity, keytype=int)
933 937
934 938 @templatekeyword('subrepos', requires={'ctx', 'templ'})
935 939 def showsubrepos(context, mapping):
936 940 """List of strings. Updated subrepositories in the changeset."""
937 941 ctx = context.resource(mapping, 'ctx')
938 942 substate = ctx.substate
939 943 if not substate:
940 944 return compatlist(context, mapping, 'subrepo', [])
941 945 psubstate = ctx.parents()[0].substate or {}
942 946 subrepos = []
943 947 for sub in substate:
944 948 if sub not in psubstate or substate[sub] != psubstate[sub]:
945 949 subrepos.append(sub) # modified or newly added in ctx
946 950 for sub in psubstate:
947 951 if sub not in substate:
948 952 subrepos.append(sub) # removed in ctx
949 953 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
950 954
951 955 # don't remove "showtags" definition, even though namespaces will put
952 956 # a helper function for "tags" keyword into "keywords" map automatically,
953 957 # because online help text is built without namespaces initialization
954 958 @templatekeyword('tags', requires={'repo', 'ctx', 'templ'})
955 959 def showtags(context, mapping):
956 960 """List of strings. Any tags associated with the changeset."""
957 961 return shownames(context, mapping, 'tags')
958 962
959 963 @templatekeyword('termwidth', requires={'ui'})
960 964 def showtermwidth(context, mapping):
961 965 """Integer. The width of the current terminal."""
962 966 ui = context.resource(mapping, 'ui')
963 967 return ui.termwidth()
964 968
965 969 @templatekeyword('instabilities', requires={'ctx', 'templ'})
966 970 def showinstabilities(context, mapping):
967 971 """List of strings. Evolution instabilities affecting the changeset.
968 972 (EXPERIMENTAL)
969 973 """
970 974 ctx = context.resource(mapping, 'ctx')
971 975 return compatlist(context, mapping, 'instability', ctx.instabilities(),
972 976 plural='instabilities')
973 977
974 978 @templatekeyword('verbosity', requires={'ui'})
975 979 def showverbosity(context, mapping):
976 980 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
977 981 or ''."""
978 982 ui = context.resource(mapping, 'ui')
979 983 # see logcmdutil.changesettemplater for priority of these flags
980 984 if ui.debugflag:
981 985 return 'debug'
982 986 elif ui.quiet:
983 987 return 'quiet'
984 988 elif ui.verbose:
985 989 return 'verbose'
986 990 return ''
987 991
988 992 def loadkeyword(ui, extname, registrarobj):
989 993 """Load template keyword from specified registrarobj
990 994 """
991 995 for name, func in registrarobj._table.iteritems():
992 996 keywords[name] = func
993 997
994 998 # tell hggettext to extract docstrings from these functions:
995 999 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now