Mercurial > public > mercurial-scm > hg-stable
diff mercurial/cmdutil.py @ 35925:7625b4f7db70
cmdutil: split functions of log-like commands to new module (API)
cmdutil.py is painfully big and makes Emacs slow. Let's split log-related
functions.
% wc -l mercurial/cmdutil.py
4027 mercurial/cmdutil.py
% wc -l mercurial/cmdutil.py mercurial/logcmdutil.py
3141 mercurial/cmdutil.py
933 mercurial/logcmdutil.py
4074 total
author | Yuya Nishihara <yuya@tcha.org> |
---|---|
date | Sun, 21 Jan 2018 12:26:42 +0900 |
parents | 1bee7762fd46 |
children | b0014780c7fc |
line wrap: on
line diff
--- 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 = []