##// END OF EJS Templates
templatekw: switch latesttags template keywords to new API
Yuya Nishihara -
r36614:b5d39a09 default
parent child Browse files
Show More
@@ -1,977 +1,983
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 100 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
101 101 """Wrap data to support both dict-like and string-like operations"""
102 102 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
103 103 lambda k: fmt % (k, data[k]))
104 104
105 105 def hybridlist(data, name, fmt='%s', gen=None):
106 106 """Wrap data to support both list-like and string-like operations"""
107 107 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
108 108
109 109 def unwraphybrid(thing):
110 110 """Return an object which can be stringified possibly by using a legacy
111 111 template"""
112 112 gen = getattr(thing, 'gen', None)
113 113 if gen is None:
114 114 return thing
115 115 if callable(gen):
116 116 return gen()
117 117 return gen
118 118
119 119 def unwrapvalue(thing):
120 120 """Move the inner value object out of the wrapper"""
121 121 if not util.safehasattr(thing, '_value'):
122 122 return thing
123 123 return thing._value
124 124
125 125 def wraphybridvalue(container, key, value):
126 126 """Wrap an element of hybrid container to be mappable
127 127
128 128 The key is passed to the makemap function of the given container, which
129 129 should be an item generated by iter(container).
130 130 """
131 131 makemap = getattr(container, '_makemap', None)
132 132 if makemap is None:
133 133 return value
134 134 if util.safehasattr(value, '_makemap'):
135 135 # a nested hybrid list/dict, which has its own way of map operation
136 136 return value
137 137 return _mappable(None, key, value, makemap)
138 138
139 139 def compatdict(context, mapping, name, data, key='key', value='value',
140 140 fmt='%s=%s', plural=None, separator=' '):
141 141 """Wrap data like hybriddict(), but also supports old-style list template
142 142
143 143 This exists for backward compatibility with the old-style template. Use
144 144 hybriddict() for new template keywords.
145 145 """
146 146 c = [{key: k, value: v} for k, v in data.iteritems()]
147 147 t = context.resource(mapping, 'templ')
148 148 f = _showlist(name, c, t, mapping, plural, separator)
149 149 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
150 150
151 151 def compatlist(context, mapping, name, data, element=None, fmt='%s',
152 152 plural=None, separator=' '):
153 153 """Wrap data like hybridlist(), but also supports old-style list template
154 154
155 155 This exists for backward compatibility with the old-style template. Use
156 156 hybridlist() for new template keywords.
157 157 """
158 158 t = context.resource(mapping, 'templ')
159 159 f = _showlist(name, data, t, mapping, plural, separator)
160 160 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
161 161
162 162 def showdict(name, data, mapping, plural=None, key='key', value='value',
163 163 fmt='%s=%s', separator=' '):
164 164 c = [{key: k, value: v} for k, v in data.iteritems()]
165 165 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
166 166 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
167 167
168 168 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
169 169 if not element:
170 170 element = name
171 171 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
172 172 return hybridlist(values, name=element, gen=f)
173 173
174 174 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
175 175 '''expand set of values.
176 176 name is name of key in template map.
177 177 values is list of strings or dicts.
178 178 plural is plural of name, if not simply name + 's'.
179 179 separator is used to join values as a string
180 180
181 181 expansion works like this, given name 'foo'.
182 182
183 183 if values is empty, expand 'no_foos'.
184 184
185 185 if 'foo' not in template map, return values as a string,
186 186 joined by 'separator'.
187 187
188 188 expand 'start_foos'.
189 189
190 190 for each value, expand 'foo'. if 'last_foo' in template
191 191 map, expand it instead of 'foo' for last key.
192 192
193 193 expand 'end_foos'.
194 194 '''
195 195 strmapping = pycompat.strkwargs(mapping)
196 196 if not plural:
197 197 plural = name + 's'
198 198 if not values:
199 199 noname = 'no_' + plural
200 200 if noname in templ:
201 201 yield templ(noname, **strmapping)
202 202 return
203 203 if name not in templ:
204 204 if isinstance(values[0], bytes):
205 205 yield separator.join(values)
206 206 else:
207 207 for v in values:
208 208 r = dict(v)
209 209 r.update(mapping)
210 210 yield r
211 211 return
212 212 startname = 'start_' + plural
213 213 if startname in templ:
214 214 yield templ(startname, **strmapping)
215 215 vmapping = mapping.copy()
216 216 def one(v, tag=name):
217 217 try:
218 218 vmapping.update(v)
219 219 # Python 2 raises ValueError if the type of v is wrong. Python
220 220 # 3 raises TypeError.
221 221 except (AttributeError, TypeError, ValueError):
222 222 try:
223 223 # Python 2 raises ValueError trying to destructure an e.g.
224 224 # bytes. Python 3 raises TypeError.
225 225 for a, b in v:
226 226 vmapping[a] = b
227 227 except (TypeError, ValueError):
228 228 vmapping[name] = v
229 229 return templ(tag, **pycompat.strkwargs(vmapping))
230 230 lastname = 'last_' + name
231 231 if lastname in templ:
232 232 last = values.pop()
233 233 else:
234 234 last = None
235 235 for v in values:
236 236 yield one(v)
237 237 if last is not None:
238 238 yield one(last, tag=lastname)
239 239 endname = 'end_' + plural
240 240 if endname in templ:
241 241 yield templ(endname, **strmapping)
242 242
243 def getlatesttags(repo, ctx, cache, pattern=None):
243 def getlatesttags(context, mapping, pattern=None):
244 244 '''return date, distance and name for the latest tag of rev'''
245 repo = context.resource(mapping, 'repo')
246 ctx = context.resource(mapping, 'ctx')
247 cache = context.resource(mapping, 'cache')
245 248
246 249 cachename = 'latesttags'
247 250 if pattern is not None:
248 251 cachename += '-' + pattern
249 252 match = util.stringmatcher(pattern)[2]
250 253 else:
251 254 match = util.always
252 255
253 256 if cachename not in cache:
254 257 # Cache mapping from rev to a tuple with tag date, tag
255 258 # distance and tag name
256 259 cache[cachename] = {-1: (0, 0, ['null'])}
257 260 latesttags = cache[cachename]
258 261
259 262 rev = ctx.rev()
260 263 todo = [rev]
261 264 while todo:
262 265 rev = todo.pop()
263 266 if rev in latesttags:
264 267 continue
265 268 ctx = repo[rev]
266 269 tags = [t for t in ctx.tags()
267 270 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
268 271 and match(t))]
269 272 if tags:
270 273 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
271 274 continue
272 275 try:
273 276 ptags = [latesttags[p.rev()] for p in ctx.parents()]
274 277 if len(ptags) > 1:
275 278 if ptags[0][2] == ptags[1][2]:
276 279 # The tuples are laid out so the right one can be found by
277 280 # comparison in this case.
278 281 pdate, pdist, ptag = max(ptags)
279 282 else:
280 283 def key(x):
281 284 changessincetag = len(repo.revs('only(%d, %s)',
282 285 ctx.rev(), x[2][0]))
283 286 # Smallest number of changes since tag wins. Date is
284 287 # used as tiebreaker.
285 288 return [-changessincetag, x[0]]
286 289 pdate, pdist, ptag = max(ptags, key=key)
287 290 else:
288 291 pdate, pdist, ptag = ptags[0]
289 292 except KeyError:
290 293 # Cache miss - recurse
291 294 todo.append(rev)
292 295 todo.extend(p.rev() for p in ctx.parents())
293 296 continue
294 297 latesttags[rev] = pdate, pdist + 1, ptag
295 298 return latesttags[rev]
296 299
297 300 def getrenamedfn(repo, endrev=None):
298 301 rcache = {}
299 302 if endrev is None:
300 303 endrev = len(repo)
301 304
302 305 def getrenamed(fn, rev):
303 306 '''looks up all renames for a file (up to endrev) the first
304 307 time the file is given. It indexes on the changerev and only
305 308 parses the manifest if linkrev != changerev.
306 309 Returns rename info for fn at changerev rev.'''
307 310 if fn not in rcache:
308 311 rcache[fn] = {}
309 312 fl = repo.file(fn)
310 313 for i in fl:
311 314 lr = fl.linkrev(i)
312 315 renamed = fl.renamed(fl.node(i))
313 316 rcache[fn][lr] = renamed
314 317 if lr >= endrev:
315 318 break
316 319 if rev in rcache[fn]:
317 320 return rcache[fn][rev]
318 321
319 322 # If linkrev != rev (i.e. rev not found in rcache) fallback to
320 323 # filectx logic.
321 324 try:
322 325 return repo[rev][fn].renamed()
323 326 except error.LookupError:
324 327 return None
325 328
326 329 return getrenamed
327 330
328 331 def getlogcolumns():
329 332 """Return a dict of log column labels"""
330 333 _ = pycompat.identity # temporarily disable gettext
331 334 # i18n: column positioning for "hg log"
332 335 columns = _('bookmark: %s\n'
333 336 'branch: %s\n'
334 337 'changeset: %s\n'
335 338 'copies: %s\n'
336 339 'date: %s\n'
337 340 'extra: %s=%s\n'
338 341 'files+: %s\n'
339 342 'files-: %s\n'
340 343 'files: %s\n'
341 344 'instability: %s\n'
342 345 'manifest: %s\n'
343 346 'obsolete: %s\n'
344 347 'parent: %s\n'
345 348 'phase: %s\n'
346 349 'summary: %s\n'
347 350 'tag: %s\n'
348 351 'user: %s\n')
349 352 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
350 353 i18n._(columns).splitlines(True)))
351 354
352 355 # default templates internally used for rendering of lists
353 356 defaulttempl = {
354 357 'parent': '{rev}:{node|formatnode} ',
355 358 'manifest': '{rev}:{node|formatnode}',
356 359 'file_copy': '{name} ({source})',
357 360 'envvar': '{key}={value}',
358 361 'extra': '{key}={value|stringescape}'
359 362 }
360 363 # filecopy is preserved for compatibility reasons
361 364 defaulttempl['filecopy'] = defaulttempl['file_copy']
362 365
363 366 # keywords are callables (see registrar.templatekeyword for details)
364 367 keywords = {}
365 368 templatekeyword = registrar.templatekeyword(keywords)
366 369
367 370 @templatekeyword('author', requires={'ctx'})
368 371 def showauthor(context, mapping):
369 372 """String. The unmodified author of the changeset."""
370 373 ctx = context.resource(mapping, 'ctx')
371 374 return ctx.user()
372 375
373 376 @templatekeyword('bisect', requires={'repo', 'ctx'})
374 377 def showbisect(context, mapping):
375 378 """String. The changeset bisection status."""
376 379 repo = context.resource(mapping, 'repo')
377 380 ctx = context.resource(mapping, 'ctx')
378 381 return hbisect.label(repo, ctx.node())
379 382
380 383 @templatekeyword('branch', requires={'ctx'})
381 384 def showbranch(context, mapping):
382 385 """String. The name of the branch on which the changeset was
383 386 committed.
384 387 """
385 388 ctx = context.resource(mapping, 'ctx')
386 389 return ctx.branch()
387 390
388 391 @templatekeyword('branches', requires={'ctx', 'templ'})
389 392 def showbranches(context, mapping):
390 393 """List of strings. The name of the branch on which the
391 394 changeset was committed. Will be empty if the branch name was
392 395 default. (DEPRECATED)
393 396 """
394 397 ctx = context.resource(mapping, 'ctx')
395 398 branch = ctx.branch()
396 399 if branch != 'default':
397 400 return compatlist(context, mapping, 'branch', [branch],
398 401 plural='branches')
399 402 return compatlist(context, mapping, 'branch', [], plural='branches')
400 403
401 404 @templatekeyword('bookmarks')
402 405 def showbookmarks(**args):
403 406 """List of strings. Any bookmarks associated with the
404 407 changeset. Also sets 'active', the name of the active bookmark.
405 408 """
406 409 args = pycompat.byteskwargs(args)
407 410 repo = args['ctx']._repo
408 411 bookmarks = args['ctx'].bookmarks()
409 412 active = repo._activebookmark
410 413 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
411 414 f = _showlist('bookmark', bookmarks, args['templ'], args)
412 415 return _hybrid(f, bookmarks, makemap, pycompat.identity)
413 416
414 417 @templatekeyword('children', requires={'ctx', 'templ'})
415 418 def showchildren(context, mapping):
416 419 """List of strings. The children of the changeset."""
417 420 ctx = context.resource(mapping, 'ctx')
418 421 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
419 422 return compatlist(context, mapping, 'children', childrevs, element='child')
420 423
421 424 # Deprecated, but kept alive for help generation a purpose.
422 425 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
423 426 def showcurrentbookmark(context, mapping):
424 427 """String. The active bookmark, if it is associated with the changeset.
425 428 (DEPRECATED)"""
426 429 return showactivebookmark(context, mapping)
427 430
428 431 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
429 432 def showactivebookmark(context, mapping):
430 433 """String. The active bookmark, if it is associated with the changeset."""
431 434 repo = context.resource(mapping, 'repo')
432 435 ctx = context.resource(mapping, 'ctx')
433 436 active = repo._activebookmark
434 437 if active and active in ctx.bookmarks():
435 438 return active
436 439 return ''
437 440
438 441 @templatekeyword('date', requires={'ctx'})
439 442 def showdate(context, mapping):
440 443 """Date information. The date when the changeset was committed."""
441 444 ctx = context.resource(mapping, 'ctx')
442 445 return ctx.date()
443 446
444 447 @templatekeyword('desc', requires={'ctx'})
445 448 def showdescription(context, mapping):
446 449 """String. The text of the changeset description."""
447 450 ctx = context.resource(mapping, 'ctx')
448 451 s = ctx.description()
449 452 if isinstance(s, encoding.localstr):
450 453 # try hard to preserve utf-8 bytes
451 454 return encoding.tolocal(encoding.fromlocal(s).strip())
452 455 else:
453 456 return s.strip()
454 457
455 458 @templatekeyword('diffstat', requires={'ctx'})
456 459 def showdiffstat(context, mapping):
457 460 """String. Statistics of changes with the following format:
458 461 "modified files: +added/-removed lines"
459 462 """
460 463 ctx = context.resource(mapping, 'ctx')
461 464 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
462 465 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
463 466 return '%d: +%d/-%d' % (len(stats), adds, removes)
464 467
465 468 @templatekeyword('envvars', requires={'ui', 'templ'})
466 469 def showenvvars(context, mapping):
467 470 """A dictionary of environment variables. (EXPERIMENTAL)"""
468 471 ui = context.resource(mapping, 'ui')
469 472 env = ui.exportableenviron()
470 473 env = util.sortdict((k, env[k]) for k in sorted(env))
471 474 return compatdict(context, mapping, 'envvar', env, plural='envvars')
472 475
473 476 @templatekeyword('extras')
474 477 def showextras(**args):
475 478 """List of dicts with key, value entries of the 'extras'
476 479 field of this changeset."""
477 480 args = pycompat.byteskwargs(args)
478 481 extras = args['ctx'].extra()
479 482 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
480 483 makemap = lambda k: {'key': k, 'value': extras[k]}
481 484 c = [makemap(k) for k in extras]
482 485 f = _showlist('extra', c, args['templ'], args, plural='extras')
483 486 return _hybrid(f, extras, makemap,
484 487 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
485 488
486 489 def _showfilesbystat(context, mapping, name, index):
487 490 repo = context.resource(mapping, 'repo')
488 491 ctx = context.resource(mapping, 'ctx')
489 492 revcache = context.resource(mapping, 'revcache')
490 493 if 'files' not in revcache:
491 494 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
492 495 files = revcache['files'][index]
493 496 return compatlist(context, mapping, name, files, element='file')
494 497
495 498 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
496 499 def showfileadds(context, mapping):
497 500 """List of strings. Files added by this changeset."""
498 501 return _showfilesbystat(context, mapping, 'file_add', 1)
499 502
500 503 @templatekeyword('file_copies',
501 504 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
502 505 def showfilecopies(context, mapping):
503 506 """List of strings. Files copied in this changeset with
504 507 their sources.
505 508 """
506 509 repo = context.resource(mapping, 'repo')
507 510 ctx = context.resource(mapping, 'ctx')
508 511 cache = context.resource(mapping, 'cache')
509 512 copies = context.resource(mapping, 'revcache').get('copies')
510 513 if copies is None:
511 514 if 'getrenamed' not in cache:
512 515 cache['getrenamed'] = getrenamedfn(repo)
513 516 copies = []
514 517 getrenamed = cache['getrenamed']
515 518 for fn in ctx.files():
516 519 rename = getrenamed(fn, ctx.rev())
517 520 if rename:
518 521 copies.append((fn, rename[0]))
519 522
520 523 copies = util.sortdict(copies)
521 524 return compatdict(context, mapping, 'file_copy', copies,
522 525 key='name', value='source', fmt='%s (%s)',
523 526 plural='file_copies')
524 527
525 528 # showfilecopiesswitch() displays file copies only if copy records are
526 529 # provided before calling the templater, usually with a --copies
527 530 # command line switch.
528 531 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
529 532 def showfilecopiesswitch(context, mapping):
530 533 """List of strings. Like "file_copies" but displayed
531 534 only if the --copied switch is set.
532 535 """
533 536 copies = context.resource(mapping, 'revcache').get('copies') or []
534 537 copies = util.sortdict(copies)
535 538 return compatdict(context, mapping, 'file_copy', copies,
536 539 key='name', value='source', fmt='%s (%s)',
537 540 plural='file_copies')
538 541
539 542 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
540 543 def showfiledels(context, mapping):
541 544 """List of strings. Files removed by this changeset."""
542 545 return _showfilesbystat(context, mapping, 'file_del', 2)
543 546
544 547 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
545 548 def showfilemods(context, mapping):
546 549 """List of strings. Files modified by this changeset."""
547 550 return _showfilesbystat(context, mapping, 'file_mod', 0)
548 551
549 552 @templatekeyword('files', requires={'ctx', 'templ'})
550 553 def showfiles(context, mapping):
551 554 """List of strings. All files modified, added, or removed by this
552 555 changeset.
553 556 """
554 557 ctx = context.resource(mapping, 'ctx')
555 558 return compatlist(context, mapping, 'file', ctx.files())
556 559
557 560 @templatekeyword('graphnode', requires={'repo', 'ctx'})
558 561 def showgraphnode(context, mapping):
559 562 """String. The character representing the changeset node in an ASCII
560 563 revision graph."""
561 564 repo = context.resource(mapping, 'repo')
562 565 ctx = context.resource(mapping, 'ctx')
563 566 return getgraphnode(repo, ctx)
564 567
565 568 def getgraphnode(repo, ctx):
566 569 wpnodes = repo.dirstate.parents()
567 570 if wpnodes[1] == nullid:
568 571 wpnodes = wpnodes[:1]
569 572 if ctx.node() in wpnodes:
570 573 return '@'
571 574 elif ctx.obsolete():
572 575 return 'x'
573 576 elif ctx.isunstable():
574 577 return '*'
575 578 elif ctx.closesbranch():
576 579 return '_'
577 580 else:
578 581 return 'o'
579 582
580 583 @templatekeyword('graphwidth', requires=())
581 584 def showgraphwidth(context, mapping):
582 585 """Integer. The width of the graph drawn by 'log --graph' or zero."""
583 586 # just hosts documentation; should be overridden by template mapping
584 587 return 0
585 588
586 589 @templatekeyword('index', requires=())
587 590 def showindex(context, mapping):
588 591 """Integer. The current iteration of the loop. (0 indexed)"""
589 592 # just hosts documentation; should be overridden by template mapping
590 593 raise error.Abort(_("can't use index in this context"))
591 594
592 @templatekeyword('latesttag')
593 def showlatesttag(**args):
595 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache', 'templ'})
596 def showlatesttag(context, mapping):
594 597 """List of strings. The global tags on the most recent globally
595 598 tagged ancestor of this changeset. If no such tags exist, the list
596 599 consists of the single string "null".
597 600 """
598 return showlatesttags(None, **args)
601 return showlatesttags(context, mapping, None)
599 602
600 def showlatesttags(pattern, **args):
603 def showlatesttags(context, mapping, pattern):
601 604 """helper method for the latesttag keyword and function"""
602 args = pycompat.byteskwargs(args)
603 repo, ctx = args['repo'], args['ctx']
604 cache = args['cache']
605 latesttags = getlatesttags(repo, ctx, cache, pattern)
605 latesttags = getlatesttags(context, mapping, pattern)
606 606
607 607 # latesttag[0] is an implementation detail for sorting csets on different
608 608 # branches in a stable manner- it is the date the tagged cset was created,
609 609 # not the date the tag was created. Therefore it isn't made visible here.
610 610 makemap = lambda v: {
611 611 'changes': _showchangessincetag,
612 612 'distance': latesttags[1],
613 613 'latesttag': v, # BC with {latesttag % '{latesttag}'}
614 614 'tag': v
615 615 }
616 616
617 617 tags = latesttags[2]
618 f = _showlist('latesttag', tags, args['templ'], args, separator=':')
618 templ = context.resource(mapping, 'templ')
619 f = _showlist('latesttag', tags, templ, mapping, separator=':')
619 620 return _hybrid(f, tags, makemap, pycompat.identity)
620 621
621 @templatekeyword('latesttagdistance')
622 def showlatesttagdistance(repo, ctx, templ, cache, **args):
622 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
623 def showlatesttagdistance(context, mapping):
623 624 """Integer. Longest path to the latest tag."""
624 return getlatesttags(repo, ctx, cache)[1]
625 return getlatesttags(context, mapping)[1]
625 626
626 @templatekeyword('changessincelatesttag')
627 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
627 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
628 def showchangessincelatesttag(context, mapping):
628 629 """Integer. All ancestors not in the latest tag."""
629 latesttag = getlatesttags(repo, ctx, cache)[2][0]
630 mapping = mapping.copy()
631 mapping['tag'] = getlatesttags(context, mapping)[2][0]
632 return _showchangessincetag(context, mapping)
630 633
631 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
632
633 def _showchangessincetag(repo, ctx, **args):
634 def _showchangessincetag(context, mapping):
635 repo = context.resource(mapping, 'repo')
636 ctx = context.resource(mapping, 'ctx')
634 637 offset = 0
635 638 revs = [ctx.rev()]
636 tag = args[r'tag']
639 tag = context.symbol(mapping, 'tag')
637 640
638 641 # The only() revset doesn't currently support wdir()
639 642 if ctx.rev() is None:
640 643 offset = 1
641 644 revs = [p.rev() for p in ctx.parents()]
642 645
643 646 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
644 647
648 # teach templater latesttags.changes is switched to (context, mapping) API
649 _showchangessincetag._requires = {'repo', 'ctx'}
650
645 651 @templatekeyword('manifest')
646 652 def showmanifest(**args):
647 653 repo, ctx, templ = args[r'repo'], args[r'ctx'], args[r'templ']
648 654 mnode = ctx.manifestnode()
649 655 if mnode is None:
650 656 # just avoid crash, we might want to use the 'ff...' hash in future
651 657 return
652 658 mrev = repo.manifestlog._revlog.rev(mnode)
653 659 mhex = hex(mnode)
654 660 args = args.copy()
655 661 args.update({r'rev': mrev, r'node': mhex})
656 662 f = templ('manifest', **args)
657 663 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
658 664 # rev and node are completely different from changeset's.
659 665 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
660 666
661 667 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
662 668 def showobsfate(context, mapping):
663 669 # this function returns a list containing pre-formatted obsfate strings.
664 670 #
665 671 # This function will be replaced by templates fragments when we will have
666 672 # the verbosity templatekw available.
667 673 succsandmarkers = showsuccsandmarkers(context, mapping)
668 674
669 675 ui = context.resource(mapping, 'ui')
670 676 values = []
671 677
672 678 for x in succsandmarkers:
673 679 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
674 680
675 681 return compatlist(context, mapping, "fate", values)
676 682
677 683 def shownames(context, mapping, namespace):
678 684 """helper method to generate a template keyword for a namespace"""
679 685 repo = context.resource(mapping, 'repo')
680 686 ctx = context.resource(mapping, 'ctx')
681 687 ns = repo.names[namespace]
682 688 names = ns.names(repo, ctx.node())
683 689 return compatlist(context, mapping, ns.templatename, names,
684 690 plural=namespace)
685 691
686 692 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
687 693 def shownamespaces(context, mapping):
688 694 """Dict of lists. Names attached to this changeset per
689 695 namespace."""
690 696 repo = context.resource(mapping, 'repo')
691 697 ctx = context.resource(mapping, 'ctx')
692 698 templ = context.resource(mapping, 'templ')
693 699
694 700 namespaces = util.sortdict()
695 701 def makensmapfn(ns):
696 702 # 'name' for iterating over namespaces, templatename for local reference
697 703 return lambda v: {'name': v, ns.templatename: v}
698 704
699 705 for k, ns in repo.names.iteritems():
700 706 names = ns.names(repo, ctx.node())
701 707 f = _showlist('name', names, templ, mapping)
702 708 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
703 709
704 710 f = _showlist('namespace', list(namespaces), templ, mapping)
705 711
706 712 def makemap(ns):
707 713 return {
708 714 'namespace': ns,
709 715 'names': namespaces[ns],
710 716 'builtin': repo.names[ns].builtin,
711 717 'colorname': repo.names[ns].colorname,
712 718 }
713 719
714 720 return _hybrid(f, namespaces, makemap, pycompat.identity)
715 721
716 722 @templatekeyword('node', requires={'ctx'})
717 723 def shownode(context, mapping):
718 724 """String. The changeset identification hash, as a 40 hexadecimal
719 725 digit string.
720 726 """
721 727 ctx = context.resource(mapping, 'ctx')
722 728 return ctx.hex()
723 729
724 730 @templatekeyword('obsolete', requires={'ctx'})
725 731 def showobsolete(context, mapping):
726 732 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
727 733 ctx = context.resource(mapping, 'ctx')
728 734 if ctx.obsolete():
729 735 return 'obsolete'
730 736 return ''
731 737
732 738 @templatekeyword('peerurls', requires={'repo'})
733 739 def showpeerurls(context, mapping):
734 740 """A dictionary of repository locations defined in the [paths] section
735 741 of your configuration file."""
736 742 repo = context.resource(mapping, 'repo')
737 743 # see commands.paths() for naming of dictionary keys
738 744 paths = repo.ui.paths
739 745 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
740 746 def makemap(k):
741 747 p = paths[k]
742 748 d = {'name': k, 'url': p.rawloc}
743 749 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
744 750 return d
745 751 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
746 752
747 753 @templatekeyword("predecessors", requires={'repo', 'ctx'})
748 754 def showpredecessors(context, mapping):
749 755 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
750 756 repo = context.resource(mapping, 'repo')
751 757 ctx = context.resource(mapping, 'ctx')
752 758 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
753 759 predecessors = map(hex, predecessors)
754 760
755 761 return _hybrid(None, predecessors,
756 762 lambda x: {'ctx': repo[x], 'revcache': {}},
757 763 lambda x: scmutil.formatchangeid(repo[x]))
758 764
759 765 @templatekeyword('reporoot', requires={'repo'})
760 766 def showreporoot(context, mapping):
761 767 """String. The root directory of the current repository."""
762 768 repo = context.resource(mapping, 'repo')
763 769 return repo.root
764 770
765 771 @templatekeyword("successorssets", requires={'repo', 'ctx'})
766 772 def showsuccessorssets(context, mapping):
767 773 """Returns a string of sets of successors for a changectx. Format used
768 774 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
769 775 while also diverged into ctx3. (EXPERIMENTAL)"""
770 776 repo = context.resource(mapping, 'repo')
771 777 ctx = context.resource(mapping, 'ctx')
772 778 if not ctx.obsolete():
773 779 return ''
774 780
775 781 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
776 782 ssets = [[hex(n) for n in ss] for ss in ssets]
777 783
778 784 data = []
779 785 for ss in ssets:
780 786 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
781 787 lambda x: scmutil.formatchangeid(repo[x]))
782 788 data.append(h)
783 789
784 790 # Format the successorssets
785 791 def render(d):
786 792 t = []
787 793 for i in d.gen():
788 794 t.append(i)
789 795 return "".join(t)
790 796
791 797 def gen(data):
792 798 yield "; ".join(render(d) for d in data)
793 799
794 800 return _hybrid(gen(data), data, lambda x: {'successorset': x},
795 801 pycompat.identity)
796 802
797 803 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
798 804 def showsuccsandmarkers(context, mapping):
799 805 """Returns a list of dict for each final successor of ctx. The dict
800 806 contains successors node id in "successors" keys and the list of
801 807 obs-markers from ctx to the set of successors in "markers".
802 808 (EXPERIMENTAL)
803 809 """
804 810 repo = context.resource(mapping, 'repo')
805 811 ctx = context.resource(mapping, 'ctx')
806 812 templ = context.resource(mapping, 'templ')
807 813
808 814 values = obsutil.successorsandmarkers(repo, ctx)
809 815
810 816 if values is None:
811 817 values = []
812 818
813 819 # Format successors and markers to avoid exposing binary to templates
814 820 data = []
815 821 for i in values:
816 822 # Format successors
817 823 successors = i['successors']
818 824
819 825 successors = [hex(n) for n in successors]
820 826 successors = _hybrid(None, successors,
821 827 lambda x: {'ctx': repo[x], 'revcache': {}},
822 828 lambda x: scmutil.formatchangeid(repo[x]))
823 829
824 830 # Format markers
825 831 finalmarkers = []
826 832 for m in i['markers']:
827 833 hexprec = hex(m[0])
828 834 hexsucs = tuple(hex(n) for n in m[1])
829 835 hexparents = None
830 836 if m[5] is not None:
831 837 hexparents = tuple(hex(n) for n in m[5])
832 838 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
833 839 finalmarkers.append(newmarker)
834 840
835 841 data.append({'successors': successors, 'markers': finalmarkers})
836 842
837 843 f = _showlist('succsandmarkers', data, templ, mapping)
838 844 return _hybrid(f, data, lambda x: x, pycompat.identity)
839 845
840 846 @templatekeyword('p1rev', requires={'ctx'})
841 847 def showp1rev(context, mapping):
842 848 """Integer. The repository-local revision number of the changeset's
843 849 first parent, or -1 if the changeset has no parents."""
844 850 ctx = context.resource(mapping, 'ctx')
845 851 return ctx.p1().rev()
846 852
847 853 @templatekeyword('p2rev', requires={'ctx'})
848 854 def showp2rev(context, mapping):
849 855 """Integer. The repository-local revision number of the changeset's
850 856 second parent, or -1 if the changeset has no second parent."""
851 857 ctx = context.resource(mapping, 'ctx')
852 858 return ctx.p2().rev()
853 859
854 860 @templatekeyword('p1node', requires={'ctx'})
855 861 def showp1node(context, mapping):
856 862 """String. The identification hash of the changeset's first parent,
857 863 as a 40 digit hexadecimal string. If the changeset has no parents, all
858 864 digits are 0."""
859 865 ctx = context.resource(mapping, 'ctx')
860 866 return ctx.p1().hex()
861 867
862 868 @templatekeyword('p2node', requires={'ctx'})
863 869 def showp2node(context, mapping):
864 870 """String. The identification hash of the changeset's second
865 871 parent, as a 40 digit hexadecimal string. If the changeset has no second
866 872 parent, all digits are 0."""
867 873 ctx = context.resource(mapping, 'ctx')
868 874 return ctx.p2().hex()
869 875
870 876 @templatekeyword('parents')
871 877 def showparents(**args):
872 878 """List of strings. The parents of the changeset in "rev:node"
873 879 format. If the changeset has only one "natural" parent (the predecessor
874 880 revision) nothing is shown."""
875 881 args = pycompat.byteskwargs(args)
876 882 repo = args['repo']
877 883 ctx = args['ctx']
878 884 pctxs = scmutil.meaningfulparents(repo, ctx)
879 885 prevs = [p.rev() for p in pctxs]
880 886 parents = [[('rev', p.rev()),
881 887 ('node', p.hex()),
882 888 ('phase', p.phasestr())]
883 889 for p in pctxs]
884 890 f = _showlist('parent', parents, args['templ'], args)
885 891 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
886 892 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
887 893
888 894 @templatekeyword('phase', requires={'ctx'})
889 895 def showphase(context, mapping):
890 896 """String. The changeset phase name."""
891 897 ctx = context.resource(mapping, 'ctx')
892 898 return ctx.phasestr()
893 899
894 900 @templatekeyword('phaseidx', requires={'ctx'})
895 901 def showphaseidx(context, mapping):
896 902 """Integer. The changeset phase index. (ADVANCED)"""
897 903 ctx = context.resource(mapping, 'ctx')
898 904 return ctx.phase()
899 905
900 906 @templatekeyword('rev', requires={'ctx'})
901 907 def showrev(context, mapping):
902 908 """Integer. The repository-local changeset revision number."""
903 909 ctx = context.resource(mapping, 'ctx')
904 910 return scmutil.intrev(ctx)
905 911
906 912 def showrevslist(context, mapping, name, revs):
907 913 """helper to generate a list of revisions in which a mapped template will
908 914 be evaluated"""
909 915 repo = context.resource(mapping, 'repo')
910 916 templ = context.resource(mapping, 'templ')
911 917 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
912 918 return _hybrid(f, revs,
913 919 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
914 920 pycompat.identity, keytype=int)
915 921
916 922 @templatekeyword('subrepos', requires={'ctx', 'templ'})
917 923 def showsubrepos(context, mapping):
918 924 """List of strings. Updated subrepositories in the changeset."""
919 925 ctx = context.resource(mapping, 'ctx')
920 926 substate = ctx.substate
921 927 if not substate:
922 928 return compatlist(context, mapping, 'subrepo', [])
923 929 psubstate = ctx.parents()[0].substate or {}
924 930 subrepos = []
925 931 for sub in substate:
926 932 if sub not in psubstate or substate[sub] != psubstate[sub]:
927 933 subrepos.append(sub) # modified or newly added in ctx
928 934 for sub in psubstate:
929 935 if sub not in substate:
930 936 subrepos.append(sub) # removed in ctx
931 937 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
932 938
933 939 # don't remove "showtags" definition, even though namespaces will put
934 940 # a helper function for "tags" keyword into "keywords" map automatically,
935 941 # because online help text is built without namespaces initialization
936 942 @templatekeyword('tags', requires={'repo', 'ctx', 'templ'})
937 943 def showtags(context, mapping):
938 944 """List of strings. Any tags associated with the changeset."""
939 945 return shownames(context, mapping, 'tags')
940 946
941 947 @templatekeyword('termwidth', requires={'ui'})
942 948 def showtermwidth(context, mapping):
943 949 """Integer. The width of the current terminal."""
944 950 ui = context.resource(mapping, 'ui')
945 951 return ui.termwidth()
946 952
947 953 @templatekeyword('instabilities', requires={'ctx', 'templ'})
948 954 def showinstabilities(context, mapping):
949 955 """List of strings. Evolution instabilities affecting the changeset.
950 956 (EXPERIMENTAL)
951 957 """
952 958 ctx = context.resource(mapping, 'ctx')
953 959 return compatlist(context, mapping, 'instability', ctx.instabilities(),
954 960 plural='instabilities')
955 961
956 962 @templatekeyword('verbosity', requires={'ui'})
957 963 def showverbosity(context, mapping):
958 964 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
959 965 or ''."""
960 966 ui = context.resource(mapping, 'ui')
961 967 # see logcmdutil.changesettemplater for priority of these flags
962 968 if ui.debugflag:
963 969 return 'debug'
964 970 elif ui.quiet:
965 971 return 'quiet'
966 972 elif ui.verbose:
967 973 return 'verbose'
968 974 return ''
969 975
970 976 def loadkeyword(ui, extname, registrarobj):
971 977 """Load template keyword from specified registrarobj
972 978 """
973 979 for name, func in registrarobj._table.iteritems():
974 980 keywords[name] = func
975 981
976 982 # tell hggettext to extract docstrings from these functions:
977 983 i18nfunctions = keywords.values()
@@ -1,1628 +1,1624
1 1 # templater.py - template expansion for output
2 2 #
3 3 # Copyright 2005, 2006 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, print_function
9 9
10 10 import os
11 11 import re
12 12 import types
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 color,
17 17 config,
18 18 encoding,
19 19 error,
20 20 minirst,
21 21 obsutil,
22 22 parser,
23 23 pycompat,
24 24 registrar,
25 25 revset as revsetmod,
26 26 revsetlang,
27 27 scmutil,
28 28 templatefilters,
29 29 templatekw,
30 30 util,
31 31 )
32 32
33 33 class ResourceUnavailable(error.Abort):
34 34 pass
35 35
36 36 class TemplateNotFound(error.Abort):
37 37 pass
38 38
39 39 # template parsing
40 40
41 41 elements = {
42 42 # token-type: binding-strength, primary, prefix, infix, suffix
43 43 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
44 44 ".": (18, None, None, (".", 18), None),
45 45 "%": (15, None, None, ("%", 15), None),
46 46 "|": (15, None, None, ("|", 15), None),
47 47 "*": (5, None, None, ("*", 5), None),
48 48 "/": (5, None, None, ("/", 5), None),
49 49 "+": (4, None, None, ("+", 4), None),
50 50 "-": (4, None, ("negate", 19), ("-", 4), None),
51 51 "=": (3, None, None, ("keyvalue", 3), None),
52 52 ",": (2, None, None, ("list", 2), None),
53 53 ")": (0, None, None, None, None),
54 54 "integer": (0, "integer", None, None, None),
55 55 "symbol": (0, "symbol", None, None, None),
56 56 "string": (0, "string", None, None, None),
57 57 "template": (0, "template", None, None, None),
58 58 "end": (0, None, None, None, None),
59 59 }
60 60
61 61 def tokenize(program, start, end, term=None):
62 62 """Parse a template expression into a stream of tokens, which must end
63 63 with term if specified"""
64 64 pos = start
65 65 program = pycompat.bytestr(program)
66 66 while pos < end:
67 67 c = program[pos]
68 68 if c.isspace(): # skip inter-token whitespace
69 69 pass
70 70 elif c in "(=,).%|+-*/": # handle simple operators
71 71 yield (c, None, pos)
72 72 elif c in '"\'': # handle quoted templates
73 73 s = pos + 1
74 74 data, pos = _parsetemplate(program, s, end, c)
75 75 yield ('template', data, s)
76 76 pos -= 1
77 77 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
78 78 # handle quoted strings
79 79 c = program[pos + 1]
80 80 s = pos = pos + 2
81 81 while pos < end: # find closing quote
82 82 d = program[pos]
83 83 if d == '\\': # skip over escaped characters
84 84 pos += 2
85 85 continue
86 86 if d == c:
87 87 yield ('string', program[s:pos], s)
88 88 break
89 89 pos += 1
90 90 else:
91 91 raise error.ParseError(_("unterminated string"), s)
92 92 elif c.isdigit():
93 93 s = pos
94 94 while pos < end:
95 95 d = program[pos]
96 96 if not d.isdigit():
97 97 break
98 98 pos += 1
99 99 yield ('integer', program[s:pos], s)
100 100 pos -= 1
101 101 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
102 102 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
103 103 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
104 104 # where some of nested templates were preprocessed as strings and
105 105 # then compiled. therefore, \"...\" was allowed. (issue4733)
106 106 #
107 107 # processing flow of _evalifliteral() at 5ab28a2e9962:
108 108 # outer template string -> stringify() -> compiletemplate()
109 109 # ------------------------ ------------ ------------------
110 110 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
111 111 # ~~~~~~~~
112 112 # escaped quoted string
113 113 if c == 'r':
114 114 pos += 1
115 115 token = 'string'
116 116 else:
117 117 token = 'template'
118 118 quote = program[pos:pos + 2]
119 119 s = pos = pos + 2
120 120 while pos < end: # find closing escaped quote
121 121 if program.startswith('\\\\\\', pos, end):
122 122 pos += 4 # skip over double escaped characters
123 123 continue
124 124 if program.startswith(quote, pos, end):
125 125 # interpret as if it were a part of an outer string
126 126 data = parser.unescapestr(program[s:pos])
127 127 if token == 'template':
128 128 data = _parsetemplate(data, 0, len(data))[0]
129 129 yield (token, data, s)
130 130 pos += 1
131 131 break
132 132 pos += 1
133 133 else:
134 134 raise error.ParseError(_("unterminated string"), s)
135 135 elif c.isalnum() or c in '_':
136 136 s = pos
137 137 pos += 1
138 138 while pos < end: # find end of symbol
139 139 d = program[pos]
140 140 if not (d.isalnum() or d == "_"):
141 141 break
142 142 pos += 1
143 143 sym = program[s:pos]
144 144 yield ('symbol', sym, s)
145 145 pos -= 1
146 146 elif c == term:
147 147 yield ('end', None, pos + 1)
148 148 return
149 149 else:
150 150 raise error.ParseError(_("syntax error"), pos)
151 151 pos += 1
152 152 if term:
153 153 raise error.ParseError(_("unterminated template expansion"), start)
154 154 yield ('end', None, pos)
155 155
156 156 def _parsetemplate(tmpl, start, stop, quote=''):
157 157 r"""
158 158 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
159 159 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
160 160 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
161 161 ([('string', 'foo'), ('symbol', 'bar')], 9)
162 162 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
163 163 ([('string', 'foo')], 4)
164 164 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
165 165 ([('string', 'foo"'), ('string', 'bar')], 9)
166 166 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
167 167 ([('string', 'foo\\')], 6)
168 168 """
169 169 parsed = []
170 170 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
171 171 if typ == 'string':
172 172 parsed.append((typ, val))
173 173 elif typ == 'template':
174 174 parsed.append(val)
175 175 elif typ == 'end':
176 176 return parsed, pos
177 177 else:
178 178 raise error.ProgrammingError('unexpected type: %s' % typ)
179 179 raise error.ProgrammingError('unterminated scanning of template')
180 180
181 181 def scantemplate(tmpl, raw=False):
182 182 r"""Scan (type, start, end) positions of outermost elements in template
183 183
184 184 If raw=True, a backslash is not taken as an escape character just like
185 185 r'' string in Python. Note that this is different from r'' literal in
186 186 template in that no template fragment can appear in r'', e.g. r'{foo}'
187 187 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
188 188 'foo'.
189 189
190 190 >>> list(scantemplate(b'foo{bar}"baz'))
191 191 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
192 192 >>> list(scantemplate(b'outer{"inner"}outer'))
193 193 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
194 194 >>> list(scantemplate(b'foo\\{escaped}'))
195 195 [('string', 0, 5), ('string', 5, 13)]
196 196 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
197 197 [('string', 0, 4), ('template', 4, 13)]
198 198 """
199 199 last = None
200 200 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
201 201 if last:
202 202 yield last + (pos,)
203 203 if typ == 'end':
204 204 return
205 205 else:
206 206 last = (typ, pos)
207 207 raise error.ProgrammingError('unterminated scanning of template')
208 208
209 209 def _scantemplate(tmpl, start, stop, quote='', raw=False):
210 210 """Parse template string into chunks of strings and template expressions"""
211 211 sepchars = '{' + quote
212 212 unescape = [parser.unescapestr, pycompat.identity][raw]
213 213 pos = start
214 214 p = parser.parser(elements)
215 215 while pos < stop:
216 216 n = min((tmpl.find(c, pos, stop) for c in sepchars),
217 217 key=lambda n: (n < 0, n))
218 218 if n < 0:
219 219 yield ('string', unescape(tmpl[pos:stop]), pos)
220 220 pos = stop
221 221 break
222 222 c = tmpl[n:n + 1]
223 223 bs = 0 # count leading backslashes
224 224 if not raw:
225 225 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
226 226 if bs % 2 == 1:
227 227 # escaped (e.g. '\{', '\\\{', but not '\\{')
228 228 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
229 229 pos = n + 1
230 230 continue
231 231 if n > pos:
232 232 yield ('string', unescape(tmpl[pos:n]), pos)
233 233 if c == quote:
234 234 yield ('end', None, n + 1)
235 235 return
236 236
237 237 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
238 238 if not tmpl.endswith('}', n + 1, pos):
239 239 raise error.ParseError(_("invalid token"), pos)
240 240 yield ('template', parseres, n)
241 241
242 242 if quote:
243 243 raise error.ParseError(_("unterminated string"), start)
244 244 yield ('end', None, pos)
245 245
246 246 def _unnesttemplatelist(tree):
247 247 """Expand list of templates to node tuple
248 248
249 249 >>> def f(tree):
250 250 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
251 251 >>> f((b'template', []))
252 252 (string '')
253 253 >>> f((b'template', [(b'string', b'foo')]))
254 254 (string 'foo')
255 255 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
256 256 (template
257 257 (string 'foo')
258 258 (symbol 'rev'))
259 259 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
260 260 (template
261 261 (symbol 'rev'))
262 262 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
263 263 (string 'foo')
264 264 """
265 265 if not isinstance(tree, tuple):
266 266 return tree
267 267 op = tree[0]
268 268 if op != 'template':
269 269 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
270 270
271 271 assert len(tree) == 2
272 272 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
273 273 if not xs:
274 274 return ('string', '') # empty template ""
275 275 elif len(xs) == 1 and xs[0][0] == 'string':
276 276 return xs[0] # fast path for string with no template fragment "x"
277 277 else:
278 278 return (op,) + xs
279 279
280 280 def parse(tmpl):
281 281 """Parse template string into tree"""
282 282 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
283 283 assert pos == len(tmpl), 'unquoted template should be consumed'
284 284 return _unnesttemplatelist(('template', parsed))
285 285
286 286 def _parseexpr(expr):
287 287 """Parse a template expression into tree
288 288
289 289 >>> _parseexpr(b'"foo"')
290 290 ('string', 'foo')
291 291 >>> _parseexpr(b'foo(bar)')
292 292 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
293 293 >>> _parseexpr(b'foo(')
294 294 Traceback (most recent call last):
295 295 ...
296 296 ParseError: ('not a prefix: end', 4)
297 297 >>> _parseexpr(b'"foo" "bar"')
298 298 Traceback (most recent call last):
299 299 ...
300 300 ParseError: ('invalid token', 7)
301 301 """
302 302 p = parser.parser(elements)
303 303 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
304 304 if pos != len(expr):
305 305 raise error.ParseError(_('invalid token'), pos)
306 306 return _unnesttemplatelist(tree)
307 307
308 308 def prettyformat(tree):
309 309 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
310 310
311 311 def compileexp(exp, context, curmethods):
312 312 """Compile parsed template tree to (func, data) pair"""
313 313 if not exp:
314 314 raise error.ParseError(_("missing argument"))
315 315 t = exp[0]
316 316 if t in curmethods:
317 317 return curmethods[t](exp, context)
318 318 raise error.ParseError(_("unknown method '%s'") % t)
319 319
320 320 # template evaluation
321 321
322 322 def getsymbol(exp):
323 323 if exp[0] == 'symbol':
324 324 return exp[1]
325 325 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
326 326
327 327 def getlist(x):
328 328 if not x:
329 329 return []
330 330 if x[0] == 'list':
331 331 return getlist(x[1]) + [x[2]]
332 332 return [x]
333 333
334 334 def gettemplate(exp, context):
335 335 """Compile given template tree or load named template from map file;
336 336 returns (func, data) pair"""
337 337 if exp[0] in ('template', 'string'):
338 338 return compileexp(exp, context, methods)
339 339 if exp[0] == 'symbol':
340 340 # unlike runsymbol(), here 'symbol' is always taken as template name
341 341 # even if it exists in mapping. this allows us to override mapping
342 342 # by web templates, e.g. 'changelogtag' is redefined in map file.
343 343 return context._load(exp[1])
344 344 raise error.ParseError(_("expected template specifier"))
345 345
346 346 def findsymbolicname(arg):
347 347 """Find symbolic name for the given compiled expression; returns None
348 348 if nothing found reliably"""
349 349 while True:
350 350 func, data = arg
351 351 if func is runsymbol:
352 352 return data
353 353 elif func is runfilter:
354 354 arg = data[0]
355 355 else:
356 356 return None
357 357
358 358 def evalrawexp(context, mapping, arg):
359 359 """Evaluate given argument as a bare template object which may require
360 360 further processing (such as folding generator of strings)"""
361 361 func, data = arg
362 362 return func(context, mapping, data)
363 363
364 364 def evalfuncarg(context, mapping, arg):
365 365 """Evaluate given argument as value type"""
366 366 thing = evalrawexp(context, mapping, arg)
367 367 thing = templatekw.unwrapvalue(thing)
368 368 # evalrawexp() may return string, generator of strings or arbitrary object
369 369 # such as date tuple, but filter does not want generator.
370 370 if isinstance(thing, types.GeneratorType):
371 371 thing = stringify(thing)
372 372 return thing
373 373
374 374 def evalboolean(context, mapping, arg):
375 375 """Evaluate given argument as boolean, but also takes boolean literals"""
376 376 func, data = arg
377 377 if func is runsymbol:
378 378 thing = func(context, mapping, data, default=None)
379 379 if thing is None:
380 380 # not a template keyword, takes as a boolean literal
381 381 thing = util.parsebool(data)
382 382 else:
383 383 thing = func(context, mapping, data)
384 384 thing = templatekw.unwrapvalue(thing)
385 385 if isinstance(thing, bool):
386 386 return thing
387 387 # other objects are evaluated as strings, which means 0 is True, but
388 388 # empty dict/list should be False as they are expected to be ''
389 389 return bool(stringify(thing))
390 390
391 391 def evalinteger(context, mapping, arg, err=None):
392 392 v = evalfuncarg(context, mapping, arg)
393 393 try:
394 394 return int(v)
395 395 except (TypeError, ValueError):
396 396 raise error.ParseError(err or _('not an integer'))
397 397
398 398 def evalstring(context, mapping, arg):
399 399 return stringify(evalrawexp(context, mapping, arg))
400 400
401 401 def evalstringliteral(context, mapping, arg):
402 402 """Evaluate given argument as string template, but returns symbol name
403 403 if it is unknown"""
404 404 func, data = arg
405 405 if func is runsymbol:
406 406 thing = func(context, mapping, data, default=data)
407 407 else:
408 408 thing = func(context, mapping, data)
409 409 return stringify(thing)
410 410
411 411 _evalfuncbytype = {
412 412 bool: evalboolean,
413 413 bytes: evalstring,
414 414 int: evalinteger,
415 415 }
416 416
417 417 def evalastype(context, mapping, arg, typ):
418 418 """Evaluate given argument and coerce its type"""
419 419 try:
420 420 f = _evalfuncbytype[typ]
421 421 except KeyError:
422 422 raise error.ProgrammingError('invalid type specified: %r' % typ)
423 423 return f(context, mapping, arg)
424 424
425 425 def runinteger(context, mapping, data):
426 426 return int(data)
427 427
428 428 def runstring(context, mapping, data):
429 429 return data
430 430
431 431 def _recursivesymbolblocker(key):
432 432 def showrecursion(**args):
433 433 raise error.Abort(_("recursive reference '%s' in template") % key)
434 434 return showrecursion
435 435
436 436 def _runrecursivesymbol(context, mapping, key):
437 437 raise error.Abort(_("recursive reference '%s' in template") % key)
438 438
439 439 def runsymbol(context, mapping, key, default=''):
440 440 v = context.symbol(mapping, key)
441 441 if v is None:
442 442 # put poison to cut recursion. we can't move this to parsing phase
443 443 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
444 444 safemapping = mapping.copy()
445 445 safemapping[key] = _recursivesymbolblocker(key)
446 446 try:
447 447 v = context.process(key, safemapping)
448 448 except TemplateNotFound:
449 449 v = default
450 450 if callable(v) and getattr(v, '_requires', None) is None:
451 451 # old templatekw: expand all keywords and resources
452 452 props = context._resources.copy()
453 453 props.update(mapping)
454 454 return v(**pycompat.strkwargs(props))
455 455 if callable(v):
456 456 # new templatekw
457 457 try:
458 458 return v(context, mapping)
459 459 except ResourceUnavailable:
460 460 # unsupported keyword is mapped to empty just like unknown keyword
461 461 return None
462 462 return v
463 463
464 464 def buildtemplate(exp, context):
465 465 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
466 466 return (runtemplate, ctmpl)
467 467
468 468 def runtemplate(context, mapping, template):
469 469 for arg in template:
470 470 yield evalrawexp(context, mapping, arg)
471 471
472 472 def buildfilter(exp, context):
473 473 n = getsymbol(exp[2])
474 474 if n in context._filters:
475 475 filt = context._filters[n]
476 476 arg = compileexp(exp[1], context, methods)
477 477 return (runfilter, (arg, filt))
478 478 if n in funcs:
479 479 f = funcs[n]
480 480 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
481 481 return (f, args)
482 482 raise error.ParseError(_("unknown function '%s'") % n)
483 483
484 484 def runfilter(context, mapping, data):
485 485 arg, filt = data
486 486 thing = evalfuncarg(context, mapping, arg)
487 487 try:
488 488 return filt(thing)
489 489 except (ValueError, AttributeError, TypeError):
490 490 sym = findsymbolicname(arg)
491 491 if sym:
492 492 msg = (_("template filter '%s' is not compatible with keyword '%s'")
493 493 % (pycompat.sysbytes(filt.__name__), sym))
494 494 else:
495 495 msg = (_("incompatible use of template filter '%s'")
496 496 % pycompat.sysbytes(filt.__name__))
497 497 raise error.Abort(msg)
498 498
499 499 def buildmap(exp, context):
500 500 darg = compileexp(exp[1], context, methods)
501 501 targ = gettemplate(exp[2], context)
502 502 return (runmap, (darg, targ))
503 503
504 504 def runmap(context, mapping, data):
505 505 darg, targ = data
506 506 d = evalrawexp(context, mapping, darg)
507 507 if util.safehasattr(d, 'itermaps'):
508 508 diter = d.itermaps()
509 509 else:
510 510 try:
511 511 diter = iter(d)
512 512 except TypeError:
513 513 sym = findsymbolicname(darg)
514 514 if sym:
515 515 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
516 516 else:
517 517 raise error.ParseError(_("%r is not iterable") % d)
518 518
519 519 for i, v in enumerate(diter):
520 520 lm = mapping.copy()
521 521 lm['index'] = i
522 522 if isinstance(v, dict):
523 523 lm.update(v)
524 524 lm['originalnode'] = mapping.get('node')
525 525 yield evalrawexp(context, lm, targ)
526 526 else:
527 527 # v is not an iterable of dicts, this happen when 'key'
528 528 # has been fully expanded already and format is useless.
529 529 # If so, return the expanded value.
530 530 yield v
531 531
532 532 def buildmember(exp, context):
533 533 darg = compileexp(exp[1], context, methods)
534 534 memb = getsymbol(exp[2])
535 535 return (runmember, (darg, memb))
536 536
537 537 def runmember(context, mapping, data):
538 538 darg, memb = data
539 539 d = evalrawexp(context, mapping, darg)
540 540 if util.safehasattr(d, 'tomap'):
541 541 lm = mapping.copy()
542 542 lm.update(d.tomap())
543 543 return runsymbol(context, lm, memb)
544 544 if util.safehasattr(d, 'get'):
545 545 return _getdictitem(d, memb)
546 546
547 547 sym = findsymbolicname(darg)
548 548 if sym:
549 549 raise error.ParseError(_("keyword '%s' has no member") % sym)
550 550 else:
551 551 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
552 552
553 553 def buildnegate(exp, context):
554 554 arg = compileexp(exp[1], context, exprmethods)
555 555 return (runnegate, arg)
556 556
557 557 def runnegate(context, mapping, data):
558 558 data = evalinteger(context, mapping, data,
559 559 _('negation needs an integer argument'))
560 560 return -data
561 561
562 562 def buildarithmetic(exp, context, func):
563 563 left = compileexp(exp[1], context, exprmethods)
564 564 right = compileexp(exp[2], context, exprmethods)
565 565 return (runarithmetic, (func, left, right))
566 566
567 567 def runarithmetic(context, mapping, data):
568 568 func, left, right = data
569 569 left = evalinteger(context, mapping, left,
570 570 _('arithmetic only defined on integers'))
571 571 right = evalinteger(context, mapping, right,
572 572 _('arithmetic only defined on integers'))
573 573 try:
574 574 return func(left, right)
575 575 except ZeroDivisionError:
576 576 raise error.Abort(_('division by zero is not defined'))
577 577
578 578 def buildfunc(exp, context):
579 579 n = getsymbol(exp[1])
580 580 if n in funcs:
581 581 f = funcs[n]
582 582 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
583 583 return (f, args)
584 584 if n in context._filters:
585 585 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
586 586 if len(args) != 1:
587 587 raise error.ParseError(_("filter %s expects one argument") % n)
588 588 f = context._filters[n]
589 589 return (runfilter, (args[0], f))
590 590 raise error.ParseError(_("unknown function '%s'") % n)
591 591
592 592 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
593 593 """Compile parsed tree of function arguments into list or dict of
594 594 (func, data) pairs
595 595
596 596 >>> context = engine(lambda t: (runsymbol, t))
597 597 >>> def fargs(expr, argspec):
598 598 ... x = _parseexpr(expr)
599 599 ... n = getsymbol(x[1])
600 600 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
601 601 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
602 602 ['l', 'k']
603 603 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
604 604 >>> list(args.keys()), list(args[b'opts'].keys())
605 605 (['opts'], ['opts', 'k'])
606 606 """
607 607 def compiledict(xs):
608 608 return util.sortdict((k, compileexp(x, context, curmethods))
609 609 for k, x in xs.iteritems())
610 610 def compilelist(xs):
611 611 return [compileexp(x, context, curmethods) for x in xs]
612 612
613 613 if not argspec:
614 614 # filter or function with no argspec: return list of positional args
615 615 return compilelist(getlist(exp))
616 616
617 617 # function with argspec: return dict of named args
618 618 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
619 619 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
620 620 keyvaluenode='keyvalue', keynode='symbol')
621 621 compargs = util.sortdict()
622 622 if varkey:
623 623 compargs[varkey] = compilelist(treeargs.pop(varkey))
624 624 if optkey:
625 625 compargs[optkey] = compiledict(treeargs.pop(optkey))
626 626 compargs.update(compiledict(treeargs))
627 627 return compargs
628 628
629 629 def buildkeyvaluepair(exp, content):
630 630 raise error.ParseError(_("can't use a key-value pair in this context"))
631 631
632 632 # dict of template built-in functions
633 633 funcs = {}
634 634
635 635 templatefunc = registrar.templatefunc(funcs)
636 636
637 637 @templatefunc('date(date[, fmt])')
638 638 def date(context, mapping, args):
639 639 """Format a date. See :hg:`help dates` for formatting
640 640 strings. The default is a Unix date format, including the timezone:
641 641 "Mon Sep 04 15:13:13 2006 0700"."""
642 642 if not (1 <= len(args) <= 2):
643 643 # i18n: "date" is a keyword
644 644 raise error.ParseError(_("date expects one or two arguments"))
645 645
646 646 date = evalfuncarg(context, mapping, args[0])
647 647 fmt = None
648 648 if len(args) == 2:
649 649 fmt = evalstring(context, mapping, args[1])
650 650 try:
651 651 if fmt is None:
652 652 return util.datestr(date)
653 653 else:
654 654 return util.datestr(date, fmt)
655 655 except (TypeError, ValueError):
656 656 # i18n: "date" is a keyword
657 657 raise error.ParseError(_("date expects a date information"))
658 658
659 659 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
660 660 def dict_(context, mapping, args):
661 661 """Construct a dict from key-value pairs. A key may be omitted if
662 662 a value expression can provide an unambiguous name."""
663 663 data = util.sortdict()
664 664
665 665 for v in args['args']:
666 666 k = findsymbolicname(v)
667 667 if not k:
668 668 raise error.ParseError(_('dict key cannot be inferred'))
669 669 if k in data or k in args['kwargs']:
670 670 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
671 671 data[k] = evalfuncarg(context, mapping, v)
672 672
673 673 data.update((k, evalfuncarg(context, mapping, v))
674 674 for k, v in args['kwargs'].iteritems())
675 675 return templatekw.hybriddict(data)
676 676
677 677 @templatefunc('diff([includepattern [, excludepattern]])')
678 678 def diff(context, mapping, args):
679 679 """Show a diff, optionally
680 680 specifying files to include or exclude."""
681 681 if len(args) > 2:
682 682 # i18n: "diff" is a keyword
683 683 raise error.ParseError(_("diff expects zero, one, or two arguments"))
684 684
685 685 def getpatterns(i):
686 686 if i < len(args):
687 687 s = evalstring(context, mapping, args[i]).strip()
688 688 if s:
689 689 return [s]
690 690 return []
691 691
692 692 ctx = context.resource(mapping, 'ctx')
693 693 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
694 694
695 695 return ''.join(chunks)
696 696
697 697 @templatefunc('extdata(source)', argspec='source')
698 698 def extdata(context, mapping, args):
699 699 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
700 700 if 'source' not in args:
701 701 # i18n: "extdata" is a keyword
702 702 raise error.ParseError(_('extdata expects one argument'))
703 703
704 704 source = evalstring(context, mapping, args['source'])
705 705 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
706 706 ctx = context.resource(mapping, 'ctx')
707 707 if source in cache:
708 708 data = cache[source]
709 709 else:
710 710 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
711 711 return data.get(ctx.rev(), '')
712 712
713 713 @templatefunc('files(pattern)')
714 714 def files(context, mapping, args):
715 715 """All files of the current changeset matching the pattern. See
716 716 :hg:`help patterns`."""
717 717 if not len(args) == 1:
718 718 # i18n: "files" is a keyword
719 719 raise error.ParseError(_("files expects one argument"))
720 720
721 721 raw = evalstring(context, mapping, args[0])
722 722 ctx = context.resource(mapping, 'ctx')
723 723 m = ctx.match([raw])
724 724 files = list(ctx.matches(m))
725 725 return templatekw.compatlist(context, mapping, "file", files)
726 726
727 727 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
728 728 def fill(context, mapping, args):
729 729 """Fill many
730 730 paragraphs with optional indentation. See the "fill" filter."""
731 731 if not (1 <= len(args) <= 4):
732 732 # i18n: "fill" is a keyword
733 733 raise error.ParseError(_("fill expects one to four arguments"))
734 734
735 735 text = evalstring(context, mapping, args[0])
736 736 width = 76
737 737 initindent = ''
738 738 hangindent = ''
739 739 if 2 <= len(args) <= 4:
740 740 width = evalinteger(context, mapping, args[1],
741 741 # i18n: "fill" is a keyword
742 742 _("fill expects an integer width"))
743 743 try:
744 744 initindent = evalstring(context, mapping, args[2])
745 745 hangindent = evalstring(context, mapping, args[3])
746 746 except IndexError:
747 747 pass
748 748
749 749 return templatefilters.fill(text, width, initindent, hangindent)
750 750
751 751 @templatefunc('formatnode(node)')
752 752 def formatnode(context, mapping, args):
753 753 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
754 754 if len(args) != 1:
755 755 # i18n: "formatnode" is a keyword
756 756 raise error.ParseError(_("formatnode expects one argument"))
757 757
758 758 ui = context.resource(mapping, 'ui')
759 759 node = evalstring(context, mapping, args[0])
760 760 if ui.debugflag:
761 761 return node
762 762 return templatefilters.short(node)
763 763
764 764 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
765 765 argspec='text width fillchar left')
766 766 def pad(context, mapping, args):
767 767 """Pad text with a
768 768 fill character."""
769 769 if 'text' not in args or 'width' not in args:
770 770 # i18n: "pad" is a keyword
771 771 raise error.ParseError(_("pad() expects two to four arguments"))
772 772
773 773 width = evalinteger(context, mapping, args['width'],
774 774 # i18n: "pad" is a keyword
775 775 _("pad() expects an integer width"))
776 776
777 777 text = evalstring(context, mapping, args['text'])
778 778
779 779 left = False
780 780 fillchar = ' '
781 781 if 'fillchar' in args:
782 782 fillchar = evalstring(context, mapping, args['fillchar'])
783 783 if len(color.stripeffects(fillchar)) != 1:
784 784 # i18n: "pad" is a keyword
785 785 raise error.ParseError(_("pad() expects a single fill character"))
786 786 if 'left' in args:
787 787 left = evalboolean(context, mapping, args['left'])
788 788
789 789 fillwidth = width - encoding.colwidth(color.stripeffects(text))
790 790 if fillwidth <= 0:
791 791 return text
792 792 if left:
793 793 return fillchar * fillwidth + text
794 794 else:
795 795 return text + fillchar * fillwidth
796 796
797 797 @templatefunc('indent(text, indentchars[, firstline])')
798 798 def indent(context, mapping, args):
799 799 """Indents all non-empty lines
800 800 with the characters given in the indentchars string. An optional
801 801 third parameter will override the indent for the first line only
802 802 if present."""
803 803 if not (2 <= len(args) <= 3):
804 804 # i18n: "indent" is a keyword
805 805 raise error.ParseError(_("indent() expects two or three arguments"))
806 806
807 807 text = evalstring(context, mapping, args[0])
808 808 indent = evalstring(context, mapping, args[1])
809 809
810 810 if len(args) == 3:
811 811 firstline = evalstring(context, mapping, args[2])
812 812 else:
813 813 firstline = indent
814 814
815 815 # the indent function doesn't indent the first line, so we do it here
816 816 return templatefilters.indent(firstline + text, indent)
817 817
818 818 @templatefunc('get(dict, key)')
819 819 def get(context, mapping, args):
820 820 """Get an attribute/key from an object. Some keywords
821 821 are complex types. This function allows you to obtain the value of an
822 822 attribute on these types."""
823 823 if len(args) != 2:
824 824 # i18n: "get" is a keyword
825 825 raise error.ParseError(_("get() expects two arguments"))
826 826
827 827 dictarg = evalfuncarg(context, mapping, args[0])
828 828 if not util.safehasattr(dictarg, 'get'):
829 829 # i18n: "get" is a keyword
830 830 raise error.ParseError(_("get() expects a dict as first argument"))
831 831
832 832 key = evalfuncarg(context, mapping, args[1])
833 833 return _getdictitem(dictarg, key)
834 834
835 835 def _getdictitem(dictarg, key):
836 836 val = dictarg.get(key)
837 837 if val is None:
838 838 return
839 839 return templatekw.wraphybridvalue(dictarg, key, val)
840 840
841 841 @templatefunc('if(expr, then[, else])')
842 842 def if_(context, mapping, args):
843 843 """Conditionally execute based on the result of
844 844 an expression."""
845 845 if not (2 <= len(args) <= 3):
846 846 # i18n: "if" is a keyword
847 847 raise error.ParseError(_("if expects two or three arguments"))
848 848
849 849 test = evalboolean(context, mapping, args[0])
850 850 if test:
851 851 yield evalrawexp(context, mapping, args[1])
852 852 elif len(args) == 3:
853 853 yield evalrawexp(context, mapping, args[2])
854 854
855 855 @templatefunc('ifcontains(needle, haystack, then[, else])')
856 856 def ifcontains(context, mapping, args):
857 857 """Conditionally execute based
858 858 on whether the item "needle" is in "haystack"."""
859 859 if not (3 <= len(args) <= 4):
860 860 # i18n: "ifcontains" is a keyword
861 861 raise error.ParseError(_("ifcontains expects three or four arguments"))
862 862
863 863 haystack = evalfuncarg(context, mapping, args[1])
864 864 try:
865 865 needle = evalastype(context, mapping, args[0],
866 866 getattr(haystack, 'keytype', None) or bytes)
867 867 found = (needle in haystack)
868 868 except error.ParseError:
869 869 found = False
870 870
871 871 if found:
872 872 yield evalrawexp(context, mapping, args[2])
873 873 elif len(args) == 4:
874 874 yield evalrawexp(context, mapping, args[3])
875 875
876 876 @templatefunc('ifeq(expr1, expr2, then[, else])')
877 877 def ifeq(context, mapping, args):
878 878 """Conditionally execute based on
879 879 whether 2 items are equivalent."""
880 880 if not (3 <= len(args) <= 4):
881 881 # i18n: "ifeq" is a keyword
882 882 raise error.ParseError(_("ifeq expects three or four arguments"))
883 883
884 884 test = evalstring(context, mapping, args[0])
885 885 match = evalstring(context, mapping, args[1])
886 886 if test == match:
887 887 yield evalrawexp(context, mapping, args[2])
888 888 elif len(args) == 4:
889 889 yield evalrawexp(context, mapping, args[3])
890 890
891 891 @templatefunc('join(list, sep)')
892 892 def join(context, mapping, args):
893 893 """Join items in a list with a delimiter."""
894 894 if not (1 <= len(args) <= 2):
895 895 # i18n: "join" is a keyword
896 896 raise error.ParseError(_("join expects one or two arguments"))
897 897
898 898 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
899 899 # abuses generator as a keyword that returns a list of dicts.
900 900 joinset = evalrawexp(context, mapping, args[0])
901 901 joinset = templatekw.unwrapvalue(joinset)
902 902 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
903 903 joiner = " "
904 904 if len(args) > 1:
905 905 joiner = evalstring(context, mapping, args[1])
906 906
907 907 first = True
908 908 for x in pycompat.maybebytestr(joinset):
909 909 if first:
910 910 first = False
911 911 else:
912 912 yield joiner
913 913 yield joinfmt(x)
914 914
915 915 @templatefunc('label(label, expr)')
916 916 def label(context, mapping, args):
917 917 """Apply a label to generated content. Content with
918 918 a label applied can result in additional post-processing, such as
919 919 automatic colorization."""
920 920 if len(args) != 2:
921 921 # i18n: "label" is a keyword
922 922 raise error.ParseError(_("label expects two arguments"))
923 923
924 924 ui = context.resource(mapping, 'ui')
925 925 thing = evalstring(context, mapping, args[1])
926 926 # preserve unknown symbol as literal so effects like 'red', 'bold',
927 927 # etc. don't need to be quoted
928 928 label = evalstringliteral(context, mapping, args[0])
929 929
930 930 return ui.label(thing, label)
931 931
932 932 @templatefunc('latesttag([pattern])')
933 933 def latesttag(context, mapping, args):
934 934 """The global tags matching the given pattern on the
935 935 most recent globally tagged ancestor of this changeset.
936 936 If no such tags exist, the "{tag}" template resolves to
937 937 the string "null"."""
938 938 if len(args) > 1:
939 939 # i18n: "latesttag" is a keyword
940 940 raise error.ParseError(_("latesttag expects at most one argument"))
941 941
942 942 pattern = None
943 943 if len(args) == 1:
944 944 pattern = evalstring(context, mapping, args[0])
945
946 # TODO: pass (context, mapping) pair to keyword function
947 props = context._resources.copy()
948 props.update(mapping)
949 return templatekw.showlatesttags(pattern, **pycompat.strkwargs(props))
945 return templatekw.showlatesttags(context, mapping, pattern)
950 946
951 947 @templatefunc('localdate(date[, tz])')
952 948 def localdate(context, mapping, args):
953 949 """Converts a date to the specified timezone.
954 950 The default is local date."""
955 951 if not (1 <= len(args) <= 2):
956 952 # i18n: "localdate" is a keyword
957 953 raise error.ParseError(_("localdate expects one or two arguments"))
958 954
959 955 date = evalfuncarg(context, mapping, args[0])
960 956 try:
961 957 date = util.parsedate(date)
962 958 except AttributeError: # not str nor date tuple
963 959 # i18n: "localdate" is a keyword
964 960 raise error.ParseError(_("localdate expects a date information"))
965 961 if len(args) >= 2:
966 962 tzoffset = None
967 963 tz = evalfuncarg(context, mapping, args[1])
968 964 if isinstance(tz, bytes):
969 965 tzoffset, remainder = util.parsetimezone(tz)
970 966 if remainder:
971 967 tzoffset = None
972 968 if tzoffset is None:
973 969 try:
974 970 tzoffset = int(tz)
975 971 except (TypeError, ValueError):
976 972 # i18n: "localdate" is a keyword
977 973 raise error.ParseError(_("localdate expects a timezone"))
978 974 else:
979 975 tzoffset = util.makedate()[1]
980 976 return (date[0], tzoffset)
981 977
982 978 @templatefunc('max(iterable)')
983 979 def max_(context, mapping, args, **kwargs):
984 980 """Return the max of an iterable"""
985 981 if len(args) != 1:
986 982 # i18n: "max" is a keyword
987 983 raise error.ParseError(_("max expects one argument"))
988 984
989 985 iterable = evalfuncarg(context, mapping, args[0])
990 986 try:
991 987 x = max(pycompat.maybebytestr(iterable))
992 988 except (TypeError, ValueError):
993 989 # i18n: "max" is a keyword
994 990 raise error.ParseError(_("max first argument should be an iterable"))
995 991 return templatekw.wraphybridvalue(iterable, x, x)
996 992
997 993 @templatefunc('min(iterable)')
998 994 def min_(context, mapping, args, **kwargs):
999 995 """Return the min of an iterable"""
1000 996 if len(args) != 1:
1001 997 # i18n: "min" is a keyword
1002 998 raise error.ParseError(_("min expects one argument"))
1003 999
1004 1000 iterable = evalfuncarg(context, mapping, args[0])
1005 1001 try:
1006 1002 x = min(pycompat.maybebytestr(iterable))
1007 1003 except (TypeError, ValueError):
1008 1004 # i18n: "min" is a keyword
1009 1005 raise error.ParseError(_("min first argument should be an iterable"))
1010 1006 return templatekw.wraphybridvalue(iterable, x, x)
1011 1007
1012 1008 @templatefunc('mod(a, b)')
1013 1009 def mod(context, mapping, args):
1014 1010 """Calculate a mod b such that a / b + a mod b == a"""
1015 1011 if not len(args) == 2:
1016 1012 # i18n: "mod" is a keyword
1017 1013 raise error.ParseError(_("mod expects two arguments"))
1018 1014
1019 1015 func = lambda a, b: a % b
1020 1016 return runarithmetic(context, mapping, (func, args[0], args[1]))
1021 1017
1022 1018 @templatefunc('obsfateoperations(markers)')
1023 1019 def obsfateoperations(context, mapping, args):
1024 1020 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1025 1021 if len(args) != 1:
1026 1022 # i18n: "obsfateoperations" is a keyword
1027 1023 raise error.ParseError(_("obsfateoperations expects one argument"))
1028 1024
1029 1025 markers = evalfuncarg(context, mapping, args[0])
1030 1026
1031 1027 try:
1032 1028 data = obsutil.markersoperations(markers)
1033 1029 return templatekw.hybridlist(data, name='operation')
1034 1030 except (TypeError, KeyError):
1035 1031 # i18n: "obsfateoperations" is a keyword
1036 1032 errmsg = _("obsfateoperations first argument should be an iterable")
1037 1033 raise error.ParseError(errmsg)
1038 1034
1039 1035 @templatefunc('obsfatedate(markers)')
1040 1036 def obsfatedate(context, mapping, args):
1041 1037 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1042 1038 if len(args) != 1:
1043 1039 # i18n: "obsfatedate" is a keyword
1044 1040 raise error.ParseError(_("obsfatedate expects one argument"))
1045 1041
1046 1042 markers = evalfuncarg(context, mapping, args[0])
1047 1043
1048 1044 try:
1049 1045 data = obsutil.markersdates(markers)
1050 1046 return templatekw.hybridlist(data, name='date', fmt='%d %d')
1051 1047 except (TypeError, KeyError):
1052 1048 # i18n: "obsfatedate" is a keyword
1053 1049 errmsg = _("obsfatedate first argument should be an iterable")
1054 1050 raise error.ParseError(errmsg)
1055 1051
1056 1052 @templatefunc('obsfateusers(markers)')
1057 1053 def obsfateusers(context, mapping, args):
1058 1054 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1059 1055 if len(args) != 1:
1060 1056 # i18n: "obsfateusers" is a keyword
1061 1057 raise error.ParseError(_("obsfateusers expects one argument"))
1062 1058
1063 1059 markers = evalfuncarg(context, mapping, args[0])
1064 1060
1065 1061 try:
1066 1062 data = obsutil.markersusers(markers)
1067 1063 return templatekw.hybridlist(data, name='user')
1068 1064 except (TypeError, KeyError, ValueError):
1069 1065 # i18n: "obsfateusers" is a keyword
1070 1066 msg = _("obsfateusers first argument should be an iterable of "
1071 1067 "obsmakers")
1072 1068 raise error.ParseError(msg)
1073 1069
1074 1070 @templatefunc('obsfateverb(successors, markers)')
1075 1071 def obsfateverb(context, mapping, args):
1076 1072 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1077 1073 if len(args) != 2:
1078 1074 # i18n: "obsfateverb" is a keyword
1079 1075 raise error.ParseError(_("obsfateverb expects two arguments"))
1080 1076
1081 1077 successors = evalfuncarg(context, mapping, args[0])
1082 1078 markers = evalfuncarg(context, mapping, args[1])
1083 1079
1084 1080 try:
1085 1081 return obsutil.obsfateverb(successors, markers)
1086 1082 except TypeError:
1087 1083 # i18n: "obsfateverb" is a keyword
1088 1084 errmsg = _("obsfateverb first argument should be countable")
1089 1085 raise error.ParseError(errmsg)
1090 1086
1091 1087 @templatefunc('relpath(path)')
1092 1088 def relpath(context, mapping, args):
1093 1089 """Convert a repository-absolute path into a filesystem path relative to
1094 1090 the current working directory."""
1095 1091 if len(args) != 1:
1096 1092 # i18n: "relpath" is a keyword
1097 1093 raise error.ParseError(_("relpath expects one argument"))
1098 1094
1099 1095 repo = context.resource(mapping, 'ctx').repo()
1100 1096 path = evalstring(context, mapping, args[0])
1101 1097 return repo.pathto(path)
1102 1098
1103 1099 @templatefunc('revset(query[, formatargs...])')
1104 1100 def revset(context, mapping, args):
1105 1101 """Execute a revision set query. See
1106 1102 :hg:`help revset`."""
1107 1103 if not len(args) > 0:
1108 1104 # i18n: "revset" is a keyword
1109 1105 raise error.ParseError(_("revset expects one or more arguments"))
1110 1106
1111 1107 raw = evalstring(context, mapping, args[0])
1112 1108 ctx = context.resource(mapping, 'ctx')
1113 1109 repo = ctx.repo()
1114 1110
1115 1111 def query(expr):
1116 1112 m = revsetmod.match(repo.ui, expr, repo=repo)
1117 1113 return m(repo)
1118 1114
1119 1115 if len(args) > 1:
1120 1116 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1121 1117 revs = query(revsetlang.formatspec(raw, *formatargs))
1122 1118 revs = list(revs)
1123 1119 else:
1124 1120 cache = context.resource(mapping, 'cache')
1125 1121 revsetcache = cache.setdefault("revsetcache", {})
1126 1122 if raw in revsetcache:
1127 1123 revs = revsetcache[raw]
1128 1124 else:
1129 1125 revs = query(raw)
1130 1126 revs = list(revs)
1131 1127 revsetcache[raw] = revs
1132 1128 return templatekw.showrevslist(context, mapping, "revision", revs)
1133 1129
1134 1130 @templatefunc('rstdoc(text, style)')
1135 1131 def rstdoc(context, mapping, args):
1136 1132 """Format reStructuredText."""
1137 1133 if len(args) != 2:
1138 1134 # i18n: "rstdoc" is a keyword
1139 1135 raise error.ParseError(_("rstdoc expects two arguments"))
1140 1136
1141 1137 text = evalstring(context, mapping, args[0])
1142 1138 style = evalstring(context, mapping, args[1])
1143 1139
1144 1140 return minirst.format(text, style=style, keep=['verbose'])
1145 1141
1146 1142 @templatefunc('separate(sep, args)', argspec='sep *args')
1147 1143 def separate(context, mapping, args):
1148 1144 """Add a separator between non-empty arguments."""
1149 1145 if 'sep' not in args:
1150 1146 # i18n: "separate" is a keyword
1151 1147 raise error.ParseError(_("separate expects at least one argument"))
1152 1148
1153 1149 sep = evalstring(context, mapping, args['sep'])
1154 1150 first = True
1155 1151 for arg in args['args']:
1156 1152 argstr = evalstring(context, mapping, arg)
1157 1153 if not argstr:
1158 1154 continue
1159 1155 if first:
1160 1156 first = False
1161 1157 else:
1162 1158 yield sep
1163 1159 yield argstr
1164 1160
1165 1161 @templatefunc('shortest(node, minlength=4)')
1166 1162 def shortest(context, mapping, args):
1167 1163 """Obtain the shortest representation of
1168 1164 a node."""
1169 1165 if not (1 <= len(args) <= 2):
1170 1166 # i18n: "shortest" is a keyword
1171 1167 raise error.ParseError(_("shortest() expects one or two arguments"))
1172 1168
1173 1169 node = evalstring(context, mapping, args[0])
1174 1170
1175 1171 minlength = 4
1176 1172 if len(args) > 1:
1177 1173 minlength = evalinteger(context, mapping, args[1],
1178 1174 # i18n: "shortest" is a keyword
1179 1175 _("shortest() expects an integer minlength"))
1180 1176
1181 1177 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1182 1178 # which would be unacceptably slow. so we look for hash collision in
1183 1179 # unfiltered space, which means some hashes may be slightly longer.
1184 1180 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1185 1181 return cl.shortest(node, minlength)
1186 1182
1187 1183 @templatefunc('strip(text[, chars])')
1188 1184 def strip(context, mapping, args):
1189 1185 """Strip characters from a string. By default,
1190 1186 strips all leading and trailing whitespace."""
1191 1187 if not (1 <= len(args) <= 2):
1192 1188 # i18n: "strip" is a keyword
1193 1189 raise error.ParseError(_("strip expects one or two arguments"))
1194 1190
1195 1191 text = evalstring(context, mapping, args[0])
1196 1192 if len(args) == 2:
1197 1193 chars = evalstring(context, mapping, args[1])
1198 1194 return text.strip(chars)
1199 1195 return text.strip()
1200 1196
1201 1197 @templatefunc('sub(pattern, replacement, expression)')
1202 1198 def sub(context, mapping, args):
1203 1199 """Perform text substitution
1204 1200 using regular expressions."""
1205 1201 if len(args) != 3:
1206 1202 # i18n: "sub" is a keyword
1207 1203 raise error.ParseError(_("sub expects three arguments"))
1208 1204
1209 1205 pat = evalstring(context, mapping, args[0])
1210 1206 rpl = evalstring(context, mapping, args[1])
1211 1207 src = evalstring(context, mapping, args[2])
1212 1208 try:
1213 1209 patre = re.compile(pat)
1214 1210 except re.error:
1215 1211 # i18n: "sub" is a keyword
1216 1212 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1217 1213 try:
1218 1214 yield patre.sub(rpl, src)
1219 1215 except re.error:
1220 1216 # i18n: "sub" is a keyword
1221 1217 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1222 1218
1223 1219 @templatefunc('startswith(pattern, text)')
1224 1220 def startswith(context, mapping, args):
1225 1221 """Returns the value from the "text" argument
1226 1222 if it begins with the content from the "pattern" argument."""
1227 1223 if len(args) != 2:
1228 1224 # i18n: "startswith" is a keyword
1229 1225 raise error.ParseError(_("startswith expects two arguments"))
1230 1226
1231 1227 patn = evalstring(context, mapping, args[0])
1232 1228 text = evalstring(context, mapping, args[1])
1233 1229 if text.startswith(patn):
1234 1230 return text
1235 1231 return ''
1236 1232
1237 1233 @templatefunc('word(number, text[, separator])')
1238 1234 def word(context, mapping, args):
1239 1235 """Return the nth word from a string."""
1240 1236 if not (2 <= len(args) <= 3):
1241 1237 # i18n: "word" is a keyword
1242 1238 raise error.ParseError(_("word expects two or three arguments, got %d")
1243 1239 % len(args))
1244 1240
1245 1241 num = evalinteger(context, mapping, args[0],
1246 1242 # i18n: "word" is a keyword
1247 1243 _("word expects an integer index"))
1248 1244 text = evalstring(context, mapping, args[1])
1249 1245 if len(args) == 3:
1250 1246 splitter = evalstring(context, mapping, args[2])
1251 1247 else:
1252 1248 splitter = None
1253 1249
1254 1250 tokens = text.split(splitter)
1255 1251 if num >= len(tokens) or num < -len(tokens):
1256 1252 return ''
1257 1253 else:
1258 1254 return tokens[num]
1259 1255
1260 1256 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1261 1257 exprmethods = {
1262 1258 "integer": lambda e, c: (runinteger, e[1]),
1263 1259 "string": lambda e, c: (runstring, e[1]),
1264 1260 "symbol": lambda e, c: (runsymbol, e[1]),
1265 1261 "template": buildtemplate,
1266 1262 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1267 1263 ".": buildmember,
1268 1264 "|": buildfilter,
1269 1265 "%": buildmap,
1270 1266 "func": buildfunc,
1271 1267 "keyvalue": buildkeyvaluepair,
1272 1268 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1273 1269 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1274 1270 "negate": buildnegate,
1275 1271 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1276 1272 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1277 1273 }
1278 1274
1279 1275 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1280 1276 methods = exprmethods.copy()
1281 1277 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1282 1278
1283 1279 class _aliasrules(parser.basealiasrules):
1284 1280 """Parsing and expansion rule set of template aliases"""
1285 1281 _section = _('template alias')
1286 1282 _parse = staticmethod(_parseexpr)
1287 1283
1288 1284 @staticmethod
1289 1285 def _trygetfunc(tree):
1290 1286 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1291 1287 None"""
1292 1288 if tree[0] == 'func' and tree[1][0] == 'symbol':
1293 1289 return tree[1][1], getlist(tree[2])
1294 1290 if tree[0] == '|' and tree[2][0] == 'symbol':
1295 1291 return tree[2][1], [tree[1]]
1296 1292
1297 1293 def expandaliases(tree, aliases):
1298 1294 """Return new tree of aliases are expanded"""
1299 1295 aliasmap = _aliasrules.buildmap(aliases)
1300 1296 return _aliasrules.expand(aliasmap, tree)
1301 1297
1302 1298 # template engine
1303 1299
1304 1300 stringify = templatefilters.stringify
1305 1301
1306 1302 def _flatten(thing):
1307 1303 '''yield a single stream from a possibly nested set of iterators'''
1308 1304 thing = templatekw.unwraphybrid(thing)
1309 1305 if isinstance(thing, bytes):
1310 1306 yield thing
1311 1307 elif isinstance(thing, str):
1312 1308 # We can only hit this on Python 3, and it's here to guard
1313 1309 # against infinite recursion.
1314 1310 raise error.ProgrammingError('Mercurial IO including templates is done'
1315 1311 ' with bytes, not strings')
1316 1312 elif thing is None:
1317 1313 pass
1318 1314 elif not util.safehasattr(thing, '__iter__'):
1319 1315 yield pycompat.bytestr(thing)
1320 1316 else:
1321 1317 for i in thing:
1322 1318 i = templatekw.unwraphybrid(i)
1323 1319 if isinstance(i, bytes):
1324 1320 yield i
1325 1321 elif i is None:
1326 1322 pass
1327 1323 elif not util.safehasattr(i, '__iter__'):
1328 1324 yield pycompat.bytestr(i)
1329 1325 else:
1330 1326 for j in _flatten(i):
1331 1327 yield j
1332 1328
1333 1329 def unquotestring(s):
1334 1330 '''unwrap quotes if any; otherwise returns unmodified string'''
1335 1331 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1336 1332 return s
1337 1333 return s[1:-1]
1338 1334
1339 1335 class engine(object):
1340 1336 '''template expansion engine.
1341 1337
1342 1338 template expansion works like this. a map file contains key=value
1343 1339 pairs. if value is quoted, it is treated as string. otherwise, it
1344 1340 is treated as name of template file.
1345 1341
1346 1342 templater is asked to expand a key in map. it looks up key, and
1347 1343 looks for strings like this: {foo}. it expands {foo} by looking up
1348 1344 foo in map, and substituting it. expansion is recursive: it stops
1349 1345 when there is no more {foo} to replace.
1350 1346
1351 1347 expansion also allows formatting and filtering.
1352 1348
1353 1349 format uses key to expand each item in list. syntax is
1354 1350 {key%format}.
1355 1351
1356 1352 filter uses function to transform value. syntax is
1357 1353 {key|filter1|filter2|...}.'''
1358 1354
1359 1355 def __init__(self, loader, filters=None, defaults=None, resources=None,
1360 1356 aliases=()):
1361 1357 self._loader = loader
1362 1358 if filters is None:
1363 1359 filters = {}
1364 1360 self._filters = filters
1365 1361 if defaults is None:
1366 1362 defaults = {}
1367 1363 if resources is None:
1368 1364 resources = {}
1369 1365 self._defaults = defaults
1370 1366 self._resources = resources
1371 1367 self._aliasmap = _aliasrules.buildmap(aliases)
1372 1368 self._cache = {} # key: (func, data)
1373 1369
1374 1370 def symbol(self, mapping, key):
1375 1371 """Resolve symbol to value or function; None if nothing found"""
1376 1372 v = None
1377 1373 if key not in self._resources:
1378 1374 v = mapping.get(key)
1379 1375 if v is None:
1380 1376 v = self._defaults.get(key)
1381 1377 return v
1382 1378
1383 1379 def resource(self, mapping, key):
1384 1380 """Return internal data (e.g. cache) used for keyword/function
1385 1381 evaluation"""
1386 1382 v = None
1387 1383 if key in self._resources:
1388 1384 v = mapping.get(key)
1389 1385 if v is None:
1390 1386 v = self._resources.get(key)
1391 1387 if v is None:
1392 1388 raise ResourceUnavailable(_('template resource not available: %s')
1393 1389 % key)
1394 1390 return v
1395 1391
1396 1392 def _load(self, t):
1397 1393 '''load, parse, and cache a template'''
1398 1394 if t not in self._cache:
1399 1395 # put poison to cut recursion while compiling 't'
1400 1396 self._cache[t] = (_runrecursivesymbol, t)
1401 1397 try:
1402 1398 x = parse(self._loader(t))
1403 1399 if self._aliasmap:
1404 1400 x = _aliasrules.expand(self._aliasmap, x)
1405 1401 self._cache[t] = compileexp(x, self, methods)
1406 1402 except: # re-raises
1407 1403 del self._cache[t]
1408 1404 raise
1409 1405 return self._cache[t]
1410 1406
1411 1407 def process(self, t, mapping):
1412 1408 '''Perform expansion. t is name of map element to expand.
1413 1409 mapping contains added elements for use during expansion. Is a
1414 1410 generator.'''
1415 1411 func, data = self._load(t)
1416 1412 return _flatten(func(self, mapping, data))
1417 1413
1418 1414 engines = {'default': engine}
1419 1415
1420 1416 def stylelist():
1421 1417 paths = templatepaths()
1422 1418 if not paths:
1423 1419 return _('no templates found, try `hg debuginstall` for more info')
1424 1420 dirlist = os.listdir(paths[0])
1425 1421 stylelist = []
1426 1422 for file in dirlist:
1427 1423 split = file.split(".")
1428 1424 if split[-1] in ('orig', 'rej'):
1429 1425 continue
1430 1426 if split[0] == "map-cmdline":
1431 1427 stylelist.append(split[1])
1432 1428 return ", ".join(sorted(stylelist))
1433 1429
1434 1430 def _readmapfile(mapfile):
1435 1431 """Load template elements from the given map file"""
1436 1432 if not os.path.exists(mapfile):
1437 1433 raise error.Abort(_("style '%s' not found") % mapfile,
1438 1434 hint=_("available styles: %s") % stylelist())
1439 1435
1440 1436 base = os.path.dirname(mapfile)
1441 1437 conf = config.config(includepaths=templatepaths())
1442 1438 conf.read(mapfile, remap={'': 'templates'})
1443 1439
1444 1440 cache = {}
1445 1441 tmap = {}
1446 1442 aliases = []
1447 1443
1448 1444 val = conf.get('templates', '__base__')
1449 1445 if val and val[0] not in "'\"":
1450 1446 # treat as a pointer to a base class for this style
1451 1447 path = util.normpath(os.path.join(base, val))
1452 1448
1453 1449 # fallback check in template paths
1454 1450 if not os.path.exists(path):
1455 1451 for p in templatepaths():
1456 1452 p2 = util.normpath(os.path.join(p, val))
1457 1453 if os.path.isfile(p2):
1458 1454 path = p2
1459 1455 break
1460 1456 p3 = util.normpath(os.path.join(p2, "map"))
1461 1457 if os.path.isfile(p3):
1462 1458 path = p3
1463 1459 break
1464 1460
1465 1461 cache, tmap, aliases = _readmapfile(path)
1466 1462
1467 1463 for key, val in conf['templates'].items():
1468 1464 if not val:
1469 1465 raise error.ParseError(_('missing value'),
1470 1466 conf.source('templates', key))
1471 1467 if val[0] in "'\"":
1472 1468 if val[0] != val[-1]:
1473 1469 raise error.ParseError(_('unmatched quotes'),
1474 1470 conf.source('templates', key))
1475 1471 cache[key] = unquotestring(val)
1476 1472 elif key != '__base__':
1477 1473 val = 'default', val
1478 1474 if ':' in val[1]:
1479 1475 val = val[1].split(':', 1)
1480 1476 tmap[key] = val[0], os.path.join(base, val[1])
1481 1477 aliases.extend(conf['templatealias'].items())
1482 1478 return cache, tmap, aliases
1483 1479
1484 1480 class templater(object):
1485 1481
1486 1482 def __init__(self, filters=None, defaults=None, resources=None,
1487 1483 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1488 1484 """Create template engine optionally with preloaded template fragments
1489 1485
1490 1486 - ``filters``: a dict of functions to transform a value into another.
1491 1487 - ``defaults``: a dict of symbol values/functions; may be overridden
1492 1488 by a ``mapping`` dict.
1493 1489 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1494 1490 from user template; may be overridden by a ``mapping`` dict.
1495 1491 - ``cache``: a dict of preloaded template fragments.
1496 1492 - ``aliases``: a list of alias (name, replacement) pairs.
1497 1493
1498 1494 self.cache may be updated later to register additional template
1499 1495 fragments.
1500 1496 """
1501 1497 if filters is None:
1502 1498 filters = {}
1503 1499 if defaults is None:
1504 1500 defaults = {}
1505 1501 if resources is None:
1506 1502 resources = {}
1507 1503 if cache is None:
1508 1504 cache = {}
1509 1505 self.cache = cache.copy()
1510 1506 self.map = {}
1511 1507 self.filters = templatefilters.filters.copy()
1512 1508 self.filters.update(filters)
1513 1509 self.defaults = defaults
1514 1510 self._resources = {'templ': self}
1515 1511 self._resources.update(resources)
1516 1512 self._aliases = aliases
1517 1513 self.minchunk, self.maxchunk = minchunk, maxchunk
1518 1514 self.ecache = {}
1519 1515
1520 1516 @classmethod
1521 1517 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1522 1518 cache=None, minchunk=1024, maxchunk=65536):
1523 1519 """Create templater from the specified map file"""
1524 1520 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1525 1521 cache, tmap, aliases = _readmapfile(mapfile)
1526 1522 t.cache.update(cache)
1527 1523 t.map = tmap
1528 1524 t._aliases = aliases
1529 1525 return t
1530 1526
1531 1527 def __contains__(self, key):
1532 1528 return key in self.cache or key in self.map
1533 1529
1534 1530 def load(self, t):
1535 1531 '''Get the template for the given template name. Use a local cache.'''
1536 1532 if t not in self.cache:
1537 1533 try:
1538 1534 self.cache[t] = util.readfile(self.map[t][1])
1539 1535 except KeyError as inst:
1540 1536 raise TemplateNotFound(_('"%s" not in template map') %
1541 1537 inst.args[0])
1542 1538 except IOError as inst:
1543 1539 reason = (_('template file %s: %s')
1544 1540 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1545 1541 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1546 1542 return self.cache[t]
1547 1543
1548 1544 def render(self, mapping):
1549 1545 """Render the default unnamed template and return result as string"""
1550 1546 mapping = pycompat.strkwargs(mapping)
1551 1547 return stringify(self('', **mapping))
1552 1548
1553 1549 def __call__(self, t, **mapping):
1554 1550 mapping = pycompat.byteskwargs(mapping)
1555 1551 ttype = t in self.map and self.map[t][0] or 'default'
1556 1552 if ttype not in self.ecache:
1557 1553 try:
1558 1554 ecls = engines[ttype]
1559 1555 except KeyError:
1560 1556 raise error.Abort(_('invalid template engine: %s') % ttype)
1561 1557 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1562 1558 self._resources, self._aliases)
1563 1559 proc = self.ecache[ttype]
1564 1560
1565 1561 stream = proc.process(t, mapping)
1566 1562 if self.minchunk:
1567 1563 stream = util.increasingchunks(stream, min=self.minchunk,
1568 1564 max=self.maxchunk)
1569 1565 return stream
1570 1566
1571 1567 def templatepaths():
1572 1568 '''return locations used for template files.'''
1573 1569 pathsrel = ['templates']
1574 1570 paths = [os.path.normpath(os.path.join(util.datapath, f))
1575 1571 for f in pathsrel]
1576 1572 return [p for p in paths if os.path.isdir(p)]
1577 1573
1578 1574 def templatepath(name):
1579 1575 '''return location of template file. returns None if not found.'''
1580 1576 for p in templatepaths():
1581 1577 f = os.path.join(p, name)
1582 1578 if os.path.exists(f):
1583 1579 return f
1584 1580 return None
1585 1581
1586 1582 def stylemap(styles, paths=None):
1587 1583 """Return path to mapfile for a given style.
1588 1584
1589 1585 Searches mapfile in the following locations:
1590 1586 1. templatepath/style/map
1591 1587 2. templatepath/map-style
1592 1588 3. templatepath/map
1593 1589 """
1594 1590
1595 1591 if paths is None:
1596 1592 paths = templatepaths()
1597 1593 elif isinstance(paths, bytes):
1598 1594 paths = [paths]
1599 1595
1600 1596 if isinstance(styles, bytes):
1601 1597 styles = [styles]
1602 1598
1603 1599 for style in styles:
1604 1600 # only plain name is allowed to honor template paths
1605 1601 if (not style
1606 1602 or style in (os.curdir, os.pardir)
1607 1603 or pycompat.ossep in style
1608 1604 or pycompat.osaltsep and pycompat.osaltsep in style):
1609 1605 continue
1610 1606 locations = [os.path.join(style, 'map'), 'map-' + style]
1611 1607 locations.append('map')
1612 1608
1613 1609 for path in paths:
1614 1610 for location in locations:
1615 1611 mapfile = os.path.join(path, location)
1616 1612 if os.path.isfile(mapfile):
1617 1613 return style, mapfile
1618 1614
1619 1615 raise RuntimeError("No hgweb templates found in %r" % paths)
1620 1616
1621 1617 def loadfunction(ui, extname, registrarobj):
1622 1618 """Load template function from specified registrarobj
1623 1619 """
1624 1620 for name, func in registrarobj._table.iteritems():
1625 1621 funcs[name] = func
1626 1622
1627 1623 # tell hggettext to extract docstrings from these functions:
1628 1624 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now