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