##// END OF EJS Templates
templatekw: make experimental {peerpaths} return a single-level dict (BC)...
Yuya Nishihara -
r34539:ac38e889 default
parent child Browse files
Show More
@@ -1,892 +1,887 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 .i18n import _
11 11 from .node import (
12 12 hex,
13 13 nullid,
14 14 )
15 15
16 16 from . import (
17 17 encoding,
18 18 error,
19 19 hbisect,
20 20 obsutil,
21 21 patch,
22 22 pycompat,
23 23 registrar,
24 24 scmutil,
25 25 util,
26 26 )
27 27
28 28 class _hybrid(object):
29 29 """Wrapper for list or dict to support legacy template
30 30
31 31 This class allows us to handle both:
32 32 - "{files}" (legacy command-line-specific list hack) and
33 33 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
34 34 and to access raw values:
35 35 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
36 36 - "{get(extras, key)}"
37 37 - "{files|json}"
38 38 """
39 39
40 40 def __init__(self, gen, values, makemap, joinfmt):
41 41 if gen is not None:
42 42 self.gen = gen # generator or function returning generator
43 43 self._values = values
44 44 self._makemap = makemap
45 45 self.joinfmt = joinfmt
46 46 def gen(self):
47 47 """Default generator to stringify this as {join(self, ' ')}"""
48 48 for i, x in enumerate(self._values):
49 49 if i > 0:
50 50 yield ' '
51 51 yield self.joinfmt(x)
52 52 def itermaps(self):
53 53 makemap = self._makemap
54 54 for x in self._values:
55 55 yield makemap(x)
56 56 def __contains__(self, x):
57 57 return x in self._values
58 58 def __getitem__(self, key):
59 59 return self._values[key]
60 60 def __len__(self):
61 61 return len(self._values)
62 62 def __iter__(self):
63 63 return iter(self._values)
64 64 def __getattr__(self, name):
65 65 if name not in ('get', 'items', 'iteritems', 'iterkeys', 'itervalues',
66 66 'keys', 'values'):
67 67 raise AttributeError(name)
68 68 return getattr(self._values, name)
69 69
70 70 class _mappable(object):
71 71 """Wrapper for non-list/dict object to support map operation
72 72
73 73 This class allows us to handle both:
74 74 - "{manifest}"
75 75 - "{manifest % '{rev}:{node}'}"
76 76 - "{manifest.rev}"
77 77
78 78 Unlike a _hybrid, this does not simulate the behavior of the underling
79 79 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
80 80 """
81 81
82 82 def __init__(self, gen, key, value, makemap):
83 83 if gen is not None:
84 84 self.gen = gen # generator or function returning generator
85 85 self._key = key
86 86 self._value = value # may be generator of strings
87 87 self._makemap = makemap
88 88
89 89 def gen(self):
90 90 yield pycompat.bytestr(self._value)
91 91
92 92 def tomap(self):
93 93 return self._makemap(self._key)
94 94
95 95 def itermaps(self):
96 96 yield self.tomap()
97 97
98 98 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
99 99 """Wrap data to support both dict-like and string-like operations"""
100 100 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
101 101 lambda k: fmt % (k, data[k]))
102 102
103 103 def hybridlist(data, name, fmt='%s', gen=None):
104 104 """Wrap data to support both list-like and string-like operations"""
105 105 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
106 106
107 107 def unwraphybrid(thing):
108 108 """Return an object which can be stringified possibly by using a legacy
109 109 template"""
110 110 gen = getattr(thing, 'gen', None)
111 111 if gen is None:
112 112 return thing
113 113 if callable(gen):
114 114 return gen()
115 115 return gen
116 116
117 117 def unwrapvalue(thing):
118 118 """Move the inner value object out of the wrapper"""
119 119 if not util.safehasattr(thing, '_value'):
120 120 return thing
121 121 return thing._value
122 122
123 123 def wraphybridvalue(container, key, value):
124 124 """Wrap an element of hybrid container to be mappable
125 125
126 126 The key is passed to the makemap function of the given container, which
127 127 should be an item generated by iter(container).
128 128 """
129 129 makemap = getattr(container, '_makemap', None)
130 130 if makemap is None:
131 131 return value
132 132 if util.safehasattr(value, '_makemap'):
133 133 # a nested hybrid list/dict, which has its own way of map operation
134 134 return value
135 135 return _mappable(None, key, value, makemap)
136 136
137 137 def showdict(name, data, mapping, plural=None, key='key', value='value',
138 138 fmt='%s=%s', separator=' '):
139 139 c = [{key: k, value: v} for k, v in data.iteritems()]
140 140 f = _showlist(name, c, mapping, plural, separator)
141 141 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
142 142
143 143 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
144 144 if not element:
145 145 element = name
146 146 f = _showlist(name, values, mapping, plural, separator)
147 147 return hybridlist(values, name=element, gen=f)
148 148
149 149 def _showlist(name, values, mapping, plural=None, separator=' '):
150 150 '''expand set of values.
151 151 name is name of key in template map.
152 152 values is list of strings or dicts.
153 153 plural is plural of name, if not simply name + 's'.
154 154 separator is used to join values as a string
155 155
156 156 expansion works like this, given name 'foo'.
157 157
158 158 if values is empty, expand 'no_foos'.
159 159
160 160 if 'foo' not in template map, return values as a string,
161 161 joined by 'separator'.
162 162
163 163 expand 'start_foos'.
164 164
165 165 for each value, expand 'foo'. if 'last_foo' in template
166 166 map, expand it instead of 'foo' for last key.
167 167
168 168 expand 'end_foos'.
169 169 '''
170 170 templ = mapping['templ']
171 171 strmapping = pycompat.strkwargs(mapping)
172 172 if not plural:
173 173 plural = name + 's'
174 174 if not values:
175 175 noname = 'no_' + plural
176 176 if noname in templ:
177 177 yield templ(noname, **strmapping)
178 178 return
179 179 if name not in templ:
180 180 if isinstance(values[0], bytes):
181 181 yield separator.join(values)
182 182 else:
183 183 for v in values:
184 184 yield dict(v, **strmapping)
185 185 return
186 186 startname = 'start_' + plural
187 187 if startname in templ:
188 188 yield templ(startname, **strmapping)
189 189 vmapping = mapping.copy()
190 190 def one(v, tag=name):
191 191 try:
192 192 vmapping.update(v)
193 193 except (AttributeError, ValueError):
194 194 try:
195 195 for a, b in v:
196 196 vmapping[a] = b
197 197 except ValueError:
198 198 vmapping[name] = v
199 199 return templ(tag, **pycompat.strkwargs(vmapping))
200 200 lastname = 'last_' + name
201 201 if lastname in templ:
202 202 last = values.pop()
203 203 else:
204 204 last = None
205 205 for v in values:
206 206 yield one(v)
207 207 if last is not None:
208 208 yield one(last, tag=lastname)
209 209 endname = 'end_' + plural
210 210 if endname in templ:
211 211 yield templ(endname, **strmapping)
212 212
213 213 def getfiles(repo, ctx, revcache):
214 214 if 'files' not in revcache:
215 215 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
216 216 return revcache['files']
217 217
218 218 def getlatesttags(repo, ctx, cache, pattern=None):
219 219 '''return date, distance and name for the latest tag of rev'''
220 220
221 221 cachename = 'latesttags'
222 222 if pattern is not None:
223 223 cachename += '-' + pattern
224 224 match = util.stringmatcher(pattern)[2]
225 225 else:
226 226 match = util.always
227 227
228 228 if cachename not in cache:
229 229 # Cache mapping from rev to a tuple with tag date, tag
230 230 # distance and tag name
231 231 cache[cachename] = {-1: (0, 0, ['null'])}
232 232 latesttags = cache[cachename]
233 233
234 234 rev = ctx.rev()
235 235 todo = [rev]
236 236 while todo:
237 237 rev = todo.pop()
238 238 if rev in latesttags:
239 239 continue
240 240 ctx = repo[rev]
241 241 tags = [t for t in ctx.tags()
242 242 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
243 243 and match(t))]
244 244 if tags:
245 245 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
246 246 continue
247 247 try:
248 248 ptags = [latesttags[p.rev()] for p in ctx.parents()]
249 249 if len(ptags) > 1:
250 250 if ptags[0][2] == ptags[1][2]:
251 251 # The tuples are laid out so the right one can be found by
252 252 # comparison in this case.
253 253 pdate, pdist, ptag = max(ptags)
254 254 else:
255 255 def key(x):
256 256 changessincetag = len(repo.revs('only(%d, %s)',
257 257 ctx.rev(), x[2][0]))
258 258 # Smallest number of changes since tag wins. Date is
259 259 # used as tiebreaker.
260 260 return [-changessincetag, x[0]]
261 261 pdate, pdist, ptag = max(ptags, key=key)
262 262 else:
263 263 pdate, pdist, ptag = ptags[0]
264 264 except KeyError:
265 265 # Cache miss - recurse
266 266 todo.append(rev)
267 267 todo.extend(p.rev() for p in ctx.parents())
268 268 continue
269 269 latesttags[rev] = pdate, pdist + 1, ptag
270 270 return latesttags[rev]
271 271
272 272 def getrenamedfn(repo, endrev=None):
273 273 rcache = {}
274 274 if endrev is None:
275 275 endrev = len(repo)
276 276
277 277 def getrenamed(fn, rev):
278 278 '''looks up all renames for a file (up to endrev) the first
279 279 time the file is given. It indexes on the changerev and only
280 280 parses the manifest if linkrev != changerev.
281 281 Returns rename info for fn at changerev rev.'''
282 282 if fn not in rcache:
283 283 rcache[fn] = {}
284 284 fl = repo.file(fn)
285 285 for i in fl:
286 286 lr = fl.linkrev(i)
287 287 renamed = fl.renamed(fl.node(i))
288 288 rcache[fn][lr] = renamed
289 289 if lr >= endrev:
290 290 break
291 291 if rev in rcache[fn]:
292 292 return rcache[fn][rev]
293 293
294 294 # If linkrev != rev (i.e. rev not found in rcache) fallback to
295 295 # filectx logic.
296 296 try:
297 297 return repo[rev][fn].renamed()
298 298 except error.LookupError:
299 299 return None
300 300
301 301 return getrenamed
302 302
303 303 # default templates internally used for rendering of lists
304 304 defaulttempl = {
305 305 'parent': '{rev}:{node|formatnode} ',
306 306 'manifest': '{rev}:{node|formatnode}',
307 307 'file_copy': '{name} ({source})',
308 308 'envvar': '{key}={value}',
309 309 'extra': '{key}={value|stringescape}'
310 310 }
311 311 # filecopy is preserved for compatibility reasons
312 312 defaulttempl['filecopy'] = defaulttempl['file_copy']
313 313
314 314 # keywords are callables like:
315 315 # fn(repo, ctx, templ, cache, revcache, **args)
316 316 # with:
317 317 # repo - current repository instance
318 318 # ctx - the changectx being displayed
319 319 # templ - the templater instance
320 320 # cache - a cache dictionary for the whole templater run
321 321 # revcache - a cache dictionary for the current revision
322 322 keywords = {}
323 323
324 324 templatekeyword = registrar.templatekeyword(keywords)
325 325
326 326 @templatekeyword('author')
327 327 def showauthor(repo, ctx, templ, **args):
328 328 """String. The unmodified author of the changeset."""
329 329 return ctx.user()
330 330
331 331 @templatekeyword('bisect')
332 332 def showbisect(repo, ctx, templ, **args):
333 333 """String. The changeset bisection status."""
334 334 return hbisect.label(repo, ctx.node())
335 335
336 336 @templatekeyword('branch')
337 337 def showbranch(**args):
338 338 """String. The name of the branch on which the changeset was
339 339 committed.
340 340 """
341 341 return args[r'ctx'].branch()
342 342
343 343 @templatekeyword('branches')
344 344 def showbranches(**args):
345 345 """List of strings. The name of the branch on which the
346 346 changeset was committed. Will be empty if the branch name was
347 347 default. (DEPRECATED)
348 348 """
349 349 args = pycompat.byteskwargs(args)
350 350 branch = args['ctx'].branch()
351 351 if branch != 'default':
352 352 return showlist('branch', [branch], args, plural='branches')
353 353 return showlist('branch', [], args, plural='branches')
354 354
355 355 @templatekeyword('bookmarks')
356 356 def showbookmarks(**args):
357 357 """List of strings. Any bookmarks associated with the
358 358 changeset. Also sets 'active', the name of the active bookmark.
359 359 """
360 360 args = pycompat.byteskwargs(args)
361 361 repo = args['ctx']._repo
362 362 bookmarks = args['ctx'].bookmarks()
363 363 active = repo._activebookmark
364 364 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
365 365 f = _showlist('bookmark', bookmarks, args)
366 366 return _hybrid(f, bookmarks, makemap, pycompat.identity)
367 367
368 368 @templatekeyword('children')
369 369 def showchildren(**args):
370 370 """List of strings. The children of the changeset."""
371 371 args = pycompat.byteskwargs(args)
372 372 ctx = args['ctx']
373 373 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
374 374 return showlist('children', childrevs, args, element='child')
375 375
376 376 # Deprecated, but kept alive for help generation a purpose.
377 377 @templatekeyword('currentbookmark')
378 378 def showcurrentbookmark(**args):
379 379 """String. The active bookmark, if it is
380 380 associated with the changeset (DEPRECATED)"""
381 381 return showactivebookmark(**args)
382 382
383 383 @templatekeyword('activebookmark')
384 384 def showactivebookmark(**args):
385 385 """String. The active bookmark, if it is
386 386 associated with the changeset"""
387 387 active = args[r'repo']._activebookmark
388 388 if active and active in args[r'ctx'].bookmarks():
389 389 return active
390 390 return ''
391 391
392 392 @templatekeyword('date')
393 393 def showdate(repo, ctx, templ, **args):
394 394 """Date information. The date when the changeset was committed."""
395 395 return ctx.date()
396 396
397 397 @templatekeyword('desc')
398 398 def showdescription(repo, ctx, templ, **args):
399 399 """String. The text of the changeset description."""
400 400 s = ctx.description()
401 401 if isinstance(s, encoding.localstr):
402 402 # try hard to preserve utf-8 bytes
403 403 return encoding.tolocal(encoding.fromlocal(s).strip())
404 404 else:
405 405 return s.strip()
406 406
407 407 @templatekeyword('diffstat')
408 408 def showdiffstat(repo, ctx, templ, **args):
409 409 """String. Statistics of changes with the following format:
410 410 "modified files: +added/-removed lines"
411 411 """
412 412 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
413 413 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
414 414 return '%s: +%s/-%s' % (len(stats), adds, removes)
415 415
416 416 @templatekeyword('envvars')
417 417 def showenvvars(repo, **args):
418 418 """A dictionary of environment variables. (EXPERIMENTAL)"""
419 419 args = pycompat.byteskwargs(args)
420 420 env = repo.ui.exportableenviron()
421 421 env = util.sortdict((k, env[k]) for k in sorted(env))
422 422 return showdict('envvar', env, args, plural='envvars')
423 423
424 424 @templatekeyword('extras')
425 425 def showextras(**args):
426 426 """List of dicts with key, value entries of the 'extras'
427 427 field of this changeset."""
428 428 args = pycompat.byteskwargs(args)
429 429 extras = args['ctx'].extra()
430 430 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
431 431 makemap = lambda k: {'key': k, 'value': extras[k]}
432 432 c = [makemap(k) for k in extras]
433 433 f = _showlist('extra', c, args, plural='extras')
434 434 return _hybrid(f, extras, makemap,
435 435 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
436 436
437 437 @templatekeyword('file_adds')
438 438 def showfileadds(**args):
439 439 """List of strings. Files added by this changeset."""
440 440 args = pycompat.byteskwargs(args)
441 441 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
442 442 return showlist('file_add', getfiles(repo, ctx, revcache)[1], args,
443 443 element='file')
444 444
445 445 @templatekeyword('file_copies')
446 446 def showfilecopies(**args):
447 447 """List of strings. Files copied in this changeset with
448 448 their sources.
449 449 """
450 450 args = pycompat.byteskwargs(args)
451 451 cache, ctx = args['cache'], args['ctx']
452 452 copies = args['revcache'].get('copies')
453 453 if copies is None:
454 454 if 'getrenamed' not in cache:
455 455 cache['getrenamed'] = getrenamedfn(args['repo'])
456 456 copies = []
457 457 getrenamed = cache['getrenamed']
458 458 for fn in ctx.files():
459 459 rename = getrenamed(fn, ctx.rev())
460 460 if rename:
461 461 copies.append((fn, rename[0]))
462 462
463 463 copies = util.sortdict(copies)
464 464 return showdict('file_copy', copies, args, plural='file_copies',
465 465 key='name', value='source', fmt='%s (%s)')
466 466
467 467 # showfilecopiesswitch() displays file copies only if copy records are
468 468 # provided before calling the templater, usually with a --copies
469 469 # command line switch.
470 470 @templatekeyword('file_copies_switch')
471 471 def showfilecopiesswitch(**args):
472 472 """List of strings. Like "file_copies" but displayed
473 473 only if the --copied switch is set.
474 474 """
475 475 args = pycompat.byteskwargs(args)
476 476 copies = args['revcache'].get('copies') or []
477 477 copies = util.sortdict(copies)
478 478 return showdict('file_copy', copies, args, plural='file_copies',
479 479 key='name', value='source', fmt='%s (%s)')
480 480
481 481 @templatekeyword('file_dels')
482 482 def showfiledels(**args):
483 483 """List of strings. Files removed by this changeset."""
484 484 args = pycompat.byteskwargs(args)
485 485 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
486 486 return showlist('file_del', getfiles(repo, ctx, revcache)[2], args,
487 487 element='file')
488 488
489 489 @templatekeyword('file_mods')
490 490 def showfilemods(**args):
491 491 """List of strings. Files modified by this changeset."""
492 492 args = pycompat.byteskwargs(args)
493 493 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
494 494 return showlist('file_mod', getfiles(repo, ctx, revcache)[0], args,
495 495 element='file')
496 496
497 497 @templatekeyword('files')
498 498 def showfiles(**args):
499 499 """List of strings. All files modified, added, or removed by this
500 500 changeset.
501 501 """
502 502 args = pycompat.byteskwargs(args)
503 503 return showlist('file', args['ctx'].files(), args)
504 504
505 505 @templatekeyword('graphnode')
506 506 def showgraphnode(repo, ctx, **args):
507 507 """String. The character representing the changeset node in
508 508 an ASCII revision graph"""
509 509 wpnodes = repo.dirstate.parents()
510 510 if wpnodes[1] == nullid:
511 511 wpnodes = wpnodes[:1]
512 512 if ctx.node() in wpnodes:
513 513 return '@'
514 514 elif ctx.obsolete():
515 515 return 'x'
516 516 elif ctx.closesbranch():
517 517 return '_'
518 518 else:
519 519 return 'o'
520 520
521 521 @templatekeyword('graphwidth')
522 522 def showgraphwidth(repo, ctx, templ, **args):
523 523 """Integer. The width of the graph drawn by 'log --graph' or zero."""
524 524 # The value args['graphwidth'] will be this function, so we use an internal
525 525 # name to pass the value through props into this function.
526 526 return args.get('_graphwidth', 0)
527 527
528 528 @templatekeyword('index')
529 529 def showindex(**args):
530 530 """Integer. The current iteration of the loop. (0 indexed)"""
531 531 # just hosts documentation; should be overridden by template mapping
532 532 raise error.Abort(_("can't use index in this context"))
533 533
534 534 @templatekeyword('latesttag')
535 535 def showlatesttag(**args):
536 536 """List of strings. The global tags on the most recent globally
537 537 tagged ancestor of this changeset. If no such tags exist, the list
538 538 consists of the single string "null".
539 539 """
540 540 return showlatesttags(None, **args)
541 541
542 542 def showlatesttags(pattern, **args):
543 543 """helper method for the latesttag keyword and function"""
544 544 args = pycompat.byteskwargs(args)
545 545 repo, ctx = args['repo'], args['ctx']
546 546 cache = args['cache']
547 547 latesttags = getlatesttags(repo, ctx, cache, pattern)
548 548
549 549 # latesttag[0] is an implementation detail for sorting csets on different
550 550 # branches in a stable manner- it is the date the tagged cset was created,
551 551 # not the date the tag was created. Therefore it isn't made visible here.
552 552 makemap = lambda v: {
553 553 'changes': _showchangessincetag,
554 554 'distance': latesttags[1],
555 555 'latesttag': v, # BC with {latesttag % '{latesttag}'}
556 556 'tag': v
557 557 }
558 558
559 559 tags = latesttags[2]
560 560 f = _showlist('latesttag', tags, args, separator=':')
561 561 return _hybrid(f, tags, makemap, pycompat.identity)
562 562
563 563 @templatekeyword('latesttagdistance')
564 564 def showlatesttagdistance(repo, ctx, templ, cache, **args):
565 565 """Integer. Longest path to the latest tag."""
566 566 return getlatesttags(repo, ctx, cache)[1]
567 567
568 568 @templatekeyword('changessincelatesttag')
569 569 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
570 570 """Integer. All ancestors not in the latest tag."""
571 571 latesttag = getlatesttags(repo, ctx, cache)[2][0]
572 572
573 573 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
574 574
575 575 def _showchangessincetag(repo, ctx, **args):
576 576 offset = 0
577 577 revs = [ctx.rev()]
578 578 tag = args[r'tag']
579 579
580 580 # The only() revset doesn't currently support wdir()
581 581 if ctx.rev() is None:
582 582 offset = 1
583 583 revs = [p.rev() for p in ctx.parents()]
584 584
585 585 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
586 586
587 587 @templatekeyword('manifest')
588 588 def showmanifest(**args):
589 589 repo, ctx, templ = args[r'repo'], args[r'ctx'], args[r'templ']
590 590 mnode = ctx.manifestnode()
591 591 if mnode is None:
592 592 # just avoid crash, we might want to use the 'ff...' hash in future
593 593 return
594 594 mrev = repo.manifestlog._revlog.rev(mnode)
595 595 mhex = hex(mnode)
596 596 args = args.copy()
597 597 args.update({r'rev': mrev, r'node': mhex})
598 598 f = templ('manifest', **args)
599 599 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
600 600 # rev and node are completely different from changeset's.
601 601 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
602 602
603 603 def shownames(namespace, **args):
604 604 """helper method to generate a template keyword for a namespace"""
605 605 args = pycompat.byteskwargs(args)
606 606 ctx = args['ctx']
607 607 repo = ctx.repo()
608 608 ns = repo.names[namespace]
609 609 names = ns.names(repo, ctx.node())
610 610 return showlist(ns.templatename, names, args, plural=namespace)
611 611
612 612 @templatekeyword('namespaces')
613 613 def shownamespaces(**args):
614 614 """Dict of lists. Names attached to this changeset per
615 615 namespace."""
616 616 args = pycompat.byteskwargs(args)
617 617 ctx = args['ctx']
618 618 repo = ctx.repo()
619 619
620 620 namespaces = util.sortdict()
621 621 colornames = {}
622 622 builtins = {}
623 623
624 624 for k, ns in repo.names.iteritems():
625 625 namespaces[k] = showlist('name', ns.names(repo, ctx.node()), args)
626 626 colornames[k] = ns.colorname
627 627 builtins[k] = ns.builtin
628 628
629 629 f = _showlist('namespace', list(namespaces), args)
630 630
631 631 def makemap(ns):
632 632 return {
633 633 'namespace': ns,
634 634 'names': namespaces[ns],
635 635 'builtin': builtins[ns],
636 636 'colorname': colornames[ns],
637 637 }
638 638
639 639 return _hybrid(f, namespaces, makemap, pycompat.identity)
640 640
641 641 @templatekeyword('node')
642 642 def shownode(repo, ctx, templ, **args):
643 643 """String. The changeset identification hash, as a 40 hexadecimal
644 644 digit string.
645 645 """
646 646 return ctx.hex()
647 647
648 648 @templatekeyword('obsolete')
649 649 def showobsolete(repo, ctx, templ, **args):
650 650 """String. Whether the changeset is obsolete.
651 651 """
652 652 if ctx.obsolete():
653 653 return 'obsolete'
654 654 return ''
655 655
656 656 @templatekeyword('peerpaths')
657 657 def showpeerpaths(repo, **args):
658 658 """A dictionary of repository locations defined in the [paths] section
659 659 of your configuration file. (EXPERIMENTAL)"""
660 660 # see commands.paths() for naming of dictionary keys
661 paths = util.sortdict()
662 for k, p in sorted(repo.ui.paths.iteritems()):
663 d = util.sortdict()
664 d['url'] = p.rawloc
661 paths = repo.ui.paths
662 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
663 def makemap(k):
664 p = paths[k]
665 d = {'name': k, 'url': p.rawloc}
665 666 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
666 def f(d):
667 yield d['url']
668 paths[k] = hybriddict(d, gen=f(d))
669
670 # no hybriddict() since d['path'] can't be formatted as a string. perhaps
671 # hybriddict() should call templatefilters.stringify(d[value]).
672 return _hybrid(None, paths, lambda k: {'name': k, 'path': paths[k]},
673 lambda k: '%s=%s' % (k, paths[k]['url']))
667 return d
668 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
674 669
675 670 @templatekeyword("predecessors")
676 671 def showpredecessors(repo, ctx, **args):
677 672 """Returns the list if the closest visible successors
678 673 """
679 674 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
680 675 predecessors = map(hex, predecessors)
681 676
682 677 return _hybrid(None, predecessors,
683 678 lambda x: {'ctx': repo[x], 'revcache': {}},
684 679 lambda x: scmutil.formatchangeid(repo[x]))
685 680
686 681 @templatekeyword("successorssets")
687 682 def showsuccessorssets(repo, ctx, **args):
688 683 """Returns a string of sets of successors for a changectx
689 684
690 685 Format used is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and
691 686 ctx2 while also diverged into ctx3"""
692 687 if not ctx.obsolete():
693 688 return ''
694 689 args = pycompat.byteskwargs(args)
695 690
696 691 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
697 692 ssets = [[hex(n) for n in ss] for ss in ssets]
698 693
699 694 data = []
700 695 for ss in ssets:
701 696 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
702 697 lambda x: scmutil.formatchangeid(repo[x]))
703 698 data.append(h)
704 699
705 700 # Format the successorssets
706 701 def render(d):
707 702 t = []
708 703 for i in d.gen():
709 704 t.append(i)
710 705 return "".join(t)
711 706
712 707 def gen(data):
713 708 yield "; ".join(render(d) for d in data)
714 709
715 710 return _hybrid(gen(data), data, lambda x: {'successorset': x},
716 711 pycompat.identity)
717 712
718 713 @templatekeyword("succsandmarkers")
719 714 def showsuccsandmarkers(repo, ctx, **args):
720 715 """Returns a list of dict for each final successor of ctx.
721 716
722 717 The dict contains successors node id in "successors" keys and the list of
723 718 obs-markers from ctx to the set of successors in "markers"
724 719
725 720 (EXPERIMENTAL)
726 721 """
727 722
728 723 values = obsutil.successorsandmarkers(repo, ctx)
729 724
730 725 if values is None:
731 726 values = []
732 727
733 728 # Format successors and markers to avoid exposing binary to templates
734 729 data = []
735 730 for i in values:
736 731 # Format successors
737 732 successors = i['successors']
738 733
739 734 successors = [hex(n) for n in successors]
740 735 successors = _hybrid(None, successors,
741 736 lambda x: {'ctx': repo[x], 'revcache': {}},
742 737 lambda x: scmutil.formatchangeid(repo[x]))
743 738
744 739 # Format markers
745 740 finalmarkers = []
746 741 for m in i['markers']:
747 742 hexprec = hex(m[0])
748 743 hexsucs = tuple(hex(n) for n in m[1])
749 744 hexparents = None
750 745 if m[5] is not None:
751 746 hexparents = tuple(hex(n) for n in m[5])
752 747 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
753 748 finalmarkers.append(newmarker)
754 749
755 750 data.append({'successors': successors, 'markers': finalmarkers})
756 751
757 752 f = _showlist('succsandmarkers', data, args)
758 753 return _hybrid(f, data, lambda x: x, pycompat.identity)
759 754
760 755 @templatekeyword('p1rev')
761 756 def showp1rev(repo, ctx, templ, **args):
762 757 """Integer. The repository-local revision number of the changeset's
763 758 first parent, or -1 if the changeset has no parents."""
764 759 return ctx.p1().rev()
765 760
766 761 @templatekeyword('p2rev')
767 762 def showp2rev(repo, ctx, templ, **args):
768 763 """Integer. The repository-local revision number of the changeset's
769 764 second parent, or -1 if the changeset has no second parent."""
770 765 return ctx.p2().rev()
771 766
772 767 @templatekeyword('p1node')
773 768 def showp1node(repo, ctx, templ, **args):
774 769 """String. The identification hash of the changeset's first parent,
775 770 as a 40 digit hexadecimal string. If the changeset has no parents, all
776 771 digits are 0."""
777 772 return ctx.p1().hex()
778 773
779 774 @templatekeyword('p2node')
780 775 def showp2node(repo, ctx, templ, **args):
781 776 """String. The identification hash of the changeset's second
782 777 parent, as a 40 digit hexadecimal string. If the changeset has no second
783 778 parent, all digits are 0."""
784 779 return ctx.p2().hex()
785 780
786 781 @templatekeyword('parents')
787 782 def showparents(**args):
788 783 """List of strings. The parents of the changeset in "rev:node"
789 784 format. If the changeset has only one "natural" parent (the predecessor
790 785 revision) nothing is shown."""
791 786 args = pycompat.byteskwargs(args)
792 787 repo = args['repo']
793 788 ctx = args['ctx']
794 789 pctxs = scmutil.meaningfulparents(repo, ctx)
795 790 # ifcontains() needs a list of str
796 791 prevs = ["%d" % p.rev() for p in pctxs]
797 792 parents = [[('rev', p.rev()),
798 793 ('node', p.hex()),
799 794 ('phase', p.phasestr())]
800 795 for p in pctxs]
801 796 f = _showlist('parent', parents, args)
802 797 return _hybrid(f, prevs, lambda x: {'ctx': repo[int(x)], 'revcache': {}},
803 798 lambda x: scmutil.formatchangeid(repo[int(x)]))
804 799
805 800 @templatekeyword('phase')
806 801 def showphase(repo, ctx, templ, **args):
807 802 """String. The changeset phase name."""
808 803 return ctx.phasestr()
809 804
810 805 @templatekeyword('phaseidx')
811 806 def showphaseidx(repo, ctx, templ, **args):
812 807 """Integer. The changeset phase index."""
813 808 return ctx.phase()
814 809
815 810 @templatekeyword('rev')
816 811 def showrev(repo, ctx, templ, **args):
817 812 """Integer. The repository-local changeset revision number."""
818 813 return scmutil.intrev(ctx)
819 814
820 815 def showrevslist(name, revs, **args):
821 816 """helper to generate a list of revisions in which a mapped template will
822 817 be evaluated"""
823 818 args = pycompat.byteskwargs(args)
824 819 repo = args['ctx'].repo()
825 820 # ifcontains() needs a list of str
826 821 revs = ["%d" % r for r in revs]
827 822 f = _showlist(name, revs, args)
828 823 return _hybrid(f, revs,
829 824 lambda x: {name: x, 'ctx': repo[int(x)], 'revcache': {}},
830 825 pycompat.identity)
831 826
832 827 @templatekeyword('subrepos')
833 828 def showsubrepos(**args):
834 829 """List of strings. Updated subrepositories in the changeset."""
835 830 args = pycompat.byteskwargs(args)
836 831 ctx = args['ctx']
837 832 substate = ctx.substate
838 833 if not substate:
839 834 return showlist('subrepo', [], args)
840 835 psubstate = ctx.parents()[0].substate or {}
841 836 subrepos = []
842 837 for sub in substate:
843 838 if sub not in psubstate or substate[sub] != psubstate[sub]:
844 839 subrepos.append(sub) # modified or newly added in ctx
845 840 for sub in psubstate:
846 841 if sub not in substate:
847 842 subrepos.append(sub) # removed in ctx
848 843 return showlist('subrepo', sorted(subrepos), args)
849 844
850 845 # don't remove "showtags" definition, even though namespaces will put
851 846 # a helper function for "tags" keyword into "keywords" map automatically,
852 847 # because online help text is built without namespaces initialization
853 848 @templatekeyword('tags')
854 849 def showtags(**args):
855 850 """List of strings. Any tags associated with the changeset."""
856 851 return shownames('tags', **args)
857 852
858 853 def loadkeyword(ui, extname, registrarobj):
859 854 """Load template keyword from specified registrarobj
860 855 """
861 856 for name, func in registrarobj._table.iteritems():
862 857 keywords[name] = func
863 858
864 859 @templatekeyword('termwidth')
865 860 def showtermwidth(repo, ctx, templ, **args):
866 861 """Integer. The width of the current terminal."""
867 862 return repo.ui.termwidth()
868 863
869 864 @templatekeyword('troubles')
870 865 def showtroubles(repo, **args):
871 866 """List of strings. Evolution troubles affecting the changeset.
872 867
873 868 (DEPRECATED)
874 869 """
875 870 msg = ("'troubles' is deprecated, "
876 871 "use 'instabilities'")
877 872 repo.ui.deprecwarn(msg, '4.4')
878 873
879 874 return showinstabilities(repo=repo, **args)
880 875
881 876 @templatekeyword('instabilities')
882 877 def showinstabilities(**args):
883 878 """List of strings. Evolution instabilities affecting the changeset.
884 879
885 880 (EXPERIMENTAL)
886 881 """
887 882 args = pycompat.byteskwargs(args)
888 883 return showlist('instability', args['ctx'].instabilities(), args,
889 884 plural='instabilities')
890 885
891 886 # tell hggettext to extract docstrings from these functions:
892 887 i18nfunctions = keywords.values()
@@ -1,222 +1,213 b''
1 1 $ hg init a
2 2 $ hg clone a b
3 3 updating to branch default
4 4 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
5 5 $ cd a
6 6
7 7 with no paths:
8 8
9 9 $ hg paths
10 10 $ hg paths unknown
11 11 not found!
12 12 [1]
13 13 $ hg paths -Tjson
14 14 [
15 15 ]
16 16
17 17 with paths:
18 18
19 19 $ echo '[paths]' >> .hg/hgrc
20 20 $ echo 'dupe = ../b#tip' >> .hg/hgrc
21 21 $ echo 'expand = $SOMETHING/bar' >> .hg/hgrc
22 22 $ hg in dupe
23 23 comparing with $TESTTMP/b (glob)
24 24 no changes found
25 25 [1]
26 26 $ cd ..
27 27 $ hg -R a in dupe
28 28 comparing with $TESTTMP/b (glob)
29 29 no changes found
30 30 [1]
31 31 $ cd a
32 32 $ hg paths
33 33 dupe = $TESTTMP/b#tip (glob)
34 34 expand = $TESTTMP/a/$SOMETHING/bar (glob)
35 35 $ SOMETHING=foo hg paths
36 36 dupe = $TESTTMP/b#tip (glob)
37 37 expand = $TESTTMP/a/foo/bar (glob)
38 38 #if msys
39 39 $ SOMETHING=//foo hg paths
40 40 dupe = $TESTTMP/b#tip (glob)
41 41 expand = /foo/bar
42 42 #else
43 43 $ SOMETHING=/foo hg paths
44 44 dupe = $TESTTMP/b#tip (glob)
45 45 expand = /foo/bar
46 46 #endif
47 47 $ hg paths -q
48 48 dupe
49 49 expand
50 50 $ hg paths dupe
51 51 $TESTTMP/b#tip (glob)
52 52 $ hg paths -q dupe
53 53 $ hg paths unknown
54 54 not found!
55 55 [1]
56 56 $ hg paths -q unknown
57 57 [1]
58 58
59 59 formatter output with paths:
60 60
61 61 $ echo 'dupe:pushurl = https://example.com/dupe' >> .hg/hgrc
62 62 $ hg paths -Tjson | sed 's|\\\\|\\|g'
63 63 [
64 64 {
65 65 "name": "dupe",
66 66 "pushurl": "https://example.com/dupe",
67 67 "url": "$TESTTMP/b#tip" (glob)
68 68 },
69 69 {
70 70 "name": "expand",
71 71 "url": "$TESTTMP/a/$SOMETHING/bar" (glob)
72 72 }
73 73 ]
74 74 $ hg paths -Tjson dupe | sed 's|\\\\|\\|g'
75 75 [
76 76 {
77 77 "name": "dupe",
78 78 "pushurl": "https://example.com/dupe",
79 79 "url": "$TESTTMP/b#tip" (glob)
80 80 }
81 81 ]
82 82 $ hg paths -Tjson -q unknown
83 83 [
84 84 ]
85 85 [1]
86 86
87 87 log template:
88 88
89 89 (behaves as a {name: path-string} dict by default)
90 90
91 91 $ hg log -rnull -T '{peerpaths}\n'
92 92 dupe=$TESTTMP/b#tip expand=$TESTTMP/a/$SOMETHING/bar (glob)
93 93 $ hg log -rnull -T '{join(peerpaths, "\n")}\n'
94 94 dupe=$TESTTMP/b#tip (glob)
95 95 expand=$TESTTMP/a/$SOMETHING/bar (glob)
96 $ hg log -rnull -T '{peerpaths % "{name}: {path}\n"}'
96 $ hg log -rnull -T '{peerpaths % "{name}: {url}\n"}'
97 97 dupe: $TESTTMP/b#tip (glob)
98 98 expand: $TESTTMP/a/$SOMETHING/bar (glob)
99 99 $ hg log -rnull -T '{get(peerpaths, "dupe")}\n'
100 100 $TESTTMP/b#tip (glob)
101 101
102 (but a path is actually a dict of url and sub-options)
102 (sub options can be populated by map/dot operation)
103 103
104 $ hg log -rnull -T '{join(get(peerpaths, "dupe"), "\n")}\n'
105 url=$TESTTMP/b#tip (glob)
106 pushurl=https://example.com/dupe
107 $ hg log -rnull -T '{get(peerpaths, "dupe") % "{key}: {value}\n"}'
104 $ hg log -rnull \
105 > -T '{get(peerpaths, "dupe") % "url: {url}\npushurl: {pushurl}\n"}'
108 106 url: $TESTTMP/b#tip (glob)
109 107 pushurl: https://example.com/dupe
110 $ hg log -rnull -T '{get(get(peerpaths, "dupe"), "pushurl")}\n'
108 $ hg log -rnull -T '{peerpaths.dupe.pushurl}\n'
111 109 https://example.com/dupe
112 110
113 (so there's weird behavior)
114
115 $ hg log -rnull -T '{get(peerpaths, "dupe")|count}\n'
116 2
117 $ hg log -rnull -T '{get(peerpaths, "dupe")|stringify|count}\n'
118 [0-9]{2,} (re)
119
120 (in JSON, it's a dict of dicts)
111 (in JSON, it's a dict of urls)
121 112
122 113 $ hg log -rnull -T '{peerpaths|json}\n' | sed 's|\\\\|/|g'
123 {"dupe": {"pushurl": "https://example.com/dupe", "url": "$TESTTMP/b#tip"}, "expand": {"url": "$TESTTMP/a/$SOMETHING/bar"}}
114 {"dupe": "$TESTTMP/b#tip", "expand": "$TESTTMP/a/$SOMETHING/bar"}
124 115
125 116 password should be masked in plain output, but not in machine-readable/template
126 117 output:
127 118
128 119 $ echo 'insecure = http://foo:insecure@example.com/' >> .hg/hgrc
129 120 $ hg paths insecure
130 121 http://foo:***@example.com/
131 122 $ hg paths -Tjson insecure
132 123 [
133 124 {
134 125 "name": "insecure",
135 126 "url": "http://foo:insecure@example.com/"
136 127 }
137 128 ]
138 129 $ hg log -rnull -T '{get(peerpaths, "insecure")}\n'
139 130 http://foo:insecure@example.com/
140 131
141 132 zeroconf wraps ui.configitems(), which shouldn't crash at least:
142 133
143 134 $ hg paths --config extensions.zeroconf=
144 135 dupe = $TESTTMP/b#tip (glob)
145 136 dupe:pushurl = https://example.com/dupe
146 137 expand = $TESTTMP/a/$SOMETHING/bar (glob)
147 138 insecure = http://foo:***@example.com/
148 139
149 140 $ cd ..
150 141
151 142 sub-options for an undeclared path are ignored
152 143
153 144 $ hg init suboptions
154 145 $ cd suboptions
155 146
156 147 $ cat > .hg/hgrc << EOF
157 148 > [paths]
158 149 > path0 = https://example.com/path0
159 150 > path1:pushurl = https://example.com/path1
160 151 > EOF
161 152 $ hg paths
162 153 path0 = https://example.com/path0
163 154
164 155 unknown sub-options aren't displayed
165 156
166 157 $ cat > .hg/hgrc << EOF
167 158 > [paths]
168 159 > path0 = https://example.com/path0
169 160 > path0:foo = https://example.com/path1
170 161 > EOF
171 162
172 163 $ hg paths
173 164 path0 = https://example.com/path0
174 165
175 166 :pushurl must be a URL
176 167
177 168 $ cat > .hg/hgrc << EOF
178 169 > [paths]
179 170 > default = /path/to/nothing
180 171 > default:pushurl = /not/a/url
181 172 > EOF
182 173
183 174 $ hg paths
184 175 (paths.default:pushurl not a URL; ignoring)
185 176 default = /path/to/nothing
186 177
187 178 #fragment is not allowed in :pushurl
188 179
189 180 $ cat > .hg/hgrc << EOF
190 181 > [paths]
191 182 > default = https://example.com/repo
192 183 > invalid = https://example.com/repo
193 184 > invalid:pushurl = https://example.com/repo#branch
194 185 > EOF
195 186
196 187 $ hg paths
197 188 ("#fragment" in paths.invalid:pushurl not supported; ignoring)
198 189 default = https://example.com/repo
199 190 invalid = https://example.com/repo
200 191 invalid:pushurl = https://example.com/repo
201 192
202 193 $ cd ..
203 194
204 195 'file:' disables [paths] entries for clone destination
205 196
206 197 $ cat >> $HGRCPATH <<EOF
207 198 > [paths]
208 199 > gpath1 = http://hg.example.com
209 200 > EOF
210 201
211 202 $ hg clone a gpath1
212 203 abort: cannot create new http repository
213 204 [255]
214 205
215 206 $ hg clone a file:gpath1
216 207 updating to branch default
217 208 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
218 209 $ cd gpath1
219 210 $ hg -q id
220 211 000000000000
221 212
222 213 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now