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
--- a/hgext/largefiles/overrides.py Fri Feb 02 13:13:46 2018 -0800
+++ b/hgext/largefiles/overrides.py Sun Jan 21 12:26:42 2018 +0900
@@ -19,6 +19,7 @@
cmdutil,
error,
hg,
+ logcmdutil,
match as matchmod,
pathutil,
pycompat,
@@ -394,14 +395,16 @@
return lambda rev: match
oldmatchandpats = installmatchandpatsfn(overridematchandpats)
- oldmakelogfilematcher = cmdutil._makenofollowlogfilematcher
- setattr(cmdutil, '_makenofollowlogfilematcher', overridemakelogfilematcher)
+ oldmakelogfilematcher = logcmdutil._makenofollowlogfilematcher
+ setattr(logcmdutil, '_makenofollowlogfilematcher',
+ overridemakelogfilematcher)
try:
return orig(ui, repo, *pats, **opts)
finally:
restorematchandpatsfn()
- setattr(cmdutil, '_makenofollowlogfilematcher', oldmakelogfilematcher)
+ setattr(logcmdutil, '_makenofollowlogfilematcher',
+ oldmakelogfilematcher)
def overrideverify(orig, ui, repo, *pats, **opts):
large = opts.pop(r'large', False)
--- a/hgext/sparse.py Fri Feb 02 13:13:46 2018 -0800
+++ b/hgext/sparse.py Sun Jan 21 12:26:42 2018 +0900
@@ -75,12 +75,12 @@
from mercurial.i18n import _
from mercurial import (
- cmdutil,
commands,
dirstate,
error,
extensions,
hg,
+ logcmdutil,
match as matchmod,
pycompat,
registrar,
@@ -135,7 +135,7 @@
return any(f for f in ctx.files() if sparsematch(f))
revs = revs.filter(ctxmatch)
return revs
- extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
+ extensions.wrapfunction(logcmdutil, '_logrevs', _logrevs)
def _clonesparsecmd(orig, ui, repo, *args, **opts):
include_pat = opts.get('include')
--- a/mercurial/cmdutil.py Fri Feb 02 13:13:46 2018 -0800
+++ b/mercurial/cmdutil.py Sun Jan 21 12:26:42 2018 +0900
@@ -8,7 +8,6 @@
from __future__ import absolute_import
import errno
-import itertools
import os
import re
import tempfile
@@ -26,32 +25,43 @@
changelog,
copies,
crecord as crecordmod,
- dagop,
dirstateguard,
encoding,
error,
formatter,
- graphmod,
+ logcmdutil,
match as matchmod,
- mdiff,
obsolete,
patch,
pathutil,
pycompat,
registrar,
revlog,
- revset,
- revsetlang,
rewriteutil,
scmutil,
smartset,
- templatekw,
templater,
util,
vfs as vfsmod,
)
stringio = util.stringio
+loglimit = logcmdutil.loglimit
+diffordiffstat = logcmdutil.diffordiffstat
+_changesetlabels = logcmdutil._changesetlabels
+changeset_printer = logcmdutil.changeset_printer
+jsonchangeset = logcmdutil.jsonchangeset
+changeset_templater = logcmdutil.changeset_templater
+logtemplatespec = logcmdutil.logtemplatespec
+makelogtemplater = logcmdutil.makelogtemplater
+show_changeset = logcmdutil.show_changeset
+getlogrevs = logcmdutil.getlogrevs
+getloglinerangerevs = logcmdutil.getloglinerangerevs
+displaygraph = logcmdutil.displaygraph
+graphlog = logcmdutil.graphlog
+checkunsupportedgraphflags = logcmdutil.checkunsupportedgraphflags
+graphrevs = logcmdutil.graphrevs
+
# templates of common command options
dryrunopts = [
@@ -898,20 +908,6 @@
else:
return commiteditor
-def loglimit(opts):
- """get the log limit according to option -l/--limit"""
- limit = opts.get('limit')
- if limit:
- try:
- limit = int(limit)
- except ValueError:
- raise error.Abort(_('limit must be a positive integer'))
- if limit <= 0:
- raise error.Abort(_('limit must be positive'))
- else:
- limit = None
- return limit
-
def makefilename(repo, pat, node, desc=None,
total=None, seqno=None, revwidth=None, pathname=None):
node_expander = {
@@ -1583,500 +1579,6 @@
if fo is not None:
fo.close()
-def diffordiffstat(ui, repo, diffopts, node1, node2, match,
- changes=None, stat=False, fp=None, prefix='',
- root='', listsubrepos=False, hunksfilterfn=None):
- '''show diff or diffstat.'''
- if fp is None:
- write = ui.write
- else:
- def write(s, **kw):
- fp.write(s)
-
- if root:
- relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
- else:
- relroot = ''
- if relroot != '':
- # XXX relative roots currently don't work if the root is within a
- # subrepo
- uirelroot = match.uipath(relroot)
- relroot += '/'
- for matchroot in match.files():
- if not matchroot.startswith(relroot):
- ui.warn(_('warning: %s not inside relative root %s\n') % (
- match.uipath(matchroot), uirelroot))
-
- if stat:
- diffopts = diffopts.copy(context=0, noprefix=False)
- width = 80
- if not ui.plain():
- width = ui.termwidth()
- chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
- prefix=prefix, relroot=relroot,
- hunksfilterfn=hunksfilterfn)
- for chunk, label in patch.diffstatui(util.iterlines(chunks),
- width=width):
- write(chunk, label=label)
- else:
- for chunk, label in patch.diffui(repo, node1, node2, match,
- changes, opts=diffopts, prefix=prefix,
- relroot=relroot,
- hunksfilterfn=hunksfilterfn):
- write(chunk, label=label)
-
- if listsubrepos:
- ctx1 = repo[node1]
- ctx2 = repo[node2]
- for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
- tempnode2 = node2
- try:
- if node2 is not None:
- tempnode2 = ctx2.substate[subpath][1]
- except KeyError:
- # A subrepo that existed in node1 was deleted between node1 and
- # node2 (inclusive). Thus, ctx2's substate won't contain that
- # subpath. The best we can do is to ignore it.
- tempnode2 = None
- submatch = matchmod.subdirmatcher(subpath, match)
- sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
- stat=stat, fp=fp, prefix=prefix)
-
-def _changesetlabels(ctx):
- labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
- if ctx.obsolete():
- labels.append('changeset.obsolete')
- if ctx.isunstable():
- labels.append('changeset.unstable')
- for instability in ctx.instabilities():
- labels.append('instability.%s' % instability)
- return ' '.join(labels)
-
-class changeset_printer(object):
- '''show changeset information when templating not requested.'''
-
- def __init__(self, ui, repo, matchfn, diffopts, buffered):
- self.ui = ui
- self.repo = repo
- self.buffered = buffered
- self.matchfn = matchfn
- self.diffopts = diffopts
- self.header = {}
- self.hunk = {}
- self.lastheader = None
- self.footer = None
- self._columns = templatekw.getlogcolumns()
-
- def flush(self, ctx):
- rev = ctx.rev()
- if rev in self.header:
- h = self.header[rev]
- if h != self.lastheader:
- self.lastheader = h
- self.ui.write(h)
- del self.header[rev]
- if rev in self.hunk:
- self.ui.write(self.hunk[rev])
- del self.hunk[rev]
-
- def close(self):
- if self.footer:
- self.ui.write(self.footer)
-
- def show(self, ctx, copies=None, matchfn=None, hunksfilterfn=None,
- **props):
- props = pycompat.byteskwargs(props)
- if self.buffered:
- self.ui.pushbuffer(labeled=True)
- self._show(ctx, copies, matchfn, hunksfilterfn, props)
- self.hunk[ctx.rev()] = self.ui.popbuffer()
- else:
- self._show(ctx, copies, matchfn, hunksfilterfn, props)
-
- def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
- '''show a single changeset or file revision'''
- changenode = ctx.node()
- rev = ctx.rev()
-
- if self.ui.quiet:
- self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
- label='log.node')
- return
-
- columns = self._columns
- self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
- label=_changesetlabels(ctx))
-
- # branches are shown first before any other names due to backwards
- # compatibility
- branch = ctx.branch()
- # don't show the default branch name
- if branch != 'default':
- self.ui.write(columns['branch'] % branch, label='log.branch')
-
- for nsname, ns in self.repo.names.iteritems():
- # branches has special logic already handled above, so here we just
- # skip it
- if nsname == 'branches':
- continue
- # we will use the templatename as the color name since those two
- # should be the same
- for name in ns.names(self.repo, changenode):
- self.ui.write(ns.logfmt % name,
- label='log.%s' % ns.colorname)
- if self.ui.debugflag:
- self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
- for pctx in scmutil.meaningfulparents(self.repo, ctx):
- label = 'log.parent changeset.%s' % pctx.phasestr()
- self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
- label=label)
-
- if self.ui.debugflag and rev is not None:
- mnode = ctx.manifestnode()
- mrev = self.repo.manifestlog._revlog.rev(mnode)
- self.ui.write(columns['manifest']
- % scmutil.formatrevnode(self.ui, mrev, mnode),
- label='ui.debug log.manifest')
- self.ui.write(columns['user'] % ctx.user(), label='log.user')
- self.ui.write(columns['date'] % util.datestr(ctx.date()),
- label='log.date')
-
- if ctx.isunstable():
- instabilities = ctx.instabilities()
- self.ui.write(columns['instability'] % ', '.join(instabilities),
- label='log.instability')
-
- elif ctx.obsolete():
- self._showobsfate(ctx)
-
- self._exthook(ctx)
-
- if self.ui.debugflag:
- files = ctx.p1().status(ctx)[:3]
- for key, value in zip(['files', 'files+', 'files-'], files):
- if value:
- self.ui.write(columns[key] % " ".join(value),
- label='ui.debug log.files')
- elif ctx.files() and self.ui.verbose:
- self.ui.write(columns['files'] % " ".join(ctx.files()),
- label='ui.note log.files')
- if copies and self.ui.verbose:
- copies = ['%s (%s)' % c for c in copies]
- self.ui.write(columns['copies'] % ' '.join(copies),
- label='ui.note log.copies')
-
- extra = ctx.extra()
- if extra and self.ui.debugflag:
- for key, value in sorted(extra.items()):
- self.ui.write(columns['extra'] % (key, util.escapestr(value)),
- label='ui.debug log.extra')
-
- description = ctx.description().strip()
- if description:
- if self.ui.verbose:
- self.ui.write(_("description:\n"),
- label='ui.note log.description')
- self.ui.write(description,
- label='ui.note log.description')
- self.ui.write("\n\n")
- else:
- self.ui.write(columns['summary'] % description.splitlines()[0],
- label='log.summary')
- self.ui.write("\n")
-
- self.showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
-
- def _showobsfate(self, ctx):
- obsfate = templatekw.showobsfate(repo=self.repo, ctx=ctx, ui=self.ui)
-
- if obsfate:
- for obsfateline in obsfate:
- self.ui.write(self._columns['obsolete'] % obsfateline,
- label='log.obsfate')
-
- def _exthook(self, ctx):
- '''empty method used by extension as a hook point
- '''
-
- def showpatch(self, ctx, matchfn, hunksfilterfn=None):
- if not matchfn:
- matchfn = self.matchfn
- if matchfn:
- stat = self.diffopts.get('stat')
- diff = self.diffopts.get('patch')
- diffopts = patch.diffallopts(self.ui, self.diffopts)
- node = ctx.node()
- prev = ctx.p1().node()
- if stat:
- diffordiffstat(self.ui, self.repo, diffopts, prev, node,
- match=matchfn, stat=True,
- hunksfilterfn=hunksfilterfn)
- if diff:
- if stat:
- self.ui.write("\n")
- diffordiffstat(self.ui, self.repo, diffopts, prev, node,
- match=matchfn, stat=False,
- hunksfilterfn=hunksfilterfn)
- if stat or diff:
- self.ui.write("\n")
-
-class jsonchangeset(changeset_printer):
- '''format changeset information.'''
-
- def __init__(self, ui, repo, matchfn, diffopts, buffered):
- changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
- self.cache = {}
- self._first = True
-
- def close(self):
- if not self._first:
- self.ui.write("\n]\n")
- else:
- self.ui.write("[]\n")
-
- def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
- '''show a single changeset or file revision'''
- rev = ctx.rev()
- if rev is None:
- jrev = jnode = 'null'
- else:
- jrev = '%d' % rev
- jnode = '"%s"' % hex(ctx.node())
- j = encoding.jsonescape
-
- if self._first:
- self.ui.write("[\n {")
- self._first = False
- else:
- self.ui.write(",\n {")
-
- if self.ui.quiet:
- self.ui.write(('\n "rev": %s') % jrev)
- self.ui.write((',\n "node": %s') % jnode)
- self.ui.write('\n }')
- return
-
- self.ui.write(('\n "rev": %s') % jrev)
- self.ui.write((',\n "node": %s') % jnode)
- self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
- self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
- self.ui.write((',\n "user": "%s"') % j(ctx.user()))
- self.ui.write((',\n "date": [%d, %d]') % ctx.date())
- self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
-
- self.ui.write((',\n "bookmarks": [%s]') %
- ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
- self.ui.write((',\n "tags": [%s]') %
- ", ".join('"%s"' % j(t) for t in ctx.tags()))
- self.ui.write((',\n "parents": [%s]') %
- ", ".join('"%s"' % c.hex() for c in ctx.parents()))
-
- if self.ui.debugflag:
- if rev is None:
- jmanifestnode = 'null'
- else:
- jmanifestnode = '"%s"' % hex(ctx.manifestnode())
- self.ui.write((',\n "manifest": %s') % jmanifestnode)
-
- self.ui.write((',\n "extra": {%s}') %
- ", ".join('"%s": "%s"' % (j(k), j(v))
- for k, v in ctx.extra().items()))
-
- files = ctx.p1().status(ctx)
- self.ui.write((',\n "modified": [%s]') %
- ", ".join('"%s"' % j(f) for f in files[0]))
- self.ui.write((',\n "added": [%s]') %
- ", ".join('"%s"' % j(f) for f in files[1]))
- self.ui.write((',\n "removed": [%s]') %
- ", ".join('"%s"' % j(f) for f in files[2]))
-
- elif self.ui.verbose:
- self.ui.write((',\n "files": [%s]') %
- ", ".join('"%s"' % j(f) for f in ctx.files()))
-
- if copies:
- self.ui.write((',\n "copies": {%s}') %
- ", ".join('"%s": "%s"' % (j(k), j(v))
- for k, v in copies))
-
- matchfn = self.matchfn
- if matchfn:
- stat = self.diffopts.get('stat')
- diff = self.diffopts.get('patch')
- diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
- node, prev = ctx.node(), ctx.p1().node()
- if stat:
- self.ui.pushbuffer()
- diffordiffstat(self.ui, self.repo, diffopts, prev, node,
- match=matchfn, stat=True)
- self.ui.write((',\n "diffstat": "%s"')
- % j(self.ui.popbuffer()))
- if diff:
- self.ui.pushbuffer()
- diffordiffstat(self.ui, self.repo, diffopts, prev, node,
- match=matchfn, stat=False)
- self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
-
- self.ui.write("\n }")
-
-class changeset_templater(changeset_printer):
- '''format changeset information.
-
- Note: there are a variety of convenience functions to build a
- changeset_templater for common cases. See functions such as:
- makelogtemplater, show_changeset, buildcommittemplate, or other
- functions that use changesest_templater.
- '''
-
- # Arguments before "buffered" used to be positional. Consider not
- # adding/removing arguments before "buffered" to not break callers.
- def __init__(self, ui, repo, tmplspec, matchfn=None, diffopts=None,
- buffered=False):
- diffopts = diffopts or {}
-
- changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
- tres = formatter.templateresources(ui, repo)
- self.t = formatter.loadtemplater(ui, tmplspec,
- defaults=templatekw.keywords,
- resources=tres,
- cache=templatekw.defaulttempl)
- self._counter = itertools.count()
- self.cache = tres['cache'] # shared with _graphnodeformatter()
-
- self._tref = tmplspec.ref
- self._parts = {'header': '', 'footer': '',
- tmplspec.ref: tmplspec.ref,
- 'docheader': '', 'docfooter': '',
- 'separator': ''}
- if tmplspec.mapfile:
- # find correct templates for current mode, for backward
- # compatibility with 'log -v/-q/--debug' using a mapfile
- tmplmodes = [
- (True, ''),
- (self.ui.verbose, '_verbose'),
- (self.ui.quiet, '_quiet'),
- (self.ui.debugflag, '_debug'),
- ]
- for mode, postfix in tmplmodes:
- for t in self._parts:
- cur = t + postfix
- if mode and cur in self.t:
- self._parts[t] = cur
- else:
- partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
- m = formatter.templatepartsmap(tmplspec, self.t, partnames)
- self._parts.update(m)
-
- if self._parts['docheader']:
- self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
-
- def close(self):
- if self._parts['docfooter']:
- if not self.footer:
- self.footer = ""
- self.footer += templater.stringify(self.t(self._parts['docfooter']))
- return super(changeset_templater, self).close()
-
- def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
- '''show a single changeset or file revision'''
- props = props.copy()
- props['ctx'] = ctx
- props['index'] = index = next(self._counter)
- props['revcache'] = {'copies': copies}
- props = pycompat.strkwargs(props)
-
- # write separator, which wouldn't work well with the header part below
- # since there's inherently a conflict between header (across items) and
- # separator (per item)
- if self._parts['separator'] and index > 0:
- self.ui.write(templater.stringify(self.t(self._parts['separator'])))
-
- # write header
- if self._parts['header']:
- h = templater.stringify(self.t(self._parts['header'], **props))
- if self.buffered:
- self.header[ctx.rev()] = h
- else:
- if self.lastheader != h:
- self.lastheader = h
- self.ui.write(h)
-
- # write changeset metadata, then patch if requested
- key = self._parts[self._tref]
- self.ui.write(templater.stringify(self.t(key, **props)))
- self.showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
-
- if self._parts['footer']:
- if not self.footer:
- self.footer = templater.stringify(
- self.t(self._parts['footer'], **props))
-
-def logtemplatespec(tmpl, mapfile):
- if mapfile:
- return formatter.templatespec('changeset', tmpl, mapfile)
- else:
- return formatter.templatespec('', tmpl, None)
-
-def _lookuplogtemplate(ui, tmpl, style):
- """Find the template matching the given template spec or style
-
- See formatter.lookuptemplate() for details.
- """
-
- # ui settings
- if not tmpl and not style: # template are stronger than style
- tmpl = ui.config('ui', 'logtemplate')
- if tmpl:
- return logtemplatespec(templater.unquotestring(tmpl), None)
- else:
- style = util.expandpath(ui.config('ui', 'style'))
-
- if not tmpl and style:
- mapfile = style
- if not os.path.split(mapfile)[0]:
- mapname = (templater.templatepath('map-cmdline.' + mapfile)
- or templater.templatepath(mapfile))
- if mapname:
- mapfile = mapname
- return logtemplatespec(None, mapfile)
-
- if not tmpl:
- return logtemplatespec(None, None)
-
- return formatter.lookuptemplate(ui, 'changeset', tmpl)
-
-def makelogtemplater(ui, repo, tmpl, buffered=False):
- """Create a changeset_templater from a literal template 'tmpl'
- byte-string."""
- spec = logtemplatespec(tmpl, None)
- return changeset_templater(ui, repo, spec, buffered=buffered)
-
-def show_changeset(ui, repo, opts, buffered=False):
- """show one changeset using template or regular display.
-
- Display format will be the first non-empty hit of:
- 1. option 'template'
- 2. option 'style'
- 3. [ui] setting 'logtemplate'
- 4. [ui] setting 'style'
- If all of these values are either the unset or the empty string,
- regular display via changeset_printer() is done.
- """
- # options
- match = None
- if opts.get('patch') or opts.get('stat'):
- match = scmutil.matchall(repo)
-
- if opts.get('template') == 'json':
- return jsonchangeset(ui, repo, match, opts, buffered)
-
- spec = _lookuplogtemplate(ui, opts.get('template'), opts.get('style'))
-
- if not spec.ref and not spec.tmpl and not spec.mapfile:
- return changeset_printer(ui, repo, match, opts, buffered)
-
- return changeset_templater(ui, repo, spec, match, opts, buffered)
-
class _regrettablereprbytes(bytes):
"""Bytes subclass that makes the repr the same on Python 3 as Python 2.
@@ -2429,394 +1931,6 @@
return iterate()
-def _makelogmatcher(repo, revs, pats, opts):
- """Build matcher and expanded patterns from log options
-
- If --follow, revs are the revisions to follow from.
-
- Returns (match, pats, slowpath) where
- - match: a matcher built from the given pats and -I/-X opts
- - pats: patterns used (globs are expanded on Windows)
- - slowpath: True if patterns aren't as simple as scanning filelogs
- """
- # pats/include/exclude are passed to match.match() directly in
- # _matchfiles() revset but walkchangerevs() builds its matcher with
- # scmutil.match(). The difference is input pats are globbed on
- # platforms without shell expansion (windows).
- wctx = repo[None]
- match, pats = scmutil.matchandpats(wctx, pats, opts)
- slowpath = match.anypats() or (not match.always() and opts.get('removed'))
- if not slowpath:
- follow = opts.get('follow') or opts.get('follow_first')
- startctxs = []
- if follow and opts.get('rev'):
- startctxs = [repo[r] for r in revs]
- for f in match.files():
- if follow and startctxs:
- # No idea if the path was a directory at that revision, so
- # take the slow path.
- if any(f not in c for c in startctxs):
- slowpath = True
- continue
- elif follow and f not in wctx:
- # If the file exists, it may be a directory, so let it
- # take the slow path.
- if os.path.exists(repo.wjoin(f)):
- slowpath = True
- continue
- else:
- raise error.Abort(_('cannot follow file not in parent '
- 'revision: "%s"') % f)
- filelog = repo.file(f)
- if not filelog:
- # A zero count may be a directory or deleted file, so
- # try to find matching entries on the slow path.
- if follow:
- raise error.Abort(
- _('cannot follow nonexistent file: "%s"') % f)
- slowpath = True
-
- # We decided to fall back to the slowpath because at least one
- # of the paths was not a file. Check to see if at least one of them
- # existed in history - in that case, we'll continue down the
- # slowpath; otherwise, we can turn off the slowpath
- if slowpath:
- for path in match.files():
- if path == '.' or path in repo.store:
- break
- else:
- slowpath = False
-
- return match, pats, slowpath
-
-def _fileancestors(repo, revs, match, followfirst):
- fctxs = []
- for r in revs:
- ctx = repo[r]
- fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
-
- # When displaying a revision with --patch --follow FILE, we have
- # to know which file of the revision must be diffed. With
- # --follow, we want the names of the ancestors of FILE in the
- # revision, stored in "fcache". "fcache" is populated as a side effect
- # of the graph traversal.
- fcache = {}
- def filematcher(rev):
- return scmutil.matchfiles(repo, fcache.get(rev, []))
-
- def revgen():
- for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
- fcache[rev] = [c.path() for c in cs]
- yield rev
- return smartset.generatorset(revgen(), iterasc=False), filematcher
-
-def _makenofollowlogfilematcher(repo, pats, opts):
- '''hook for extensions to override the filematcher for non-follow cases'''
- return None
-
-_opt2logrevset = {
- 'no_merges': ('not merge()', None),
- 'only_merges': ('merge()', None),
- '_matchfiles': (None, '_matchfiles(%ps)'),
- 'date': ('date(%s)', None),
- 'branch': ('branch(%s)', '%lr'),
- '_patslog': ('filelog(%s)', '%lr'),
- 'keyword': ('keyword(%s)', '%lr'),
- 'prune': ('ancestors(%s)', 'not %lr'),
- 'user': ('user(%s)', '%lr'),
-}
-
-def _makelogrevset(repo, match, pats, slowpath, opts):
- """Return a revset string built from log options and file patterns"""
- opts = dict(opts)
- # follow or not follow?
- follow = opts.get('follow') or opts.get('follow_first')
-
- # branch and only_branch are really aliases and must be handled at
- # the same time
- opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
- opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
-
- if slowpath:
- # See walkchangerevs() slow path.
- #
- # pats/include/exclude cannot be represented as separate
- # revset expressions as their filtering logic applies at file
- # level. For instance "-I a -X b" matches a revision touching
- # "a" and "b" while "file(a) and not file(b)" does
- # not. Besides, filesets are evaluated against the working
- # directory.
- matchargs = ['r:', 'd:relpath']
- for p in pats:
- matchargs.append('p:' + p)
- for p in opts.get('include', []):
- matchargs.append('i:' + p)
- for p in opts.get('exclude', []):
- matchargs.append('x:' + p)
- opts['_matchfiles'] = matchargs
- elif not follow:
- opts['_patslog'] = list(pats)
-
- expr = []
- for op, val in sorted(opts.iteritems()):
- if not val:
- continue
- if op not in _opt2logrevset:
- continue
- revop, listop = _opt2logrevset[op]
- if revop and '%' not in revop:
- expr.append(revop)
- elif not listop:
- expr.append(revsetlang.formatspec(revop, val))
- else:
- if revop:
- val = [revsetlang.formatspec(revop, v) for v in val]
- expr.append(revsetlang.formatspec(listop, val))
-
- if expr:
- expr = '(' + ' and '.join(expr) + ')'
- else:
- expr = None
- return expr
-
-def _logrevs(repo, opts):
- """Return the initial set of revisions to be filtered or followed"""
- follow = opts.get('follow') or opts.get('follow_first')
- if opts.get('rev'):
- revs = scmutil.revrange(repo, opts['rev'])
- elif follow and repo.dirstate.p1() == nullid:
- revs = smartset.baseset()
- elif follow:
- revs = repo.revs('.')
- else:
- revs = smartset.spanset(repo)
- revs.reverse()
- return revs
-
-def getlogrevs(repo, pats, opts):
- """Return (revs, filematcher) where revs is a smartset
-
- filematcher is a callable taking a revision number and returning a match
- objects filtering the files to be detailed when displaying the revision.
- """
- follow = opts.get('follow') or opts.get('follow_first')
- followfirst = opts.get('follow_first')
- limit = loglimit(opts)
- revs = _logrevs(repo, opts)
- if not revs:
- return smartset.baseset(), None
- match, pats, slowpath = _makelogmatcher(repo, revs, pats, opts)
- filematcher = None
- if follow:
- if slowpath or match.always():
- revs = dagop.revancestors(repo, revs, followfirst=followfirst)
- else:
- revs, filematcher = _fileancestors(repo, revs, match, followfirst)
- revs.reverse()
- if filematcher is None:
- filematcher = _makenofollowlogfilematcher(repo, pats, opts)
- if filematcher is None:
- def filematcher(rev):
- return match
-
- expr = _makelogrevset(repo, match, pats, slowpath, opts)
- if opts.get('graph') and opts.get('rev'):
- # User-specified revs might be unsorted, but don't sort before
- # _makelogrevset because it might depend on the order of revs
- if not (revs.isdescending() or revs.istopo()):
- revs.sort(reverse=True)
- if expr:
- matcher = revset.match(None, expr)
- revs = matcher(repo, revs)
- if limit is not None:
- revs = revs.slice(0, limit)
- return revs, filematcher
-
-def _parselinerangelogopt(repo, opts):
- """Parse --line-range log option and return a list of tuples (filename,
- (fromline, toline)).
- """
- linerangebyfname = []
- for pat in opts.get('line_range', []):
- try:
- pat, linerange = pat.rsplit(',', 1)
- except ValueError:
- raise error.Abort(_('malformatted line-range pattern %s') % pat)
- try:
- fromline, toline = map(int, linerange.split(':'))
- except ValueError:
- raise error.Abort(_("invalid line range for %s") % pat)
- msg = _("line range pattern '%s' must match exactly one file") % pat
- fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
- linerangebyfname.append(
- (fname, util.processlinerange(fromline, toline)))
- return linerangebyfname
-
-def getloglinerangerevs(repo, userrevs, opts):
- """Return (revs, filematcher, hunksfilter).
-
- "revs" are revisions obtained by processing "line-range" log options and
- walking block ancestors of each specified file/line-range.
-
- "filematcher(rev) -> match" is a factory function returning a match object
- for a given revision for file patterns specified in --line-range option.
- If neither --stat nor --patch options are passed, "filematcher" is None.
-
- "hunksfilter(rev) -> filterfn(fctx, hunks)" is a factory function
- returning a hunks filtering function.
- If neither --stat nor --patch options are passed, "filterhunks" is None.
- """
- wctx = repo[None]
-
- # Two-levels map of "rev -> file ctx -> [line range]".
- linerangesbyrev = {}
- for fname, (fromline, toline) in _parselinerangelogopt(repo, opts):
- if fname not in wctx:
- raise error.Abort(_('cannot follow file not in parent '
- 'revision: "%s"') % fname)
- fctx = wctx.filectx(fname)
- for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
- rev = fctx.introrev()
- if rev not in userrevs:
- continue
- linerangesbyrev.setdefault(
- rev, {}).setdefault(
- fctx.path(), []).append(linerange)
-
- filematcher = None
- hunksfilter = None
- if opts.get('patch') or opts.get('stat'):
-
- def nofilterhunksfn(fctx, hunks):
- return hunks
-
- def hunksfilter(rev):
- fctxlineranges = linerangesbyrev.get(rev)
- if fctxlineranges is None:
- return nofilterhunksfn
-
- def filterfn(fctx, hunks):
- lineranges = fctxlineranges.get(fctx.path())
- if lineranges is not None:
- for hr, lines in hunks:
- if hr is None: # binary
- yield hr, lines
- continue
- if any(mdiff.hunkinrange(hr[2:], lr)
- for lr in lineranges):
- yield hr, lines
- else:
- for hunk in hunks:
- yield hunk
-
- return filterfn
-
- def filematcher(rev):
- files = list(linerangesbyrev.get(rev, []))
- return scmutil.matchfiles(repo, files)
-
- revs = sorted(linerangesbyrev, reverse=True)
-
- return revs, filematcher, hunksfilter
-
-def _graphnodeformatter(ui, displayer):
- spec = ui.config('ui', 'graphnodetemplate')
- if not spec:
- return templatekw.showgraphnode # fast path for "{graphnode}"
-
- spec = templater.unquotestring(spec)
- tres = formatter.templateresources(ui)
- if isinstance(displayer, changeset_templater):
- tres['cache'] = displayer.cache # reuse cache of slow templates
- templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
- resources=tres)
- def formatnode(repo, ctx):
- props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
- return templ.render(props)
- return formatnode
-
-def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
- filematcher=None, props=None):
- props = props or {}
- formatnode = _graphnodeformatter(ui, displayer)
- state = graphmod.asciistate()
- styles = state['styles']
-
- # only set graph styling if HGPLAIN is not set.
- if ui.plain('graph'):
- # set all edge styles to |, the default pre-3.8 behaviour
- styles.update(dict.fromkeys(styles, '|'))
- else:
- edgetypes = {
- 'parent': graphmod.PARENT,
- 'grandparent': graphmod.GRANDPARENT,
- 'missing': graphmod.MISSINGPARENT
- }
- for name, key in edgetypes.items():
- # experimental config: experimental.graphstyle.*
- styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
- styles[key])
- if not styles[key]:
- styles[key] = None
-
- # experimental config: experimental.graphshorten
- state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
-
- for rev, type, ctx, parents in dag:
- char = formatnode(repo, ctx)
- copies = None
- if getrenamed and ctx.rev():
- copies = []
- for fn in ctx.files():
- rename = getrenamed(fn, ctx.rev())
- if rename:
- copies.append((fn, rename[0]))
- revmatchfn = None
- if filematcher is not None:
- revmatchfn = filematcher(ctx.rev())
- edges = edgefn(type, char, state, rev, parents)
- firstedge = next(edges)
- width = firstedge[2]
- displayer.show(ctx, copies=copies, matchfn=revmatchfn,
- _graphwidth=width, **pycompat.strkwargs(props))
- lines = displayer.hunk.pop(rev).split('\n')
- if not lines[-1]:
- del lines[-1]
- displayer.flush(ctx)
- for type, char, width, coldata in itertools.chain([firstedge], edges):
- graphmod.ascii(ui, state, type, char, lines, coldata)
- lines = []
- displayer.close()
-
-def graphlog(ui, repo, revs, filematcher, opts):
- # Parameters are identical to log command ones
- revdag = graphmod.dagwalker(repo, revs)
-
- getrenamed = None
- if opts.get('copies'):
- endrev = None
- if opts.get('rev'):
- endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
- getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
-
- ui.pager('log')
- displayer = show_changeset(ui, repo, opts, buffered=True)
- displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
- filematcher)
-
-def checkunsupportedgraphflags(pats, opts):
- for op in ["newest_first"]:
- if op in opts and opts[op]:
- raise error.Abort(_("-G/--graph option is incompatible with --%s")
- % op.replace("_", "-"))
-
-def graphrevs(repo, nodes, opts):
- limit = loglimit(opts)
- nodes.reverse()
- if limit is not None:
- nodes = nodes[:limit]
- return graphmod.nodes(repo, nodes)
-
def add(ui, repo, match, prefix, explicitonly, **opts):
join = lambda f: os.path.join(prefix, f)
bad = []
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/logcmdutil.py Sun Jan 21 12:26:42 2018 +0900
@@ -0,0 +1,933 @@
+# logcmdutil.py - utility for log-like commands
+#
+# Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from __future__ import absolute_import
+
+import itertools
+import os
+
+from .i18n import _
+from .node import (
+ hex,
+ nullid,
+)
+
+from . import (
+ dagop,
+ encoding,
+ error,
+ formatter,
+ graphmod,
+ match as matchmod,
+ mdiff,
+ patch,
+ pathutil,
+ pycompat,
+ revset,
+ revsetlang,
+ scmutil,
+ smartset,
+ templatekw,
+ templater,
+ util,
+)
+
+def loglimit(opts):
+ """get the log limit according to option -l/--limit"""
+ limit = opts.get('limit')
+ if limit:
+ try:
+ limit = int(limit)
+ except ValueError:
+ raise error.Abort(_('limit must be a positive integer'))
+ if limit <= 0:
+ raise error.Abort(_('limit must be positive'))
+ else:
+ limit = None
+ return limit
+
+def diffordiffstat(ui, repo, diffopts, node1, node2, match,
+ changes=None, stat=False, fp=None, prefix='',
+ root='', listsubrepos=False, hunksfilterfn=None):
+ '''show diff or diffstat.'''
+ if fp is None:
+ write = ui.write
+ else:
+ def write(s, **kw):
+ fp.write(s)
+
+ if root:
+ relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
+ else:
+ relroot = ''
+ if relroot != '':
+ # XXX relative roots currently don't work if the root is within a
+ # subrepo
+ uirelroot = match.uipath(relroot)
+ relroot += '/'
+ for matchroot in match.files():
+ if not matchroot.startswith(relroot):
+ ui.warn(_('warning: %s not inside relative root %s\n') % (
+ match.uipath(matchroot), uirelroot))
+
+ if stat:
+ diffopts = diffopts.copy(context=0, noprefix=False)
+ width = 80
+ if not ui.plain():
+ width = ui.termwidth()
+ chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
+ prefix=prefix, relroot=relroot,
+ hunksfilterfn=hunksfilterfn)
+ for chunk, label in patch.diffstatui(util.iterlines(chunks),
+ width=width):
+ write(chunk, label=label)
+ else:
+ for chunk, label in patch.diffui(repo, node1, node2, match,
+ changes, opts=diffopts, prefix=prefix,
+ relroot=relroot,
+ hunksfilterfn=hunksfilterfn):
+ write(chunk, label=label)
+
+ if listsubrepos:
+ ctx1 = repo[node1]
+ ctx2 = repo[node2]
+ for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
+ tempnode2 = node2
+ try:
+ if node2 is not None:
+ tempnode2 = ctx2.substate[subpath][1]
+ except KeyError:
+ # A subrepo that existed in node1 was deleted between node1 and
+ # node2 (inclusive). Thus, ctx2's substate won't contain that
+ # subpath. The best we can do is to ignore it.
+ tempnode2 = None
+ submatch = matchmod.subdirmatcher(subpath, match)
+ sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
+ stat=stat, fp=fp, prefix=prefix)
+
+def _changesetlabels(ctx):
+ labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
+ if ctx.obsolete():
+ labels.append('changeset.obsolete')
+ if ctx.isunstable():
+ labels.append('changeset.unstable')
+ for instability in ctx.instabilities():
+ labels.append('instability.%s' % instability)
+ return ' '.join(labels)
+
+class changeset_printer(object):
+ '''show changeset information when templating not requested.'''
+
+ def __init__(self, ui, repo, matchfn, diffopts, buffered):
+ self.ui = ui
+ self.repo = repo
+ self.buffered = buffered
+ self.matchfn = matchfn
+ self.diffopts = diffopts
+ self.header = {}
+ self.hunk = {}
+ self.lastheader = None
+ self.footer = None
+ self._columns = templatekw.getlogcolumns()
+
+ def flush(self, ctx):
+ rev = ctx.rev()
+ if rev in self.header:
+ h = self.header[rev]
+ if h != self.lastheader:
+ self.lastheader = h
+ self.ui.write(h)
+ del self.header[rev]
+ if rev in self.hunk:
+ self.ui.write(self.hunk[rev])
+ del self.hunk[rev]
+
+ def close(self):
+ if self.footer:
+ self.ui.write(self.footer)
+
+ def show(self, ctx, copies=None, matchfn=None, hunksfilterfn=None,
+ **props):
+ props = pycompat.byteskwargs(props)
+ if self.buffered:
+ self.ui.pushbuffer(labeled=True)
+ self._show(ctx, copies, matchfn, hunksfilterfn, props)
+ self.hunk[ctx.rev()] = self.ui.popbuffer()
+ else:
+ self._show(ctx, copies, matchfn, hunksfilterfn, props)
+
+ def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
+ '''show a single changeset or file revision'''
+ changenode = ctx.node()
+ rev = ctx.rev()
+
+ if self.ui.quiet:
+ self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
+ label='log.node')
+ return
+
+ columns = self._columns
+ self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
+ label=_changesetlabels(ctx))
+
+ # branches are shown first before any other names due to backwards
+ # compatibility
+ branch = ctx.branch()
+ # don't show the default branch name
+ if branch != 'default':
+ self.ui.write(columns['branch'] % branch, label='log.branch')
+
+ for nsname, ns in self.repo.names.iteritems():
+ # branches has special logic already handled above, so here we just
+ # skip it
+ if nsname == 'branches':
+ continue
+ # we will use the templatename as the color name since those two
+ # should be the same
+ for name in ns.names(self.repo, changenode):
+ self.ui.write(ns.logfmt % name,
+ label='log.%s' % ns.colorname)
+ if self.ui.debugflag:
+ self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
+ for pctx in scmutil.meaningfulparents(self.repo, ctx):
+ label = 'log.parent changeset.%s' % pctx.phasestr()
+ self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
+ label=label)
+
+ if self.ui.debugflag and rev is not None:
+ mnode = ctx.manifestnode()
+ mrev = self.repo.manifestlog._revlog.rev(mnode)
+ self.ui.write(columns['manifest']
+ % scmutil.formatrevnode(self.ui, mrev, mnode),
+ label='ui.debug log.manifest')
+ self.ui.write(columns['user'] % ctx.user(), label='log.user')
+ self.ui.write(columns['date'] % util.datestr(ctx.date()),
+ label='log.date')
+
+ if ctx.isunstable():
+ instabilities = ctx.instabilities()
+ self.ui.write(columns['instability'] % ', '.join(instabilities),
+ label='log.instability')
+
+ elif ctx.obsolete():
+ self._showobsfate(ctx)
+
+ self._exthook(ctx)
+
+ if self.ui.debugflag:
+ files = ctx.p1().status(ctx)[:3]
+ for key, value in zip(['files', 'files+', 'files-'], files):
+ if value:
+ self.ui.write(columns[key] % " ".join(value),
+ label='ui.debug log.files')
+ elif ctx.files() and self.ui.verbose:
+ self.ui.write(columns['files'] % " ".join(ctx.files()),
+ label='ui.note log.files')
+ if copies and self.ui.verbose:
+ copies = ['%s (%s)' % c for c in copies]
+ self.ui.write(columns['copies'] % ' '.join(copies),
+ label='ui.note log.copies')
+
+ extra = ctx.extra()
+ if extra and self.ui.debugflag:
+ for key, value in sorted(extra.items()):
+ self.ui.write(columns['extra'] % (key, util.escapestr(value)),
+ label='ui.debug log.extra')
+
+ description = ctx.description().strip()
+ if description:
+ if self.ui.verbose:
+ self.ui.write(_("description:\n"),
+ label='ui.note log.description')
+ self.ui.write(description,
+ label='ui.note log.description')
+ self.ui.write("\n\n")
+ else:
+ self.ui.write(columns['summary'] % description.splitlines()[0],
+ label='log.summary')
+ self.ui.write("\n")
+
+ self.showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
+
+ def _showobsfate(self, ctx):
+ obsfate = templatekw.showobsfate(repo=self.repo, ctx=ctx, ui=self.ui)
+
+ if obsfate:
+ for obsfateline in obsfate:
+ self.ui.write(self._columns['obsolete'] % obsfateline,
+ label='log.obsfate')
+
+ def _exthook(self, ctx):
+ '''empty method used by extension as a hook point
+ '''
+
+ def showpatch(self, ctx, matchfn, hunksfilterfn=None):
+ if not matchfn:
+ matchfn = self.matchfn
+ if matchfn:
+ stat = self.diffopts.get('stat')
+ diff = self.diffopts.get('patch')
+ diffopts = patch.diffallopts(self.ui, self.diffopts)
+ node = ctx.node()
+ prev = ctx.p1().node()
+ if stat:
+ diffordiffstat(self.ui, self.repo, diffopts, prev, node,
+ match=matchfn, stat=True,
+ hunksfilterfn=hunksfilterfn)
+ if diff:
+ if stat:
+ self.ui.write("\n")
+ diffordiffstat(self.ui, self.repo, diffopts, prev, node,
+ match=matchfn, stat=False,
+ hunksfilterfn=hunksfilterfn)
+ if stat or diff:
+ self.ui.write("\n")
+
+class jsonchangeset(changeset_printer):
+ '''format changeset information.'''
+
+ def __init__(self, ui, repo, matchfn, diffopts, buffered):
+ changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
+ self.cache = {}
+ self._first = True
+
+ def close(self):
+ if not self._first:
+ self.ui.write("\n]\n")
+ else:
+ self.ui.write("[]\n")
+
+ def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
+ '''show a single changeset or file revision'''
+ rev = ctx.rev()
+ if rev is None:
+ jrev = jnode = 'null'
+ else:
+ jrev = '%d' % rev
+ jnode = '"%s"' % hex(ctx.node())
+ j = encoding.jsonescape
+
+ if self._first:
+ self.ui.write("[\n {")
+ self._first = False
+ else:
+ self.ui.write(",\n {")
+
+ if self.ui.quiet:
+ self.ui.write(('\n "rev": %s') % jrev)
+ self.ui.write((',\n "node": %s') % jnode)
+ self.ui.write('\n }')
+ return
+
+ self.ui.write(('\n "rev": %s') % jrev)
+ self.ui.write((',\n "node": %s') % jnode)
+ self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
+ self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
+ self.ui.write((',\n "user": "%s"') % j(ctx.user()))
+ self.ui.write((',\n "date": [%d, %d]') % ctx.date())
+ self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
+
+ self.ui.write((',\n "bookmarks": [%s]') %
+ ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
+ self.ui.write((',\n "tags": [%s]') %
+ ", ".join('"%s"' % j(t) for t in ctx.tags()))
+ self.ui.write((',\n "parents": [%s]') %
+ ", ".join('"%s"' % c.hex() for c in ctx.parents()))
+
+ if self.ui.debugflag:
+ if rev is None:
+ jmanifestnode = 'null'
+ else:
+ jmanifestnode = '"%s"' % hex(ctx.manifestnode())
+ self.ui.write((',\n "manifest": %s') % jmanifestnode)
+
+ self.ui.write((',\n "extra": {%s}') %
+ ", ".join('"%s": "%s"' % (j(k), j(v))
+ for k, v in ctx.extra().items()))
+
+ files = ctx.p1().status(ctx)
+ self.ui.write((',\n "modified": [%s]') %
+ ", ".join('"%s"' % j(f) for f in files[0]))
+ self.ui.write((',\n "added": [%s]') %
+ ", ".join('"%s"' % j(f) for f in files[1]))
+ self.ui.write((',\n "removed": [%s]') %
+ ", ".join('"%s"' % j(f) for f in files[2]))
+
+ elif self.ui.verbose:
+ self.ui.write((',\n "files": [%s]') %
+ ", ".join('"%s"' % j(f) for f in ctx.files()))
+
+ if copies:
+ self.ui.write((',\n "copies": {%s}') %
+ ", ".join('"%s": "%s"' % (j(k), j(v))
+ for k, v in copies))
+
+ matchfn = self.matchfn
+ if matchfn:
+ stat = self.diffopts.get('stat')
+ diff = self.diffopts.get('patch')
+ diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
+ node, prev = ctx.node(), ctx.p1().node()
+ if stat:
+ self.ui.pushbuffer()
+ diffordiffstat(self.ui, self.repo, diffopts, prev, node,
+ match=matchfn, stat=True)
+ self.ui.write((',\n "diffstat": "%s"')
+ % j(self.ui.popbuffer()))
+ if diff:
+ self.ui.pushbuffer()
+ diffordiffstat(self.ui, self.repo, diffopts, prev, node,
+ match=matchfn, stat=False)
+ self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
+
+ self.ui.write("\n }")
+
+class changeset_templater(changeset_printer):
+ '''format changeset information.
+
+ Note: there are a variety of convenience functions to build a
+ changeset_templater for common cases. See functions such as:
+ makelogtemplater, show_changeset, buildcommittemplate, or other
+ functions that use changesest_templater.
+ '''
+
+ # Arguments before "buffered" used to be positional. Consider not
+ # adding/removing arguments before "buffered" to not break callers.
+ def __init__(self, ui, repo, tmplspec, matchfn=None, diffopts=None,
+ buffered=False):
+ diffopts = diffopts or {}
+
+ changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
+ tres = formatter.templateresources(ui, repo)
+ self.t = formatter.loadtemplater(ui, tmplspec,
+ defaults=templatekw.keywords,
+ resources=tres,
+ cache=templatekw.defaulttempl)
+ self._counter = itertools.count()
+ self.cache = tres['cache'] # shared with _graphnodeformatter()
+
+ self._tref = tmplspec.ref
+ self._parts = {'header': '', 'footer': '',
+ tmplspec.ref: tmplspec.ref,
+ 'docheader': '', 'docfooter': '',
+ 'separator': ''}
+ if tmplspec.mapfile:
+ # find correct templates for current mode, for backward
+ # compatibility with 'log -v/-q/--debug' using a mapfile
+ tmplmodes = [
+ (True, ''),
+ (self.ui.verbose, '_verbose'),
+ (self.ui.quiet, '_quiet'),
+ (self.ui.debugflag, '_debug'),
+ ]
+ for mode, postfix in tmplmodes:
+ for t in self._parts:
+ cur = t + postfix
+ if mode and cur in self.t:
+ self._parts[t] = cur
+ else:
+ partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
+ m = formatter.templatepartsmap(tmplspec, self.t, partnames)
+ self._parts.update(m)
+
+ if self._parts['docheader']:
+ self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
+
+ def close(self):
+ if self._parts['docfooter']:
+ if not self.footer:
+ self.footer = ""
+ self.footer += templater.stringify(self.t(self._parts['docfooter']))
+ return super(changeset_templater, self).close()
+
+ def _show(self, ctx, copies, matchfn, hunksfilterfn, props):
+ '''show a single changeset or file revision'''
+ props = props.copy()
+ props['ctx'] = ctx
+ props['index'] = index = next(self._counter)
+ props['revcache'] = {'copies': copies}
+ props = pycompat.strkwargs(props)
+
+ # write separator, which wouldn't work well with the header part below
+ # since there's inherently a conflict between header (across items) and
+ # separator (per item)
+ if self._parts['separator'] and index > 0:
+ self.ui.write(templater.stringify(self.t(self._parts['separator'])))
+
+ # write header
+ if self._parts['header']:
+ h = templater.stringify(self.t(self._parts['header'], **props))
+ if self.buffered:
+ self.header[ctx.rev()] = h
+ else:
+ if self.lastheader != h:
+ self.lastheader = h
+ self.ui.write(h)
+
+ # write changeset metadata, then patch if requested
+ key = self._parts[self._tref]
+ self.ui.write(templater.stringify(self.t(key, **props)))
+ self.showpatch(ctx, matchfn, hunksfilterfn=hunksfilterfn)
+
+ if self._parts['footer']:
+ if not self.footer:
+ self.footer = templater.stringify(
+ self.t(self._parts['footer'], **props))
+
+def logtemplatespec(tmpl, mapfile):
+ if mapfile:
+ return formatter.templatespec('changeset', tmpl, mapfile)
+ else:
+ return formatter.templatespec('', tmpl, None)
+
+def _lookuplogtemplate(ui, tmpl, style):
+ """Find the template matching the given template spec or style
+
+ See formatter.lookuptemplate() for details.
+ """
+
+ # ui settings
+ if not tmpl and not style: # template are stronger than style
+ tmpl = ui.config('ui', 'logtemplate')
+ if tmpl:
+ return logtemplatespec(templater.unquotestring(tmpl), None)
+ else:
+ style = util.expandpath(ui.config('ui', 'style'))
+
+ if not tmpl and style:
+ mapfile = style
+ if not os.path.split(mapfile)[0]:
+ mapname = (templater.templatepath('map-cmdline.' + mapfile)
+ or templater.templatepath(mapfile))
+ if mapname:
+ mapfile = mapname
+ return logtemplatespec(None, mapfile)
+
+ if not tmpl:
+ return logtemplatespec(None, None)
+
+ return formatter.lookuptemplate(ui, 'changeset', tmpl)
+
+def makelogtemplater(ui, repo, tmpl, buffered=False):
+ """Create a changeset_templater from a literal template 'tmpl'
+ byte-string."""
+ spec = logtemplatespec(tmpl, None)
+ return changeset_templater(ui, repo, spec, buffered=buffered)
+
+def show_changeset(ui, repo, opts, buffered=False):
+ """show one changeset using template or regular display.
+
+ Display format will be the first non-empty hit of:
+ 1. option 'template'
+ 2. option 'style'
+ 3. [ui] setting 'logtemplate'
+ 4. [ui] setting 'style'
+ If all of these values are either the unset or the empty string,
+ regular display via changeset_printer() is done.
+ """
+ # options
+ match = None
+ if opts.get('patch') or opts.get('stat'):
+ match = scmutil.matchall(repo)
+
+ if opts.get('template') == 'json':
+ return jsonchangeset(ui, repo, match, opts, buffered)
+
+ spec = _lookuplogtemplate(ui, opts.get('template'), opts.get('style'))
+
+ if not spec.ref and not spec.tmpl and not spec.mapfile:
+ return changeset_printer(ui, repo, match, opts, buffered)
+
+ return changeset_templater(ui, repo, spec, match, opts, buffered)
+
+def _makelogmatcher(repo, revs, pats, opts):
+ """Build matcher and expanded patterns from log options
+
+ If --follow, revs are the revisions to follow from.
+
+ Returns (match, pats, slowpath) where
+ - match: a matcher built from the given pats and -I/-X opts
+ - pats: patterns used (globs are expanded on Windows)
+ - slowpath: True if patterns aren't as simple as scanning filelogs
+ """
+ # pats/include/exclude are passed to match.match() directly in
+ # _matchfiles() revset but walkchangerevs() builds its matcher with
+ # scmutil.match(). The difference is input pats are globbed on
+ # platforms without shell expansion (windows).
+ wctx = repo[None]
+ match, pats = scmutil.matchandpats(wctx, pats, opts)
+ slowpath = match.anypats() or (not match.always() and opts.get('removed'))
+ if not slowpath:
+ follow = opts.get('follow') or opts.get('follow_first')
+ startctxs = []
+ if follow and opts.get('rev'):
+ startctxs = [repo[r] for r in revs]
+ for f in match.files():
+ if follow and startctxs:
+ # No idea if the path was a directory at that revision, so
+ # take the slow path.
+ if any(f not in c for c in startctxs):
+ slowpath = True
+ continue
+ elif follow and f not in wctx:
+ # If the file exists, it may be a directory, so let it
+ # take the slow path.
+ if os.path.exists(repo.wjoin(f)):
+ slowpath = True
+ continue
+ else:
+ raise error.Abort(_('cannot follow file not in parent '
+ 'revision: "%s"') % f)
+ filelog = repo.file(f)
+ if not filelog:
+ # A zero count may be a directory or deleted file, so
+ # try to find matching entries on the slow path.
+ if follow:
+ raise error.Abort(
+ _('cannot follow nonexistent file: "%s"') % f)
+ slowpath = True
+
+ # We decided to fall back to the slowpath because at least one
+ # of the paths was not a file. Check to see if at least one of them
+ # existed in history - in that case, we'll continue down the
+ # slowpath; otherwise, we can turn off the slowpath
+ if slowpath:
+ for path in match.files():
+ if path == '.' or path in repo.store:
+ break
+ else:
+ slowpath = False
+
+ return match, pats, slowpath
+
+def _fileancestors(repo, revs, match, followfirst):
+ fctxs = []
+ for r in revs:
+ ctx = repo[r]
+ fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
+
+ # When displaying a revision with --patch --follow FILE, we have
+ # to know which file of the revision must be diffed. With
+ # --follow, we want the names of the ancestors of FILE in the
+ # revision, stored in "fcache". "fcache" is populated as a side effect
+ # of the graph traversal.
+ fcache = {}
+ def filematcher(rev):
+ return scmutil.matchfiles(repo, fcache.get(rev, []))
+
+ def revgen():
+ for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
+ fcache[rev] = [c.path() for c in cs]
+ yield rev
+ return smartset.generatorset(revgen(), iterasc=False), filematcher
+
+def _makenofollowlogfilematcher(repo, pats, opts):
+ '''hook for extensions to override the filematcher for non-follow cases'''
+ return None
+
+_opt2logrevset = {
+ 'no_merges': ('not merge()', None),
+ 'only_merges': ('merge()', None),
+ '_matchfiles': (None, '_matchfiles(%ps)'),
+ 'date': ('date(%s)', None),
+ 'branch': ('branch(%s)', '%lr'),
+ '_patslog': ('filelog(%s)', '%lr'),
+ 'keyword': ('keyword(%s)', '%lr'),
+ 'prune': ('ancestors(%s)', 'not %lr'),
+ 'user': ('user(%s)', '%lr'),
+}
+
+def _makelogrevset(repo, match, pats, slowpath, opts):
+ """Return a revset string built from log options and file patterns"""
+ opts = dict(opts)
+ # follow or not follow?
+ follow = opts.get('follow') or opts.get('follow_first')
+
+ # branch and only_branch are really aliases and must be handled at
+ # the same time
+ opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
+ opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
+
+ if slowpath:
+ # See walkchangerevs() slow path.
+ #
+ # pats/include/exclude cannot be represented as separate
+ # revset expressions as their filtering logic applies at file
+ # level. For instance "-I a -X b" matches a revision touching
+ # "a" and "b" while "file(a) and not file(b)" does
+ # not. Besides, filesets are evaluated against the working
+ # directory.
+ matchargs = ['r:', 'd:relpath']
+ for p in pats:
+ matchargs.append('p:' + p)
+ for p in opts.get('include', []):
+ matchargs.append('i:' + p)
+ for p in opts.get('exclude', []):
+ matchargs.append('x:' + p)
+ opts['_matchfiles'] = matchargs
+ elif not follow:
+ opts['_patslog'] = list(pats)
+
+ expr = []
+ for op, val in sorted(opts.iteritems()):
+ if not val:
+ continue
+ if op not in _opt2logrevset:
+ continue
+ revop, listop = _opt2logrevset[op]
+ if revop and '%' not in revop:
+ expr.append(revop)
+ elif not listop:
+ expr.append(revsetlang.formatspec(revop, val))
+ else:
+ if revop:
+ val = [revsetlang.formatspec(revop, v) for v in val]
+ expr.append(revsetlang.formatspec(listop, val))
+
+ if expr:
+ expr = '(' + ' and '.join(expr) + ')'
+ else:
+ expr = None
+ return expr
+
+def _logrevs(repo, opts):
+ """Return the initial set of revisions to be filtered or followed"""
+ follow = opts.get('follow') or opts.get('follow_first')
+ if opts.get('rev'):
+ revs = scmutil.revrange(repo, opts['rev'])
+ elif follow and repo.dirstate.p1() == nullid:
+ revs = smartset.baseset()
+ elif follow:
+ revs = repo.revs('.')
+ else:
+ revs = smartset.spanset(repo)
+ revs.reverse()
+ return revs
+
+def getlogrevs(repo, pats, opts):
+ """Return (revs, filematcher) where revs is a smartset
+
+ filematcher is a callable taking a revision number and returning a match
+ objects filtering the files to be detailed when displaying the revision.
+ """
+ follow = opts.get('follow') or opts.get('follow_first')
+ followfirst = opts.get('follow_first')
+ limit = loglimit(opts)
+ revs = _logrevs(repo, opts)
+ if not revs:
+ return smartset.baseset(), None
+ match, pats, slowpath = _makelogmatcher(repo, revs, pats, opts)
+ filematcher = None
+ if follow:
+ if slowpath or match.always():
+ revs = dagop.revancestors(repo, revs, followfirst=followfirst)
+ else:
+ revs, filematcher = _fileancestors(repo, revs, match, followfirst)
+ revs.reverse()
+ if filematcher is None:
+ filematcher = _makenofollowlogfilematcher(repo, pats, opts)
+ if filematcher is None:
+ def filematcher(rev):
+ return match
+
+ expr = _makelogrevset(repo, match, pats, slowpath, opts)
+ if opts.get('graph') and opts.get('rev'):
+ # User-specified revs might be unsorted, but don't sort before
+ # _makelogrevset because it might depend on the order of revs
+ if not (revs.isdescending() or revs.istopo()):
+ revs.sort(reverse=True)
+ if expr:
+ matcher = revset.match(None, expr)
+ revs = matcher(repo, revs)
+ if limit is not None:
+ revs = revs.slice(0, limit)
+ return revs, filematcher
+
+def _parselinerangelogopt(repo, opts):
+ """Parse --line-range log option and return a list of tuples (filename,
+ (fromline, toline)).
+ """
+ linerangebyfname = []
+ for pat in opts.get('line_range', []):
+ try:
+ pat, linerange = pat.rsplit(',', 1)
+ except ValueError:
+ raise error.Abort(_('malformatted line-range pattern %s') % pat)
+ try:
+ fromline, toline = map(int, linerange.split(':'))
+ except ValueError:
+ raise error.Abort(_("invalid line range for %s") % pat)
+ msg = _("line range pattern '%s' must match exactly one file") % pat
+ fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
+ linerangebyfname.append(
+ (fname, util.processlinerange(fromline, toline)))
+ return linerangebyfname
+
+def getloglinerangerevs(repo, userrevs, opts):
+ """Return (revs, filematcher, hunksfilter).
+
+ "revs" are revisions obtained by processing "line-range" log options and
+ walking block ancestors of each specified file/line-range.
+
+ "filematcher(rev) -> match" is a factory function returning a match object
+ for a given revision for file patterns specified in --line-range option.
+ If neither --stat nor --patch options are passed, "filematcher" is None.
+
+ "hunksfilter(rev) -> filterfn(fctx, hunks)" is a factory function
+ returning a hunks filtering function.
+ If neither --stat nor --patch options are passed, "filterhunks" is None.
+ """
+ wctx = repo[None]
+
+ # Two-levels map of "rev -> file ctx -> [line range]".
+ linerangesbyrev = {}
+ for fname, (fromline, toline) in _parselinerangelogopt(repo, opts):
+ if fname not in wctx:
+ raise error.Abort(_('cannot follow file not in parent '
+ 'revision: "%s"') % fname)
+ fctx = wctx.filectx(fname)
+ for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
+ rev = fctx.introrev()
+ if rev not in userrevs:
+ continue
+ linerangesbyrev.setdefault(
+ rev, {}).setdefault(
+ fctx.path(), []).append(linerange)
+
+ filematcher = None
+ hunksfilter = None
+ if opts.get('patch') or opts.get('stat'):
+
+ def nofilterhunksfn(fctx, hunks):
+ return hunks
+
+ def hunksfilter(rev):
+ fctxlineranges = linerangesbyrev.get(rev)
+ if fctxlineranges is None:
+ return nofilterhunksfn
+
+ def filterfn(fctx, hunks):
+ lineranges = fctxlineranges.get(fctx.path())
+ if lineranges is not None:
+ for hr, lines in hunks:
+ if hr is None: # binary
+ yield hr, lines
+ continue
+ if any(mdiff.hunkinrange(hr[2:], lr)
+ for lr in lineranges):
+ yield hr, lines
+ else:
+ for hunk in hunks:
+ yield hunk
+
+ return filterfn
+
+ def filematcher(rev):
+ files = list(linerangesbyrev.get(rev, []))
+ return scmutil.matchfiles(repo, files)
+
+ revs = sorted(linerangesbyrev, reverse=True)
+
+ return revs, filematcher, hunksfilter
+
+def _graphnodeformatter(ui, displayer):
+ spec = ui.config('ui', 'graphnodetemplate')
+ if not spec:
+ return templatekw.showgraphnode # fast path for "{graphnode}"
+
+ spec = templater.unquotestring(spec)
+ tres = formatter.templateresources(ui)
+ if isinstance(displayer, changeset_templater):
+ tres['cache'] = displayer.cache # reuse cache of slow templates
+ templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
+ resources=tres)
+ def formatnode(repo, ctx):
+ props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
+ return templ.render(props)
+ return formatnode
+
+def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
+ filematcher=None, props=None):
+ props = props or {}
+ formatnode = _graphnodeformatter(ui, displayer)
+ state = graphmod.asciistate()
+ styles = state['styles']
+
+ # only set graph styling if HGPLAIN is not set.
+ if ui.plain('graph'):
+ # set all edge styles to |, the default pre-3.8 behaviour
+ styles.update(dict.fromkeys(styles, '|'))
+ else:
+ edgetypes = {
+ 'parent': graphmod.PARENT,
+ 'grandparent': graphmod.GRANDPARENT,
+ 'missing': graphmod.MISSINGPARENT
+ }
+ for name, key in edgetypes.items():
+ # experimental config: experimental.graphstyle.*
+ styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
+ styles[key])
+ if not styles[key]:
+ styles[key] = None
+
+ # experimental config: experimental.graphshorten
+ state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
+
+ for rev, type, ctx, parents in dag:
+ char = formatnode(repo, ctx)
+ copies = None
+ if getrenamed and ctx.rev():
+ copies = []
+ for fn in ctx.files():
+ rename = getrenamed(fn, ctx.rev())
+ if rename:
+ copies.append((fn, rename[0]))
+ revmatchfn = None
+ if filematcher is not None:
+ revmatchfn = filematcher(ctx.rev())
+ edges = edgefn(type, char, state, rev, parents)
+ firstedge = next(edges)
+ width = firstedge[2]
+ displayer.show(ctx, copies=copies, matchfn=revmatchfn,
+ _graphwidth=width, **pycompat.strkwargs(props))
+ lines = displayer.hunk.pop(rev).split('\n')
+ if not lines[-1]:
+ del lines[-1]
+ displayer.flush(ctx)
+ for type, char, width, coldata in itertools.chain([firstedge], edges):
+ graphmod.ascii(ui, state, type, char, lines, coldata)
+ lines = []
+ displayer.close()
+
+def graphlog(ui, repo, revs, filematcher, opts):
+ # Parameters are identical to log command ones
+ revdag = graphmod.dagwalker(repo, revs)
+
+ getrenamed = None
+ if opts.get('copies'):
+ endrev = None
+ if opts.get('rev'):
+ endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
+ getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
+
+ ui.pager('log')
+ displayer = show_changeset(ui, repo, opts, buffered=True)
+ displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
+ filematcher)
+
+def checkunsupportedgraphflags(pats, opts):
+ for op in ["newest_first"]:
+ if op in opts and opts[op]:
+ raise error.Abort(_("-G/--graph option is incompatible with --%s")
+ % op.replace("_", "-"))
+
+def graphrevs(repo, nodes, opts):
+ limit = loglimit(opts)
+ nodes.reverse()
+ if limit is not None:
+ nodes = nodes[:limit]
+ return graphmod.nodes(repo, nodes)
--- a/tests/test-glog.t Fri Feb 02 13:13:46 2018 -0800
+++ b/tests/test-glog.t Sun Jan 21 12:26:42 2018 +0900
@@ -87,16 +87,17 @@
> cmdutil,
> commands,
> extensions,
+ > logcmdutil,
> revsetlang,
> smartset,
> )
>
> def logrevset(repo, pats, opts):
- > revs = cmdutil._logrevs(repo, opts)
+ > revs = logcmdutil._logrevs(repo, opts)
> if not revs:
> return None
- > match, pats, slowpath = cmdutil._makelogmatcher(repo, revs, pats, opts)
- > return cmdutil._makelogrevset(repo, match, pats, slowpath, opts)
+ > match, pats, slowpath = logcmdutil._makelogmatcher(repo, revs, pats, opts)
+ > return logcmdutil._makelogrevset(repo, match, pats, slowpath, opts)
>
> def uisetup(ui):
> def printrevset(orig, repo, pats, opts):