# HG changeset patch # User Yuya Nishihara # Date 1516505202 -32400 # Node ID 7625b4f7db7032ececdd8ea6282310d74c96c74c # Parent 197d10e157ce848129ff5e7a53cf81d4ca63a932 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 diff -r 197d10e157ce -r 7625b4f7db70 hgext/largefiles/overrides.py --- 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) diff -r 197d10e157ce -r 7625b4f7db70 hgext/sparse.py --- 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') diff -r 197d10e157ce -r 7625b4f7db70 mercurial/cmdutil.py --- 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 = [] diff -r 197d10e157ce -r 7625b4f7db70 mercurial/logcmdutil.py --- /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 +# +# 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) diff -r 197d10e157ce -r 7625b4f7db70 tests/test-glog.t --- 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):