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