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