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