log: introduce struct that carries log traversal options
authorYuya Nishihara <yuya@tcha.org>
Sat, 12 Sep 2020 21:06:16 +0900
changeset 45565 c1d0f83d62c4
parent 45564 a717de1cb624
child 45566 24df19a9ab87
log: introduce struct that carries log traversal options I tried to refactor logcmdutil.getrevs() without using an options struct, but none of these attempts didn't work out. Since every stage of getrevs() needs various log command options (e.g. both matcher and revset query need file patterns), it isn't possible to cleanly split getrevs() into a command layer and a core logic. So, this patch introduces a named struct to carry command options in slightly abstracted way, which will be later used by "hg grep" and "hg churn". More fields will be added to the walkopt struct. Type hints aren't verified. I couldn't figure out how to teach pytype to load its own attr type stubs in place of our .thirdparty.attr. Conditional import didn't work. s/^from \.thirdparty // is the only way I found pytype could parse the @attr.ib decorator.
hgext/sparse.py
mercurial/commands.py
mercurial/logcmdutil.py
tests/printrevset.py
--- a/hgext/sparse.py	Sat Sep 12 16:19:01 2020 +0900
+++ b/hgext/sparse.py	Sat Sep 12 21:06:16 2020 +0900
@@ -137,9 +137,9 @@
         )
     )
 
-    def _initialrevs(orig, repo, opts):
-        revs = orig(repo, opts)
-        if opts.get(b'sparse'):
+    def _initialrevs(orig, repo, wopts):
+        revs = orig(repo, wopts)
+        if wopts.opts.get(b'sparse'):
             sparsematch = sparse.matcher(repo)
 
             def ctxmatch(rev):
--- a/mercurial/commands.py	Sat Sep 12 16:19:01 2020 +0900
+++ b/mercurial/commands.py	Sat Sep 12 21:06:16 2020 +0900
@@ -4734,7 +4734,9 @@
         )
 
     repo = scmutil.unhidehashlikerevs(repo, opts.get(b'rev'), b'nowarn')
-    revs, differ = logcmdutil.getrevs(repo, pats, opts)
+    revs, differ = logcmdutil.getrevs(
+        repo, logcmdutil.parseopts(ui, pats, opts)
+    )
     if linerange:
         # TODO: should follow file history from logcmdutil._initialrevs(),
         # then filter the result by logcmdutil._makerevset() and --limit
--- a/mercurial/logcmdutil.py	Sat Sep 12 16:19:01 2020 +0900
+++ b/mercurial/logcmdutil.py	Sat Sep 12 21:06:16 2020 +0900
@@ -18,6 +18,8 @@
     wdirrev,
 )
 
+from .thirdparty import attr
+
 from . import (
     dagop,
     error,
@@ -45,11 +47,13 @@
 if pycompat.TYPE_CHECKING:
     from typing import (
         Any,
+        Dict,
+        List,
         Optional,
         Tuple,
     )
 
-    for t in (Any, Optional, Tuple):
+    for t in (Any, Dict, List, Optional, Tuple):
         assert t
 
 
@@ -672,7 +676,27 @@
     return changesettemplater(ui, repo, spec, *postargs)
 
 
-def _makematcher(repo, revs, pats, opts):
+@attr.s
+class walkopts(object):
+    """Options to configure a set of revisions and file matcher factory
+    to scan revision/file history
+    """
+
+    # raw command-line parameters, which a matcher will be built from
+    pats = attr.ib()  # type: List[bytes]
+    opts = attr.ib()  # type: Dict[bytes, Any]
+
+
+def parseopts(ui, pats, opts):
+    # type: (Any, List[bytes], Dict[bytes, Any]) -> walkopts
+    """Parse log command options into walkopts
+
+    The returned walkopts will be passed in to getrevs().
+    """
+    return walkopts(pats=pats, opts=opts)
+
+
+def _makematcher(repo, revs, wopts):
     """Build matcher and expanded patterns from log options
 
     If --follow, revs are the revisions to follow from.
@@ -687,11 +711,13 @@
     # 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(b'removed'))
+    match, pats = scmutil.matchandpats(wctx, wopts.pats, wopts.opts)
+    slowpath = match.anypats() or (
+        not match.always() and wopts.opts.get(b'removed')
+    )
     if not slowpath:
-        follow = opts.get(b'follow') or opts.get(b'follow_first')
-        if follow and opts.get(b'rev'):
+        follow = wopts.opts.get(b'follow') or wopts.opts.get(b'follow_first')
+        if follow and wopts.opts.get(b'rev'):
             # There may be the case that a path doesn't exist in some (but
             # not all) of the specified start revisions, but let's consider
             # the path is valid. Missing files will be warned by the matcher.
@@ -800,9 +826,9 @@
 }
 
 
-def _makerevset(repo, pats, slowpath, opts):
+def _makerevset(repo, wopts, slowpath):
     """Return a revset string built from log options and file patterns"""
-    opts = dict(opts)
+    opts = dict(wopts.opts)
     # follow or not follow?
     follow = opts.get(b'follow') or opts.get(b'follow_first')
 
@@ -821,7 +847,7 @@
         # not. Besides, filesets are evaluated against the working
         # directory.
         matchargs = [b'r:', b'd:relpath']
-        for p in pats:
+        for p in wopts.pats:
             matchargs.append(b'p:' + p)
         for p in opts.get(b'include', []):
             matchargs.append(b'i:' + p)
@@ -829,7 +855,7 @@
             matchargs.append(b'x:' + p)
         opts[b'_matchfiles'] = matchargs
     elif not follow:
-        opts[b'_patslog'] = list(pats)
+        opts[b'_patslog'] = list(wopts.pats)
 
     expr = []
     for op, val in sorted(pycompat.iteritems(opts)):
@@ -854,11 +880,11 @@
     return expr
 
 
-def _initialrevs(repo, opts):
+def _initialrevs(repo, wopts):
     """Return the initial set of revisions to be filtered or followed"""
-    follow = opts.get(b'follow') or opts.get(b'follow_first')
-    if opts.get(b'rev'):
-        revs = scmutil.revrange(repo, opts[b'rev'])
+    follow = wopts.opts.get(b'follow') or wopts.opts.get(b'follow_first')
+    if wopts.opts.get(b'rev'):
+        revs = scmutil.revrange(repo, wopts.opts[b'rev'])
     elif follow and repo.dirstate.p1() == nullid:
         revs = smartset.baseset()
     elif follow:
@@ -869,19 +895,21 @@
     return revs
 
 
-def getrevs(repo, pats, opts):
-    # type: (Any, Any, Any) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
+def getrevs(repo, wopts):
+    # type: (Any, walkopts) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
     """Return (revs, differ) where revs is a smartset
 
     differ is a changesetdiffer with pre-configured file matcher.
     """
-    follow = opts.get(b'follow') or opts.get(b'follow_first')
-    followfirst = opts.get(b'follow_first')
-    limit = getlimit(opts)
-    revs = _initialrevs(repo, opts)
+    follow = wopts.opts.get(b'follow') or wopts.opts.get(b'follow_first')
+    followfirst = wopts.opts.get(b'follow_first')
+    limit = getlimit(wopts.opts)
+    revs = _initialrevs(repo, wopts)
     if not revs:
         return smartset.baseset(), None
-    match, pats, slowpath = _makematcher(repo, revs, pats, opts)
+    match, pats, slowpath = _makematcher(repo, revs, wopts)
+    wopts = attr.evolve(wopts, pats=pats)
+
     filematcher = None
     if follow:
         if slowpath or match.always():
@@ -890,14 +918,14 @@
             revs, filematcher = _fileancestors(repo, revs, match, followfirst)
         revs.reverse()
     if filematcher is None:
-        filematcher = _makenofollowfilematcher(repo, pats, opts)
+        filematcher = _makenofollowfilematcher(repo, wopts.pats, wopts.opts)
     if filematcher is None:
 
         def filematcher(ctx):
             return match
 
-    expr = _makerevset(repo, pats, slowpath, opts)
-    if opts.get(b'graph'):
+    expr = _makerevset(repo, wopts, slowpath)
+    if wopts.opts.get(b'graph'):
         if repo.ui.configbool(b'experimental', b'log.topo'):
             if not revs.istopo():
                 revs = dagop.toposort(revs, repo.changelog.parentrevs)
--- a/tests/printrevset.py	Sat Sep 12 16:19:01 2020 +0900
+++ b/tests/printrevset.py	Sat Sep 12 21:06:16 2020 +0900
@@ -1,4 +1,5 @@
 from __future__ import absolute_import
+from mercurial.thirdparty import attr
 from mercurial import (
     cmdutil,
     commands,
@@ -11,26 +12,27 @@
 from mercurial.utils import stringutil
 
 
-def logrevset(repo, pats, opts):
-    revs = logcmdutil._initialrevs(repo, opts)
+def logrevset(repo, wopts):
+    revs = logcmdutil._initialrevs(repo, wopts)
     if not revs:
         return None
-    match, pats, slowpath = logcmdutil._makematcher(repo, revs, pats, opts)
-    return logcmdutil._makerevset(repo, pats, slowpath, opts)
+    match, pats, slowpath = logcmdutil._makematcher(repo, revs, wopts)
+    wopts = attr.evolve(wopts, pats=pats)
+    return logcmdutil._makerevset(repo, wopts, slowpath)
 
 
 def uisetup(ui):
-    def printrevset(orig, repo, pats, opts):
-        revs, filematcher = orig(repo, pats, opts)
-        if opts.get(b'print_revset'):
-            expr = logrevset(repo, pats, opts)
+    def printrevset(orig, repo, wopts):
+        revs, filematcher = orig(repo, wopts)
+        if wopts.opts.get(b'print_revset'):
+            expr = logrevset(repo, wopts)
             if expr:
                 tree = revsetlang.parse(expr)
                 tree = revsetlang.analyze(tree)
             else:
                 tree = []
             ui = repo.ui
-            ui.write(b'%s\n' % stringutil.pprint(opts.get(b'rev', [])))
+            ui.write(b'%s\n' % stringutil.pprint(wopts.opts.get(b'rev', [])))
             ui.write(revsetlang.prettyformat(tree) + b'\n')
             ui.write(stringutil.prettyrepr(revs) + b'\n')
             revs = smartset.baseset()  # display no revisions