##// END OF EJS Templates
templatekw: change joinfmt to a mandatory argument of _hybrid object...
Yuya Nishihara -
r29669:bdc81970 default
parent child Browse files
Show More
@@ -1,596 +1,593
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 .node import hex, nullid
11 11 from . import (
12 12 encoding,
13 13 error,
14 14 hbisect,
15 15 patch,
16 16 registrar,
17 17 scmutil,
18 18 util,
19 19 )
20 20
21 21 # This helper class allows us to handle both:
22 22 # "{files}" (legacy command-line-specific list hack) and
23 23 # "{files % '{file}\n'}" (hgweb-style with inlining and function support)
24 24 # and to access raw values:
25 25 # "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
26 26 # "{get(extras, key)}"
27 27
28 28 class _hybrid(object):
29 def __init__(self, gen, values, makemap, joinfmt=None):
29 def __init__(self, gen, values, makemap, joinfmt):
30 30 self.gen = gen
31 31 self.values = values
32 32 self._makemap = makemap
33 if joinfmt:
34 33 self.joinfmt = joinfmt
35 else:
36 self.joinfmt = lambda x: x.values()[0]
37 34 def __iter__(self):
38 35 return self.gen
39 36 def itermaps(self):
40 37 makemap = self._makemap
41 38 for x in self.values:
42 39 yield makemap(x)
43 40 def __contains__(self, x):
44 41 return x in self.values
45 42 def __len__(self):
46 43 return len(self.values)
47 44 def __getattr__(self, name):
48 45 if name != 'get':
49 46 raise AttributeError(name)
50 47 return getattr(self.values, name)
51 48
52 49 def showlist(name, values, plural=None, element=None, separator=' ', **args):
53 50 if not element:
54 51 element = name
55 52 f = _showlist(name, values, plural, separator, **args)
56 return _hybrid(f, values, lambda x: {element: x})
53 return _hybrid(f, values, lambda x: {element: x}, lambda d: d[element])
57 54
58 55 def _showlist(name, values, plural=None, separator=' ', **args):
59 56 '''expand set of values.
60 57 name is name of key in template map.
61 58 values is list of strings or dicts.
62 59 plural is plural of name, if not simply name + 's'.
63 60 separator is used to join values as a string
64 61
65 62 expansion works like this, given name 'foo'.
66 63
67 64 if values is empty, expand 'no_foos'.
68 65
69 66 if 'foo' not in template map, return values as a string,
70 67 joined by 'separator'.
71 68
72 69 expand 'start_foos'.
73 70
74 71 for each value, expand 'foo'. if 'last_foo' in template
75 72 map, expand it instead of 'foo' for last key.
76 73
77 74 expand 'end_foos'.
78 75 '''
79 76 templ = args['templ']
80 77 if plural:
81 78 names = plural
82 79 else: names = name + 's'
83 80 if not values:
84 81 noname = 'no_' + names
85 82 if noname in templ:
86 83 yield templ(noname, **args)
87 84 return
88 85 if name not in templ:
89 86 if isinstance(values[0], str):
90 87 yield separator.join(values)
91 88 else:
92 89 for v in values:
93 90 yield dict(v, **args)
94 91 return
95 92 startname = 'start_' + names
96 93 if startname in templ:
97 94 yield templ(startname, **args)
98 95 vargs = args.copy()
99 96 def one(v, tag=name):
100 97 try:
101 98 vargs.update(v)
102 99 except (AttributeError, ValueError):
103 100 try:
104 101 for a, b in v:
105 102 vargs[a] = b
106 103 except ValueError:
107 104 vargs[name] = v
108 105 return templ(tag, **vargs)
109 106 lastname = 'last_' + name
110 107 if lastname in templ:
111 108 last = values.pop()
112 109 else:
113 110 last = None
114 111 for v in values:
115 112 yield one(v)
116 113 if last is not None:
117 114 yield one(last, tag=lastname)
118 115 endname = 'end_' + names
119 116 if endname in templ:
120 117 yield templ(endname, **args)
121 118
122 119 def _formatrevnode(ctx):
123 120 """Format changeset as '{rev}:{node|formatnode}', which is the default
124 121 template provided by cmdutil.changeset_templater"""
125 122 repo = ctx.repo()
126 123 if repo.ui.debugflag:
127 124 hexnode = ctx.hex()
128 125 else:
129 126 hexnode = ctx.hex()[:12]
130 127 return '%d:%s' % (scmutil.intrev(ctx.rev()), hexnode)
131 128
132 129 def getfiles(repo, ctx, revcache):
133 130 if 'files' not in revcache:
134 131 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
135 132 return revcache['files']
136 133
137 134 def getlatesttags(repo, ctx, cache, pattern=None):
138 135 '''return date, distance and name for the latest tag of rev'''
139 136
140 137 cachename = 'latesttags'
141 138 if pattern is not None:
142 139 cachename += '-' + pattern
143 140 match = util.stringmatcher(pattern)[2]
144 141 else:
145 142 match = util.always
146 143
147 144 if cachename not in cache:
148 145 # Cache mapping from rev to a tuple with tag date, tag
149 146 # distance and tag name
150 147 cache[cachename] = {-1: (0, 0, ['null'])}
151 148 latesttags = cache[cachename]
152 149
153 150 rev = ctx.rev()
154 151 todo = [rev]
155 152 while todo:
156 153 rev = todo.pop()
157 154 if rev in latesttags:
158 155 continue
159 156 ctx = repo[rev]
160 157 tags = [t for t in ctx.tags()
161 158 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
162 159 and match(t))]
163 160 if tags:
164 161 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
165 162 continue
166 163 try:
167 164 # The tuples are laid out so the right one can be found by
168 165 # comparison.
169 166 pdate, pdist, ptag = max(
170 167 latesttags[p.rev()] for p in ctx.parents())
171 168 except KeyError:
172 169 # Cache miss - recurse
173 170 todo.append(rev)
174 171 todo.extend(p.rev() for p in ctx.parents())
175 172 continue
176 173 latesttags[rev] = pdate, pdist + 1, ptag
177 174 return latesttags[rev]
178 175
179 176 def getrenamedfn(repo, endrev=None):
180 177 rcache = {}
181 178 if endrev is None:
182 179 endrev = len(repo)
183 180
184 181 def getrenamed(fn, rev):
185 182 '''looks up all renames for a file (up to endrev) the first
186 183 time the file is given. It indexes on the changerev and only
187 184 parses the manifest if linkrev != changerev.
188 185 Returns rename info for fn at changerev rev.'''
189 186 if fn not in rcache:
190 187 rcache[fn] = {}
191 188 fl = repo.file(fn)
192 189 for i in fl:
193 190 lr = fl.linkrev(i)
194 191 renamed = fl.renamed(fl.node(i))
195 192 rcache[fn][lr] = renamed
196 193 if lr >= endrev:
197 194 break
198 195 if rev in rcache[fn]:
199 196 return rcache[fn][rev]
200 197
201 198 # If linkrev != rev (i.e. rev not found in rcache) fallback to
202 199 # filectx logic.
203 200 try:
204 201 return repo[rev][fn].renamed()
205 202 except error.LookupError:
206 203 return None
207 204
208 205 return getrenamed
209 206
210 207 # keywords are callables like:
211 208 # fn(repo, ctx, templ, cache, revcache, **args)
212 209 # with:
213 210 # repo - current repository instance
214 211 # ctx - the changectx being displayed
215 212 # templ - the templater instance
216 213 # cache - a cache dictionary for the whole templater run
217 214 # revcache - a cache dictionary for the current revision
218 215 keywords = {}
219 216
220 217 templatekeyword = registrar.templatekeyword(keywords)
221 218
222 219 @templatekeyword('author')
223 220 def showauthor(repo, ctx, templ, **args):
224 221 """String. The unmodified author of the changeset."""
225 222 return ctx.user()
226 223
227 224 @templatekeyword('bisect')
228 225 def showbisect(repo, ctx, templ, **args):
229 226 """String. The changeset bisection status."""
230 227 return hbisect.label(repo, ctx.node())
231 228
232 229 @templatekeyword('branch')
233 230 def showbranch(**args):
234 231 """String. The name of the branch on which the changeset was
235 232 committed.
236 233 """
237 234 return args['ctx'].branch()
238 235
239 236 @templatekeyword('branches')
240 237 def showbranches(**args):
241 238 """List of strings. The name of the branch on which the
242 239 changeset was committed. Will be empty if the branch name was
243 240 default. (DEPRECATED)
244 241 """
245 242 branch = args['ctx'].branch()
246 243 if branch != 'default':
247 244 return showlist('branch', [branch], plural='branches', **args)
248 245 return showlist('branch', [], plural='branches', **args)
249 246
250 247 @templatekeyword('bookmarks')
251 248 def showbookmarks(**args):
252 249 """List of strings. Any bookmarks associated with the
253 250 changeset. Also sets 'active', the name of the active bookmark.
254 251 """
255 252 repo = args['ctx']._repo
256 253 bookmarks = args['ctx'].bookmarks()
257 254 active = repo._activebookmark
258 255 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
259 256 f = _showlist('bookmark', bookmarks, **args)
260 257 return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark'])
261 258
262 259 @templatekeyword('children')
263 260 def showchildren(**args):
264 261 """List of strings. The children of the changeset."""
265 262 ctx = args['ctx']
266 263 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
267 264 return showlist('children', childrevs, element='child', **args)
268 265
269 266 # Deprecated, but kept alive for help generation a purpose.
270 267 @templatekeyword('currentbookmark')
271 268 def showcurrentbookmark(**args):
272 269 """String. The active bookmark, if it is
273 270 associated with the changeset (DEPRECATED)"""
274 271 return showactivebookmark(**args)
275 272
276 273 @templatekeyword('activebookmark')
277 274 def showactivebookmark(**args):
278 275 """String. The active bookmark, if it is
279 276 associated with the changeset"""
280 277 active = args['repo']._activebookmark
281 278 if active and active in args['ctx'].bookmarks():
282 279 return active
283 280 return ''
284 281
285 282 @templatekeyword('date')
286 283 def showdate(repo, ctx, templ, **args):
287 284 """Date information. The date when the changeset was committed."""
288 285 return ctx.date()
289 286
290 287 @templatekeyword('desc')
291 288 def showdescription(repo, ctx, templ, **args):
292 289 """String. The text of the changeset description."""
293 290 s = ctx.description()
294 291 if isinstance(s, encoding.localstr):
295 292 # try hard to preserve utf-8 bytes
296 293 return encoding.tolocal(encoding.fromlocal(s).strip())
297 294 else:
298 295 return s.strip()
299 296
300 297 @templatekeyword('diffstat')
301 298 def showdiffstat(repo, ctx, templ, **args):
302 299 """String. Statistics of changes with the following format:
303 300 "modified files: +added/-removed lines"
304 301 """
305 302 stats = patch.diffstatdata(util.iterlines(ctx.diff()))
306 303 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
307 304 return '%s: +%s/-%s' % (len(stats), adds, removes)
308 305
309 306 @templatekeyword('extras')
310 307 def showextras(**args):
311 308 """List of dicts with key, value entries of the 'extras'
312 309 field of this changeset."""
313 310 extras = args['ctx'].extra()
314 311 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
315 312 makemap = lambda k: {'key': k, 'value': extras[k]}
316 313 c = [makemap(k) for k in extras]
317 314 f = _showlist('extra', c, plural='extras', **args)
318 315 return _hybrid(f, extras, makemap,
319 316 lambda x: '%s=%s' % (x['key'], x['value']))
320 317
321 318 @templatekeyword('file_adds')
322 319 def showfileadds(**args):
323 320 """List of strings. Files added by this changeset."""
324 321 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
325 322 return showlist('file_add', getfiles(repo, ctx, revcache)[1],
326 323 element='file', **args)
327 324
328 325 @templatekeyword('file_copies')
329 326 def showfilecopies(**args):
330 327 """List of strings. Files copied in this changeset with
331 328 their sources.
332 329 """
333 330 cache, ctx = args['cache'], args['ctx']
334 331 copies = args['revcache'].get('copies')
335 332 if copies is None:
336 333 if 'getrenamed' not in cache:
337 334 cache['getrenamed'] = getrenamedfn(args['repo'])
338 335 copies = []
339 336 getrenamed = cache['getrenamed']
340 337 for fn in ctx.files():
341 338 rename = getrenamed(fn, ctx.rev())
342 339 if rename:
343 340 copies.append((fn, rename[0]))
344 341
345 342 copies = util.sortdict(copies)
346 343 makemap = lambda k: {'name': k, 'source': copies[k]}
347 344 c = [makemap(k) for k in copies]
348 345 f = _showlist('file_copy', c, plural='file_copies', **args)
349 346 return _hybrid(f, copies, makemap,
350 347 lambda x: '%s (%s)' % (x['name'], x['source']))
351 348
352 349 # showfilecopiesswitch() displays file copies only if copy records are
353 350 # provided before calling the templater, usually with a --copies
354 351 # command line switch.
355 352 @templatekeyword('file_copies_switch')
356 353 def showfilecopiesswitch(**args):
357 354 """List of strings. Like "file_copies" but displayed
358 355 only if the --copied switch is set.
359 356 """
360 357 copies = args['revcache'].get('copies') or []
361 358 copies = util.sortdict(copies)
362 359 makemap = lambda k: {'name': k, 'source': copies[k]}
363 360 c = [makemap(k) for k in copies]
364 361 f = _showlist('file_copy', c, plural='file_copies', **args)
365 362 return _hybrid(f, copies, makemap,
366 363 lambda x: '%s (%s)' % (x['name'], x['source']))
367 364
368 365 @templatekeyword('file_dels')
369 366 def showfiledels(**args):
370 367 """List of strings. Files removed by this changeset."""
371 368 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
372 369 return showlist('file_del', getfiles(repo, ctx, revcache)[2],
373 370 element='file', **args)
374 371
375 372 @templatekeyword('file_mods')
376 373 def showfilemods(**args):
377 374 """List of strings. Files modified by this changeset."""
378 375 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
379 376 return showlist('file_mod', getfiles(repo, ctx, revcache)[0],
380 377 element='file', **args)
381 378
382 379 @templatekeyword('files')
383 380 def showfiles(**args):
384 381 """List of strings. All files modified, added, or removed by this
385 382 changeset.
386 383 """
387 384 return showlist('file', args['ctx'].files(), **args)
388 385
389 386 @templatekeyword('graphnode')
390 387 def showgraphnode(repo, ctx, **args):
391 388 """String. The character representing the changeset node in
392 389 an ASCII revision graph"""
393 390 wpnodes = repo.dirstate.parents()
394 391 if wpnodes[1] == nullid:
395 392 wpnodes = wpnodes[:1]
396 393 if ctx.node() in wpnodes:
397 394 return '@'
398 395 elif ctx.obsolete():
399 396 return 'x'
400 397 elif ctx.closesbranch():
401 398 return '_'
402 399 else:
403 400 return 'o'
404 401
405 402 @templatekeyword('latesttag')
406 403 def showlatesttag(**args):
407 404 """List of strings. The global tags on the most recent globally
408 405 tagged ancestor of this changeset.
409 406 """
410 407 return showlatesttags(None, **args)
411 408
412 409 def showlatesttags(pattern, **args):
413 410 """helper method for the latesttag keyword and function"""
414 411 repo, ctx = args['repo'], args['ctx']
415 412 cache = args['cache']
416 413 latesttags = getlatesttags(repo, ctx, cache, pattern)
417 414
418 415 # latesttag[0] is an implementation detail for sorting csets on different
419 416 # branches in a stable manner- it is the date the tagged cset was created,
420 417 # not the date the tag was created. Therefore it isn't made visible here.
421 418 makemap = lambda v: {
422 419 'changes': _showchangessincetag,
423 420 'distance': latesttags[1],
424 421 'latesttag': v, # BC with {latesttag % '{latesttag}'}
425 422 'tag': v
426 423 }
427 424
428 425 tags = latesttags[2]
429 426 f = _showlist('latesttag', tags, separator=':', **args)
430 427 return _hybrid(f, tags, makemap, lambda x: x['latesttag'])
431 428
432 429 @templatekeyword('latesttagdistance')
433 430 def showlatesttagdistance(repo, ctx, templ, cache, **args):
434 431 """Integer. Longest path to the latest tag."""
435 432 return getlatesttags(repo, ctx, cache)[1]
436 433
437 434 @templatekeyword('changessincelatesttag')
438 435 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
439 436 """Integer. All ancestors not in the latest tag."""
440 437 latesttag = getlatesttags(repo, ctx, cache)[2][0]
441 438
442 439 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
443 440
444 441 def _showchangessincetag(repo, ctx, **args):
445 442 offset = 0
446 443 revs = [ctx.rev()]
447 444 tag = args['tag']
448 445
449 446 # The only() revset doesn't currently support wdir()
450 447 if ctx.rev() is None:
451 448 offset = 1
452 449 revs = [p.rev() for p in ctx.parents()]
453 450
454 451 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
455 452
456 453 @templatekeyword('manifest')
457 454 def showmanifest(**args):
458 455 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
459 456 mnode = ctx.manifestnode()
460 457 if mnode is None:
461 458 # just avoid crash, we might want to use the 'ff...' hash in future
462 459 return
463 460 args = args.copy()
464 461 args.update({'rev': repo.manifest.rev(mnode), 'node': hex(mnode)})
465 462 return templ('manifest', **args)
466 463
467 464 def shownames(namespace, **args):
468 465 """helper method to generate a template keyword for a namespace"""
469 466 ctx = args['ctx']
470 467 repo = ctx.repo()
471 468 ns = repo.names[namespace]
472 469 names = ns.names(repo, ctx.node())
473 470 return showlist(ns.templatename, names, plural=namespace, **args)
474 471
475 472 @templatekeyword('namespaces')
476 473 def shownamespaces(**args):
477 474 """Dict of lists. Names attached to this changeset per
478 475 namespace."""
479 476 ctx = args['ctx']
480 477 repo = ctx.repo()
481 478 namespaces = util.sortdict((k, showlist('name', ns.names(repo, ctx.node()),
482 479 **args))
483 480 for k, ns in repo.names.iteritems())
484 481 f = _showlist('namespace', list(namespaces), **args)
485 482 return _hybrid(f, namespaces,
486 483 lambda k: {'namespace': k, 'names': namespaces[k]},
487 484 lambda x: x['namespace'])
488 485
489 486 @templatekeyword('node')
490 487 def shownode(repo, ctx, templ, **args):
491 488 """String. The changeset identification hash, as a 40 hexadecimal
492 489 digit string.
493 490 """
494 491 return ctx.hex()
495 492
496 493 @templatekeyword('p1rev')
497 494 def showp1rev(repo, ctx, templ, **args):
498 495 """Integer. The repository-local revision number of the changeset's
499 496 first parent, or -1 if the changeset has no parents."""
500 497 return ctx.p1().rev()
501 498
502 499 @templatekeyword('p2rev')
503 500 def showp2rev(repo, ctx, templ, **args):
504 501 """Integer. The repository-local revision number of the changeset's
505 502 second parent, or -1 if the changeset has no second parent."""
506 503 return ctx.p2().rev()
507 504
508 505 @templatekeyword('p1node')
509 506 def showp1node(repo, ctx, templ, **args):
510 507 """String. The identification hash of the changeset's first parent,
511 508 as a 40 digit hexadecimal string. If the changeset has no parents, all
512 509 digits are 0."""
513 510 return ctx.p1().hex()
514 511
515 512 @templatekeyword('p2node')
516 513 def showp2node(repo, ctx, templ, **args):
517 514 """String. The identification hash of the changeset's second
518 515 parent, as a 40 digit hexadecimal string. If the changeset has no second
519 516 parent, all digits are 0."""
520 517 return ctx.p2().hex()
521 518
522 519 @templatekeyword('parents')
523 520 def showparents(**args):
524 521 """List of strings. The parents of the changeset in "rev:node"
525 522 format. If the changeset has only one "natural" parent (the predecessor
526 523 revision) nothing is shown."""
527 524 repo = args['repo']
528 525 ctx = args['ctx']
529 526 pctxs = scmutil.meaningfulparents(repo, ctx)
530 527 prevs = [str(p.rev()) for p in pctxs] # ifcontains() needs a list of str
531 528 parents = [[('rev', p.rev()),
532 529 ('node', p.hex()),
533 530 ('phase', p.phasestr())]
534 531 for p in pctxs]
535 532 f = _showlist('parent', parents, **args)
536 533 return _hybrid(f, prevs, lambda x: {'ctx': repo[int(x)], 'revcache': {}},
537 534 lambda d: _formatrevnode(d['ctx']))
538 535
539 536 @templatekeyword('phase')
540 537 def showphase(repo, ctx, templ, **args):
541 538 """String. The changeset phase name."""
542 539 return ctx.phasestr()
543 540
544 541 @templatekeyword('phaseidx')
545 542 def showphaseidx(repo, ctx, templ, **args):
546 543 """Integer. The changeset phase index."""
547 544 return ctx.phase()
548 545
549 546 @templatekeyword('rev')
550 547 def showrev(repo, ctx, templ, **args):
551 548 """Integer. The repository-local changeset revision number."""
552 549 return scmutil.intrev(ctx.rev())
553 550
554 551 def showrevslist(name, revs, **args):
555 552 """helper to generate a list of revisions in which a mapped template will
556 553 be evaluated"""
557 554 repo = args['ctx'].repo()
558 555 revs = [str(r) for r in revs] # ifcontains() needs a list of str
559 556 f = _showlist(name, revs, **args)
560 557 return _hybrid(f, revs,
561 558 lambda x: {name: x, 'ctx': repo[int(x)], 'revcache': {}},
562 559 lambda d: d[name])
563 560
564 561 @templatekeyword('subrepos')
565 562 def showsubrepos(**args):
566 563 """List of strings. Updated subrepositories in the changeset."""
567 564 ctx = args['ctx']
568 565 substate = ctx.substate
569 566 if not substate:
570 567 return showlist('subrepo', [], **args)
571 568 psubstate = ctx.parents()[0].substate or {}
572 569 subrepos = []
573 570 for sub in substate:
574 571 if sub not in psubstate or substate[sub] != psubstate[sub]:
575 572 subrepos.append(sub) # modified or newly added in ctx
576 573 for sub in psubstate:
577 574 if sub not in substate:
578 575 subrepos.append(sub) # removed in ctx
579 576 return showlist('subrepo', sorted(subrepos), **args)
580 577
581 578 # don't remove "showtags" definition, even though namespaces will put
582 579 # a helper function for "tags" keyword into "keywords" map automatically,
583 580 # because online help text is built without namespaces initialization
584 581 @templatekeyword('tags')
585 582 def showtags(**args):
586 583 """List of strings. Any tags associated with the changeset."""
587 584 return shownames('tags', **args)
588 585
589 586 def loadkeyword(ui, extname, registrarobj):
590 587 """Load template keyword from specified registrarobj
591 588 """
592 589 for name, func in registrarobj._table.iteritems():
593 590 keywords[name] = func
594 591
595 592 # tell hggettext to extract docstrings from these functions:
596 593 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now