##// END OF EJS Templates
templatekw: extract non-templatekw function as getgraphnode()...
Yuya Nishihara -
r36530:6ad140dc default
parent child Browse files
Show More
@@ -1,931 +1,931 b''
1 1 # logcmdutil.py - utility for log-like commands
2 2 #
3 3 # Copyright 2005-2007 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 import itertools
11 11 import os
12 12
13 13 from .i18n import _
14 14 from .node import (
15 15 hex,
16 16 nullid,
17 17 )
18 18
19 19 from . import (
20 20 dagop,
21 21 encoding,
22 22 error,
23 23 formatter,
24 24 graphmod,
25 25 match as matchmod,
26 26 mdiff,
27 27 patch,
28 28 pathutil,
29 29 pycompat,
30 30 revset,
31 31 revsetlang,
32 32 scmutil,
33 33 smartset,
34 34 templatekw,
35 35 templater,
36 36 util,
37 37 )
38 38
39 39 def getlimit(opts):
40 40 """get the log limit according to option -l/--limit"""
41 41 limit = opts.get('limit')
42 42 if limit:
43 43 try:
44 44 limit = int(limit)
45 45 except ValueError:
46 46 raise error.Abort(_('limit must be a positive integer'))
47 47 if limit <= 0:
48 48 raise error.Abort(_('limit must be positive'))
49 49 else:
50 50 limit = None
51 51 return limit
52 52
53 53 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
54 54 changes=None, stat=False, fp=None, prefix='',
55 55 root='', listsubrepos=False, hunksfilterfn=None):
56 56 '''show diff or diffstat.'''
57 57 if root:
58 58 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
59 59 else:
60 60 relroot = ''
61 61 if relroot != '':
62 62 # XXX relative roots currently don't work if the root is within a
63 63 # subrepo
64 64 uirelroot = match.uipath(relroot)
65 65 relroot += '/'
66 66 for matchroot in match.files():
67 67 if not matchroot.startswith(relroot):
68 68 ui.warn(_('warning: %s not inside relative root %s\n') % (
69 69 match.uipath(matchroot), uirelroot))
70 70
71 71 if stat:
72 72 diffopts = diffopts.copy(context=0, noprefix=False)
73 73 width = 80
74 74 if not ui.plain():
75 75 width = ui.termwidth()
76 76
77 77 chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
78 78 prefix=prefix, relroot=relroot,
79 79 hunksfilterfn=hunksfilterfn)
80 80
81 81 if fp is not None or ui.canwritewithoutlabels():
82 82 out = fp or ui
83 83 if stat:
84 84 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
85 85 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
86 86 out.write(chunk)
87 87 else:
88 88 if stat:
89 89 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
90 90 else:
91 91 chunks = patch.difflabel(lambda chunks, **kwargs: chunks, chunks,
92 92 opts=diffopts)
93 93 if ui.canbatchlabeledwrites():
94 94 def gen():
95 95 for chunk, label in chunks:
96 96 yield ui.label(chunk, label=label)
97 97 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
98 98 ui.write(chunk)
99 99 else:
100 100 for chunk, label in chunks:
101 101 ui.write(chunk, label=label)
102 102
103 103 if listsubrepos:
104 104 ctx1 = repo[node1]
105 105 ctx2 = repo[node2]
106 106 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
107 107 tempnode2 = node2
108 108 try:
109 109 if node2 is not None:
110 110 tempnode2 = ctx2.substate[subpath][1]
111 111 except KeyError:
112 112 # A subrepo that existed in node1 was deleted between node1 and
113 113 # node2 (inclusive). Thus, ctx2's substate won't contain that
114 114 # subpath. The best we can do is to ignore it.
115 115 tempnode2 = None
116 116 submatch = matchmod.subdirmatcher(subpath, match)
117 117 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
118 118 stat=stat, fp=fp, prefix=prefix)
119 119
120 120 class changesetdiffer(object):
121 121 """Generate diff of changeset with pre-configured filtering functions"""
122 122
123 123 def _makefilematcher(self, ctx):
124 124 return scmutil.matchall(ctx.repo())
125 125
126 126 def _makehunksfilter(self, ctx):
127 127 return None
128 128
129 129 def showdiff(self, ui, ctx, diffopts, stat=False):
130 130 repo = ctx.repo()
131 131 node = ctx.node()
132 132 prev = ctx.p1().node()
133 133 diffordiffstat(ui, repo, diffopts, prev, node,
134 134 match=self._makefilematcher(ctx), stat=stat,
135 135 hunksfilterfn=self._makehunksfilter(ctx))
136 136
137 137 def changesetlabels(ctx):
138 138 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
139 139 if ctx.obsolete():
140 140 labels.append('changeset.obsolete')
141 141 if ctx.isunstable():
142 142 labels.append('changeset.unstable')
143 143 for instability in ctx.instabilities():
144 144 labels.append('instability.%s' % instability)
145 145 return ' '.join(labels)
146 146
147 147 class changesetprinter(object):
148 148 '''show changeset information when templating not requested.'''
149 149
150 150 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
151 151 self.ui = ui
152 152 self.repo = repo
153 153 self.buffered = buffered
154 154 self._differ = differ or changesetdiffer()
155 155 self.diffopts = diffopts or {}
156 156 self.header = {}
157 157 self.hunk = {}
158 158 self.lastheader = None
159 159 self.footer = None
160 160 self._columns = templatekw.getlogcolumns()
161 161
162 162 def flush(self, ctx):
163 163 rev = ctx.rev()
164 164 if rev in self.header:
165 165 h = self.header[rev]
166 166 if h != self.lastheader:
167 167 self.lastheader = h
168 168 self.ui.write(h)
169 169 del self.header[rev]
170 170 if rev in self.hunk:
171 171 self.ui.write(self.hunk[rev])
172 172 del self.hunk[rev]
173 173
174 174 def close(self):
175 175 if self.footer:
176 176 self.ui.write(self.footer)
177 177
178 178 def show(self, ctx, copies=None, **props):
179 179 props = pycompat.byteskwargs(props)
180 180 if self.buffered:
181 181 self.ui.pushbuffer(labeled=True)
182 182 self._show(ctx, copies, props)
183 183 self.hunk[ctx.rev()] = self.ui.popbuffer()
184 184 else:
185 185 self._show(ctx, copies, props)
186 186
187 187 def _show(self, ctx, copies, props):
188 188 '''show a single changeset or file revision'''
189 189 changenode = ctx.node()
190 190 rev = ctx.rev()
191 191
192 192 if self.ui.quiet:
193 193 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
194 194 label='log.node')
195 195 return
196 196
197 197 columns = self._columns
198 198 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
199 199 label=changesetlabels(ctx))
200 200
201 201 # branches are shown first before any other names due to backwards
202 202 # compatibility
203 203 branch = ctx.branch()
204 204 # don't show the default branch name
205 205 if branch != 'default':
206 206 self.ui.write(columns['branch'] % branch, label='log.branch')
207 207
208 208 for nsname, ns in self.repo.names.iteritems():
209 209 # branches has special logic already handled above, so here we just
210 210 # skip it
211 211 if nsname == 'branches':
212 212 continue
213 213 # we will use the templatename as the color name since those two
214 214 # should be the same
215 215 for name in ns.names(self.repo, changenode):
216 216 self.ui.write(ns.logfmt % name,
217 217 label='log.%s' % ns.colorname)
218 218 if self.ui.debugflag:
219 219 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
220 220 for pctx in scmutil.meaningfulparents(self.repo, ctx):
221 221 label = 'log.parent changeset.%s' % pctx.phasestr()
222 222 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
223 223 label=label)
224 224
225 225 if self.ui.debugflag and rev is not None:
226 226 mnode = ctx.manifestnode()
227 227 mrev = self.repo.manifestlog._revlog.rev(mnode)
228 228 self.ui.write(columns['manifest']
229 229 % scmutil.formatrevnode(self.ui, mrev, mnode),
230 230 label='ui.debug log.manifest')
231 231 self.ui.write(columns['user'] % ctx.user(), label='log.user')
232 232 self.ui.write(columns['date'] % util.datestr(ctx.date()),
233 233 label='log.date')
234 234
235 235 if ctx.isunstable():
236 236 instabilities = ctx.instabilities()
237 237 self.ui.write(columns['instability'] % ', '.join(instabilities),
238 238 label='log.instability')
239 239
240 240 elif ctx.obsolete():
241 241 self._showobsfate(ctx)
242 242
243 243 self._exthook(ctx)
244 244
245 245 if self.ui.debugflag:
246 246 files = ctx.p1().status(ctx)[:3]
247 247 for key, value in zip(['files', 'files+', 'files-'], files):
248 248 if value:
249 249 self.ui.write(columns[key] % " ".join(value),
250 250 label='ui.debug log.files')
251 251 elif ctx.files() and self.ui.verbose:
252 252 self.ui.write(columns['files'] % " ".join(ctx.files()),
253 253 label='ui.note log.files')
254 254 if copies and self.ui.verbose:
255 255 copies = ['%s (%s)' % c for c in copies]
256 256 self.ui.write(columns['copies'] % ' '.join(copies),
257 257 label='ui.note log.copies')
258 258
259 259 extra = ctx.extra()
260 260 if extra and self.ui.debugflag:
261 261 for key, value in sorted(extra.items()):
262 262 self.ui.write(columns['extra'] % (key, util.escapestr(value)),
263 263 label='ui.debug log.extra')
264 264
265 265 description = ctx.description().strip()
266 266 if description:
267 267 if self.ui.verbose:
268 268 self.ui.write(_("description:\n"),
269 269 label='ui.note log.description')
270 270 self.ui.write(description,
271 271 label='ui.note log.description')
272 272 self.ui.write("\n\n")
273 273 else:
274 274 self.ui.write(columns['summary'] % description.splitlines()[0],
275 275 label='log.summary')
276 276 self.ui.write("\n")
277 277
278 278 self._showpatch(ctx)
279 279
280 280 def _showobsfate(self, ctx):
281 281 obsfate = templatekw.showobsfate(repo=self.repo, ctx=ctx, ui=self.ui)
282 282
283 283 if obsfate:
284 284 for obsfateline in obsfate:
285 285 self.ui.write(self._columns['obsolete'] % obsfateline,
286 286 label='log.obsfate')
287 287
288 288 def _exthook(self, ctx):
289 289 '''empty method used by extension as a hook point
290 290 '''
291 291
292 292 def _showpatch(self, ctx):
293 293 stat = self.diffopts.get('stat')
294 294 diff = self.diffopts.get('patch')
295 295 diffopts = patch.diffallopts(self.ui, self.diffopts)
296 296 if stat:
297 297 self._differ.showdiff(self.ui, ctx, diffopts, stat=True)
298 298 if stat and diff:
299 299 self.ui.write("\n")
300 300 if diff:
301 301 self._differ.showdiff(self.ui, ctx, diffopts, stat=False)
302 302 if stat or diff:
303 303 self.ui.write("\n")
304 304
305 305 class jsonchangeset(changesetprinter):
306 306 '''format changeset information.'''
307 307
308 308 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
309 309 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
310 310 self.cache = {}
311 311 self._first = True
312 312
313 313 def close(self):
314 314 if not self._first:
315 315 self.ui.write("\n]\n")
316 316 else:
317 317 self.ui.write("[]\n")
318 318
319 319 def _show(self, ctx, copies, props):
320 320 '''show a single changeset or file revision'''
321 321 rev = ctx.rev()
322 322 if rev is None:
323 323 jrev = jnode = 'null'
324 324 else:
325 325 jrev = '%d' % rev
326 326 jnode = '"%s"' % hex(ctx.node())
327 327 j = encoding.jsonescape
328 328
329 329 if self._first:
330 330 self.ui.write("[\n {")
331 331 self._first = False
332 332 else:
333 333 self.ui.write(",\n {")
334 334
335 335 if self.ui.quiet:
336 336 self.ui.write(('\n "rev": %s') % jrev)
337 337 self.ui.write((',\n "node": %s') % jnode)
338 338 self.ui.write('\n }')
339 339 return
340 340
341 341 self.ui.write(('\n "rev": %s') % jrev)
342 342 self.ui.write((',\n "node": %s') % jnode)
343 343 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
344 344 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
345 345 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
346 346 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
347 347 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
348 348
349 349 self.ui.write((',\n "bookmarks": [%s]') %
350 350 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
351 351 self.ui.write((',\n "tags": [%s]') %
352 352 ", ".join('"%s"' % j(t) for t in ctx.tags()))
353 353 self.ui.write((',\n "parents": [%s]') %
354 354 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
355 355
356 356 if self.ui.debugflag:
357 357 if rev is None:
358 358 jmanifestnode = 'null'
359 359 else:
360 360 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
361 361 self.ui.write((',\n "manifest": %s') % jmanifestnode)
362 362
363 363 self.ui.write((',\n "extra": {%s}') %
364 364 ", ".join('"%s": "%s"' % (j(k), j(v))
365 365 for k, v in ctx.extra().items()))
366 366
367 367 files = ctx.p1().status(ctx)
368 368 self.ui.write((',\n "modified": [%s]') %
369 369 ", ".join('"%s"' % j(f) for f in files[0]))
370 370 self.ui.write((',\n "added": [%s]') %
371 371 ", ".join('"%s"' % j(f) for f in files[1]))
372 372 self.ui.write((',\n "removed": [%s]') %
373 373 ", ".join('"%s"' % j(f) for f in files[2]))
374 374
375 375 elif self.ui.verbose:
376 376 self.ui.write((',\n "files": [%s]') %
377 377 ", ".join('"%s"' % j(f) for f in ctx.files()))
378 378
379 379 if copies:
380 380 self.ui.write((',\n "copies": {%s}') %
381 381 ", ".join('"%s": "%s"' % (j(k), j(v))
382 382 for k, v in copies))
383 383
384 384 stat = self.diffopts.get('stat')
385 385 diff = self.diffopts.get('patch')
386 386 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
387 387 if stat:
388 388 self.ui.pushbuffer()
389 389 self._differ.showdiff(self.ui, ctx, diffopts, stat=True)
390 390 self.ui.write((',\n "diffstat": "%s"')
391 391 % j(self.ui.popbuffer()))
392 392 if diff:
393 393 self.ui.pushbuffer()
394 394 self._differ.showdiff(self.ui, ctx, diffopts, stat=False)
395 395 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
396 396
397 397 self.ui.write("\n }")
398 398
399 399 class changesettemplater(changesetprinter):
400 400 '''format changeset information.
401 401
402 402 Note: there are a variety of convenience functions to build a
403 403 changesettemplater for common cases. See functions such as:
404 404 maketemplater, changesetdisplayer, buildcommittemplate, or other
405 405 functions that use changesest_templater.
406 406 '''
407 407
408 408 # Arguments before "buffered" used to be positional. Consider not
409 409 # adding/removing arguments before "buffered" to not break callers.
410 410 def __init__(self, ui, repo, tmplspec, differ=None, diffopts=None,
411 411 buffered=False):
412 412 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
413 413 tres = formatter.templateresources(ui, repo)
414 414 self.t = formatter.loadtemplater(ui, tmplspec,
415 415 defaults=templatekw.keywords,
416 416 resources=tres,
417 417 cache=templatekw.defaulttempl)
418 418 self._counter = itertools.count()
419 419 self.cache = tres['cache'] # shared with _graphnodeformatter()
420 420
421 421 self._tref = tmplspec.ref
422 422 self._parts = {'header': '', 'footer': '',
423 423 tmplspec.ref: tmplspec.ref,
424 424 'docheader': '', 'docfooter': '',
425 425 'separator': ''}
426 426 if tmplspec.mapfile:
427 427 # find correct templates for current mode, for backward
428 428 # compatibility with 'log -v/-q/--debug' using a mapfile
429 429 tmplmodes = [
430 430 (True, ''),
431 431 (self.ui.verbose, '_verbose'),
432 432 (self.ui.quiet, '_quiet'),
433 433 (self.ui.debugflag, '_debug'),
434 434 ]
435 435 for mode, postfix in tmplmodes:
436 436 for t in self._parts:
437 437 cur = t + postfix
438 438 if mode and cur in self.t:
439 439 self._parts[t] = cur
440 440 else:
441 441 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
442 442 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
443 443 self._parts.update(m)
444 444
445 445 if self._parts['docheader']:
446 446 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
447 447
448 448 def close(self):
449 449 if self._parts['docfooter']:
450 450 if not self.footer:
451 451 self.footer = ""
452 452 self.footer += templater.stringify(self.t(self._parts['docfooter']))
453 453 return super(changesettemplater, self).close()
454 454
455 455 def _show(self, ctx, copies, props):
456 456 '''show a single changeset or file revision'''
457 457 props = props.copy()
458 458 props['ctx'] = ctx
459 459 props['index'] = index = next(self._counter)
460 460 props['revcache'] = {'copies': copies}
461 461 props = pycompat.strkwargs(props)
462 462
463 463 # write separator, which wouldn't work well with the header part below
464 464 # since there's inherently a conflict between header (across items) and
465 465 # separator (per item)
466 466 if self._parts['separator'] and index > 0:
467 467 self.ui.write(templater.stringify(self.t(self._parts['separator'])))
468 468
469 469 # write header
470 470 if self._parts['header']:
471 471 h = templater.stringify(self.t(self._parts['header'], **props))
472 472 if self.buffered:
473 473 self.header[ctx.rev()] = h
474 474 else:
475 475 if self.lastheader != h:
476 476 self.lastheader = h
477 477 self.ui.write(h)
478 478
479 479 # write changeset metadata, then patch if requested
480 480 key = self._parts[self._tref]
481 481 self.ui.write(templater.stringify(self.t(key, **props)))
482 482 self._showpatch(ctx)
483 483
484 484 if self._parts['footer']:
485 485 if not self.footer:
486 486 self.footer = templater.stringify(
487 487 self.t(self._parts['footer'], **props))
488 488
489 489 def templatespec(tmpl, mapfile):
490 490 if mapfile:
491 491 return formatter.templatespec('changeset', tmpl, mapfile)
492 492 else:
493 493 return formatter.templatespec('', tmpl, None)
494 494
495 495 def _lookuptemplate(ui, tmpl, style):
496 496 """Find the template matching the given template spec or style
497 497
498 498 See formatter.lookuptemplate() for details.
499 499 """
500 500
501 501 # ui settings
502 502 if not tmpl and not style: # template are stronger than style
503 503 tmpl = ui.config('ui', 'logtemplate')
504 504 if tmpl:
505 505 return templatespec(templater.unquotestring(tmpl), None)
506 506 else:
507 507 style = util.expandpath(ui.config('ui', 'style'))
508 508
509 509 if not tmpl and style:
510 510 mapfile = style
511 511 if not os.path.split(mapfile)[0]:
512 512 mapname = (templater.templatepath('map-cmdline.' + mapfile)
513 513 or templater.templatepath(mapfile))
514 514 if mapname:
515 515 mapfile = mapname
516 516 return templatespec(None, mapfile)
517 517
518 518 if not tmpl:
519 519 return templatespec(None, None)
520 520
521 521 return formatter.lookuptemplate(ui, 'changeset', tmpl)
522 522
523 523 def maketemplater(ui, repo, tmpl, buffered=False):
524 524 """Create a changesettemplater from a literal template 'tmpl'
525 525 byte-string."""
526 526 spec = templatespec(tmpl, None)
527 527 return changesettemplater(ui, repo, spec, buffered=buffered)
528 528
529 529 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
530 530 """show one changeset using template or regular display.
531 531
532 532 Display format will be the first non-empty hit of:
533 533 1. option 'template'
534 534 2. option 'style'
535 535 3. [ui] setting 'logtemplate'
536 536 4. [ui] setting 'style'
537 537 If all of these values are either the unset or the empty string,
538 538 regular display via changesetprinter() is done.
539 539 """
540 540 postargs = (differ, opts, buffered)
541 541 if opts.get('template') == 'json':
542 542 return jsonchangeset(ui, repo, *postargs)
543 543
544 544 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
545 545
546 546 if not spec.ref and not spec.tmpl and not spec.mapfile:
547 547 return changesetprinter(ui, repo, *postargs)
548 548
549 549 return changesettemplater(ui, repo, spec, *postargs)
550 550
551 551 def _makematcher(repo, revs, pats, opts):
552 552 """Build matcher and expanded patterns from log options
553 553
554 554 If --follow, revs are the revisions to follow from.
555 555
556 556 Returns (match, pats, slowpath) where
557 557 - match: a matcher built from the given pats and -I/-X opts
558 558 - pats: patterns used (globs are expanded on Windows)
559 559 - slowpath: True if patterns aren't as simple as scanning filelogs
560 560 """
561 561 # pats/include/exclude are passed to match.match() directly in
562 562 # _matchfiles() revset but walkchangerevs() builds its matcher with
563 563 # scmutil.match(). The difference is input pats are globbed on
564 564 # platforms without shell expansion (windows).
565 565 wctx = repo[None]
566 566 match, pats = scmutil.matchandpats(wctx, pats, opts)
567 567 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
568 568 if not slowpath:
569 569 follow = opts.get('follow') or opts.get('follow_first')
570 570 startctxs = []
571 571 if follow and opts.get('rev'):
572 572 startctxs = [repo[r] for r in revs]
573 573 for f in match.files():
574 574 if follow and startctxs:
575 575 # No idea if the path was a directory at that revision, so
576 576 # take the slow path.
577 577 if any(f not in c for c in startctxs):
578 578 slowpath = True
579 579 continue
580 580 elif follow and f not in wctx:
581 581 # If the file exists, it may be a directory, so let it
582 582 # take the slow path.
583 583 if os.path.exists(repo.wjoin(f)):
584 584 slowpath = True
585 585 continue
586 586 else:
587 587 raise error.Abort(_('cannot follow file not in parent '
588 588 'revision: "%s"') % f)
589 589 filelog = repo.file(f)
590 590 if not filelog:
591 591 # A zero count may be a directory or deleted file, so
592 592 # try to find matching entries on the slow path.
593 593 if follow:
594 594 raise error.Abort(
595 595 _('cannot follow nonexistent file: "%s"') % f)
596 596 slowpath = True
597 597
598 598 # We decided to fall back to the slowpath because at least one
599 599 # of the paths was not a file. Check to see if at least one of them
600 600 # existed in history - in that case, we'll continue down the
601 601 # slowpath; otherwise, we can turn off the slowpath
602 602 if slowpath:
603 603 for path in match.files():
604 604 if path == '.' or path in repo.store:
605 605 break
606 606 else:
607 607 slowpath = False
608 608
609 609 return match, pats, slowpath
610 610
611 611 def _fileancestors(repo, revs, match, followfirst):
612 612 fctxs = []
613 613 for r in revs:
614 614 ctx = repo[r]
615 615 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
616 616
617 617 # When displaying a revision with --patch --follow FILE, we have
618 618 # to know which file of the revision must be diffed. With
619 619 # --follow, we want the names of the ancestors of FILE in the
620 620 # revision, stored in "fcache". "fcache" is populated as a side effect
621 621 # of the graph traversal.
622 622 fcache = {}
623 623 def filematcher(ctx):
624 624 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
625 625
626 626 def revgen():
627 627 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
628 628 fcache[rev] = [c.path() for c in cs]
629 629 yield rev
630 630 return smartset.generatorset(revgen(), iterasc=False), filematcher
631 631
632 632 def _makenofollowfilematcher(repo, pats, opts):
633 633 '''hook for extensions to override the filematcher for non-follow cases'''
634 634 return None
635 635
636 636 _opt2logrevset = {
637 637 'no_merges': ('not merge()', None),
638 638 'only_merges': ('merge()', None),
639 639 '_matchfiles': (None, '_matchfiles(%ps)'),
640 640 'date': ('date(%s)', None),
641 641 'branch': ('branch(%s)', '%lr'),
642 642 '_patslog': ('filelog(%s)', '%lr'),
643 643 'keyword': ('keyword(%s)', '%lr'),
644 644 'prune': ('ancestors(%s)', 'not %lr'),
645 645 'user': ('user(%s)', '%lr'),
646 646 }
647 647
648 648 def _makerevset(repo, match, pats, slowpath, opts):
649 649 """Return a revset string built from log options and file patterns"""
650 650 opts = dict(opts)
651 651 # follow or not follow?
652 652 follow = opts.get('follow') or opts.get('follow_first')
653 653
654 654 # branch and only_branch are really aliases and must be handled at
655 655 # the same time
656 656 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
657 657 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
658 658
659 659 if slowpath:
660 660 # See walkchangerevs() slow path.
661 661 #
662 662 # pats/include/exclude cannot be represented as separate
663 663 # revset expressions as their filtering logic applies at file
664 664 # level. For instance "-I a -X b" matches a revision touching
665 665 # "a" and "b" while "file(a) and not file(b)" does
666 666 # not. Besides, filesets are evaluated against the working
667 667 # directory.
668 668 matchargs = ['r:', 'd:relpath']
669 669 for p in pats:
670 670 matchargs.append('p:' + p)
671 671 for p in opts.get('include', []):
672 672 matchargs.append('i:' + p)
673 673 for p in opts.get('exclude', []):
674 674 matchargs.append('x:' + p)
675 675 opts['_matchfiles'] = matchargs
676 676 elif not follow:
677 677 opts['_patslog'] = list(pats)
678 678
679 679 expr = []
680 680 for op, val in sorted(opts.iteritems()):
681 681 if not val:
682 682 continue
683 683 if op not in _opt2logrevset:
684 684 continue
685 685 revop, listop = _opt2logrevset[op]
686 686 if revop and '%' not in revop:
687 687 expr.append(revop)
688 688 elif not listop:
689 689 expr.append(revsetlang.formatspec(revop, val))
690 690 else:
691 691 if revop:
692 692 val = [revsetlang.formatspec(revop, v) for v in val]
693 693 expr.append(revsetlang.formatspec(listop, val))
694 694
695 695 if expr:
696 696 expr = '(' + ' and '.join(expr) + ')'
697 697 else:
698 698 expr = None
699 699 return expr
700 700
701 701 def _initialrevs(repo, opts):
702 702 """Return the initial set of revisions to be filtered or followed"""
703 703 follow = opts.get('follow') or opts.get('follow_first')
704 704 if opts.get('rev'):
705 705 revs = scmutil.revrange(repo, opts['rev'])
706 706 elif follow and repo.dirstate.p1() == nullid:
707 707 revs = smartset.baseset()
708 708 elif follow:
709 709 revs = repo.revs('.')
710 710 else:
711 711 revs = smartset.spanset(repo)
712 712 revs.reverse()
713 713 return revs
714 714
715 715 def getrevs(repo, pats, opts):
716 716 """Return (revs, differ) where revs is a smartset
717 717
718 718 differ is a changesetdiffer with pre-configured file matcher.
719 719 """
720 720 follow = opts.get('follow') or opts.get('follow_first')
721 721 followfirst = opts.get('follow_first')
722 722 limit = getlimit(opts)
723 723 revs = _initialrevs(repo, opts)
724 724 if not revs:
725 725 return smartset.baseset(), None
726 726 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
727 727 filematcher = None
728 728 if follow:
729 729 if slowpath or match.always():
730 730 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
731 731 else:
732 732 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
733 733 revs.reverse()
734 734 if filematcher is None:
735 735 filematcher = _makenofollowfilematcher(repo, pats, opts)
736 736 if filematcher is None:
737 737 def filematcher(ctx):
738 738 return match
739 739
740 740 expr = _makerevset(repo, match, pats, slowpath, opts)
741 741 if opts.get('graph') and opts.get('rev'):
742 742 # User-specified revs might be unsorted, but don't sort before
743 743 # _makerevset because it might depend on the order of revs
744 744 if not (revs.isdescending() or revs.istopo()):
745 745 revs.sort(reverse=True)
746 746 if expr:
747 747 matcher = revset.match(None, expr)
748 748 revs = matcher(repo, revs)
749 749 if limit is not None:
750 750 revs = revs.slice(0, limit)
751 751
752 752 differ = changesetdiffer()
753 753 differ._makefilematcher = filematcher
754 754 return revs, differ
755 755
756 756 def _parselinerangeopt(repo, opts):
757 757 """Parse --line-range log option and return a list of tuples (filename,
758 758 (fromline, toline)).
759 759 """
760 760 linerangebyfname = []
761 761 for pat in opts.get('line_range', []):
762 762 try:
763 763 pat, linerange = pat.rsplit(',', 1)
764 764 except ValueError:
765 765 raise error.Abort(_('malformatted line-range pattern %s') % pat)
766 766 try:
767 767 fromline, toline = map(int, linerange.split(':'))
768 768 except ValueError:
769 769 raise error.Abort(_("invalid line range for %s") % pat)
770 770 msg = _("line range pattern '%s' must match exactly one file") % pat
771 771 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
772 772 linerangebyfname.append(
773 773 (fname, util.processlinerange(fromline, toline)))
774 774 return linerangebyfname
775 775
776 776 def getlinerangerevs(repo, userrevs, opts):
777 777 """Return (revs, differ).
778 778
779 779 "revs" are revisions obtained by processing "line-range" log options and
780 780 walking block ancestors of each specified file/line-range.
781 781
782 782 "differ" is a changesetdiffer with pre-configured file matcher and hunks
783 783 filter.
784 784 """
785 785 wctx = repo[None]
786 786
787 787 # Two-levels map of "rev -> file ctx -> [line range]".
788 788 linerangesbyrev = {}
789 789 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
790 790 if fname not in wctx:
791 791 raise error.Abort(_('cannot follow file not in parent '
792 792 'revision: "%s"') % fname)
793 793 fctx = wctx.filectx(fname)
794 794 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
795 795 rev = fctx.introrev()
796 796 if rev not in userrevs:
797 797 continue
798 798 linerangesbyrev.setdefault(
799 799 rev, {}).setdefault(
800 800 fctx.path(), []).append(linerange)
801 801
802 802 def nofilterhunksfn(fctx, hunks):
803 803 return hunks
804 804
805 805 def hunksfilter(ctx):
806 806 fctxlineranges = linerangesbyrev.get(ctx.rev())
807 807 if fctxlineranges is None:
808 808 return nofilterhunksfn
809 809
810 810 def filterfn(fctx, hunks):
811 811 lineranges = fctxlineranges.get(fctx.path())
812 812 if lineranges is not None:
813 813 for hr, lines in hunks:
814 814 if hr is None: # binary
815 815 yield hr, lines
816 816 continue
817 817 if any(mdiff.hunkinrange(hr[2:], lr)
818 818 for lr in lineranges):
819 819 yield hr, lines
820 820 else:
821 821 for hunk in hunks:
822 822 yield hunk
823 823
824 824 return filterfn
825 825
826 826 def filematcher(ctx):
827 827 files = list(linerangesbyrev.get(ctx.rev(), []))
828 828 return scmutil.matchfiles(repo, files)
829 829
830 830 revs = sorted(linerangesbyrev, reverse=True)
831 831
832 832 differ = changesetdiffer()
833 833 differ._makefilematcher = filematcher
834 834 differ._makehunksfilter = hunksfilter
835 835 return revs, differ
836 836
837 837 def _graphnodeformatter(ui, displayer):
838 838 spec = ui.config('ui', 'graphnodetemplate')
839 839 if not spec:
840 return templatekw.showgraphnode # fast path for "{graphnode}"
840 return templatekw.getgraphnode # fast path for "{graphnode}"
841 841
842 842 spec = templater.unquotestring(spec)
843 843 tres = formatter.templateresources(ui)
844 844 if isinstance(displayer, changesettemplater):
845 845 tres['cache'] = displayer.cache # reuse cache of slow templates
846 846 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
847 847 resources=tres)
848 848 def formatnode(repo, ctx):
849 849 props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
850 850 return templ.render(props)
851 851 return formatnode
852 852
853 853 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None, props=None):
854 854 props = props or {}
855 855 formatnode = _graphnodeformatter(ui, displayer)
856 856 state = graphmod.asciistate()
857 857 styles = state['styles']
858 858
859 859 # only set graph styling if HGPLAIN is not set.
860 860 if ui.plain('graph'):
861 861 # set all edge styles to |, the default pre-3.8 behaviour
862 862 styles.update(dict.fromkeys(styles, '|'))
863 863 else:
864 864 edgetypes = {
865 865 'parent': graphmod.PARENT,
866 866 'grandparent': graphmod.GRANDPARENT,
867 867 'missing': graphmod.MISSINGPARENT
868 868 }
869 869 for name, key in edgetypes.items():
870 870 # experimental config: experimental.graphstyle.*
871 871 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
872 872 styles[key])
873 873 if not styles[key]:
874 874 styles[key] = None
875 875
876 876 # experimental config: experimental.graphshorten
877 877 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
878 878
879 879 for rev, type, ctx, parents in dag:
880 880 char = formatnode(repo, ctx)
881 881 copies = None
882 882 if getrenamed and ctx.rev():
883 883 copies = []
884 884 for fn in ctx.files():
885 885 rename = getrenamed(fn, ctx.rev())
886 886 if rename:
887 887 copies.append((fn, rename[0]))
888 888 edges = edgefn(type, char, state, rev, parents)
889 889 firstedge = next(edges)
890 890 width = firstedge[2]
891 891 displayer.show(ctx, copies=copies,
892 892 graphwidth=width, **pycompat.strkwargs(props))
893 893 lines = displayer.hunk.pop(rev).split('\n')
894 894 if not lines[-1]:
895 895 del lines[-1]
896 896 displayer.flush(ctx)
897 897 for type, char, width, coldata in itertools.chain([firstedge], edges):
898 898 graphmod.ascii(ui, state, type, char, lines, coldata)
899 899 lines = []
900 900 displayer.close()
901 901
902 902 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
903 903 revdag = graphmod.dagwalker(repo, revs)
904 904 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
905 905
906 906 def displayrevs(ui, repo, revs, displayer, getrenamed):
907 907 for rev in revs:
908 908 ctx = repo[rev]
909 909 copies = None
910 910 if getrenamed is not None and rev:
911 911 copies = []
912 912 for fn in ctx.files():
913 913 rename = getrenamed(fn, rev)
914 914 if rename:
915 915 copies.append((fn, rename[0]))
916 916 displayer.show(ctx, copies=copies)
917 917 displayer.flush(ctx)
918 918 displayer.close()
919 919
920 920 def checkunsupportedgraphflags(pats, opts):
921 921 for op in ["newest_first"]:
922 922 if op in opts and opts[op]:
923 923 raise error.Abort(_("-G/--graph option is incompatible with --%s")
924 924 % op.replace("_", "-"))
925 925
926 926 def graphrevs(repo, nodes, opts):
927 927 limit = getlimit(opts)
928 928 nodes.reverse()
929 929 if limit is not None:
930 930 nodes = nodes[:limit]
931 931 return graphmod.nodes(repo, nodes)
@@ -1,926 +1,929 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 i18n,
21 21 obsutil,
22 22 patch,
23 23 pycompat,
24 24 registrar,
25 25 scmutil,
26 26 util,
27 27 )
28 28
29 29 class _hybrid(object):
30 30 """Wrapper for list or dict to support legacy template
31 31
32 32 This class allows us to handle both:
33 33 - "{files}" (legacy command-line-specific list hack) and
34 34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
35 35 and to access raw values:
36 36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
37 37 - "{get(extras, key)}"
38 38 - "{files|json}"
39 39 """
40 40
41 41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
42 42 if gen is not None:
43 43 self.gen = gen # generator or function returning generator
44 44 self._values = values
45 45 self._makemap = makemap
46 46 self.joinfmt = joinfmt
47 47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
48 48 def gen(self):
49 49 """Default generator to stringify this as {join(self, ' ')}"""
50 50 for i, x in enumerate(self._values):
51 51 if i > 0:
52 52 yield ' '
53 53 yield self.joinfmt(x)
54 54 def itermaps(self):
55 55 makemap = self._makemap
56 56 for x in self._values:
57 57 yield makemap(x)
58 58 def __contains__(self, x):
59 59 return x in self._values
60 60 def __getitem__(self, key):
61 61 return self._values[key]
62 62 def __len__(self):
63 63 return len(self._values)
64 64 def __iter__(self):
65 65 return iter(self._values)
66 66 def __getattr__(self, name):
67 67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
68 68 r'itervalues', r'keys', r'values'):
69 69 raise AttributeError(name)
70 70 return getattr(self._values, name)
71 71
72 72 class _mappable(object):
73 73 """Wrapper for non-list/dict object to support map operation
74 74
75 75 This class allows us to handle both:
76 76 - "{manifest}"
77 77 - "{manifest % '{rev}:{node}'}"
78 78 - "{manifest.rev}"
79 79
80 80 Unlike a _hybrid, this does not simulate the behavior of the underling
81 81 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
82 82 """
83 83
84 84 def __init__(self, gen, key, value, makemap):
85 85 if gen is not None:
86 86 self.gen = gen # generator or function returning generator
87 87 self._key = key
88 88 self._value = value # may be generator of strings
89 89 self._makemap = makemap
90 90
91 91 def gen(self):
92 92 yield pycompat.bytestr(self._value)
93 93
94 94 def tomap(self):
95 95 return self._makemap(self._key)
96 96
97 97 def itermaps(self):
98 98 yield self.tomap()
99 99
100 100 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
101 101 """Wrap data to support both dict-like and string-like operations"""
102 102 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
103 103 lambda k: fmt % (k, data[k]))
104 104
105 105 def hybridlist(data, name, fmt='%s', gen=None):
106 106 """Wrap data to support both list-like and string-like operations"""
107 107 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
108 108
109 109 def unwraphybrid(thing):
110 110 """Return an object which can be stringified possibly by using a legacy
111 111 template"""
112 112 gen = getattr(thing, 'gen', None)
113 113 if gen is None:
114 114 return thing
115 115 if callable(gen):
116 116 return gen()
117 117 return gen
118 118
119 119 def unwrapvalue(thing):
120 120 """Move the inner value object out of the wrapper"""
121 121 if not util.safehasattr(thing, '_value'):
122 122 return thing
123 123 return thing._value
124 124
125 125 def wraphybridvalue(container, key, value):
126 126 """Wrap an element of hybrid container to be mappable
127 127
128 128 The key is passed to the makemap function of the given container, which
129 129 should be an item generated by iter(container).
130 130 """
131 131 makemap = getattr(container, '_makemap', None)
132 132 if makemap is None:
133 133 return value
134 134 if util.safehasattr(value, '_makemap'):
135 135 # a nested hybrid list/dict, which has its own way of map operation
136 136 return value
137 137 return _mappable(None, key, value, makemap)
138 138
139 139 def showdict(name, data, mapping, plural=None, key='key', value='value',
140 140 fmt='%s=%s', separator=' '):
141 141 c = [{key: k, value: v} for k, v in data.iteritems()]
142 142 f = _showlist(name, c, mapping, plural, separator)
143 143 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
144 144
145 145 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
146 146 if not element:
147 147 element = name
148 148 f = _showlist(name, values, mapping, plural, separator)
149 149 return hybridlist(values, name=element, gen=f)
150 150
151 151 def _showlist(name, values, mapping, plural=None, separator=' '):
152 152 '''expand set of values.
153 153 name is name of key in template map.
154 154 values is list of strings or dicts.
155 155 plural is plural of name, if not simply name + 's'.
156 156 separator is used to join values as a string
157 157
158 158 expansion works like this, given name 'foo'.
159 159
160 160 if values is empty, expand 'no_foos'.
161 161
162 162 if 'foo' not in template map, return values as a string,
163 163 joined by 'separator'.
164 164
165 165 expand 'start_foos'.
166 166
167 167 for each value, expand 'foo'. if 'last_foo' in template
168 168 map, expand it instead of 'foo' for last key.
169 169
170 170 expand 'end_foos'.
171 171 '''
172 172 templ = mapping['templ']
173 173 strmapping = pycompat.strkwargs(mapping)
174 174 if not plural:
175 175 plural = name + 's'
176 176 if not values:
177 177 noname = 'no_' + plural
178 178 if noname in templ:
179 179 yield templ(noname, **strmapping)
180 180 return
181 181 if name not in templ:
182 182 if isinstance(values[0], bytes):
183 183 yield separator.join(values)
184 184 else:
185 185 for v in values:
186 186 yield dict(v, **strmapping)
187 187 return
188 188 startname = 'start_' + plural
189 189 if startname in templ:
190 190 yield templ(startname, **strmapping)
191 191 vmapping = mapping.copy()
192 192 def one(v, tag=name):
193 193 try:
194 194 vmapping.update(v)
195 195 # Python 2 raises ValueError if the type of v is wrong. Python
196 196 # 3 raises TypeError.
197 197 except (AttributeError, TypeError, ValueError):
198 198 try:
199 199 # Python 2 raises ValueError trying to destructure an e.g.
200 200 # bytes. Python 3 raises TypeError.
201 201 for a, b in v:
202 202 vmapping[a] = b
203 203 except (TypeError, ValueError):
204 204 vmapping[name] = v
205 205 return templ(tag, **pycompat.strkwargs(vmapping))
206 206 lastname = 'last_' + name
207 207 if lastname in templ:
208 208 last = values.pop()
209 209 else:
210 210 last = None
211 211 for v in values:
212 212 yield one(v)
213 213 if last is not None:
214 214 yield one(last, tag=lastname)
215 215 endname = 'end_' + plural
216 216 if endname in templ:
217 217 yield templ(endname, **strmapping)
218 218
219 219 def getfiles(repo, ctx, revcache):
220 220 if 'files' not in revcache:
221 221 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
222 222 return revcache['files']
223 223
224 224 def getlatesttags(repo, ctx, cache, pattern=None):
225 225 '''return date, distance and name for the latest tag of rev'''
226 226
227 227 cachename = 'latesttags'
228 228 if pattern is not None:
229 229 cachename += '-' + pattern
230 230 match = util.stringmatcher(pattern)[2]
231 231 else:
232 232 match = util.always
233 233
234 234 if cachename not in cache:
235 235 # Cache mapping from rev to a tuple with tag date, tag
236 236 # distance and tag name
237 237 cache[cachename] = {-1: (0, 0, ['null'])}
238 238 latesttags = cache[cachename]
239 239
240 240 rev = ctx.rev()
241 241 todo = [rev]
242 242 while todo:
243 243 rev = todo.pop()
244 244 if rev in latesttags:
245 245 continue
246 246 ctx = repo[rev]
247 247 tags = [t for t in ctx.tags()
248 248 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
249 249 and match(t))]
250 250 if tags:
251 251 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
252 252 continue
253 253 try:
254 254 ptags = [latesttags[p.rev()] for p in ctx.parents()]
255 255 if len(ptags) > 1:
256 256 if ptags[0][2] == ptags[1][2]:
257 257 # The tuples are laid out so the right one can be found by
258 258 # comparison in this case.
259 259 pdate, pdist, ptag = max(ptags)
260 260 else:
261 261 def key(x):
262 262 changessincetag = len(repo.revs('only(%d, %s)',
263 263 ctx.rev(), x[2][0]))
264 264 # Smallest number of changes since tag wins. Date is
265 265 # used as tiebreaker.
266 266 return [-changessincetag, x[0]]
267 267 pdate, pdist, ptag = max(ptags, key=key)
268 268 else:
269 269 pdate, pdist, ptag = ptags[0]
270 270 except KeyError:
271 271 # Cache miss - recurse
272 272 todo.append(rev)
273 273 todo.extend(p.rev() for p in ctx.parents())
274 274 continue
275 275 latesttags[rev] = pdate, pdist + 1, ptag
276 276 return latesttags[rev]
277 277
278 278 def getrenamedfn(repo, endrev=None):
279 279 rcache = {}
280 280 if endrev is None:
281 281 endrev = len(repo)
282 282
283 283 def getrenamed(fn, rev):
284 284 '''looks up all renames for a file (up to endrev) the first
285 285 time the file is given. It indexes on the changerev and only
286 286 parses the manifest if linkrev != changerev.
287 287 Returns rename info for fn at changerev rev.'''
288 288 if fn not in rcache:
289 289 rcache[fn] = {}
290 290 fl = repo.file(fn)
291 291 for i in fl:
292 292 lr = fl.linkrev(i)
293 293 renamed = fl.renamed(fl.node(i))
294 294 rcache[fn][lr] = renamed
295 295 if lr >= endrev:
296 296 break
297 297 if rev in rcache[fn]:
298 298 return rcache[fn][rev]
299 299
300 300 # If linkrev != rev (i.e. rev not found in rcache) fallback to
301 301 # filectx logic.
302 302 try:
303 303 return repo[rev][fn].renamed()
304 304 except error.LookupError:
305 305 return None
306 306
307 307 return getrenamed
308 308
309 309 def getlogcolumns():
310 310 """Return a dict of log column labels"""
311 311 _ = pycompat.identity # temporarily disable gettext
312 312 # i18n: column positioning for "hg log"
313 313 columns = _('bookmark: %s\n'
314 314 'branch: %s\n'
315 315 'changeset: %s\n'
316 316 'copies: %s\n'
317 317 'date: %s\n'
318 318 'extra: %s=%s\n'
319 319 'files+: %s\n'
320 320 'files-: %s\n'
321 321 'files: %s\n'
322 322 'instability: %s\n'
323 323 'manifest: %s\n'
324 324 'obsolete: %s\n'
325 325 'parent: %s\n'
326 326 'phase: %s\n'
327 327 'summary: %s\n'
328 328 'tag: %s\n'
329 329 'user: %s\n')
330 330 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
331 331 i18n._(columns).splitlines(True)))
332 332
333 333 # default templates internally used for rendering of lists
334 334 defaulttempl = {
335 335 'parent': '{rev}:{node|formatnode} ',
336 336 'manifest': '{rev}:{node|formatnode}',
337 337 'file_copy': '{name} ({source})',
338 338 'envvar': '{key}={value}',
339 339 'extra': '{key}={value|stringescape}'
340 340 }
341 341 # filecopy is preserved for compatibility reasons
342 342 defaulttempl['filecopy'] = defaulttempl['file_copy']
343 343
344 344 # keywords are callables (see registrar.templatekeyword for details)
345 345 keywords = {}
346 346 templatekeyword = registrar.templatekeyword(keywords)
347 347
348 348 @templatekeyword('author', requires={'ctx'})
349 349 def showauthor(context, mapping):
350 350 """String. The unmodified author of the changeset."""
351 351 ctx = context.resource(mapping, 'ctx')
352 352 return ctx.user()
353 353
354 354 @templatekeyword('bisect')
355 355 def showbisect(repo, ctx, templ, **args):
356 356 """String. The changeset bisection status."""
357 357 return hbisect.label(repo, ctx.node())
358 358
359 359 @templatekeyword('branch')
360 360 def showbranch(**args):
361 361 """String. The name of the branch on which the changeset was
362 362 committed.
363 363 """
364 364 return args[r'ctx'].branch()
365 365
366 366 @templatekeyword('branches')
367 367 def showbranches(**args):
368 368 """List of strings. The name of the branch on which the
369 369 changeset was committed. Will be empty if the branch name was
370 370 default. (DEPRECATED)
371 371 """
372 372 args = pycompat.byteskwargs(args)
373 373 branch = args['ctx'].branch()
374 374 if branch != 'default':
375 375 return showlist('branch', [branch], args, plural='branches')
376 376 return showlist('branch', [], args, plural='branches')
377 377
378 378 @templatekeyword('bookmarks')
379 379 def showbookmarks(**args):
380 380 """List of strings. Any bookmarks associated with the
381 381 changeset. Also sets 'active', the name of the active bookmark.
382 382 """
383 383 args = pycompat.byteskwargs(args)
384 384 repo = args['ctx']._repo
385 385 bookmarks = args['ctx'].bookmarks()
386 386 active = repo._activebookmark
387 387 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
388 388 f = _showlist('bookmark', bookmarks, args)
389 389 return _hybrid(f, bookmarks, makemap, pycompat.identity)
390 390
391 391 @templatekeyword('children')
392 392 def showchildren(**args):
393 393 """List of strings. The children of the changeset."""
394 394 args = pycompat.byteskwargs(args)
395 395 ctx = args['ctx']
396 396 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
397 397 return showlist('children', childrevs, args, element='child')
398 398
399 399 # Deprecated, but kept alive for help generation a purpose.
400 400 @templatekeyword('currentbookmark')
401 401 def showcurrentbookmark(**args):
402 402 """String. The active bookmark, if it is associated with the changeset.
403 403 (DEPRECATED)"""
404 404 return showactivebookmark(**args)
405 405
406 406 @templatekeyword('activebookmark')
407 407 def showactivebookmark(**args):
408 408 """String. The active bookmark, if it is associated with the changeset."""
409 409 active = args[r'repo']._activebookmark
410 410 if active and active in args[r'ctx'].bookmarks():
411 411 return active
412 412 return ''
413 413
414 414 @templatekeyword('date')
415 415 def showdate(repo, ctx, templ, **args):
416 416 """Date information. The date when the changeset was committed."""
417 417 return ctx.date()
418 418
419 419 @templatekeyword('desc')
420 420 def showdescription(repo, ctx, templ, **args):
421 421 """String. The text of the changeset description."""
422 422 s = ctx.description()
423 423 if isinstance(s, encoding.localstr):
424 424 # try hard to preserve utf-8 bytes
425 425 return encoding.tolocal(encoding.fromlocal(s).strip())
426 426 else:
427 427 return s.strip()
428 428
429 429 @templatekeyword('diffstat')
430 430 def showdiffstat(repo, ctx, templ, **args):
431 431 """String. Statistics of changes with the following format:
432 432 "modified files: +added/-removed lines"
433 433 """
434 434 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
435 435 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
436 436 return '%d: +%d/-%d' % (len(stats), adds, removes)
437 437
438 438 @templatekeyword('envvars')
439 439 def showenvvars(ui, **args):
440 440 """A dictionary of environment variables. (EXPERIMENTAL)"""
441 441 args = pycompat.byteskwargs(args)
442 442 env = ui.exportableenviron()
443 443 env = util.sortdict((k, env[k]) for k in sorted(env))
444 444 return showdict('envvar', env, args, plural='envvars')
445 445
446 446 @templatekeyword('extras')
447 447 def showextras(**args):
448 448 """List of dicts with key, value entries of the 'extras'
449 449 field of this changeset."""
450 450 args = pycompat.byteskwargs(args)
451 451 extras = args['ctx'].extra()
452 452 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
453 453 makemap = lambda k: {'key': k, 'value': extras[k]}
454 454 c = [makemap(k) for k in extras]
455 455 f = _showlist('extra', c, args, plural='extras')
456 456 return _hybrid(f, extras, makemap,
457 457 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
458 458
459 459 @templatekeyword('file_adds')
460 460 def showfileadds(**args):
461 461 """List of strings. Files added by this changeset."""
462 462 args = pycompat.byteskwargs(args)
463 463 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
464 464 return showlist('file_add', getfiles(repo, ctx, revcache)[1], args,
465 465 element='file')
466 466
467 467 @templatekeyword('file_copies')
468 468 def showfilecopies(**args):
469 469 """List of strings. Files copied in this changeset with
470 470 their sources.
471 471 """
472 472 args = pycompat.byteskwargs(args)
473 473 cache, ctx = args['cache'], args['ctx']
474 474 copies = args['revcache'].get('copies')
475 475 if copies is None:
476 476 if 'getrenamed' not in cache:
477 477 cache['getrenamed'] = getrenamedfn(args['repo'])
478 478 copies = []
479 479 getrenamed = cache['getrenamed']
480 480 for fn in ctx.files():
481 481 rename = getrenamed(fn, ctx.rev())
482 482 if rename:
483 483 copies.append((fn, rename[0]))
484 484
485 485 copies = util.sortdict(copies)
486 486 return showdict('file_copy', copies, args, plural='file_copies',
487 487 key='name', value='source', fmt='%s (%s)')
488 488
489 489 # showfilecopiesswitch() displays file copies only if copy records are
490 490 # provided before calling the templater, usually with a --copies
491 491 # command line switch.
492 492 @templatekeyword('file_copies_switch')
493 493 def showfilecopiesswitch(**args):
494 494 """List of strings. Like "file_copies" but displayed
495 495 only if the --copied switch is set.
496 496 """
497 497 args = pycompat.byteskwargs(args)
498 498 copies = args['revcache'].get('copies') or []
499 499 copies = util.sortdict(copies)
500 500 return showdict('file_copy', copies, args, plural='file_copies',
501 501 key='name', value='source', fmt='%s (%s)')
502 502
503 503 @templatekeyword('file_dels')
504 504 def showfiledels(**args):
505 505 """List of strings. Files removed by this changeset."""
506 506 args = pycompat.byteskwargs(args)
507 507 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
508 508 return showlist('file_del', getfiles(repo, ctx, revcache)[2], args,
509 509 element='file')
510 510
511 511 @templatekeyword('file_mods')
512 512 def showfilemods(**args):
513 513 """List of strings. Files modified by this changeset."""
514 514 args = pycompat.byteskwargs(args)
515 515 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
516 516 return showlist('file_mod', getfiles(repo, ctx, revcache)[0], args,
517 517 element='file')
518 518
519 519 @templatekeyword('files')
520 520 def showfiles(**args):
521 521 """List of strings. All files modified, added, or removed by this
522 522 changeset.
523 523 """
524 524 args = pycompat.byteskwargs(args)
525 525 return showlist('file', args['ctx'].files(), args)
526 526
527 527 @templatekeyword('graphnode')
528 528 def showgraphnode(repo, ctx, **args):
529 529 """String. The character representing the changeset node in an ASCII
530 530 revision graph."""
531 return getgraphnode(repo, ctx)
532
533 def getgraphnode(repo, ctx):
531 534 wpnodes = repo.dirstate.parents()
532 535 if wpnodes[1] == nullid:
533 536 wpnodes = wpnodes[:1]
534 537 if ctx.node() in wpnodes:
535 538 return '@'
536 539 elif ctx.obsolete():
537 540 return 'x'
538 541 elif ctx.isunstable():
539 542 return '*'
540 543 elif ctx.closesbranch():
541 544 return '_'
542 545 else:
543 546 return 'o'
544 547
545 548 @templatekeyword('graphwidth')
546 549 def showgraphwidth(repo, ctx, templ, **args):
547 550 """Integer. The width of the graph drawn by 'log --graph' or zero."""
548 551 # just hosts documentation; should be overridden by template mapping
549 552 return 0
550 553
551 554 @templatekeyword('index')
552 555 def showindex(**args):
553 556 """Integer. The current iteration of the loop. (0 indexed)"""
554 557 # just hosts documentation; should be overridden by template mapping
555 558 raise error.Abort(_("can't use index in this context"))
556 559
557 560 @templatekeyword('latesttag')
558 561 def showlatesttag(**args):
559 562 """List of strings. The global tags on the most recent globally
560 563 tagged ancestor of this changeset. If no such tags exist, the list
561 564 consists of the single string "null".
562 565 """
563 566 return showlatesttags(None, **args)
564 567
565 568 def showlatesttags(pattern, **args):
566 569 """helper method for the latesttag keyword and function"""
567 570 args = pycompat.byteskwargs(args)
568 571 repo, ctx = args['repo'], args['ctx']
569 572 cache = args['cache']
570 573 latesttags = getlatesttags(repo, ctx, cache, pattern)
571 574
572 575 # latesttag[0] is an implementation detail for sorting csets on different
573 576 # branches in a stable manner- it is the date the tagged cset was created,
574 577 # not the date the tag was created. Therefore it isn't made visible here.
575 578 makemap = lambda v: {
576 579 'changes': _showchangessincetag,
577 580 'distance': latesttags[1],
578 581 'latesttag': v, # BC with {latesttag % '{latesttag}'}
579 582 'tag': v
580 583 }
581 584
582 585 tags = latesttags[2]
583 586 f = _showlist('latesttag', tags, args, separator=':')
584 587 return _hybrid(f, tags, makemap, pycompat.identity)
585 588
586 589 @templatekeyword('latesttagdistance')
587 590 def showlatesttagdistance(repo, ctx, templ, cache, **args):
588 591 """Integer. Longest path to the latest tag."""
589 592 return getlatesttags(repo, ctx, cache)[1]
590 593
591 594 @templatekeyword('changessincelatesttag')
592 595 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
593 596 """Integer. All ancestors not in the latest tag."""
594 597 latesttag = getlatesttags(repo, ctx, cache)[2][0]
595 598
596 599 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
597 600
598 601 def _showchangessincetag(repo, ctx, **args):
599 602 offset = 0
600 603 revs = [ctx.rev()]
601 604 tag = args[r'tag']
602 605
603 606 # The only() revset doesn't currently support wdir()
604 607 if ctx.rev() is None:
605 608 offset = 1
606 609 revs = [p.rev() for p in ctx.parents()]
607 610
608 611 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
609 612
610 613 @templatekeyword('manifest')
611 614 def showmanifest(**args):
612 615 repo, ctx, templ = args[r'repo'], args[r'ctx'], args[r'templ']
613 616 mnode = ctx.manifestnode()
614 617 if mnode is None:
615 618 # just avoid crash, we might want to use the 'ff...' hash in future
616 619 return
617 620 mrev = repo.manifestlog._revlog.rev(mnode)
618 621 mhex = hex(mnode)
619 622 args = args.copy()
620 623 args.update({r'rev': mrev, r'node': mhex})
621 624 f = templ('manifest', **args)
622 625 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
623 626 # rev and node are completely different from changeset's.
624 627 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
625 628
626 629 @templatekeyword('obsfate')
627 630 def showobsfate(**args):
628 631 # this function returns a list containing pre-formatted obsfate strings.
629 632 #
630 633 # This function will be replaced by templates fragments when we will have
631 634 # the verbosity templatekw available.
632 635 succsandmarkers = showsuccsandmarkers(**args)
633 636
634 637 args = pycompat.byteskwargs(args)
635 638 ui = args['ui']
636 639
637 640 values = []
638 641
639 642 for x in succsandmarkers:
640 643 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
641 644
642 645 return showlist("fate", values, args)
643 646
644 647 def shownames(namespace, **args):
645 648 """helper method to generate a template keyword for a namespace"""
646 649 args = pycompat.byteskwargs(args)
647 650 ctx = args['ctx']
648 651 repo = ctx.repo()
649 652 ns = repo.names[namespace]
650 653 names = ns.names(repo, ctx.node())
651 654 return showlist(ns.templatename, names, args, plural=namespace)
652 655
653 656 @templatekeyword('namespaces')
654 657 def shownamespaces(**args):
655 658 """Dict of lists. Names attached to this changeset per
656 659 namespace."""
657 660 args = pycompat.byteskwargs(args)
658 661 ctx = args['ctx']
659 662 repo = ctx.repo()
660 663
661 664 namespaces = util.sortdict()
662 665 def makensmapfn(ns):
663 666 # 'name' for iterating over namespaces, templatename for local reference
664 667 return lambda v: {'name': v, ns.templatename: v}
665 668
666 669 for k, ns in repo.names.iteritems():
667 670 names = ns.names(repo, ctx.node())
668 671 f = _showlist('name', names, args)
669 672 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
670 673
671 674 f = _showlist('namespace', list(namespaces), args)
672 675
673 676 def makemap(ns):
674 677 return {
675 678 'namespace': ns,
676 679 'names': namespaces[ns],
677 680 'builtin': repo.names[ns].builtin,
678 681 'colorname': repo.names[ns].colorname,
679 682 }
680 683
681 684 return _hybrid(f, namespaces, makemap, pycompat.identity)
682 685
683 686 @templatekeyword('node')
684 687 def shownode(repo, ctx, templ, **args):
685 688 """String. The changeset identification hash, as a 40 hexadecimal
686 689 digit string.
687 690 """
688 691 return ctx.hex()
689 692
690 693 @templatekeyword('obsolete')
691 694 def showobsolete(repo, ctx, templ, **args):
692 695 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
693 696 if ctx.obsolete():
694 697 return 'obsolete'
695 698 return ''
696 699
697 700 @templatekeyword('peerurls')
698 701 def showpeerurls(repo, **args):
699 702 """A dictionary of repository locations defined in the [paths] section
700 703 of your configuration file."""
701 704 # see commands.paths() for naming of dictionary keys
702 705 paths = repo.ui.paths
703 706 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
704 707 def makemap(k):
705 708 p = paths[k]
706 709 d = {'name': k, 'url': p.rawloc}
707 710 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
708 711 return d
709 712 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
710 713
711 714 @templatekeyword("predecessors")
712 715 def showpredecessors(repo, ctx, **args):
713 716 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
714 717 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
715 718 predecessors = map(hex, predecessors)
716 719
717 720 return _hybrid(None, predecessors,
718 721 lambda x: {'ctx': repo[x], 'revcache': {}},
719 722 lambda x: scmutil.formatchangeid(repo[x]))
720 723
721 724 @templatekeyword('reporoot')
722 725 def showreporoot(repo, **args):
723 726 """String. The root directory of the current repository."""
724 727 return repo.root
725 728
726 729 @templatekeyword("successorssets")
727 730 def showsuccessorssets(repo, ctx, **args):
728 731 """Returns a string of sets of successors for a changectx. Format used
729 732 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
730 733 while also diverged into ctx3. (EXPERIMENTAL)"""
731 734 if not ctx.obsolete():
732 735 return ''
733 736 args = pycompat.byteskwargs(args)
734 737
735 738 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
736 739 ssets = [[hex(n) for n in ss] for ss in ssets]
737 740
738 741 data = []
739 742 for ss in ssets:
740 743 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
741 744 lambda x: scmutil.formatchangeid(repo[x]))
742 745 data.append(h)
743 746
744 747 # Format the successorssets
745 748 def render(d):
746 749 t = []
747 750 for i in d.gen():
748 751 t.append(i)
749 752 return "".join(t)
750 753
751 754 def gen(data):
752 755 yield "; ".join(render(d) for d in data)
753 756
754 757 return _hybrid(gen(data), data, lambda x: {'successorset': x},
755 758 pycompat.identity)
756 759
757 760 @templatekeyword("succsandmarkers")
758 761 def showsuccsandmarkers(repo, ctx, **args):
759 762 """Returns a list of dict for each final successor of ctx. The dict
760 763 contains successors node id in "successors" keys and the list of
761 764 obs-markers from ctx to the set of successors in "markers".
762 765 (EXPERIMENTAL)
763 766 """
764 767
765 768 values = obsutil.successorsandmarkers(repo, ctx)
766 769
767 770 if values is None:
768 771 values = []
769 772
770 773 # Format successors and markers to avoid exposing binary to templates
771 774 data = []
772 775 for i in values:
773 776 # Format successors
774 777 successors = i['successors']
775 778
776 779 successors = [hex(n) for n in successors]
777 780 successors = _hybrid(None, successors,
778 781 lambda x: {'ctx': repo[x], 'revcache': {}},
779 782 lambda x: scmutil.formatchangeid(repo[x]))
780 783
781 784 # Format markers
782 785 finalmarkers = []
783 786 for m in i['markers']:
784 787 hexprec = hex(m[0])
785 788 hexsucs = tuple(hex(n) for n in m[1])
786 789 hexparents = None
787 790 if m[5] is not None:
788 791 hexparents = tuple(hex(n) for n in m[5])
789 792 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
790 793 finalmarkers.append(newmarker)
791 794
792 795 data.append({'successors': successors, 'markers': finalmarkers})
793 796
794 797 f = _showlist('succsandmarkers', data, pycompat.byteskwargs(args))
795 798 return _hybrid(f, data, lambda x: x, pycompat.identity)
796 799
797 800 @templatekeyword('p1rev')
798 801 def showp1rev(repo, ctx, templ, **args):
799 802 """Integer. The repository-local revision number of the changeset's
800 803 first parent, or -1 if the changeset has no parents."""
801 804 return ctx.p1().rev()
802 805
803 806 @templatekeyword('p2rev')
804 807 def showp2rev(repo, ctx, templ, **args):
805 808 """Integer. The repository-local revision number of the changeset's
806 809 second parent, or -1 if the changeset has no second parent."""
807 810 return ctx.p2().rev()
808 811
809 812 @templatekeyword('p1node')
810 813 def showp1node(repo, ctx, templ, **args):
811 814 """String. The identification hash of the changeset's first parent,
812 815 as a 40 digit hexadecimal string. If the changeset has no parents, all
813 816 digits are 0."""
814 817 return ctx.p1().hex()
815 818
816 819 @templatekeyword('p2node')
817 820 def showp2node(repo, ctx, templ, **args):
818 821 """String. The identification hash of the changeset's second
819 822 parent, as a 40 digit hexadecimal string. If the changeset has no second
820 823 parent, all digits are 0."""
821 824 return ctx.p2().hex()
822 825
823 826 @templatekeyword('parents')
824 827 def showparents(**args):
825 828 """List of strings. The parents of the changeset in "rev:node"
826 829 format. If the changeset has only one "natural" parent (the predecessor
827 830 revision) nothing is shown."""
828 831 args = pycompat.byteskwargs(args)
829 832 repo = args['repo']
830 833 ctx = args['ctx']
831 834 pctxs = scmutil.meaningfulparents(repo, ctx)
832 835 prevs = [p.rev() for p in pctxs]
833 836 parents = [[('rev', p.rev()),
834 837 ('node', p.hex()),
835 838 ('phase', p.phasestr())]
836 839 for p in pctxs]
837 840 f = _showlist('parent', parents, args)
838 841 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
839 842 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
840 843
841 844 @templatekeyword('phase')
842 845 def showphase(repo, ctx, templ, **args):
843 846 """String. The changeset phase name."""
844 847 return ctx.phasestr()
845 848
846 849 @templatekeyword('phaseidx')
847 850 def showphaseidx(repo, ctx, templ, **args):
848 851 """Integer. The changeset phase index. (ADVANCED)"""
849 852 return ctx.phase()
850 853
851 854 @templatekeyword('rev')
852 855 def showrev(repo, ctx, templ, **args):
853 856 """Integer. The repository-local changeset revision number."""
854 857 return scmutil.intrev(ctx)
855 858
856 859 def showrevslist(name, revs, **args):
857 860 """helper to generate a list of revisions in which a mapped template will
858 861 be evaluated"""
859 862 args = pycompat.byteskwargs(args)
860 863 repo = args['ctx'].repo()
861 864 f = _showlist(name, ['%d' % r for r in revs], args)
862 865 return _hybrid(f, revs,
863 866 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
864 867 pycompat.identity, keytype=int)
865 868
866 869 @templatekeyword('subrepos')
867 870 def showsubrepos(**args):
868 871 """List of strings. Updated subrepositories in the changeset."""
869 872 args = pycompat.byteskwargs(args)
870 873 ctx = args['ctx']
871 874 substate = ctx.substate
872 875 if not substate:
873 876 return showlist('subrepo', [], args)
874 877 psubstate = ctx.parents()[0].substate or {}
875 878 subrepos = []
876 879 for sub in substate:
877 880 if sub not in psubstate or substate[sub] != psubstate[sub]:
878 881 subrepos.append(sub) # modified or newly added in ctx
879 882 for sub in psubstate:
880 883 if sub not in substate:
881 884 subrepos.append(sub) # removed in ctx
882 885 return showlist('subrepo', sorted(subrepos), args)
883 886
884 887 # don't remove "showtags" definition, even though namespaces will put
885 888 # a helper function for "tags" keyword into "keywords" map automatically,
886 889 # because online help text is built without namespaces initialization
887 890 @templatekeyword('tags')
888 891 def showtags(**args):
889 892 """List of strings. Any tags associated with the changeset."""
890 893 return shownames('tags', **args)
891 894
892 895 @templatekeyword('termwidth')
893 896 def showtermwidth(ui, **args):
894 897 """Integer. The width of the current terminal."""
895 898 return ui.termwidth()
896 899
897 900 @templatekeyword('instabilities')
898 901 def showinstabilities(**args):
899 902 """List of strings. Evolution instabilities affecting the changeset.
900 903 (EXPERIMENTAL)
901 904 """
902 905 args = pycompat.byteskwargs(args)
903 906 return showlist('instability', args['ctx'].instabilities(), args,
904 907 plural='instabilities')
905 908
906 909 @templatekeyword('verbosity')
907 910 def showverbosity(ui, **args):
908 911 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
909 912 or ''."""
910 913 # see logcmdutil.changesettemplater for priority of these flags
911 914 if ui.debugflag:
912 915 return 'debug'
913 916 elif ui.quiet:
914 917 return 'quiet'
915 918 elif ui.verbose:
916 919 return 'verbose'
917 920 return ''
918 921
919 922 def loadkeyword(ui, extname, registrarobj):
920 923 """Load template keyword from specified registrarobj
921 924 """
922 925 for name, func in registrarobj._table.iteritems():
923 926 keywords[name] = func
924 927
925 928 # tell hggettext to extract docstrings from these functions:
926 929 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now