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