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