##// END OF EJS Templates
templatekw: allow getlatesttags() to match a specific tag pattern...
Matt Harbison -
r26482:d2e69584 default
parent child Browse files
Show More
@@ -1,509 +1,517 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 def getlatesttags(repo, ctx, cache):
125 def getlatesttags(repo, ctx, cache, pattern=None):
126 126 '''return date, distance and name for the latest tag of rev'''
127 127
128 if 'latesttags' not in cache:
128 cachename = 'latesttags'
129 if pattern is not None:
130 cachename += '-' + pattern
131 match = util.stringmatcher(pattern)[2]
132 else:
133 match = util.always
134
135 if cachename not in cache:
129 136 # Cache mapping from rev to a tuple with tag date, tag
130 137 # distance and tag name
131 cache['latesttags'] = {-1: (0, 0, ['null'])}
132 latesttags = cache['latesttags']
138 cache[cachename] = {-1: (0, 0, ['null'])}
139 latesttags = cache[cachename]
133 140
134 141 rev = ctx.rev()
135 142 todo = [rev]
136 143 while todo:
137 144 rev = todo.pop()
138 145 if rev in latesttags:
139 146 continue
140 147 ctx = repo[rev]
141 148 tags = [t for t in ctx.tags()
142 if (repo.tagtype(t) and repo.tagtype(t) != 'local')]
149 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
150 and match(t))]
143 151 if tags:
144 152 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
145 153 continue
146 154 try:
147 155 # The tuples are laid out so the right one can be found by
148 156 # comparison.
149 157 pdate, pdist, ptag = max(
150 158 latesttags[p.rev()] for p in ctx.parents())
151 159 except KeyError:
152 160 # Cache miss - recurse
153 161 todo.append(rev)
154 162 todo.extend(p.rev() for p in ctx.parents())
155 163 continue
156 164 latesttags[rev] = pdate, pdist + 1, ptag
157 165 return latesttags[rev]
158 166
159 167 def getrenamedfn(repo, endrev=None):
160 168 rcache = {}
161 169 if endrev is None:
162 170 endrev = len(repo)
163 171
164 172 def getrenamed(fn, rev):
165 173 '''looks up all renames for a file (up to endrev) the first
166 174 time the file is given. It indexes on the changerev and only
167 175 parses the manifest if linkrev != changerev.
168 176 Returns rename info for fn at changerev rev.'''
169 177 if fn not in rcache:
170 178 rcache[fn] = {}
171 179 fl = repo.file(fn)
172 180 for i in fl:
173 181 lr = fl.linkrev(i)
174 182 renamed = fl.renamed(fl.node(i))
175 183 rcache[fn][lr] = renamed
176 184 if lr >= endrev:
177 185 break
178 186 if rev in rcache[fn]:
179 187 return rcache[fn][rev]
180 188
181 189 # If linkrev != rev (i.e. rev not found in rcache) fallback to
182 190 # filectx logic.
183 191 try:
184 192 return repo[rev][fn].renamed()
185 193 except error.LookupError:
186 194 return None
187 195
188 196 return getrenamed
189 197
190 198
191 199 def showauthor(repo, ctx, templ, **args):
192 200 """:author: String. The unmodified author of the changeset."""
193 201 return ctx.user()
194 202
195 203 def showbisect(repo, ctx, templ, **args):
196 204 """:bisect: String. The changeset bisection status."""
197 205 return hbisect.label(repo, ctx.node())
198 206
199 207 def showbranch(**args):
200 208 """:branch: String. The name of the branch on which the changeset was
201 209 committed.
202 210 """
203 211 return args['ctx'].branch()
204 212
205 213 def showbranches(**args):
206 214 """:branches: List of strings. The name of the branch on which the
207 215 changeset was committed. Will be empty if the branch name was
208 216 default. (DEPRECATED)
209 217 """
210 218 branch = args['ctx'].branch()
211 219 if branch != 'default':
212 220 return showlist('branch', [branch], plural='branches', **args)
213 221 return showlist('branch', [], plural='branches', **args)
214 222
215 223 def showbookmarks(**args):
216 224 """:bookmarks: List of strings. Any bookmarks associated with the
217 225 changeset. Also sets 'active', the name of the active bookmark.
218 226 """
219 227 repo = args['ctx']._repo
220 228 bookmarks = args['ctx'].bookmarks()
221 229 active = repo._activebookmark
222 230 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
223 231 f = _showlist('bookmark', bookmarks, **args)
224 232 return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark'])
225 233
226 234 def showchildren(**args):
227 235 """:children: List of strings. The children of the changeset."""
228 236 ctx = args['ctx']
229 237 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
230 238 return showlist('children', childrevs, element='child', **args)
231 239
232 240 # Deprecated, but kept alive for help generation a purpose.
233 241 def showcurrentbookmark(**args):
234 242 """:currentbookmark: String. The active bookmark, if it is
235 243 associated with the changeset (DEPRECATED)"""
236 244 return showactivebookmark(**args)
237 245
238 246 def showactivebookmark(**args):
239 247 """:activebookmark: String. The active bookmark, if it is
240 248 associated with the changeset"""
241 249 active = args['repo']._activebookmark
242 250 if active and active in args['ctx'].bookmarks():
243 251 return active
244 252 return ''
245 253
246 254 def showdate(repo, ctx, templ, **args):
247 255 """:date: Date information. The date when the changeset was committed."""
248 256 return ctx.date()
249 257
250 258 def showdescription(repo, ctx, templ, **args):
251 259 """:desc: String. The text of the changeset description."""
252 260 return ctx.description().strip()
253 261
254 262 def showdiffstat(repo, ctx, templ, **args):
255 263 """:diffstat: String. Statistics of changes with the following format:
256 264 "modified files: +added/-removed lines"
257 265 """
258 266 stats = patch.diffstatdata(util.iterlines(ctx.diff()))
259 267 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
260 268 return '%s: +%s/-%s' % (len(stats), adds, removes)
261 269
262 270 def showextras(**args):
263 271 """:extras: List of dicts with key, value entries of the 'extras'
264 272 field of this changeset."""
265 273 extras = args['ctx'].extra()
266 274 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
267 275 makemap = lambda k: {'key': k, 'value': extras[k]}
268 276 c = [makemap(k) for k in extras]
269 277 f = _showlist('extra', c, plural='extras', **args)
270 278 return _hybrid(f, extras, makemap,
271 279 lambda x: '%s=%s' % (x['key'], x['value']))
272 280
273 281 def showfileadds(**args):
274 282 """:file_adds: List of strings. Files added by this changeset."""
275 283 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
276 284 return showlist('file_add', getfiles(repo, ctx, revcache)[1],
277 285 element='file', **args)
278 286
279 287 def showfilecopies(**args):
280 288 """:file_copies: List of strings. Files copied in this changeset with
281 289 their sources.
282 290 """
283 291 cache, ctx = args['cache'], args['ctx']
284 292 copies = args['revcache'].get('copies')
285 293 if copies is None:
286 294 if 'getrenamed' not in cache:
287 295 cache['getrenamed'] = getrenamedfn(args['repo'])
288 296 copies = []
289 297 getrenamed = cache['getrenamed']
290 298 for fn in ctx.files():
291 299 rename = getrenamed(fn, ctx.rev())
292 300 if rename:
293 301 copies.append((fn, rename[0]))
294 302
295 303 copies = util.sortdict(copies)
296 304 makemap = lambda k: {'name': k, 'source': copies[k]}
297 305 c = [makemap(k) for k in copies]
298 306 f = _showlist('file_copy', c, plural='file_copies', **args)
299 307 return _hybrid(f, copies, makemap,
300 308 lambda x: '%s (%s)' % (x['name'], x['source']))
301 309
302 310 # showfilecopiesswitch() displays file copies only if copy records are
303 311 # provided before calling the templater, usually with a --copies
304 312 # command line switch.
305 313 def showfilecopiesswitch(**args):
306 314 """:file_copies_switch: List of strings. Like "file_copies" but displayed
307 315 only if the --copied switch is set.
308 316 """
309 317 copies = args['revcache'].get('copies') or []
310 318 copies = util.sortdict(copies)
311 319 makemap = lambda k: {'name': k, 'source': copies[k]}
312 320 c = [makemap(k) for k in copies]
313 321 f = _showlist('file_copy', c, plural='file_copies', **args)
314 322 return _hybrid(f, copies, makemap,
315 323 lambda x: '%s (%s)' % (x['name'], x['source']))
316 324
317 325 def showfiledels(**args):
318 326 """:file_dels: List of strings. Files removed by this changeset."""
319 327 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
320 328 return showlist('file_del', getfiles(repo, ctx, revcache)[2],
321 329 element='file', **args)
322 330
323 331 def showfilemods(**args):
324 332 """:file_mods: List of strings. Files modified by this changeset."""
325 333 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
326 334 return showlist('file_mod', getfiles(repo, ctx, revcache)[0],
327 335 element='file', **args)
328 336
329 337 def showfiles(**args):
330 338 """:files: List of strings. All files modified, added, or removed by this
331 339 changeset.
332 340 """
333 341 return showlist('file', args['ctx'].files(), **args)
334 342
335 343 def showlatesttag(**args):
336 344 """:latesttag: List of strings. The global tags on the most recent globally
337 345 tagged ancestor of this changeset.
338 346 """
339 347 repo, ctx = args['repo'], args['ctx']
340 348 cache = args['cache']
341 349 latesttags = getlatesttags(repo, ctx, cache)[2]
342 350
343 351 return showlist('latesttag', latesttags, separator=':', **args)
344 352
345 353 def showlatesttagdistance(repo, ctx, templ, cache, **args):
346 354 """:latesttagdistance: Integer. Longest path to the latest tag."""
347 355 return getlatesttags(repo, ctx, cache)[1]
348 356
349 357 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
350 358 """:changessincelatesttag: Integer. All ancestors not in the latest tag."""
351 359 latesttag = getlatesttags(repo, ctx, cache)[2][0]
352 360 offset = 0
353 361 revs = [ctx.rev()]
354 362
355 363 # The only() revset doesn't currently support wdir()
356 364 if ctx.rev() is None:
357 365 offset = 1
358 366 revs = [p.rev() for p in ctx.parents()]
359 367
360 368 return len(repo.revs('only(%ld, %s)', revs, latesttag)) + offset
361 369
362 370 def showmanifest(**args):
363 371 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
364 372 mnode = ctx.manifestnode()
365 373 if mnode is None:
366 374 # just avoid crash, we might want to use the 'ff...' hash in future
367 375 return
368 376 args = args.copy()
369 377 args.update({'rev': repo.manifest.rev(mnode), 'node': hex(mnode)})
370 378 return templ('manifest', **args)
371 379
372 380 def shownode(repo, ctx, templ, **args):
373 381 """:node: String. The changeset identification hash, as a 40 hexadecimal
374 382 digit string.
375 383 """
376 384 return ctx.hex()
377 385
378 386 def showp1rev(repo, ctx, templ, **args):
379 387 """:p1rev: Integer. The repository-local revision number of the changeset's
380 388 first parent, or -1 if the changeset has no parents."""
381 389 return ctx.p1().rev()
382 390
383 391 def showp2rev(repo, ctx, templ, **args):
384 392 """:p2rev: Integer. The repository-local revision number of the changeset's
385 393 second parent, or -1 if the changeset has no second parent."""
386 394 return ctx.p2().rev()
387 395
388 396 def showp1node(repo, ctx, templ, **args):
389 397 """:p1node: String. The identification hash of the changeset's first parent,
390 398 as a 40 digit hexadecimal string. If the changeset has no parents, all
391 399 digits are 0."""
392 400 return ctx.p1().hex()
393 401
394 402 def showp2node(repo, ctx, templ, **args):
395 403 """:p2node: String. The identification hash of the changeset's second
396 404 parent, as a 40 digit hexadecimal string. If the changeset has no second
397 405 parent, all digits are 0."""
398 406 return ctx.p2().hex()
399 407
400 408 def showparents(**args):
401 409 """:parents: List of strings. The parents of the changeset in "rev:node"
402 410 format. If the changeset has only one "natural" parent (the predecessor
403 411 revision) nothing is shown."""
404 412 repo = args['repo']
405 413 ctx = args['ctx']
406 414 parents = [[('rev', p.rev()),
407 415 ('node', p.hex()),
408 416 ('phase', p.phasestr())]
409 417 for p in scmutil.meaningfulparents(repo, ctx)]
410 418 return showlist('parent', parents, **args)
411 419
412 420 def showphase(repo, ctx, templ, **args):
413 421 """:phase: String. The changeset phase name."""
414 422 return ctx.phasestr()
415 423
416 424 def showphaseidx(repo, ctx, templ, **args):
417 425 """:phaseidx: Integer. The changeset phase index."""
418 426 return ctx.phase()
419 427
420 428 def showrev(repo, ctx, templ, **args):
421 429 """:rev: Integer. The repository-local changeset revision number."""
422 430 return scmutil.intrev(ctx.rev())
423 431
424 432 def showrevslist(name, revs, **args):
425 433 """helper to generate a list of revisions in which a mapped template will
426 434 be evaluated"""
427 435 repo = args['ctx'].repo()
428 436 f = _showlist(name, revs, **args)
429 437 return _hybrid(f, revs,
430 438 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}})
431 439
432 440 def showsubrepos(**args):
433 441 """:subrepos: List of strings. Updated subrepositories in the changeset."""
434 442 ctx = args['ctx']
435 443 substate = ctx.substate
436 444 if not substate:
437 445 return showlist('subrepo', [], **args)
438 446 psubstate = ctx.parents()[0].substate or {}
439 447 subrepos = []
440 448 for sub in substate:
441 449 if sub not in psubstate or substate[sub] != psubstate[sub]:
442 450 subrepos.append(sub) # modified or newly added in ctx
443 451 for sub in psubstate:
444 452 if sub not in substate:
445 453 subrepos.append(sub) # removed in ctx
446 454 return showlist('subrepo', sorted(subrepos), **args)
447 455
448 456 def shownames(namespace, **args):
449 457 """helper method to generate a template keyword for a namespace"""
450 458 ctx = args['ctx']
451 459 repo = ctx.repo()
452 460 ns = repo.names[namespace]
453 461 names = ns.names(repo, ctx.node())
454 462 return showlist(ns.templatename, names, plural=namespace, **args)
455 463
456 464 # don't remove "showtags" definition, even though namespaces will put
457 465 # a helper function for "tags" keyword into "keywords" map automatically,
458 466 # because online help text is built without namespaces initialization
459 467 def showtags(**args):
460 468 """:tags: List of strings. Any tags associated with the changeset."""
461 469 return shownames('tags', **args)
462 470
463 471 # keywords are callables like:
464 472 # fn(repo, ctx, templ, cache, revcache, **args)
465 473 # with:
466 474 # repo - current repository instance
467 475 # ctx - the changectx being displayed
468 476 # templ - the templater instance
469 477 # cache - a cache dictionary for the whole templater run
470 478 # revcache - a cache dictionary for the current revision
471 479 keywords = {
472 480 'activebookmark': showactivebookmark,
473 481 'author': showauthor,
474 482 'bisect': showbisect,
475 483 'branch': showbranch,
476 484 'branches': showbranches,
477 485 'bookmarks': showbookmarks,
478 486 'changessincelatesttag': showchangessincelatesttag,
479 487 'children': showchildren,
480 488 # currentbookmark is deprecated
481 489 'currentbookmark': showcurrentbookmark,
482 490 'date': showdate,
483 491 'desc': showdescription,
484 492 'diffstat': showdiffstat,
485 493 'extras': showextras,
486 494 'file_adds': showfileadds,
487 495 'file_copies': showfilecopies,
488 496 'file_copies_switch': showfilecopiesswitch,
489 497 'file_dels': showfiledels,
490 498 'file_mods': showfilemods,
491 499 'files': showfiles,
492 500 'latesttag': showlatesttag,
493 501 'latesttagdistance': showlatesttagdistance,
494 502 'manifest': showmanifest,
495 503 'node': shownode,
496 504 'p1rev': showp1rev,
497 505 'p1node': showp1node,
498 506 'p2rev': showp2rev,
499 507 'p2node': showp2node,
500 508 'parents': showparents,
501 509 'phase': showphase,
502 510 'phaseidx': showphaseidx,
503 511 'rev': showrev,
504 512 'subrepos': showsubrepos,
505 513 'tags': showtags,
506 514 }
507 515
508 516 # tell hggettext to extract docstrings from these functions:
509 517 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now