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