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