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