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