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