##// END OF EJS Templates
templatespec: use new factory functions in logcmdutil...
Martin von Zweigbergk -
r45827:c1915cfa default
parent child Browse files
Show More
@@ -1,1084 +1,1084 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 606 assert not (tmpl and mapfile)
607 607 if mapfile:
608 608 return formatter.mapfile_templatespec(b'changeset', mapfile)
609 609 else:
610 610 return formatter.literal_templatespec(tmpl)
611 611
612 612
613 613 def _lookuptemplate(ui, tmpl, style):
614 614 """Find the template matching the given template spec or style
615 615
616 616 See formatter.lookuptemplate() for details.
617 617 """
618 618
619 619 # ui settings
620 620 if not tmpl and not style: # template are stronger than style
621 621 tmpl = ui.config(b'ui', b'logtemplate')
622 622 if tmpl:
623 return templatespec(templater.unquotestring(tmpl), None)
623 return formatter.literal_templatespec(templater.unquotestring(tmpl))
624 624 else:
625 625 style = util.expandpath(ui.config(b'ui', b'style'))
626 626
627 627 if not tmpl and style:
628 628 mapfile = style
629 629 if not os.path.split(mapfile)[0]:
630 630 mapname = templater.templatepath(
631 631 b'map-cmdline.' + mapfile
632 632 ) or templater.templatepath(mapfile)
633 633 if mapname:
634 634 mapfile = mapname
635 return templatespec(None, mapfile)
635 return formatter.mapfile_templatespec(b'changeset', mapfile)
636 636
637 637 return formatter.lookuptemplate(ui, b'changeset', tmpl)
638 638
639 639
640 640 def maketemplater(ui, repo, tmpl, buffered=False):
641 641 """Create a changesettemplater from a literal template 'tmpl'
642 642 byte-string."""
643 spec = templatespec(tmpl, None)
643 spec = formatter.literal_templatespec(tmpl)
644 644 return changesettemplater(ui, repo, spec, buffered=buffered)
645 645
646 646
647 647 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
648 648 """show one changeset using template or regular display.
649 649
650 650 Display format will be the first non-empty hit of:
651 651 1. option 'template'
652 652 2. option 'style'
653 653 3. [ui] setting 'logtemplate'
654 654 4. [ui] setting 'style'
655 655 If all of these values are either the unset or the empty string,
656 656 regular display via changesetprinter() is done.
657 657 """
658 658 postargs = (differ, opts, buffered)
659 659 spec = _lookuptemplate(ui, opts.get(b'template'), opts.get(b'style'))
660 660
661 661 # machine-readable formats have slightly different keyword set than
662 662 # plain templates, which are handled by changesetformatter.
663 663 # note that {b'pickle', b'debug'} can also be added to the list if needed.
664 664 if spec.ref in {b'cbor', b'json'}:
665 665 fm = ui.formatter(b'log', opts)
666 666 return changesetformatter(ui, repo, fm, *postargs)
667 667
668 668 if not spec.ref and not spec.tmpl and not spec.mapfile:
669 669 return changesetprinter(ui, repo, *postargs)
670 670
671 671 return changesettemplater(ui, repo, spec, *postargs)
672 672
673 673
674 674 def _makematcher(repo, revs, pats, opts):
675 675 """Build matcher and expanded patterns from log options
676 676
677 677 If --follow, revs are the revisions to follow from.
678 678
679 679 Returns (match, pats, slowpath) where
680 680 - match: a matcher built from the given pats and -I/-X opts
681 681 - pats: patterns used (globs are expanded on Windows)
682 682 - slowpath: True if patterns aren't as simple as scanning filelogs
683 683 """
684 684 # pats/include/exclude are passed to match.match() directly in
685 685 # _matchfiles() revset but walkchangerevs() builds its matcher with
686 686 # scmutil.match(). The difference is input pats are globbed on
687 687 # platforms without shell expansion (windows).
688 688 wctx = repo[None]
689 689 match, pats = scmutil.matchandpats(wctx, pats, opts)
690 690 slowpath = match.anypats() or (not match.always() and opts.get(b'removed'))
691 691 if not slowpath:
692 692 follow = opts.get(b'follow') or opts.get(b'follow_first')
693 693 startctxs = []
694 694 if follow and opts.get(b'rev'):
695 695 startctxs = [repo[r] for r in revs]
696 696 for f in match.files():
697 697 if follow and startctxs:
698 698 # No idea if the path was a directory at that revision, so
699 699 # take the slow path.
700 700 if any(f not in c for c in startctxs):
701 701 slowpath = True
702 702 continue
703 703 elif follow and f not in wctx:
704 704 # If the file exists, it may be a directory, so let it
705 705 # take the slow path.
706 706 if os.path.exists(repo.wjoin(f)):
707 707 slowpath = True
708 708 continue
709 709 else:
710 710 raise error.Abort(
711 711 _(
712 712 b'cannot follow file not in parent '
713 713 b'revision: "%s"'
714 714 )
715 715 % f
716 716 )
717 717 filelog = repo.file(f)
718 718 if not filelog:
719 719 # A zero count may be a directory or deleted file, so
720 720 # try to find matching entries on the slow path.
721 721 if follow:
722 722 raise error.Abort(
723 723 _(b'cannot follow nonexistent file: "%s"') % f
724 724 )
725 725 slowpath = True
726 726
727 727 # We decided to fall back to the slowpath because at least one
728 728 # of the paths was not a file. Check to see if at least one of them
729 729 # existed in history - in that case, we'll continue down the
730 730 # slowpath; otherwise, we can turn off the slowpath
731 731 if slowpath:
732 732 for path in match.files():
733 733 if path == b'.' or path in repo.store:
734 734 break
735 735 else:
736 736 slowpath = False
737 737
738 738 return match, pats, slowpath
739 739
740 740
741 741 def _fileancestors(repo, revs, match, followfirst):
742 742 fctxs = []
743 743 for r in revs:
744 744 ctx = repo[r]
745 745 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
746 746
747 747 # When displaying a revision with --patch --follow FILE, we have
748 748 # to know which file of the revision must be diffed. With
749 749 # --follow, we want the names of the ancestors of FILE in the
750 750 # revision, stored in "fcache". "fcache" is populated as a side effect
751 751 # of the graph traversal.
752 752 fcache = {}
753 753
754 754 def filematcher(ctx):
755 755 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
756 756
757 757 def revgen():
758 758 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
759 759 fcache[rev] = [c.path() for c in cs]
760 760 yield rev
761 761
762 762 return smartset.generatorset(revgen(), iterasc=False), filematcher
763 763
764 764
765 765 def _makenofollowfilematcher(repo, pats, opts):
766 766 '''hook for extensions to override the filematcher for non-follow cases'''
767 767 return None
768 768
769 769
770 770 _opt2logrevset = {
771 771 b'no_merges': (b'not merge()', None),
772 772 b'only_merges': (b'merge()', None),
773 773 b'_matchfiles': (None, b'_matchfiles(%ps)'),
774 774 b'date': (b'date(%s)', None),
775 775 b'branch': (b'branch(%s)', b'%lr'),
776 776 b'_patslog': (b'filelog(%s)', b'%lr'),
777 777 b'keyword': (b'keyword(%s)', b'%lr'),
778 778 b'prune': (b'ancestors(%s)', b'not %lr'),
779 779 b'user': (b'user(%s)', b'%lr'),
780 780 }
781 781
782 782
783 783 def _makerevset(repo, match, pats, slowpath, opts):
784 784 """Return a revset string built from log options and file patterns"""
785 785 opts = dict(opts)
786 786 # follow or not follow?
787 787 follow = opts.get(b'follow') or opts.get(b'follow_first')
788 788
789 789 # branch and only_branch are really aliases and must be handled at
790 790 # the same time
791 791 opts[b'branch'] = opts.get(b'branch', []) + opts.get(b'only_branch', [])
792 792 opts[b'branch'] = [repo.lookupbranch(b) for b in opts[b'branch']]
793 793
794 794 if slowpath:
795 795 # See walkchangerevs() slow path.
796 796 #
797 797 # pats/include/exclude cannot be represented as separate
798 798 # revset expressions as their filtering logic applies at file
799 799 # level. For instance "-I a -X b" matches a revision touching
800 800 # "a" and "b" while "file(a) and not file(b)" does
801 801 # not. Besides, filesets are evaluated against the working
802 802 # directory.
803 803 matchargs = [b'r:', b'd:relpath']
804 804 for p in pats:
805 805 matchargs.append(b'p:' + p)
806 806 for p in opts.get(b'include', []):
807 807 matchargs.append(b'i:' + p)
808 808 for p in opts.get(b'exclude', []):
809 809 matchargs.append(b'x:' + p)
810 810 opts[b'_matchfiles'] = matchargs
811 811 elif not follow:
812 812 opts[b'_patslog'] = list(pats)
813 813
814 814 expr = []
815 815 for op, val in sorted(pycompat.iteritems(opts)):
816 816 if not val:
817 817 continue
818 818 if op not in _opt2logrevset:
819 819 continue
820 820 revop, listop = _opt2logrevset[op]
821 821 if revop and b'%' not in revop:
822 822 expr.append(revop)
823 823 elif not listop:
824 824 expr.append(revsetlang.formatspec(revop, val))
825 825 else:
826 826 if revop:
827 827 val = [revsetlang.formatspec(revop, v) for v in val]
828 828 expr.append(revsetlang.formatspec(listop, val))
829 829
830 830 if expr:
831 831 expr = b'(' + b' and '.join(expr) + b')'
832 832 else:
833 833 expr = None
834 834 return expr
835 835
836 836
837 837 def _initialrevs(repo, opts):
838 838 """Return the initial set of revisions to be filtered or followed"""
839 839 follow = opts.get(b'follow') or opts.get(b'follow_first')
840 840 if opts.get(b'rev'):
841 841 revs = scmutil.revrange(repo, opts[b'rev'])
842 842 elif follow and repo.dirstate.p1() == nullid:
843 843 revs = smartset.baseset()
844 844 elif follow:
845 845 revs = repo.revs(b'.')
846 846 else:
847 847 revs = smartset.spanset(repo)
848 848 revs.reverse()
849 849 return revs
850 850
851 851
852 852 def getrevs(repo, pats, opts):
853 853 # type: (Any, Any, Any) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
854 854 """Return (revs, differ) where revs is a smartset
855 855
856 856 differ is a changesetdiffer with pre-configured file matcher.
857 857 """
858 858 follow = opts.get(b'follow') or opts.get(b'follow_first')
859 859 followfirst = opts.get(b'follow_first')
860 860 limit = getlimit(opts)
861 861 revs = _initialrevs(repo, opts)
862 862 if not revs:
863 863 return smartset.baseset(), None
864 864 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
865 865 filematcher = None
866 866 if follow:
867 867 if slowpath or match.always():
868 868 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
869 869 else:
870 870 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
871 871 revs.reverse()
872 872 if filematcher is None:
873 873 filematcher = _makenofollowfilematcher(repo, pats, opts)
874 874 if filematcher is None:
875 875
876 876 def filematcher(ctx):
877 877 return match
878 878
879 879 expr = _makerevset(repo, match, pats, slowpath, opts)
880 880 if opts.get(b'graph'):
881 881 # User-specified revs might be unsorted, but don't sort before
882 882 # _makerevset because it might depend on the order of revs
883 883 if repo.ui.configbool(b'experimental', b'log.topo'):
884 884 if not revs.istopo():
885 885 revs = dagop.toposort(revs, repo.changelog.parentrevs)
886 886 # TODO: try to iterate the set lazily
887 887 revs = revset.baseset(list(revs), istopo=True)
888 888 elif not (revs.isdescending() or revs.istopo()):
889 889 revs.sort(reverse=True)
890 890 if expr:
891 891 matcher = revset.match(None, expr)
892 892 revs = matcher(repo, revs)
893 893 if limit is not None:
894 894 revs = revs.slice(0, limit)
895 895
896 896 differ = changesetdiffer()
897 897 differ._makefilematcher = filematcher
898 898 return revs, differ
899 899
900 900
901 901 def _parselinerangeopt(repo, opts):
902 902 """Parse --line-range log option and return a list of tuples (filename,
903 903 (fromline, toline)).
904 904 """
905 905 linerangebyfname = []
906 906 for pat in opts.get(b'line_range', []):
907 907 try:
908 908 pat, linerange = pat.rsplit(b',', 1)
909 909 except ValueError:
910 910 raise error.Abort(_(b'malformatted line-range pattern %s') % pat)
911 911 try:
912 912 fromline, toline = map(int, linerange.split(b':'))
913 913 except ValueError:
914 914 raise error.Abort(_(b"invalid line range for %s") % pat)
915 915 msg = _(b"line range pattern '%s' must match exactly one file") % pat
916 916 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
917 917 linerangebyfname.append(
918 918 (fname, util.processlinerange(fromline, toline))
919 919 )
920 920 return linerangebyfname
921 921
922 922
923 923 def getlinerangerevs(repo, userrevs, opts):
924 924 """Return (revs, differ).
925 925
926 926 "revs" are revisions obtained by processing "line-range" log options and
927 927 walking block ancestors of each specified file/line-range.
928 928
929 929 "differ" is a changesetdiffer with pre-configured file matcher and hunks
930 930 filter.
931 931 """
932 932 wctx = repo[None]
933 933
934 934 # Two-levels map of "rev -> file ctx -> [line range]".
935 935 linerangesbyrev = {}
936 936 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
937 937 if fname not in wctx:
938 938 raise error.Abort(
939 939 _(b'cannot follow file not in parent revision: "%s"') % fname
940 940 )
941 941 fctx = wctx.filectx(fname)
942 942 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
943 943 rev = fctx.introrev()
944 944 if rev is None:
945 945 rev = wdirrev
946 946 if rev not in userrevs:
947 947 continue
948 948 linerangesbyrev.setdefault(rev, {}).setdefault(
949 949 fctx.path(), []
950 950 ).append(linerange)
951 951
952 952 def nofilterhunksfn(fctx, hunks):
953 953 return hunks
954 954
955 955 def hunksfilter(ctx):
956 956 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
957 957 if fctxlineranges is None:
958 958 return nofilterhunksfn
959 959
960 960 def filterfn(fctx, hunks):
961 961 lineranges = fctxlineranges.get(fctx.path())
962 962 if lineranges is not None:
963 963 for hr, lines in hunks:
964 964 if hr is None: # binary
965 965 yield hr, lines
966 966 continue
967 967 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
968 968 yield hr, lines
969 969 else:
970 970 for hunk in hunks:
971 971 yield hunk
972 972
973 973 return filterfn
974 974
975 975 def filematcher(ctx):
976 976 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
977 977 return scmutil.matchfiles(repo, files)
978 978
979 979 revs = sorted(linerangesbyrev, reverse=True)
980 980
981 981 differ = changesetdiffer()
982 982 differ._makefilematcher = filematcher
983 983 differ._makehunksfilter = hunksfilter
984 984 return smartset.baseset(revs), differ
985 985
986 986
987 987 def _graphnodeformatter(ui, displayer):
988 988 spec = ui.config(b'ui', b'graphnodetemplate')
989 989 if not spec:
990 990 return templatekw.getgraphnode # fast path for "{graphnode}"
991 991
992 992 spec = templater.unquotestring(spec)
993 993 if isinstance(displayer, changesettemplater):
994 994 # reuse cache of slow templates
995 995 tres = displayer._tresources
996 996 else:
997 997 tres = formatter.templateresources(ui)
998 998 templ = formatter.maketemplater(
999 999 ui, spec, defaults=templatekw.keywords, resources=tres
1000 1000 )
1001 1001
1002 1002 def formatnode(repo, ctx, cache):
1003 1003 props = {b'ctx': ctx, b'repo': repo}
1004 1004 return templ.renderdefault(props)
1005 1005
1006 1006 return formatnode
1007 1007
1008 1008
1009 1009 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
1010 1010 props = props or {}
1011 1011 formatnode = _graphnodeformatter(ui, displayer)
1012 1012 state = graphmod.asciistate()
1013 1013 styles = state.styles
1014 1014
1015 1015 # only set graph styling if HGPLAIN is not set.
1016 1016 if ui.plain(b'graph'):
1017 1017 # set all edge styles to |, the default pre-3.8 behaviour
1018 1018 styles.update(dict.fromkeys(styles, b'|'))
1019 1019 else:
1020 1020 edgetypes = {
1021 1021 b'parent': graphmod.PARENT,
1022 1022 b'grandparent': graphmod.GRANDPARENT,
1023 1023 b'missing': graphmod.MISSINGPARENT,
1024 1024 }
1025 1025 for name, key in edgetypes.items():
1026 1026 # experimental config: experimental.graphstyle.*
1027 1027 styles[key] = ui.config(
1028 1028 b'experimental', b'graphstyle.%s' % name, styles[key]
1029 1029 )
1030 1030 if not styles[key]:
1031 1031 styles[key] = None
1032 1032
1033 1033 # experimental config: experimental.graphshorten
1034 1034 state.graphshorten = ui.configbool(b'experimental', b'graphshorten')
1035 1035
1036 1036 formatnode_cache = {}
1037 1037 for rev, type, ctx, parents in dag:
1038 1038 char = formatnode(repo, ctx, formatnode_cache)
1039 1039 copies = getcopies(ctx) if getcopies else None
1040 1040 edges = edgefn(type, char, state, rev, parents)
1041 1041 firstedge = next(edges)
1042 1042 width = firstedge[2]
1043 1043 displayer.show(
1044 1044 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1045 1045 )
1046 1046 lines = displayer.hunk.pop(rev).split(b'\n')
1047 1047 if not lines[-1]:
1048 1048 del lines[-1]
1049 1049 displayer.flush(ctx)
1050 1050 for type, char, width, coldata in itertools.chain([firstedge], edges):
1051 1051 graphmod.ascii(ui, state, type, char, lines, coldata)
1052 1052 lines = []
1053 1053 displayer.close()
1054 1054
1055 1055
1056 1056 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1057 1057 revdag = graphmod.dagwalker(repo, revs)
1058 1058 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1059 1059
1060 1060
1061 1061 def displayrevs(ui, repo, revs, displayer, getcopies):
1062 1062 for rev in revs:
1063 1063 ctx = repo[rev]
1064 1064 copies = getcopies(ctx) if getcopies else None
1065 1065 displayer.show(ctx, copies=copies)
1066 1066 displayer.flush(ctx)
1067 1067 displayer.close()
1068 1068
1069 1069
1070 1070 def checkunsupportedgraphflags(pats, opts):
1071 1071 for op in [b"newest_first"]:
1072 1072 if op in opts and opts[op]:
1073 1073 raise error.Abort(
1074 1074 _(b"-G/--graph option is incompatible with --%s")
1075 1075 % op.replace(b"_", b"-")
1076 1076 )
1077 1077
1078 1078
1079 1079 def graphrevs(repo, nodes, opts):
1080 1080 limit = getlimit(opts)
1081 1081 nodes.reverse()
1082 1082 if limit is not None:
1083 1083 nodes = nodes[:limit]
1084 1084 return graphmod.nodes(repo, nodes)
General Comments 0
You need to be logged in to leave comments. Login now