comparison mercurial/logcmdutil.py @ 35885:7625b4f7db70

cmdutil: split functions of log-like commands to new module (API) cmdutil.py is painfully big and makes Emacs slow. Let's split log-related functions. % wc -l mercurial/cmdutil.py 4027 mercurial/cmdutil.py % wc -l mercurial/cmdutil.py mercurial/logcmdutil.py 3141 mercurial/cmdutil.py 933 mercurial/logcmdutil.py 4074 total
author Yuya Nishihara <yuya@tcha.org>
date Sun, 21 Jan 2018 12:26:42 +0900
parents mercurial/cmdutil.py@1bee7762fd46
children b0014780c7fc
comparison
equal deleted inserted replaced
35884:197d10e157ce 35885:7625b4f7db70
1 # logcmdutil.py - utility for log-like commands
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 from __future__ import absolute_import
9
10 import itertools
11 import os
12
13 from .i18n import _
14 from .node import (
15 hex,
16 nullid,
17 )
18
19 from . import (
20 dagop,
21 encoding,
22 error,
23 formatter,
24 graphmod,
25 match as matchmod,
26 mdiff,
27 patch,
28 pathutil,
29 pycompat,
30 revset,
31 revsetlang,
32 scmutil,
33 smartset,
34 templatekw,
35 templater,
36 util,
37 )
38
39 def loglimit(opts):
40 """get the log limit according to option -l/--limit"""
41 limit = opts.get('limit')
42 if limit:
43 try:
44 limit = int(limit)
45 except ValueError:
46 raise error.Abort(_('limit must be a positive integer'))
47 if limit <= 0:
48 raise error.Abort(_('limit must be positive'))
49 else:
50 limit = None
51 return limit
52
53 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
54 changes=None, stat=False, fp=None, prefix='',
55 root='', listsubrepos=False, hunksfilterfn=None):
56 '''show diff or diffstat.'''
57 if fp is None:
58 write = ui.write
59 else:
60 def write(s, **kw):
61 fp.write(s)
62
63 if root:
64 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
65 else:
66 relroot = ''
67 if relroot != '':
68 # XXX relative roots currently don't work if the root is within a
69 # subrepo
70 uirelroot = match.uipath(relroot)
71 relroot += '/'
72 for matchroot in match.files():
73 if not matchroot.startswith(relroot):
74 ui.warn(_('warning: %s not inside relative root %s\n') % (
75 match.uipath(matchroot), uirelroot))
76
77 if stat:
78 diffopts = diffopts.copy(context=0, noprefix=False)
79 width = 80
80 if not ui.plain():
81 width = ui.termwidth()
82 chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
83 prefix=prefix, relroot=relroot,
84 hunksfilterfn=hunksfilterfn)
85 for chunk, label in patch.diffstatui(util.iterlines(chunks),
86 width=width):
87 write(chunk, label=label)
88 else:
89 for chunk, label in patch.diffui(repo, node1, node2, match,
90 changes, opts=diffopts, prefix=prefix,
91 relroot=relroot,
92 hunksfilterfn=hunksfilterfn):
93 write(chunk, label=label)
94
95 if listsubrepos:
96 ctx1 = repo[node1]
97 ctx2 = repo[node2]
98 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
99 tempnode2 = node2
100 try:
101 if node2 is not None:
102 tempnode2 = ctx2.substate[subpath][1]
103 except KeyError:
104 # A subrepo that existed in node1 was deleted between node1 and
105 # node2 (inclusive). Thus, ctx2's substate won't contain that
106 # subpath. The best we can do is to ignore it.
107 tempnode2 = None
108 submatch = matchmod.subdirmatcher(subpath, match)
109 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
110 stat=stat, fp=fp, prefix=prefix)
111
112 def _changesetlabels(ctx):
113 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
114 if ctx.obsolete():
115 labels.append('changeset.obsolete')
116 if ctx.isunstable():
117 labels.append('changeset.unstable')
118 for instability in ctx.instabilities():
119 labels.append('instability.%s' % instability)
120 return ' '.join(labels)
121
122 class changeset_printer(object):
123 '''show changeset information when templating not requested.'''
124
125 def __init__(self, ui, repo, matchfn, diffopts, buffered):
126 self.ui = ui
127 self.repo = repo
128 self.buffered = buffered
129 self.matchfn = matchfn
130 self.diffopts = diffopts
131 self.header = {}
132 self.hunk = {}
133 self.lastheader = None
134 self.footer = None
135 self._columns = templatekw.getlogcolumns()
136
137 def flush(self, ctx):
138 rev = ctx.rev()
139 if rev in self.header:
140 h = self.header[rev]
141 if h != self.lastheader:
142 self.lastheader = h
143 self.ui.write(h)
144 del self.header[rev]
145 if rev in self.hunk:
146 self.ui.write(self.hunk[rev])
147 del self.hunk[rev]
148
149 def close(self):
150 if self.footer:
151 self.ui.write(self.footer)
152
153 def show(self, ctx, copies=None, matchfn=None, hunksfilterfn=None,
154 **props):
155 props = pycompat.byteskwargs(props)
156 if self.buffered:
157 self.ui.pushbuffer(labeled=True)
158 self._show(ctx, copies, matchfn, hunksfilterfn, props)
159 self.hunk[ctx.rev()] = self.ui.popbuffer()
160 else:
161 self._show(ctx, copies, matchfn, hunksfilterfn, props)
162
163 def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
164 '''show a single changeset or file revision'''
165 changenode = ctx.node()
166 rev = ctx.rev()
167
168 if self.ui.quiet:
169 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
170 label='log.node')
171 return
172
173 columns = self._columns
174 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
175 label=_changesetlabels(ctx))
176
177 # branches are shown first before any other names due to backwards
178 # compatibility
179 branch = ctx.branch()
180 # don't show the default branch name
181 if branch != 'default':
182 self.ui.write(columns['branch'] % branch, label='log.branch')
183
184 for nsname, ns in self.repo.names.iteritems():
185 # branches has special logic already handled above, so here we just
186 # skip it
187 if nsname == 'branches':
188 continue
189 # we will use the templatename as the color name since those two
190 # should be the same
191 for name in ns.names(self.repo, changenode):
192 self.ui.write(ns.logfmt % name,
193 label='log.%s' % ns.colorname)
194 if self.ui.debugflag:
195 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
196 for pctx in scmutil.meaningfulparents(self.repo, ctx):
197 label = 'log.parent changeset.%s' % pctx.phasestr()
198 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
199 label=label)
200
201 if self.ui.debugflag and rev is not None:
202 mnode = ctx.manifestnode()
203 mrev = self.repo.manifestlog._revlog.rev(mnode)
204 self.ui.write(columns['manifest']
205 % scmutil.formatrevnode(self.ui, mrev, mnode),
206 label='ui.debug log.manifest')
207 self.ui.write(columns['user'] % ctx.user(), label='log.user')
208 self.ui.write(columns['date'] % util.datestr(ctx.date()),
209 label='log.date')
210
211 if ctx.isunstable():
212 instabilities = ctx.instabilities()
213 self.ui.write(columns['instability'] % ', '.join(instabilities),
214 label='log.instability')
215
216 elif ctx.obsolete():
217 self._showobsfate(ctx)
218
219 self._exthook(ctx)
220
221 if self.ui.debugflag:
222 files = ctx.p1().status(ctx)[:3]
223 for key, value in zip(['files', 'files+', 'files-'], files):
224 if value:
225 self.ui.write(columns[key] % " ".join(value),
226 label='ui.debug log.files')
227 elif ctx.files() and self.ui.verbose:
228 self.ui.write(columns['files'] % " ".join(ctx.files()),
229 label='ui.note log.files')
230 if copies and self.ui.verbose:
231 copies = ['%s (%s)' % c for c in copies]
232 self.ui.write(columns['copies'] % ' '.join(copies),
233 label='ui.note log.copies')
234
235 extra = ctx.extra()
236 if extra and self.ui.debugflag:
237 for key, value in sorted(extra.items()):
238 self.ui.write(columns['extra'] % (key, util.escapestr(value)),
239 label='ui.debug log.extra')
240
241 description = ctx.description().strip()
242 if description:
243 if self.ui.verbose:
244 self.ui.write(_("description:\n"),
245 label='ui.note log.description')
246 self.ui.write(description,
247 label='ui.note log.description')
248 self.ui.write("\n\n")
249 else:
250 self.ui.write(columns['summary'] % description.splitlines()[0],
251 label='log.summary')
252 self.ui.write("\n")
253
254 self.showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
255
256 def _showobsfate(self, ctx):
257 obsfate = templatekw.showobsfate(repo=self.repo, ctx=ctx, ui=self.ui)
258
259 if obsfate:
260 for obsfateline in obsfate:
261 self.ui.write(self._columns['obsolete'] % obsfateline,
262 label='log.obsfate')
263
264 def _exthook(self, ctx):
265 '''empty method used by extension as a hook point
266 '''
267
268 def showpatch(self, ctx, matchfn, hunksfilterfn=None):
269 if not matchfn:
270 matchfn = self.matchfn
271 if matchfn:
272 stat = self.diffopts.get('stat')
273 diff = self.diffopts.get('patch')
274 diffopts = patch.diffallopts(self.ui, self.diffopts)
275 node = ctx.node()
276 prev = ctx.p1().node()
277 if stat:
278 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
279 match=matchfn, stat=True,
280 hunksfilterfn=hunksfilterfn)
281 if diff:
282 if stat:
283 self.ui.write("\n")
284 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
285 match=matchfn, stat=False,
286 hunksfilterfn=hunksfilterfn)
287 if stat or diff:
288 self.ui.write("\n")
289
290 class jsonchangeset(changeset_printer):
291 '''format changeset information.'''
292
293 def __init__(self, ui, repo, matchfn, diffopts, buffered):
294 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
295 self.cache = {}
296 self._first = True
297
298 def close(self):
299 if not self._first:
300 self.ui.write("\n]\n")
301 else:
302 self.ui.write("[]\n")
303
304 def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
305 '''show a single changeset or file revision'''
306 rev = ctx.rev()
307 if rev is None:
308 jrev = jnode = 'null'
309 else:
310 jrev = '%d' % rev
311 jnode = '"%s"' % hex(ctx.node())
312 j = encoding.jsonescape
313
314 if self._first:
315 self.ui.write("[\n {")
316 self._first = False
317 else:
318 self.ui.write(",\n {")
319
320 if self.ui.quiet:
321 self.ui.write(('\n "rev": %s') % jrev)
322 self.ui.write((',\n "node": %s') % jnode)
323 self.ui.write('\n }')
324 return
325
326 self.ui.write(('\n "rev": %s') % jrev)
327 self.ui.write((',\n "node": %s') % jnode)
328 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
329 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
330 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
331 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
332 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
333
334 self.ui.write((',\n "bookmarks": [%s]') %
335 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
336 self.ui.write((',\n "tags": [%s]') %
337 ", ".join('"%s"' % j(t) for t in ctx.tags()))
338 self.ui.write((',\n "parents": [%s]') %
339 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
340
341 if self.ui.debugflag:
342 if rev is None:
343 jmanifestnode = 'null'
344 else:
345 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
346 self.ui.write((',\n "manifest": %s') % jmanifestnode)
347
348 self.ui.write((',\n "extra": {%s}') %
349 ", ".join('"%s": "%s"' % (j(k), j(v))
350 for k, v in ctx.extra().items()))
351
352 files = ctx.p1().status(ctx)
353 self.ui.write((',\n "modified": [%s]') %
354 ", ".join('"%s"' % j(f) for f in files[0]))
355 self.ui.write((',\n "added": [%s]') %
356 ", ".join('"%s"' % j(f) for f in files[1]))
357 self.ui.write((',\n "removed": [%s]') %
358 ", ".join('"%s"' % j(f) for f in files[2]))
359
360 elif self.ui.verbose:
361 self.ui.write((',\n "files": [%s]') %
362 ", ".join('"%s"' % j(f) for f in ctx.files()))
363
364 if copies:
365 self.ui.write((',\n "copies": {%s}') %
366 ", ".join('"%s": "%s"' % (j(k), j(v))
367 for k, v in copies))
368
369 matchfn = self.matchfn
370 if matchfn:
371 stat = self.diffopts.get('stat')
372 diff = self.diffopts.get('patch')
373 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
374 node, prev = ctx.node(), ctx.p1().node()
375 if stat:
376 self.ui.pushbuffer()
377 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
378 match=matchfn, stat=True)
379 self.ui.write((',\n "diffstat": "%s"')
380 % j(self.ui.popbuffer()))
381 if diff:
382 self.ui.pushbuffer()
383 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
384 match=matchfn, stat=False)
385 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
386
387 self.ui.write("\n }")
388
389 class changeset_templater(changeset_printer):
390 '''format changeset information.
391
392 Note: there are a variety of convenience functions to build a
393 changeset_templater for common cases. See functions such as:
394 makelogtemplater, show_changeset, buildcommittemplate, or other
395 functions that use changesest_templater.
396 '''
397
398 # Arguments before "buffered" used to be positional. Consider not
399 # adding/removing arguments before "buffered" to not break callers.
400 def __init__(self, ui, repo, tmplspec, matchfn=None, diffopts=None,
401 buffered=False):
402 diffopts = diffopts or {}
403
404 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
405 tres = formatter.templateresources(ui, repo)
406 self.t = formatter.loadtemplater(ui, tmplspec,
407 defaults=templatekw.keywords,
408 resources=tres,
409 cache=templatekw.defaulttempl)
410 self._counter = itertools.count()
411 self.cache = tres['cache'] # shared with _graphnodeformatter()
412
413 self._tref = tmplspec.ref
414 self._parts = {'header': '', 'footer': '',
415 tmplspec.ref: tmplspec.ref,
416 'docheader': '', 'docfooter': '',
417 'separator': ''}
418 if tmplspec.mapfile:
419 # find correct templates for current mode, for backward
420 # compatibility with 'log -v/-q/--debug' using a mapfile
421 tmplmodes = [
422 (True, ''),
423 (self.ui.verbose, '_verbose'),
424 (self.ui.quiet, '_quiet'),
425 (self.ui.debugflag, '_debug'),
426 ]
427 for mode, postfix in tmplmodes:
428 for t in self._parts:
429 cur = t + postfix
430 if mode and cur in self.t:
431 self._parts[t] = cur
432 else:
433 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
434 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
435 self._parts.update(m)
436
437 if self._parts['docheader']:
438 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
439
440 def close(self):
441 if self._parts['docfooter']:
442 if not self.footer:
443 self.footer = ""
444 self.footer += templater.stringify(self.t(self._parts['docfooter']))
445 return super(changeset_templater, self).close()
446
447 def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
448 '''show a single changeset or file revision'''
449 props = props.copy()
450 props['ctx'] = ctx
451 props['index'] = index = next(self._counter)
452 props['revcache'] = {'copies': copies}
453 props = pycompat.strkwargs(props)
454
455 # write separator, which wouldn't work well with the header part below
456 # since there's inherently a conflict between header (across items) and
457 # separator (per item)
458 if self._parts['separator'] and index > 0:
459 self.ui.write(templater.stringify(self.t(self._parts['separator'])))
460
461 # write header
462 if self._parts['header']:
463 h = templater.stringify(self.t(self._parts['header'], **props))
464 if self.buffered:
465 self.header[ctx.rev()] = h
466 else:
467 if self.lastheader != h:
468 self.lastheader = h
469 self.ui.write(h)
470
471 # write changeset metadata, then patch if requested
472 key = self._parts[self._tref]
473 self.ui.write(templater.stringify(self.t(key, **props)))
474 self.showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
475
476 if self._parts['footer']:
477 if not self.footer:
478 self.footer = templater.stringify(
479 self.t(self._parts['footer'], **props))
480
481 def logtemplatespec(tmpl, mapfile):
482 if mapfile:
483 return formatter.templatespec('changeset', tmpl, mapfile)
484 else:
485 return formatter.templatespec('', tmpl, None)
486
487 def _lookuplogtemplate(ui, tmpl, style):
488 """Find the template matching the given template spec or style
489
490 See formatter.lookuptemplate() for details.
491 """
492
493 # ui settings
494 if not tmpl and not style: # template are stronger than style
495 tmpl = ui.config('ui', 'logtemplate')
496 if tmpl:
497 return logtemplatespec(templater.unquotestring(tmpl), None)
498 else:
499 style = util.expandpath(ui.config('ui', 'style'))
500
501 if not tmpl and style:
502 mapfile = style
503 if not os.path.split(mapfile)[0]:
504 mapname = (templater.templatepath('map-cmdline.' + mapfile)
505 or templater.templatepath(mapfile))
506 if mapname:
507 mapfile = mapname
508 return logtemplatespec(None, mapfile)
509
510 if not tmpl:
511 return logtemplatespec(None, None)
512
513 return formatter.lookuptemplate(ui, 'changeset', tmpl)
514
515 def makelogtemplater(ui, repo, tmpl, buffered=False):
516 """Create a changeset_templater from a literal template 'tmpl'
517 byte-string."""
518 spec = logtemplatespec(tmpl, None)
519 return changeset_templater(ui, repo, spec, buffered=buffered)
520
521 def show_changeset(ui, repo, opts, buffered=False):
522 """show one changeset using template or regular display.
523
524 Display format will be the first non-empty hit of:
525 1. option 'template'
526 2. option 'style'
527 3. [ui] setting 'logtemplate'
528 4. [ui] setting 'style'
529 If all of these values are either the unset or the empty string,
530 regular display via changeset_printer() is done.
531 """
532 # options
533 match = None
534 if opts.get('patch') or opts.get('stat'):
535 match = scmutil.matchall(repo)
536
537 if opts.get('template') == 'json':
538 return jsonchangeset(ui, repo, match, opts, buffered)
539
540 spec = _lookuplogtemplate(ui, opts.get('template'), opts.get('style'))
541
542 if not spec.ref and not spec.tmpl and not spec.mapfile:
543 return changeset_printer(ui, repo, match, opts, buffered)
544
545 return changeset_templater(ui, repo, spec, match, opts, buffered)
546
547 def _makelogmatcher(repo, revs, pats, opts):
548 """Build matcher and expanded patterns from log options
549
550 If --follow, revs are the revisions to follow from.
551
552 Returns (match, pats, slowpath) where
553 - match: a matcher built from the given pats and -I/-X opts
554 - pats: patterns used (globs are expanded on Windows)
555 - slowpath: True if patterns aren't as simple as scanning filelogs
556 """
557 # pats/include/exclude are passed to match.match() directly in
558 # _matchfiles() revset but walkchangerevs() builds its matcher with
559 # scmutil.match(). The difference is input pats are globbed on
560 # platforms without shell expansion (windows).
561 wctx = repo[None]
562 match, pats = scmutil.matchandpats(wctx, pats, opts)
563 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
564 if not slowpath:
565 follow = opts.get('follow') or opts.get('follow_first')
566 startctxs = []
567 if follow and opts.get('rev'):
568 startctxs = [repo[r] for r in revs]
569 for f in match.files():
570 if follow and startctxs:
571 # No idea if the path was a directory at that revision, so
572 # take the slow path.
573 if any(f not in c for c in startctxs):
574 slowpath = True
575 continue
576 elif follow and f not in wctx:
577 # If the file exists, it may be a directory, so let it
578 # take the slow path.
579 if os.path.exists(repo.wjoin(f)):
580 slowpath = True
581 continue
582 else:
583 raise error.Abort(_('cannot follow file not in parent '
584 'revision: "%s"') % f)
585 filelog = repo.file(f)
586 if not filelog:
587 # A zero count may be a directory or deleted file, so
588 # try to find matching entries on the slow path.
589 if follow:
590 raise error.Abort(
591 _('cannot follow nonexistent file: "%s"') % f)
592 slowpath = True
593
594 # We decided to fall back to the slowpath because at least one
595 # of the paths was not a file. Check to see if at least one of them
596 # existed in history - in that case, we'll continue down the
597 # slowpath; otherwise, we can turn off the slowpath
598 if slowpath:
599 for path in match.files():
600 if path == '.' or path in repo.store:
601 break
602 else:
603 slowpath = False
604
605 return match, pats, slowpath
606
607 def _fileancestors(repo, revs, match, followfirst):
608 fctxs = []
609 for r in revs:
610 ctx = repo[r]
611 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
612
613 # When displaying a revision with --patch --follow FILE, we have
614 # to know which file of the revision must be diffed. With
615 # --follow, we want the names of the ancestors of FILE in the
616 # revision, stored in "fcache". "fcache" is populated as a side effect
617 # of the graph traversal.
618 fcache = {}
619 def filematcher(rev):
620 return scmutil.matchfiles(repo, fcache.get(rev, []))
621
622 def revgen():
623 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
624 fcache[rev] = [c.path() for c in cs]
625 yield rev
626 return smartset.generatorset(revgen(), iterasc=False), filematcher
627
628 def _makenofollowlogfilematcher(repo, pats, opts):
629 '''hook for extensions to override the filematcher for non-follow cases'''
630 return None
631
632 _opt2logrevset = {
633 'no_merges': ('not merge()', None),
634 'only_merges': ('merge()', None),
635 '_matchfiles': (None, '_matchfiles(%ps)'),
636 'date': ('date(%s)', None),
637 'branch': ('branch(%s)', '%lr'),
638 '_patslog': ('filelog(%s)', '%lr'),
639 'keyword': ('keyword(%s)', '%lr'),
640 'prune': ('ancestors(%s)', 'not %lr'),
641 'user': ('user(%s)', '%lr'),
642 }
643
644 def _makelogrevset(repo, match, pats, slowpath, opts):
645 """Return a revset string built from log options and file patterns"""
646 opts = dict(opts)
647 # follow or not follow?
648 follow = opts.get('follow') or opts.get('follow_first')
649
650 # branch and only_branch are really aliases and must be handled at
651 # the same time
652 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
653 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
654
655 if slowpath:
656 # See walkchangerevs() slow path.
657 #
658 # pats/include/exclude cannot be represented as separate
659 # revset expressions as their filtering logic applies at file
660 # level. For instance "-I a -X b" matches a revision touching
661 # "a" and "b" while "file(a) and not file(b)" does
662 # not. Besides, filesets are evaluated against the working
663 # directory.
664 matchargs = ['r:', 'd:relpath']
665 for p in pats:
666 matchargs.append('p:' + p)
667 for p in opts.get('include', []):
668 matchargs.append('i:' + p)
669 for p in opts.get('exclude', []):
670 matchargs.append('x:' + p)
671 opts['_matchfiles'] = matchargs
672 elif not follow:
673 opts['_patslog'] = list(pats)
674
675 expr = []
676 for op, val in sorted(opts.iteritems()):
677 if not val:
678 continue
679 if op not in _opt2logrevset:
680 continue
681 revop, listop = _opt2logrevset[op]
682 if revop and '%' not in revop:
683 expr.append(revop)
684 elif not listop:
685 expr.append(revsetlang.formatspec(revop, val))
686 else:
687 if revop:
688 val = [revsetlang.formatspec(revop, v) for v in val]
689 expr.append(revsetlang.formatspec(listop, val))
690
691 if expr:
692 expr = '(' + ' and '.join(expr) + ')'
693 else:
694 expr = None
695 return expr
696
697 def _logrevs(repo, opts):
698 """Return the initial set of revisions to be filtered or followed"""
699 follow = opts.get('follow') or opts.get('follow_first')
700 if opts.get('rev'):
701 revs = scmutil.revrange(repo, opts['rev'])
702 elif follow and repo.dirstate.p1() == nullid:
703 revs = smartset.baseset()
704 elif follow:
705 revs = repo.revs('.')
706 else:
707 revs = smartset.spanset(repo)
708 revs.reverse()
709 return revs
710
711 def getlogrevs(repo, pats, opts):
712 """Return (revs, filematcher) where revs is a smartset
713
714 filematcher is a callable taking a revision number and returning a match
715 objects filtering the files to be detailed when displaying the revision.
716 """
717 follow = opts.get('follow') or opts.get('follow_first')
718 followfirst = opts.get('follow_first')
719 limit = loglimit(opts)
720 revs = _logrevs(repo, opts)
721 if not revs:
722 return smartset.baseset(), None
723 match, pats, slowpath = _makelogmatcher(repo, revs, pats, opts)
724 filematcher = None
725 if follow:
726 if slowpath or match.always():
727 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
728 else:
729 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
730 revs.reverse()
731 if filematcher is None:
732 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
733 if filematcher is None:
734 def filematcher(rev):
735 return match
736
737 expr = _makelogrevset(repo, match, pats, slowpath, opts)
738 if opts.get('graph') and opts.get('rev'):
739 # User-specified revs might be unsorted, but don't sort before
740 # _makelogrevset because it might depend on the order of revs
741 if not (revs.isdescending() or revs.istopo()):
742 revs.sort(reverse=True)
743 if expr:
744 matcher = revset.match(None, expr)
745 revs = matcher(repo, revs)
746 if limit is not None:
747 revs = revs.slice(0, limit)
748 return revs, filematcher
749
750 def _parselinerangelogopt(repo, opts):
751 """Parse --line-range log option and return a list of tuples (filename,
752 (fromline, toline)).
753 """
754 linerangebyfname = []
755 for pat in opts.get('line_range', []):
756 try:
757 pat, linerange = pat.rsplit(',', 1)
758 except ValueError:
759 raise error.Abort(_('malformatted line-range pattern %s') % pat)
760 try:
761 fromline, toline = map(int, linerange.split(':'))
762 except ValueError:
763 raise error.Abort(_("invalid line range for %s") % pat)
764 msg = _("line range pattern '%s' must match exactly one file") % pat
765 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
766 linerangebyfname.append(
767 (fname, util.processlinerange(fromline, toline)))
768 return linerangebyfname
769
770 def getloglinerangerevs(repo, userrevs, opts):
771 """Return (revs, filematcher, hunksfilter).
772
773 "revs" are revisions obtained by processing "line-range" log options and
774 walking block ancestors of each specified file/line-range.
775
776 "filematcher(rev) -> match" is a factory function returning a match object
777 for a given revision for file patterns specified in --line-range option.
778 If neither --stat nor --patch options are passed, "filematcher" is None.
779
780 "hunksfilter(rev) -> filterfn(fctx, hunks)" is a factory function
781 returning a hunks filtering function.
782 If neither --stat nor --patch options are passed, "filterhunks" is None.
783 """
784 wctx = repo[None]
785
786 # Two-levels map of "rev -> file ctx -> [line range]".
787 linerangesbyrev = {}
788 for fname, (fromline, toline) in _parselinerangelogopt(repo, opts):
789 if fname not in wctx:
790 raise error.Abort(_('cannot follow file not in parent '
791 'revision: "%s"') % fname)
792 fctx = wctx.filectx(fname)
793 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
794 rev = fctx.introrev()
795 if rev not in userrevs:
796 continue
797 linerangesbyrev.setdefault(
798 rev, {}).setdefault(
799 fctx.path(), []).append(linerange)
800
801 filematcher = None
802 hunksfilter = None
803 if opts.get('patch') or opts.get('stat'):
804
805 def nofilterhunksfn(fctx, hunks):
806 return hunks
807
808 def hunksfilter(rev):
809 fctxlineranges = linerangesbyrev.get(rev)
810 if fctxlineranges is None:
811 return nofilterhunksfn
812
813 def filterfn(fctx, hunks):
814 lineranges = fctxlineranges.get(fctx.path())
815 if lineranges is not None:
816 for hr, lines in hunks:
817 if hr is None: # binary
818 yield hr, lines
819 continue
820 if any(mdiff.hunkinrange(hr[2:], lr)
821 for lr in lineranges):
822 yield hr, lines
823 else:
824 for hunk in hunks:
825 yield hunk
826
827 return filterfn
828
829 def filematcher(rev):
830 files = list(linerangesbyrev.get(rev, []))
831 return scmutil.matchfiles(repo, files)
832
833 revs = sorted(linerangesbyrev, reverse=True)
834
835 return revs, filematcher, hunksfilter
836
837 def _graphnodeformatter(ui, displayer):
838 spec = ui.config('ui', 'graphnodetemplate')
839 if not spec:
840 return templatekw.showgraphnode # fast path for "{graphnode}"
841
842 spec = templater.unquotestring(spec)
843 tres = formatter.templateresources(ui)
844 if isinstance(displayer, changeset_templater):
845 tres['cache'] = displayer.cache # reuse cache of slow templates
846 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
847 resources=tres)
848 def formatnode(repo, ctx):
849 props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
850 return templ.render(props)
851 return formatnode
852
853 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
854 filematcher=None, props=None):
855 props = props or {}
856 formatnode = _graphnodeformatter(ui, displayer)
857 state = graphmod.asciistate()
858 styles = state['styles']
859
860 # only set graph styling if HGPLAIN is not set.
861 if ui.plain('graph'):
862 # set all edge styles to |, the default pre-3.8 behaviour
863 styles.update(dict.fromkeys(styles, '|'))
864 else:
865 edgetypes = {
866 'parent': graphmod.PARENT,
867 'grandparent': graphmod.GRANDPARENT,
868 'missing': graphmod.MISSINGPARENT
869 }
870 for name, key in edgetypes.items():
871 # experimental config: experimental.graphstyle.*
872 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
873 styles[key])
874 if not styles[key]:
875 styles[key] = None
876
877 # experimental config: experimental.graphshorten
878 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
879
880 for rev, type, ctx, parents in dag:
881 char = formatnode(repo, ctx)
882 copies = None
883 if getrenamed and ctx.rev():
884 copies = []
885 for fn in ctx.files():
886 rename = getrenamed(fn, ctx.rev())
887 if rename:
888 copies.append((fn, rename[0]))
889 revmatchfn = None
890 if filematcher is not None:
891 revmatchfn = filematcher(ctx.rev())
892 edges = edgefn(type, char, state, rev, parents)
893 firstedge = next(edges)
894 width = firstedge[2]
895 displayer.show(ctx, copies=copies, matchfn=revmatchfn,
896 _graphwidth=width, **pycompat.strkwargs(props))
897 lines = displayer.hunk.pop(rev).split('\n')
898 if not lines[-1]:
899 del lines[-1]
900 displayer.flush(ctx)
901 for type, char, width, coldata in itertools.chain([firstedge], edges):
902 graphmod.ascii(ui, state, type, char, lines, coldata)
903 lines = []
904 displayer.close()
905
906 def graphlog(ui, repo, revs, filematcher, opts):
907 # Parameters are identical to log command ones
908 revdag = graphmod.dagwalker(repo, revs)
909
910 getrenamed = None
911 if opts.get('copies'):
912 endrev = None
913 if opts.get('rev'):
914 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
915 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
916
917 ui.pager('log')
918 displayer = show_changeset(ui, repo, opts, buffered=True)
919 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
920 filematcher)
921
922 def checkunsupportedgraphflags(pats, opts):
923 for op in ["newest_first"]:
924 if op in opts and opts[op]:
925 raise error.Abort(_("-G/--graph option is incompatible with --%s")
926 % op.replace("_", "-"))
927
928 def graphrevs(repo, nodes, opts):
929 limit = loglimit(opts)
930 nodes.reverse()
931 if limit is not None:
932 nodes = nodes[:limit]
933 return graphmod.nodes(repo, nodes)