# HG changeset patch # User Yuya Nishihara # Date 1599912376 -32400 # Node ID c1d0f83d62c4174fc80668e797292bb520d237e9 # Parent a717de1cb624e8aee32dac7c388ecc1cc891e508 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. diff -r a717de1cb624 -r c1d0f83d62c4 hgext/sparse.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): diff -r a717de1cb624 -r c1d0f83d62c4 mercurial/commands.py --- 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 diff -r a717de1cb624 -r c1d0f83d62c4 mercurial/logcmdutil.py --- 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) diff -r a717de1cb624 -r c1d0f83d62c4 tests/printrevset.py --- 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