##// END OF EJS Templates
templatekw: introduce showlatesttags() to handle {latesttag} keywords...
Matt Harbison -
r26484:93c80e7e default
parent child Browse files
Show More
@@ -1,522 +1,542 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 .node import hex
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 showlatesttag(**args):
344 344 """:latesttag: List of strings. The global tags on the most recent globally
345 345 tagged ancestor of this changeset.
346 346 """
347 347 repo, ctx = args['repo'], args['ctx']
348 348 cache = args['cache']
349 349 latesttags = getlatesttags(repo, ctx, cache)[2]
350 350
351 351 return showlist('latesttag', latesttags, separator=':', **args)
352 352
353 def showlatesttags(pattern, **args):
354 """helper method for the latesttag keyword and function"""
355 repo, ctx = args['repo'], args['ctx']
356 cache = args['cache']
357 latesttags = getlatesttags(repo, ctx, cache, pattern)
358
359 # latesttag[0] is an implementation detail for sorting csets on different
360 # branches in a stable manner- it is the date the tagged cset was created,
361 # not the date the tag was created. Therefore it isn't made visible here.
362 makemap = lambda v: {
363 'changes': _showchangessincetag,
364 'distance': latesttags[1],
365 'latesttag': v, # BC with {latesttag % '{latesttag}'}
366 'tag': v
367 }
368
369 tags = latesttags[2]
370 f = _showlist('latesttag', tags, separator=':', **args)
371 return _hybrid(f, tags, makemap, lambda x: x['latesttag'])
372
353 373 def showlatesttagdistance(repo, ctx, templ, cache, **args):
354 374 """:latesttagdistance: Integer. Longest path to the latest tag."""
355 375 return getlatesttags(repo, ctx, cache)[1]
356 376
357 377 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
358 378 """:changessincelatesttag: Integer. All ancestors not in the latest tag."""
359 379 latesttag = getlatesttags(repo, ctx, cache)[2][0]
360 380
361 381 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
362 382
363 383 def _showchangessincetag(repo, ctx, **args):
364 384 offset = 0
365 385 revs = [ctx.rev()]
366 386 tag = args['tag']
367 387
368 388 # The only() revset doesn't currently support wdir()
369 389 if ctx.rev() is None:
370 390 offset = 1
371 391 revs = [p.rev() for p in ctx.parents()]
372 392
373 393 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
374 394
375 395 def showmanifest(**args):
376 396 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
377 397 mnode = ctx.manifestnode()
378 398 if mnode is None:
379 399 # just avoid crash, we might want to use the 'ff...' hash in future
380 400 return
381 401 args = args.copy()
382 402 args.update({'rev': repo.manifest.rev(mnode), 'node': hex(mnode)})
383 403 return templ('manifest', **args)
384 404
385 405 def shownode(repo, ctx, templ, **args):
386 406 """:node: String. The changeset identification hash, as a 40 hexadecimal
387 407 digit string.
388 408 """
389 409 return ctx.hex()
390 410
391 411 def showp1rev(repo, ctx, templ, **args):
392 412 """:p1rev: Integer. The repository-local revision number of the changeset's
393 413 first parent, or -1 if the changeset has no parents."""
394 414 return ctx.p1().rev()
395 415
396 416 def showp2rev(repo, ctx, templ, **args):
397 417 """:p2rev: Integer. The repository-local revision number of the changeset's
398 418 second parent, or -1 if the changeset has no second parent."""
399 419 return ctx.p2().rev()
400 420
401 421 def showp1node(repo, ctx, templ, **args):
402 422 """:p1node: String. The identification hash of the changeset's first parent,
403 423 as a 40 digit hexadecimal string. If the changeset has no parents, all
404 424 digits are 0."""
405 425 return ctx.p1().hex()
406 426
407 427 def showp2node(repo, ctx, templ, **args):
408 428 """:p2node: String. The identification hash of the changeset's second
409 429 parent, as a 40 digit hexadecimal string. If the changeset has no second
410 430 parent, all digits are 0."""
411 431 return ctx.p2().hex()
412 432
413 433 def showparents(**args):
414 434 """:parents: List of strings. The parents of the changeset in "rev:node"
415 435 format. If the changeset has only one "natural" parent (the predecessor
416 436 revision) nothing is shown."""
417 437 repo = args['repo']
418 438 ctx = args['ctx']
419 439 parents = [[('rev', p.rev()),
420 440 ('node', p.hex()),
421 441 ('phase', p.phasestr())]
422 442 for p in scmutil.meaningfulparents(repo, ctx)]
423 443 return showlist('parent', parents, **args)
424 444
425 445 def showphase(repo, ctx, templ, **args):
426 446 """:phase: String. The changeset phase name."""
427 447 return ctx.phasestr()
428 448
429 449 def showphaseidx(repo, ctx, templ, **args):
430 450 """:phaseidx: Integer. The changeset phase index."""
431 451 return ctx.phase()
432 452
433 453 def showrev(repo, ctx, templ, **args):
434 454 """:rev: Integer. The repository-local changeset revision number."""
435 455 return scmutil.intrev(ctx.rev())
436 456
437 457 def showrevslist(name, revs, **args):
438 458 """helper to generate a list of revisions in which a mapped template will
439 459 be evaluated"""
440 460 repo = args['ctx'].repo()
441 461 f = _showlist(name, revs, **args)
442 462 return _hybrid(f, revs,
443 463 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}})
444 464
445 465 def showsubrepos(**args):
446 466 """:subrepos: List of strings. Updated subrepositories in the changeset."""
447 467 ctx = args['ctx']
448 468 substate = ctx.substate
449 469 if not substate:
450 470 return showlist('subrepo', [], **args)
451 471 psubstate = ctx.parents()[0].substate or {}
452 472 subrepos = []
453 473 for sub in substate:
454 474 if sub not in psubstate or substate[sub] != psubstate[sub]:
455 475 subrepos.append(sub) # modified or newly added in ctx
456 476 for sub in psubstate:
457 477 if sub not in substate:
458 478 subrepos.append(sub) # removed in ctx
459 479 return showlist('subrepo', sorted(subrepos), **args)
460 480
461 481 def shownames(namespace, **args):
462 482 """helper method to generate a template keyword for a namespace"""
463 483 ctx = args['ctx']
464 484 repo = ctx.repo()
465 485 ns = repo.names[namespace]
466 486 names = ns.names(repo, ctx.node())
467 487 return showlist(ns.templatename, names, plural=namespace, **args)
468 488
469 489 # don't remove "showtags" definition, even though namespaces will put
470 490 # a helper function for "tags" keyword into "keywords" map automatically,
471 491 # because online help text is built without namespaces initialization
472 492 def showtags(**args):
473 493 """:tags: List of strings. Any tags associated with the changeset."""
474 494 return shownames('tags', **args)
475 495
476 496 # keywords are callables like:
477 497 # fn(repo, ctx, templ, cache, revcache, **args)
478 498 # with:
479 499 # repo - current repository instance
480 500 # ctx - the changectx being displayed
481 501 # templ - the templater instance
482 502 # cache - a cache dictionary for the whole templater run
483 503 # revcache - a cache dictionary for the current revision
484 504 keywords = {
485 505 'activebookmark': showactivebookmark,
486 506 'author': showauthor,
487 507 'bisect': showbisect,
488 508 'branch': showbranch,
489 509 'branches': showbranches,
490 510 'bookmarks': showbookmarks,
491 511 'changessincelatesttag': showchangessincelatesttag,
492 512 'children': showchildren,
493 513 # currentbookmark is deprecated
494 514 'currentbookmark': showcurrentbookmark,
495 515 'date': showdate,
496 516 'desc': showdescription,
497 517 'diffstat': showdiffstat,
498 518 'extras': showextras,
499 519 'file_adds': showfileadds,
500 520 'file_copies': showfilecopies,
501 521 'file_copies_switch': showfilecopiesswitch,
502 522 'file_dels': showfiledels,
503 523 'file_mods': showfilemods,
504 524 'files': showfiles,
505 525 'latesttag': showlatesttag,
506 526 'latesttagdistance': showlatesttagdistance,
507 527 'manifest': showmanifest,
508 528 'node': shownode,
509 529 'p1rev': showp1rev,
510 530 'p1node': showp1node,
511 531 'p2rev': showp2rev,
512 532 'p2node': showp2node,
513 533 'parents': showparents,
514 534 'phase': showphase,
515 535 'phaseidx': showphaseidx,
516 536 'rev': showrev,
517 537 'subrepos': showsubrepos,
518 538 'tags': showtags,
519 539 }
520 540
521 541 # tell hggettext to extract docstrings from these functions:
522 542 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now