##// END OF EJS Templates
templatespec: logcmdutil.templatespec() gets either template or mapfile...
Martin von Zweigbergk -
r45825:dfb67cd1 default
parent child Browse files
Show More
@@ -1,1085 +1,1086 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 import posixpath
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 nullid,
17 17 wdirid,
18 18 wdirrev,
19 19 )
20 20
21 21 from . import (
22 22 dagop,
23 23 error,
24 24 formatter,
25 25 graphmod,
26 26 match as matchmod,
27 27 mdiff,
28 28 patch,
29 29 pathutil,
30 30 pycompat,
31 31 revset,
32 32 revsetlang,
33 33 scmutil,
34 34 smartset,
35 35 templatekw,
36 36 templater,
37 37 util,
38 38 )
39 39 from .utils import (
40 40 dateutil,
41 41 stringutil,
42 42 )
43 43
44 44
45 45 if pycompat.TYPE_CHECKING:
46 46 from typing import (
47 47 Any,
48 48 Optional,
49 49 Tuple,
50 50 )
51 51
52 52 for t in (Any, Optional, Tuple):
53 53 assert t
54 54
55 55
56 56 def getlimit(opts):
57 57 """get the log limit according to option -l/--limit"""
58 58 limit = opts.get(b'limit')
59 59 if limit:
60 60 try:
61 61 limit = int(limit)
62 62 except ValueError:
63 63 raise error.Abort(_(b'limit must be a positive integer'))
64 64 if limit <= 0:
65 65 raise error.Abort(_(b'limit must be positive'))
66 66 else:
67 67 limit = None
68 68 return limit
69 69
70 70
71 71 def diffordiffstat(
72 72 ui,
73 73 repo,
74 74 diffopts,
75 75 ctx1,
76 76 ctx2,
77 77 match,
78 78 changes=None,
79 79 stat=False,
80 80 fp=None,
81 81 graphwidth=0,
82 82 prefix=b'',
83 83 root=b'',
84 84 listsubrepos=False,
85 85 hunksfilterfn=None,
86 86 ):
87 87 '''show diff or diffstat.'''
88 88 if root:
89 89 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
90 90 else:
91 91 relroot = b''
92 92 copysourcematch = None
93 93
94 94 def compose(f, g):
95 95 return lambda x: f(g(x))
96 96
97 97 def pathfn(f):
98 98 return posixpath.join(prefix, f)
99 99
100 100 if relroot != b'':
101 101 # XXX relative roots currently don't work if the root is within a
102 102 # subrepo
103 103 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
104 104 uirelroot = uipathfn(pathfn(relroot))
105 105 relroot += b'/'
106 106 for matchroot in match.files():
107 107 if not matchroot.startswith(relroot):
108 108 ui.warn(
109 109 _(b'warning: %s not inside relative root %s\n')
110 110 % (uipathfn(pathfn(matchroot)), uirelroot)
111 111 )
112 112
113 113 relrootmatch = scmutil.match(ctx2, pats=[relroot], default=b'path')
114 114 match = matchmod.intersectmatchers(match, relrootmatch)
115 115 copysourcematch = relrootmatch
116 116
117 117 checkroot = repo.ui.configbool(
118 118 b'devel', b'all-warnings'
119 119 ) or repo.ui.configbool(b'devel', b'check-relroot')
120 120
121 121 def relrootpathfn(f):
122 122 if checkroot and not f.startswith(relroot):
123 123 raise AssertionError(
124 124 b"file %s doesn't start with relroot %s" % (f, relroot)
125 125 )
126 126 return f[len(relroot) :]
127 127
128 128 pathfn = compose(relrootpathfn, pathfn)
129 129
130 130 if stat:
131 131 diffopts = diffopts.copy(context=0, noprefix=False)
132 132 width = 80
133 133 if not ui.plain():
134 134 width = ui.termwidth() - graphwidth
135 135 # If an explicit --root was given, don't respect ui.relative-paths
136 136 if not relroot:
137 137 pathfn = compose(scmutil.getuipathfn(repo), pathfn)
138 138
139 139 chunks = ctx2.diff(
140 140 ctx1,
141 141 match,
142 142 changes,
143 143 opts=diffopts,
144 144 pathfn=pathfn,
145 145 copysourcematch=copysourcematch,
146 146 hunksfilterfn=hunksfilterfn,
147 147 )
148 148
149 149 if fp is not None or ui.canwritewithoutlabels():
150 150 out = fp or ui
151 151 if stat:
152 152 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
153 153 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
154 154 out.write(chunk)
155 155 else:
156 156 if stat:
157 157 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
158 158 else:
159 159 chunks = patch.difflabel(
160 160 lambda chunks, **kwargs: chunks, chunks, opts=diffopts
161 161 )
162 162 if ui.canbatchlabeledwrites():
163 163
164 164 def gen():
165 165 for chunk, label in chunks:
166 166 yield ui.label(chunk, label=label)
167 167
168 168 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
169 169 ui.write(chunk)
170 170 else:
171 171 for chunk, label in chunks:
172 172 ui.write(chunk, label=label)
173 173
174 174 node2 = ctx2.node()
175 175 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
176 176 tempnode2 = node2
177 177 try:
178 178 if node2 is not None:
179 179 tempnode2 = ctx2.substate[subpath][1]
180 180 except KeyError:
181 181 # A subrepo that existed in node1 was deleted between node1 and
182 182 # node2 (inclusive). Thus, ctx2's substate won't contain that
183 183 # subpath. The best we can do is to ignore it.
184 184 tempnode2 = None
185 185 submatch = matchmod.subdirmatcher(subpath, match)
186 186 subprefix = repo.wvfs.reljoin(prefix, subpath)
187 187 if listsubrepos or match.exact(subpath) or any(submatch.files()):
188 188 sub.diff(
189 189 ui,
190 190 diffopts,
191 191 tempnode2,
192 192 submatch,
193 193 changes=changes,
194 194 stat=stat,
195 195 fp=fp,
196 196 prefix=subprefix,
197 197 )
198 198
199 199
200 200 class changesetdiffer(object):
201 201 """Generate diff of changeset with pre-configured filtering functions"""
202 202
203 203 def _makefilematcher(self, ctx):
204 204 return scmutil.matchall(ctx.repo())
205 205
206 206 def _makehunksfilter(self, ctx):
207 207 return None
208 208
209 209 def showdiff(self, ui, ctx, diffopts, graphwidth=0, stat=False):
210 210 diffordiffstat(
211 211 ui,
212 212 ctx.repo(),
213 213 diffopts,
214 214 ctx.p1(),
215 215 ctx,
216 216 match=self._makefilematcher(ctx),
217 217 stat=stat,
218 218 graphwidth=graphwidth,
219 219 hunksfilterfn=self._makehunksfilter(ctx),
220 220 )
221 221
222 222
223 223 def changesetlabels(ctx):
224 224 labels = [b'log.changeset', b'changeset.%s' % ctx.phasestr()]
225 225 if ctx.obsolete():
226 226 labels.append(b'changeset.obsolete')
227 227 if ctx.isunstable():
228 228 labels.append(b'changeset.unstable')
229 229 for instability in ctx.instabilities():
230 230 labels.append(b'instability.%s' % instability)
231 231 return b' '.join(labels)
232 232
233 233
234 234 class changesetprinter(object):
235 235 '''show changeset information when templating not requested.'''
236 236
237 237 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
238 238 self.ui = ui
239 239 self.repo = repo
240 240 self.buffered = buffered
241 241 self._differ = differ or changesetdiffer()
242 242 self._diffopts = patch.diffallopts(ui, diffopts)
243 243 self._includestat = diffopts and diffopts.get(b'stat')
244 244 self._includediff = diffopts and diffopts.get(b'patch')
245 245 self.header = {}
246 246 self.hunk = {}
247 247 self.lastheader = None
248 248 self.footer = None
249 249 self._columns = templatekw.getlogcolumns()
250 250
251 251 def flush(self, ctx):
252 252 rev = ctx.rev()
253 253 if rev in self.header:
254 254 h = self.header[rev]
255 255 if h != self.lastheader:
256 256 self.lastheader = h
257 257 self.ui.write(h)
258 258 del self.header[rev]
259 259 if rev in self.hunk:
260 260 self.ui.write(self.hunk[rev])
261 261 del self.hunk[rev]
262 262
263 263 def close(self):
264 264 if self.footer:
265 265 self.ui.write(self.footer)
266 266
267 267 def show(self, ctx, copies=None, **props):
268 268 props = pycompat.byteskwargs(props)
269 269 if self.buffered:
270 270 self.ui.pushbuffer(labeled=True)
271 271 self._show(ctx, copies, props)
272 272 self.hunk[ctx.rev()] = self.ui.popbuffer()
273 273 else:
274 274 self._show(ctx, copies, props)
275 275
276 276 def _show(self, ctx, copies, props):
277 277 '''show a single changeset or file revision'''
278 278 changenode = ctx.node()
279 279 graphwidth = props.get(b'graphwidth', 0)
280 280
281 281 if self.ui.quiet:
282 282 self.ui.write(
283 283 b"%s\n" % scmutil.formatchangeid(ctx), label=b'log.node'
284 284 )
285 285 return
286 286
287 287 columns = self._columns
288 288 self.ui.write(
289 289 columns[b'changeset'] % scmutil.formatchangeid(ctx),
290 290 label=changesetlabels(ctx),
291 291 )
292 292
293 293 # branches are shown first before any other names due to backwards
294 294 # compatibility
295 295 branch = ctx.branch()
296 296 # don't show the default branch name
297 297 if branch != b'default':
298 298 self.ui.write(columns[b'branch'] % branch, label=b'log.branch')
299 299
300 300 for nsname, ns in pycompat.iteritems(self.repo.names):
301 301 # branches has special logic already handled above, so here we just
302 302 # skip it
303 303 if nsname == b'branches':
304 304 continue
305 305 # we will use the templatename as the color name since those two
306 306 # should be the same
307 307 for name in ns.names(self.repo, changenode):
308 308 self.ui.write(ns.logfmt % name, label=b'log.%s' % ns.colorname)
309 309 if self.ui.debugflag:
310 310 self.ui.write(
311 311 columns[b'phase'] % ctx.phasestr(), label=b'log.phase'
312 312 )
313 313 for pctx in scmutil.meaningfulparents(self.repo, ctx):
314 314 label = b'log.parent changeset.%s' % pctx.phasestr()
315 315 self.ui.write(
316 316 columns[b'parent'] % scmutil.formatchangeid(pctx), label=label
317 317 )
318 318
319 319 if self.ui.debugflag:
320 320 mnode = ctx.manifestnode()
321 321 if mnode is None:
322 322 mnode = wdirid
323 323 mrev = wdirrev
324 324 else:
325 325 mrev = self.repo.manifestlog.rev(mnode)
326 326 self.ui.write(
327 327 columns[b'manifest']
328 328 % scmutil.formatrevnode(self.ui, mrev, mnode),
329 329 label=b'ui.debug log.manifest',
330 330 )
331 331 self.ui.write(columns[b'user'] % ctx.user(), label=b'log.user')
332 332 self.ui.write(
333 333 columns[b'date'] % dateutil.datestr(ctx.date()), label=b'log.date'
334 334 )
335 335
336 336 if ctx.isunstable():
337 337 instabilities = ctx.instabilities()
338 338 self.ui.write(
339 339 columns[b'instability'] % b', '.join(instabilities),
340 340 label=b'log.instability',
341 341 )
342 342
343 343 elif ctx.obsolete():
344 344 self._showobsfate(ctx)
345 345
346 346 self._exthook(ctx)
347 347
348 348 if self.ui.debugflag:
349 349 files = ctx.p1().status(ctx)
350 350 for key, value in zip(
351 351 [b'files', b'files+', b'files-'],
352 352 [files.modified, files.added, files.removed],
353 353 ):
354 354 if value:
355 355 self.ui.write(
356 356 columns[key] % b" ".join(value),
357 357 label=b'ui.debug log.files',
358 358 )
359 359 elif ctx.files() and self.ui.verbose:
360 360 self.ui.write(
361 361 columns[b'files'] % b" ".join(ctx.files()),
362 362 label=b'ui.note log.files',
363 363 )
364 364 if copies and self.ui.verbose:
365 365 copies = [b'%s (%s)' % c for c in copies]
366 366 self.ui.write(
367 367 columns[b'copies'] % b' '.join(copies),
368 368 label=b'ui.note log.copies',
369 369 )
370 370
371 371 extra = ctx.extra()
372 372 if extra and self.ui.debugflag:
373 373 for key, value in sorted(extra.items()):
374 374 self.ui.write(
375 375 columns[b'extra'] % (key, stringutil.escapestr(value)),
376 376 label=b'ui.debug log.extra',
377 377 )
378 378
379 379 description = ctx.description().strip()
380 380 if description:
381 381 if self.ui.verbose:
382 382 self.ui.write(
383 383 _(b"description:\n"), label=b'ui.note log.description'
384 384 )
385 385 self.ui.write(description, label=b'ui.note log.description')
386 386 self.ui.write(b"\n\n")
387 387 else:
388 388 self.ui.write(
389 389 columns[b'summary'] % description.splitlines()[0],
390 390 label=b'log.summary',
391 391 )
392 392 self.ui.write(b"\n")
393 393
394 394 self._showpatch(ctx, graphwidth)
395 395
396 396 def _showobsfate(self, ctx):
397 397 # TODO: do not depend on templater
398 398 tres = formatter.templateresources(self.repo.ui, self.repo)
399 399 t = formatter.maketemplater(
400 400 self.repo.ui,
401 401 b'{join(obsfate, "\n")}',
402 402 defaults=templatekw.keywords,
403 403 resources=tres,
404 404 )
405 405 obsfate = t.renderdefault({b'ctx': ctx}).splitlines()
406 406
407 407 if obsfate:
408 408 for obsfateline in obsfate:
409 409 self.ui.write(
410 410 self._columns[b'obsolete'] % obsfateline,
411 411 label=b'log.obsfate',
412 412 )
413 413
414 414 def _exthook(self, ctx):
415 415 '''empty method used by extension as a hook point
416 416 '''
417 417
418 418 def _showpatch(self, ctx, graphwidth=0):
419 419 if self._includestat:
420 420 self._differ.showdiff(
421 421 self.ui, ctx, self._diffopts, graphwidth, stat=True
422 422 )
423 423 if self._includestat and self._includediff:
424 424 self.ui.write(b"\n")
425 425 if self._includediff:
426 426 self._differ.showdiff(
427 427 self.ui, ctx, self._diffopts, graphwidth, stat=False
428 428 )
429 429 if self._includestat or self._includediff:
430 430 self.ui.write(b"\n")
431 431
432 432
433 433 class changesetformatter(changesetprinter):
434 434 """Format changeset information by generic formatter"""
435 435
436 436 def __init__(
437 437 self, ui, repo, fm, differ=None, diffopts=None, buffered=False
438 438 ):
439 439 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
440 440 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
441 441 self._fm = fm
442 442
443 443 def close(self):
444 444 self._fm.end()
445 445
446 446 def _show(self, ctx, copies, props):
447 447 '''show a single changeset or file revision'''
448 448 fm = self._fm
449 449 fm.startitem()
450 450 fm.context(ctx=ctx)
451 451 fm.data(rev=scmutil.intrev(ctx), node=fm.hexfunc(scmutil.binnode(ctx)))
452 452
453 453 datahint = fm.datahint()
454 454 if self.ui.quiet and not datahint:
455 455 return
456 456
457 457 fm.data(
458 458 branch=ctx.branch(),
459 459 phase=ctx.phasestr(),
460 460 user=ctx.user(),
461 461 date=fm.formatdate(ctx.date()),
462 462 desc=ctx.description(),
463 463 bookmarks=fm.formatlist(ctx.bookmarks(), name=b'bookmark'),
464 464 tags=fm.formatlist(ctx.tags(), name=b'tag'),
465 465 parents=fm.formatlist(
466 466 [fm.hexfunc(c.node()) for c in ctx.parents()], name=b'node'
467 467 ),
468 468 )
469 469
470 470 if self.ui.debugflag or b'manifest' in datahint:
471 471 fm.data(manifest=fm.hexfunc(ctx.manifestnode() or wdirid))
472 472 if self.ui.debugflag or b'extra' in datahint:
473 473 fm.data(extra=fm.formatdict(ctx.extra()))
474 474
475 475 if (
476 476 self.ui.debugflag
477 477 or b'modified' in datahint
478 478 or b'added' in datahint
479 479 or b'removed' in datahint
480 480 ):
481 481 files = ctx.p1().status(ctx)
482 482 fm.data(
483 483 modified=fm.formatlist(files.modified, name=b'file'),
484 484 added=fm.formatlist(files.added, name=b'file'),
485 485 removed=fm.formatlist(files.removed, name=b'file'),
486 486 )
487 487
488 488 verbose = not self.ui.debugflag and self.ui.verbose
489 489 if verbose or b'files' in datahint:
490 490 fm.data(files=fm.formatlist(ctx.files(), name=b'file'))
491 491 if verbose and copies or b'copies' in datahint:
492 492 fm.data(
493 493 copies=fm.formatdict(copies or {}, key=b'name', value=b'source')
494 494 )
495 495
496 496 if self._includestat or b'diffstat' in datahint:
497 497 self.ui.pushbuffer()
498 498 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
499 499 fm.data(diffstat=self.ui.popbuffer())
500 500 if self._includediff or b'diff' in datahint:
501 501 self.ui.pushbuffer()
502 502 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
503 503 fm.data(diff=self.ui.popbuffer())
504 504
505 505
506 506 class changesettemplater(changesetprinter):
507 507 '''format changeset information.
508 508
509 509 Note: there are a variety of convenience functions to build a
510 510 changesettemplater for common cases. See functions such as:
511 511 maketemplater, changesetdisplayer, buildcommittemplate, or other
512 512 functions that use changesest_templater.
513 513 '''
514 514
515 515 # Arguments before "buffered" used to be positional. Consider not
516 516 # adding/removing arguments before "buffered" to not break callers.
517 517 def __init__(
518 518 self, ui, repo, tmplspec, differ=None, diffopts=None, buffered=False
519 519 ):
520 520 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
521 521 # tres is shared with _graphnodeformatter()
522 522 self._tresources = tres = formatter.templateresources(ui, repo)
523 523 self.t = formatter.loadtemplater(
524 524 ui,
525 525 tmplspec,
526 526 defaults=templatekw.keywords,
527 527 resources=tres,
528 528 cache=templatekw.defaulttempl,
529 529 )
530 530 self._counter = itertools.count()
531 531
532 532 self._tref = tmplspec.ref
533 533 self._parts = {
534 534 b'header': b'',
535 535 b'footer': b'',
536 536 tmplspec.ref: tmplspec.ref,
537 537 b'docheader': b'',
538 538 b'docfooter': b'',
539 539 b'separator': b'',
540 540 }
541 541 if tmplspec.mapfile:
542 542 # find correct templates for current mode, for backward
543 543 # compatibility with 'log -v/-q/--debug' using a mapfile
544 544 tmplmodes = [
545 545 (True, b''),
546 546 (self.ui.verbose, b'_verbose'),
547 547 (self.ui.quiet, b'_quiet'),
548 548 (self.ui.debugflag, b'_debug'),
549 549 ]
550 550 for mode, postfix in tmplmodes:
551 551 for t in self._parts:
552 552 cur = t + postfix
553 553 if mode and cur in self.t:
554 554 self._parts[t] = cur
555 555 else:
556 556 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
557 557 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
558 558 self._parts.update(m)
559 559
560 560 if self._parts[b'docheader']:
561 561 self.ui.write(self.t.render(self._parts[b'docheader'], {}))
562 562
563 563 def close(self):
564 564 if self._parts[b'docfooter']:
565 565 if not self.footer:
566 566 self.footer = b""
567 567 self.footer += self.t.render(self._parts[b'docfooter'], {})
568 568 return super(changesettemplater, self).close()
569 569
570 570 def _show(self, ctx, copies, props):
571 571 '''show a single changeset or file revision'''
572 572 props = props.copy()
573 573 props[b'ctx'] = ctx
574 574 props[b'index'] = index = next(self._counter)
575 575 props[b'revcache'] = {b'copies': copies}
576 576 graphwidth = props.get(b'graphwidth', 0)
577 577
578 578 # write separator, which wouldn't work well with the header part below
579 579 # since there's inherently a conflict between header (across items) and
580 580 # separator (per item)
581 581 if self._parts[b'separator'] and index > 0:
582 582 self.ui.write(self.t.render(self._parts[b'separator'], {}))
583 583
584 584 # write header
585 585 if self._parts[b'header']:
586 586 h = self.t.render(self._parts[b'header'], props)
587 587 if self.buffered:
588 588 self.header[ctx.rev()] = h
589 589 else:
590 590 if self.lastheader != h:
591 591 self.lastheader = h
592 592 self.ui.write(h)
593 593
594 594 # write changeset metadata, then patch if requested
595 595 key = self._parts[self._tref]
596 596 self.ui.write(self.t.render(key, props))
597 597 self._exthook(ctx)
598 598 self._showpatch(ctx, graphwidth)
599 599
600 600 if self._parts[b'footer']:
601 601 if not self.footer:
602 602 self.footer = self.t.render(self._parts[b'footer'], props)
603 603
604 604
605 605 def templatespec(tmpl, mapfile):
606 if pycompat.ispy3:
607 assert not isinstance(tmpl, str), b'tmpl must not be a str'
606 assert not (tmpl and mapfile)
608 607 if mapfile:
609 return formatter.templatespec(b'changeset', tmpl, mapfile)
608 return formatter.mapfile_templatespec(b'changeset', mapfile)
610 609 else:
611 return formatter.templatespec(b'', tmpl, None)
610 if pycompat.ispy3:
611 assert not isinstance(tmpl, str), b'tmpl must not be a str'
612 return formatter.literal_templatespec(tmpl)
612 613
613 614
614 615 def _lookuptemplate(ui, tmpl, style):
615 616 """Find the template matching the given template spec or style
616 617
617 618 See formatter.lookuptemplate() for details.
618 619 """
619 620
620 621 # ui settings
621 622 if not tmpl and not style: # template are stronger than style
622 623 tmpl = ui.config(b'ui', b'logtemplate')
623 624 if tmpl:
624 625 return templatespec(templater.unquotestring(tmpl), None)
625 626 else:
626 627 style = util.expandpath(ui.config(b'ui', b'style'))
627 628
628 629 if not tmpl and style:
629 630 mapfile = style
630 631 if not os.path.split(mapfile)[0]:
631 632 mapname = templater.templatepath(
632 633 b'map-cmdline.' + mapfile
633 634 ) or templater.templatepath(mapfile)
634 635 if mapname:
635 636 mapfile = mapname
636 637 return templatespec(None, mapfile)
637 638
638 639 return formatter.lookuptemplate(ui, b'changeset', tmpl)
639 640
640 641
641 642 def maketemplater(ui, repo, tmpl, buffered=False):
642 643 """Create a changesettemplater from a literal template 'tmpl'
643 644 byte-string."""
644 645 spec = templatespec(tmpl, None)
645 646 return changesettemplater(ui, repo, spec, buffered=buffered)
646 647
647 648
648 649 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
649 650 """show one changeset using template or regular display.
650 651
651 652 Display format will be the first non-empty hit of:
652 653 1. option 'template'
653 654 2. option 'style'
654 655 3. [ui] setting 'logtemplate'
655 656 4. [ui] setting 'style'
656 657 If all of these values are either the unset or the empty string,
657 658 regular display via changesetprinter() is done.
658 659 """
659 660 postargs = (differ, opts, buffered)
660 661 spec = _lookuptemplate(ui, opts.get(b'template'), opts.get(b'style'))
661 662
662 663 # machine-readable formats have slightly different keyword set than
663 664 # plain templates, which are handled by changesetformatter.
664 665 # note that {b'pickle', b'debug'} can also be added to the list if needed.
665 666 if spec.ref in {b'cbor', b'json'}:
666 667 fm = ui.formatter(b'log', opts)
667 668 return changesetformatter(ui, repo, fm, *postargs)
668 669
669 670 if not spec.ref and not spec.tmpl and not spec.mapfile:
670 671 return changesetprinter(ui, repo, *postargs)
671 672
672 673 return changesettemplater(ui, repo, spec, *postargs)
673 674
674 675
675 676 def _makematcher(repo, revs, pats, opts):
676 677 """Build matcher and expanded patterns from log options
677 678
678 679 If --follow, revs are the revisions to follow from.
679 680
680 681 Returns (match, pats, slowpath) where
681 682 - match: a matcher built from the given pats and -I/-X opts
682 683 - pats: patterns used (globs are expanded on Windows)
683 684 - slowpath: True if patterns aren't as simple as scanning filelogs
684 685 """
685 686 # pats/include/exclude are passed to match.match() directly in
686 687 # _matchfiles() revset but walkchangerevs() builds its matcher with
687 688 # scmutil.match(). The difference is input pats are globbed on
688 689 # platforms without shell expansion (windows).
689 690 wctx = repo[None]
690 691 match, pats = scmutil.matchandpats(wctx, pats, opts)
691 692 slowpath = match.anypats() or (not match.always() and opts.get(b'removed'))
692 693 if not slowpath:
693 694 follow = opts.get(b'follow') or opts.get(b'follow_first')
694 695 startctxs = []
695 696 if follow and opts.get(b'rev'):
696 697 startctxs = [repo[r] for r in revs]
697 698 for f in match.files():
698 699 if follow and startctxs:
699 700 # No idea if the path was a directory at that revision, so
700 701 # take the slow path.
701 702 if any(f not in c for c in startctxs):
702 703 slowpath = True
703 704 continue
704 705 elif follow and f not in wctx:
705 706 # If the file exists, it may be a directory, so let it
706 707 # take the slow path.
707 708 if os.path.exists(repo.wjoin(f)):
708 709 slowpath = True
709 710 continue
710 711 else:
711 712 raise error.Abort(
712 713 _(
713 714 b'cannot follow file not in parent '
714 715 b'revision: "%s"'
715 716 )
716 717 % f
717 718 )
718 719 filelog = repo.file(f)
719 720 if not filelog:
720 721 # A zero count may be a directory or deleted file, so
721 722 # try to find matching entries on the slow path.
722 723 if follow:
723 724 raise error.Abort(
724 725 _(b'cannot follow nonexistent file: "%s"') % f
725 726 )
726 727 slowpath = True
727 728
728 729 # We decided to fall back to the slowpath because at least one
729 730 # of the paths was not a file. Check to see if at least one of them
730 731 # existed in history - in that case, we'll continue down the
731 732 # slowpath; otherwise, we can turn off the slowpath
732 733 if slowpath:
733 734 for path in match.files():
734 735 if path == b'.' or path in repo.store:
735 736 break
736 737 else:
737 738 slowpath = False
738 739
739 740 return match, pats, slowpath
740 741
741 742
742 743 def _fileancestors(repo, revs, match, followfirst):
743 744 fctxs = []
744 745 for r in revs:
745 746 ctx = repo[r]
746 747 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
747 748
748 749 # When displaying a revision with --patch --follow FILE, we have
749 750 # to know which file of the revision must be diffed. With
750 751 # --follow, we want the names of the ancestors of FILE in the
751 752 # revision, stored in "fcache". "fcache" is populated as a side effect
752 753 # of the graph traversal.
753 754 fcache = {}
754 755
755 756 def filematcher(ctx):
756 757 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
757 758
758 759 def revgen():
759 760 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
760 761 fcache[rev] = [c.path() for c in cs]
761 762 yield rev
762 763
763 764 return smartset.generatorset(revgen(), iterasc=False), filematcher
764 765
765 766
766 767 def _makenofollowfilematcher(repo, pats, opts):
767 768 '''hook for extensions to override the filematcher for non-follow cases'''
768 769 return None
769 770
770 771
771 772 _opt2logrevset = {
772 773 b'no_merges': (b'not merge()', None),
773 774 b'only_merges': (b'merge()', None),
774 775 b'_matchfiles': (None, b'_matchfiles(%ps)'),
775 776 b'date': (b'date(%s)', None),
776 777 b'branch': (b'branch(%s)', b'%lr'),
777 778 b'_patslog': (b'filelog(%s)', b'%lr'),
778 779 b'keyword': (b'keyword(%s)', b'%lr'),
779 780 b'prune': (b'ancestors(%s)', b'not %lr'),
780 781 b'user': (b'user(%s)', b'%lr'),
781 782 }
782 783
783 784
784 785 def _makerevset(repo, match, pats, slowpath, opts):
785 786 """Return a revset string built from log options and file patterns"""
786 787 opts = dict(opts)
787 788 # follow or not follow?
788 789 follow = opts.get(b'follow') or opts.get(b'follow_first')
789 790
790 791 # branch and only_branch are really aliases and must be handled at
791 792 # the same time
792 793 opts[b'branch'] = opts.get(b'branch', []) + opts.get(b'only_branch', [])
793 794 opts[b'branch'] = [repo.lookupbranch(b) for b in opts[b'branch']]
794 795
795 796 if slowpath:
796 797 # See walkchangerevs() slow path.
797 798 #
798 799 # pats/include/exclude cannot be represented as separate
799 800 # revset expressions as their filtering logic applies at file
800 801 # level. For instance "-I a -X b" matches a revision touching
801 802 # "a" and "b" while "file(a) and not file(b)" does
802 803 # not. Besides, filesets are evaluated against the working
803 804 # directory.
804 805 matchargs = [b'r:', b'd:relpath']
805 806 for p in pats:
806 807 matchargs.append(b'p:' + p)
807 808 for p in opts.get(b'include', []):
808 809 matchargs.append(b'i:' + p)
809 810 for p in opts.get(b'exclude', []):
810 811 matchargs.append(b'x:' + p)
811 812 opts[b'_matchfiles'] = matchargs
812 813 elif not follow:
813 814 opts[b'_patslog'] = list(pats)
814 815
815 816 expr = []
816 817 for op, val in sorted(pycompat.iteritems(opts)):
817 818 if not val:
818 819 continue
819 820 if op not in _opt2logrevset:
820 821 continue
821 822 revop, listop = _opt2logrevset[op]
822 823 if revop and b'%' not in revop:
823 824 expr.append(revop)
824 825 elif not listop:
825 826 expr.append(revsetlang.formatspec(revop, val))
826 827 else:
827 828 if revop:
828 829 val = [revsetlang.formatspec(revop, v) for v in val]
829 830 expr.append(revsetlang.formatspec(listop, val))
830 831
831 832 if expr:
832 833 expr = b'(' + b' and '.join(expr) + b')'
833 834 else:
834 835 expr = None
835 836 return expr
836 837
837 838
838 839 def _initialrevs(repo, opts):
839 840 """Return the initial set of revisions to be filtered or followed"""
840 841 follow = opts.get(b'follow') or opts.get(b'follow_first')
841 842 if opts.get(b'rev'):
842 843 revs = scmutil.revrange(repo, opts[b'rev'])
843 844 elif follow and repo.dirstate.p1() == nullid:
844 845 revs = smartset.baseset()
845 846 elif follow:
846 847 revs = repo.revs(b'.')
847 848 else:
848 849 revs = smartset.spanset(repo)
849 850 revs.reverse()
850 851 return revs
851 852
852 853
853 854 def getrevs(repo, pats, opts):
854 855 # type: (Any, Any, Any) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
855 856 """Return (revs, differ) where revs is a smartset
856 857
857 858 differ is a changesetdiffer with pre-configured file matcher.
858 859 """
859 860 follow = opts.get(b'follow') or opts.get(b'follow_first')
860 861 followfirst = opts.get(b'follow_first')
861 862 limit = getlimit(opts)
862 863 revs = _initialrevs(repo, opts)
863 864 if not revs:
864 865 return smartset.baseset(), None
865 866 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
866 867 filematcher = None
867 868 if follow:
868 869 if slowpath or match.always():
869 870 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
870 871 else:
871 872 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
872 873 revs.reverse()
873 874 if filematcher is None:
874 875 filematcher = _makenofollowfilematcher(repo, pats, opts)
875 876 if filematcher is None:
876 877
877 878 def filematcher(ctx):
878 879 return match
879 880
880 881 expr = _makerevset(repo, match, pats, slowpath, opts)
881 882 if opts.get(b'graph'):
882 883 # User-specified revs might be unsorted, but don't sort before
883 884 # _makerevset because it might depend on the order of revs
884 885 if repo.ui.configbool(b'experimental', b'log.topo'):
885 886 if not revs.istopo():
886 887 revs = dagop.toposort(revs, repo.changelog.parentrevs)
887 888 # TODO: try to iterate the set lazily
888 889 revs = revset.baseset(list(revs), istopo=True)
889 890 elif not (revs.isdescending() or revs.istopo()):
890 891 revs.sort(reverse=True)
891 892 if expr:
892 893 matcher = revset.match(None, expr)
893 894 revs = matcher(repo, revs)
894 895 if limit is not None:
895 896 revs = revs.slice(0, limit)
896 897
897 898 differ = changesetdiffer()
898 899 differ._makefilematcher = filematcher
899 900 return revs, differ
900 901
901 902
902 903 def _parselinerangeopt(repo, opts):
903 904 """Parse --line-range log option and return a list of tuples (filename,
904 905 (fromline, toline)).
905 906 """
906 907 linerangebyfname = []
907 908 for pat in opts.get(b'line_range', []):
908 909 try:
909 910 pat, linerange = pat.rsplit(b',', 1)
910 911 except ValueError:
911 912 raise error.Abort(_(b'malformatted line-range pattern %s') % pat)
912 913 try:
913 914 fromline, toline = map(int, linerange.split(b':'))
914 915 except ValueError:
915 916 raise error.Abort(_(b"invalid line range for %s") % pat)
916 917 msg = _(b"line range pattern '%s' must match exactly one file") % pat
917 918 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
918 919 linerangebyfname.append(
919 920 (fname, util.processlinerange(fromline, toline))
920 921 )
921 922 return linerangebyfname
922 923
923 924
924 925 def getlinerangerevs(repo, userrevs, opts):
925 926 """Return (revs, differ).
926 927
927 928 "revs" are revisions obtained by processing "line-range" log options and
928 929 walking block ancestors of each specified file/line-range.
929 930
930 931 "differ" is a changesetdiffer with pre-configured file matcher and hunks
931 932 filter.
932 933 """
933 934 wctx = repo[None]
934 935
935 936 # Two-levels map of "rev -> file ctx -> [line range]".
936 937 linerangesbyrev = {}
937 938 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
938 939 if fname not in wctx:
939 940 raise error.Abort(
940 941 _(b'cannot follow file not in parent revision: "%s"') % fname
941 942 )
942 943 fctx = wctx.filectx(fname)
943 944 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
944 945 rev = fctx.introrev()
945 946 if rev is None:
946 947 rev = wdirrev
947 948 if rev not in userrevs:
948 949 continue
949 950 linerangesbyrev.setdefault(rev, {}).setdefault(
950 951 fctx.path(), []
951 952 ).append(linerange)
952 953
953 954 def nofilterhunksfn(fctx, hunks):
954 955 return hunks
955 956
956 957 def hunksfilter(ctx):
957 958 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
958 959 if fctxlineranges is None:
959 960 return nofilterhunksfn
960 961
961 962 def filterfn(fctx, hunks):
962 963 lineranges = fctxlineranges.get(fctx.path())
963 964 if lineranges is not None:
964 965 for hr, lines in hunks:
965 966 if hr is None: # binary
966 967 yield hr, lines
967 968 continue
968 969 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
969 970 yield hr, lines
970 971 else:
971 972 for hunk in hunks:
972 973 yield hunk
973 974
974 975 return filterfn
975 976
976 977 def filematcher(ctx):
977 978 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
978 979 return scmutil.matchfiles(repo, files)
979 980
980 981 revs = sorted(linerangesbyrev, reverse=True)
981 982
982 983 differ = changesetdiffer()
983 984 differ._makefilematcher = filematcher
984 985 differ._makehunksfilter = hunksfilter
985 986 return smartset.baseset(revs), differ
986 987
987 988
988 989 def _graphnodeformatter(ui, displayer):
989 990 spec = ui.config(b'ui', b'graphnodetemplate')
990 991 if not spec:
991 992 return templatekw.getgraphnode # fast path for "{graphnode}"
992 993
993 994 spec = templater.unquotestring(spec)
994 995 if isinstance(displayer, changesettemplater):
995 996 # reuse cache of slow templates
996 997 tres = displayer._tresources
997 998 else:
998 999 tres = formatter.templateresources(ui)
999 1000 templ = formatter.maketemplater(
1000 1001 ui, spec, defaults=templatekw.keywords, resources=tres
1001 1002 )
1002 1003
1003 1004 def formatnode(repo, ctx, cache):
1004 1005 props = {b'ctx': ctx, b'repo': repo}
1005 1006 return templ.renderdefault(props)
1006 1007
1007 1008 return formatnode
1008 1009
1009 1010
1010 1011 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
1011 1012 props = props or {}
1012 1013 formatnode = _graphnodeformatter(ui, displayer)
1013 1014 state = graphmod.asciistate()
1014 1015 styles = state.styles
1015 1016
1016 1017 # only set graph styling if HGPLAIN is not set.
1017 1018 if ui.plain(b'graph'):
1018 1019 # set all edge styles to |, the default pre-3.8 behaviour
1019 1020 styles.update(dict.fromkeys(styles, b'|'))
1020 1021 else:
1021 1022 edgetypes = {
1022 1023 b'parent': graphmod.PARENT,
1023 1024 b'grandparent': graphmod.GRANDPARENT,
1024 1025 b'missing': graphmod.MISSINGPARENT,
1025 1026 }
1026 1027 for name, key in edgetypes.items():
1027 1028 # experimental config: experimental.graphstyle.*
1028 1029 styles[key] = ui.config(
1029 1030 b'experimental', b'graphstyle.%s' % name, styles[key]
1030 1031 )
1031 1032 if not styles[key]:
1032 1033 styles[key] = None
1033 1034
1034 1035 # experimental config: experimental.graphshorten
1035 1036 state.graphshorten = ui.configbool(b'experimental', b'graphshorten')
1036 1037
1037 1038 formatnode_cache = {}
1038 1039 for rev, type, ctx, parents in dag:
1039 1040 char = formatnode(repo, ctx, formatnode_cache)
1040 1041 copies = getcopies(ctx) if getcopies else None
1041 1042 edges = edgefn(type, char, state, rev, parents)
1042 1043 firstedge = next(edges)
1043 1044 width = firstedge[2]
1044 1045 displayer.show(
1045 1046 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1046 1047 )
1047 1048 lines = displayer.hunk.pop(rev).split(b'\n')
1048 1049 if not lines[-1]:
1049 1050 del lines[-1]
1050 1051 displayer.flush(ctx)
1051 1052 for type, char, width, coldata in itertools.chain([firstedge], edges):
1052 1053 graphmod.ascii(ui, state, type, char, lines, coldata)
1053 1054 lines = []
1054 1055 displayer.close()
1055 1056
1056 1057
1057 1058 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1058 1059 revdag = graphmod.dagwalker(repo, revs)
1059 1060 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1060 1061
1061 1062
1062 1063 def displayrevs(ui, repo, revs, displayer, getcopies):
1063 1064 for rev in revs:
1064 1065 ctx = repo[rev]
1065 1066 copies = getcopies(ctx) if getcopies else None
1066 1067 displayer.show(ctx, copies=copies)
1067 1068 displayer.flush(ctx)
1068 1069 displayer.close()
1069 1070
1070 1071
1071 1072 def checkunsupportedgraphflags(pats, opts):
1072 1073 for op in [b"newest_first"]:
1073 1074 if op in opts and opts[op]:
1074 1075 raise error.Abort(
1075 1076 _(b"-G/--graph option is incompatible with --%s")
1076 1077 % op.replace(b"_", b"-")
1077 1078 )
1078 1079
1079 1080
1080 1081 def graphrevs(repo, nodes, opts):
1081 1082 limit = getlimit(opts)
1082 1083 nodes.reverse()
1083 1084 if limit is not None:
1084 1085 nodes = nodes[:limit]
1085 1086 return graphmod.nodes(repo, nodes)
General Comments 0
You need to be logged in to leave comments. Login now