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