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