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