##// END OF EJS Templates
log: remove unused argument from _makerevset()...
Yuya Nishihara -
r46138:a717de1c default
parent child Browse files
Show More
@@ -1,1103 +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 def _makerevset(repo, match, pats, slowpath, opts):
803 def _makerevset(repo, 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 expr = _makerevset(repo, match, pats, slowpath, opts)
899 expr = _makerevset(repo, pats, slowpath, opts)
900 900 if opts.get(b'graph'):
901 901 if repo.ui.configbool(b'experimental', b'log.topo'):
902 902 if not revs.istopo():
903 903 revs = dagop.toposort(revs, repo.changelog.parentrevs)
904 904 # TODO: try to iterate the set lazily
905 905 revs = revset.baseset(list(revs), istopo=True)
906 906 elif not (revs.isdescending() or revs.istopo()):
907 907 # User-specified revs might be unsorted
908 908 revs.sort(reverse=True)
909 909 if expr:
910 910 matcher = revset.match(None, expr)
911 911 revs = matcher(repo, revs)
912 912 if limit is not None:
913 913 revs = revs.slice(0, limit)
914 914
915 915 differ = changesetdiffer()
916 916 differ._makefilematcher = filematcher
917 917 return revs, differ
918 918
919 919
920 920 def _parselinerangeopt(repo, opts):
921 921 """Parse --line-range log option and return a list of tuples (filename,
922 922 (fromline, toline)).
923 923 """
924 924 linerangebyfname = []
925 925 for pat in opts.get(b'line_range', []):
926 926 try:
927 927 pat, linerange = pat.rsplit(b',', 1)
928 928 except ValueError:
929 929 raise error.Abort(_(b'malformatted line-range pattern %s') % pat)
930 930 try:
931 931 fromline, toline = map(int, linerange.split(b':'))
932 932 except ValueError:
933 933 raise error.Abort(_(b"invalid line range for %s") % pat)
934 934 msg = _(b"line range pattern '%s' must match exactly one file") % pat
935 935 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
936 936 linerangebyfname.append(
937 937 (fname, util.processlinerange(fromline, toline))
938 938 )
939 939 return linerangebyfname
940 940
941 941
942 942 def getlinerangerevs(repo, userrevs, opts):
943 943 """Return (revs, differ).
944 944
945 945 "revs" are revisions obtained by processing "line-range" log options and
946 946 walking block ancestors of each specified file/line-range.
947 947
948 948 "differ" is a changesetdiffer with pre-configured file matcher and hunks
949 949 filter.
950 950 """
951 951 wctx = repo[None]
952 952
953 953 # Two-levels map of "rev -> file ctx -> [line range]".
954 954 linerangesbyrev = {}
955 955 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
956 956 if fname not in wctx:
957 957 raise error.Abort(
958 958 _(b'cannot follow file not in parent revision: "%s"') % fname
959 959 )
960 960 fctx = wctx.filectx(fname)
961 961 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
962 962 rev = fctx.introrev()
963 963 if rev is None:
964 964 rev = wdirrev
965 965 if rev not in userrevs:
966 966 continue
967 967 linerangesbyrev.setdefault(rev, {}).setdefault(
968 968 fctx.path(), []
969 969 ).append(linerange)
970 970
971 971 def nofilterhunksfn(fctx, hunks):
972 972 return hunks
973 973
974 974 def hunksfilter(ctx):
975 975 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
976 976 if fctxlineranges is None:
977 977 return nofilterhunksfn
978 978
979 979 def filterfn(fctx, hunks):
980 980 lineranges = fctxlineranges.get(fctx.path())
981 981 if lineranges is not None:
982 982 for hr, lines in hunks:
983 983 if hr is None: # binary
984 984 yield hr, lines
985 985 continue
986 986 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
987 987 yield hr, lines
988 988 else:
989 989 for hunk in hunks:
990 990 yield hunk
991 991
992 992 return filterfn
993 993
994 994 def filematcher(ctx):
995 995 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
996 996 return scmutil.matchfiles(repo, files)
997 997
998 998 revs = sorted(linerangesbyrev, reverse=True)
999 999
1000 1000 differ = changesetdiffer()
1001 1001 differ._makefilematcher = filematcher
1002 1002 differ._makehunksfilter = hunksfilter
1003 1003 return smartset.baseset(revs), differ
1004 1004
1005 1005
1006 1006 def _graphnodeformatter(ui, displayer):
1007 1007 spec = ui.config(b'ui', b'graphnodetemplate')
1008 1008 if not spec:
1009 1009 return templatekw.getgraphnode # fast path for "{graphnode}"
1010 1010
1011 1011 spec = templater.unquotestring(spec)
1012 1012 if isinstance(displayer, changesettemplater):
1013 1013 # reuse cache of slow templates
1014 1014 tres = displayer._tresources
1015 1015 else:
1016 1016 tres = formatter.templateresources(ui)
1017 1017 templ = formatter.maketemplater(
1018 1018 ui, spec, defaults=templatekw.keywords, resources=tres
1019 1019 )
1020 1020
1021 1021 def formatnode(repo, ctx, cache):
1022 1022 props = {b'ctx': ctx, b'repo': repo}
1023 1023 return templ.renderdefault(props)
1024 1024
1025 1025 return formatnode
1026 1026
1027 1027
1028 1028 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
1029 1029 props = props or {}
1030 1030 formatnode = _graphnodeformatter(ui, displayer)
1031 1031 state = graphmod.asciistate()
1032 1032 styles = state.styles
1033 1033
1034 1034 # only set graph styling if HGPLAIN is not set.
1035 1035 if ui.plain(b'graph'):
1036 1036 # set all edge styles to |, the default pre-3.8 behaviour
1037 1037 styles.update(dict.fromkeys(styles, b'|'))
1038 1038 else:
1039 1039 edgetypes = {
1040 1040 b'parent': graphmod.PARENT,
1041 1041 b'grandparent': graphmod.GRANDPARENT,
1042 1042 b'missing': graphmod.MISSINGPARENT,
1043 1043 }
1044 1044 for name, key in edgetypes.items():
1045 1045 # experimental config: experimental.graphstyle.*
1046 1046 styles[key] = ui.config(
1047 1047 b'experimental', b'graphstyle.%s' % name, styles[key]
1048 1048 )
1049 1049 if not styles[key]:
1050 1050 styles[key] = None
1051 1051
1052 1052 # experimental config: experimental.graphshorten
1053 1053 state.graphshorten = ui.configbool(b'experimental', b'graphshorten')
1054 1054
1055 1055 formatnode_cache = {}
1056 1056 for rev, type, ctx, parents in dag:
1057 1057 char = formatnode(repo, ctx, formatnode_cache)
1058 1058 copies = getcopies(ctx) if getcopies else None
1059 1059 edges = edgefn(type, char, state, rev, parents)
1060 1060 firstedge = next(edges)
1061 1061 width = firstedge[2]
1062 1062 displayer.show(
1063 1063 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1064 1064 )
1065 1065 lines = displayer.hunk.pop(rev).split(b'\n')
1066 1066 if not lines[-1]:
1067 1067 del lines[-1]
1068 1068 displayer.flush(ctx)
1069 1069 for type, char, width, coldata in itertools.chain([firstedge], edges):
1070 1070 graphmod.ascii(ui, state, type, char, lines, coldata)
1071 1071 lines = []
1072 1072 displayer.close()
1073 1073
1074 1074
1075 1075 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1076 1076 revdag = graphmod.dagwalker(repo, revs)
1077 1077 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1078 1078
1079 1079
1080 1080 def displayrevs(ui, repo, revs, displayer, getcopies):
1081 1081 for rev in revs:
1082 1082 ctx = repo[rev]
1083 1083 copies = getcopies(ctx) if getcopies else None
1084 1084 displayer.show(ctx, copies=copies)
1085 1085 displayer.flush(ctx)
1086 1086 displayer.close()
1087 1087
1088 1088
1089 1089 def checkunsupportedgraphflags(pats, opts):
1090 1090 for op in [b"newest_first"]:
1091 1091 if op in opts and opts[op]:
1092 1092 raise error.Abort(
1093 1093 _(b"-G/--graph option is incompatible with --%s")
1094 1094 % op.replace(b"_", b"-")
1095 1095 )
1096 1096
1097 1097
1098 1098 def graphrevs(repo, nodes, opts):
1099 1099 limit = getlimit(opts)
1100 1100 nodes.reverse()
1101 1101 if limit is not None:
1102 1102 nodes = nodes[:limit]
1103 1103 return graphmod.nodes(repo, nodes)
@@ -1,48 +1,48
1 1 from __future__ import absolute_import
2 2 from mercurial import (
3 3 cmdutil,
4 4 commands,
5 5 extensions,
6 6 logcmdutil,
7 7 revsetlang,
8 8 smartset,
9 9 )
10 10
11 11 from mercurial.utils import stringutil
12 12
13 13
14 14 def logrevset(repo, pats, opts):
15 15 revs = logcmdutil._initialrevs(repo, opts)
16 16 if not revs:
17 17 return None
18 18 match, pats, slowpath = logcmdutil._makematcher(repo, revs, pats, opts)
19 return logcmdutil._makerevset(repo, match, pats, slowpath, opts)
19 return logcmdutil._makerevset(repo, pats, slowpath, opts)
20 20
21 21
22 22 def uisetup(ui):
23 23 def printrevset(orig, repo, pats, opts):
24 24 revs, filematcher = orig(repo, pats, opts)
25 25 if opts.get(b'print_revset'):
26 26 expr = logrevset(repo, pats, opts)
27 27 if expr:
28 28 tree = revsetlang.parse(expr)
29 29 tree = revsetlang.analyze(tree)
30 30 else:
31 31 tree = []
32 32 ui = repo.ui
33 33 ui.write(b'%s\n' % stringutil.pprint(opts.get(b'rev', [])))
34 34 ui.write(revsetlang.prettyformat(tree) + b'\n')
35 35 ui.write(stringutil.prettyrepr(revs) + b'\n')
36 36 revs = smartset.baseset() # display no revisions
37 37 return revs, filematcher
38 38
39 39 extensions.wrapfunction(logcmdutil, 'getrevs', printrevset)
40 40 aliases, entry = cmdutil.findcmd(b'log', commands.table)
41 41 entry[1].append(
42 42 (
43 43 b'',
44 44 b'print-revset',
45 45 False,
46 46 b'print generated revset and exit (DEPRECATED)',
47 47 )
48 48 )
General Comments 0
You need to be logged in to leave comments. Login now