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