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