graphlog: extract revset/support functions into cmdutil
authorPatrick Mezard <patrick@mezard.eu>
Sat, 14 Jul 2012 18:55:21 +0200
changeset 17180 ae0629161090
parent 17179 0849d725e2f9
child 17181 6f71167292f2
graphlog: extract revset/support functions into cmdutil
hgext/graphlog.py
mercurial/cmdutil.py
tests/test-glog.t
--- a/hgext/graphlog.py	Wed Jul 11 17:13:39 2012 +0200
+++ b/hgext/graphlog.py	Sat Jul 14 18:55:21 2012 +0200
@@ -15,7 +15,7 @@
 from mercurial.cmdutil import show_changeset
 from mercurial.i18n import _
 from mercurial import cmdutil, commands, extensions, scmutil
-from mercurial import hg, util, graphmod, templatekw, revset
+from mercurial import hg, util, graphmod, templatekw
 
 cmdtable = {}
 command = cmdutil.command(cmdtable)
@@ -27,244 +27,6 @@
             raise util.Abort(_("-G/--graph option is incompatible with --%s")
                              % op.replace("_", "-"))
 
-def _makefilematcher(repo, pats, followfirst):
-    # 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 by
-    # reproducing the graph traversal already done by --follow revset
-    # and relating linkrevs to file names (which is not "correct" but
-    # good enough).
-    fcache = {}
-    fcacheready = [False]
-    pctx = repo['.']
-    wctx = repo[None]
-
-    def populate():
-        for fn in pats:
-            for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
-                for c in i:
-                    fcache.setdefault(c.linkrev(), set()).add(c.path())
-
-    def filematcher(rev):
-        if not fcacheready[0]:
-            # Lazy initialization
-            fcacheready[0] = True
-            populate()
-        return scmutil.match(wctx, fcache.get(rev, []), default='path')
-
-    return filematcher
-
-def _makelogrevset(repo, pats, opts, revs):
-    """Return (expr, filematcher) where expr is a revset string built
-    from log options and file patterns or None. If --stat or --patch
-    are not passed filematcher is None. Otherwise it is a callable
-    taking a revision number and returning a match objects filtering
-    the files to be detailed when displaying the revision.
-    """
-    opt2revset = {
-        'no_merges':        ('not merge()', None),
-        'only_merges':      ('merge()', None),
-        '_ancestors':       ('ancestors(%(val)s)', None),
-        '_fancestors':      ('_firstancestors(%(val)s)', None),
-        '_descendants':     ('descendants(%(val)s)', None),
-        '_fdescendants':    ('_firstdescendants(%(val)s)', None),
-        '_matchfiles':      ('_matchfiles(%(val)s)', None),
-        'date':             ('date(%(val)r)', None),
-        'branch':           ('branch(%(val)r)', ' or '),
-        '_patslog':         ('filelog(%(val)r)', ' or '),
-        '_patsfollow':      ('follow(%(val)r)', ' or '),
-        '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
-        'keyword':          ('keyword(%(val)r)', ' or '),
-        'prune':            ('not (%(val)r or ancestors(%(val)r))', ' and '),
-        'user':             ('user(%(val)r)', ' or '),
-        }
-
-    opts = dict(opts)
-    # follow or not follow?
-    follow = opts.get('follow') or opts.get('follow_first')
-    followfirst = opts.get('follow_first') and 1 or 0
-    # --follow with FILE behaviour depends on revs...
-    startrev = revs[0]
-    followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
-
-    # 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']]
-    # pats/include/exclude are passed to match.match() directly in
-    # _matchfile() revset but walkchangerevs() builds its matcher with
-    # scmutil.match(). The difference is input pats are globbed on
-    # platforms without shell expansion (windows).
-    pctx = repo[None]
-    match, pats = scmutil.matchandpats(pctx, pats, opts)
-    slowpath = match.anypats() or (match.files() and opts.get('removed'))
-    if not slowpath:
-        for f in match.files():
-            if follow and f not in pctx:
-                raise util.Abort(_('cannot follow file not in parent '
-                                   'revision: "%s"') % f)
-            filelog = repo.file(f)
-            if not len(filelog):
-                # A zero count may be a directory or deleted file, so
-                # try to find matching entries on the slow path.
-                if follow:
-                    raise util.Abort(
-                        _('cannot follow nonexistent file: "%s"') % f)
-                slowpath = True
-    if slowpath:
-        # See cmdutil.walkchangerevs() slow path.
-        #
-        if follow:
-            raise util.Abort(_('can only follow copies/renames for explicit '
-                               'filenames'))
-        # pats/include/exclude cannot be represented as separate
-        # revset expressions as their filtering logic applies at file
-        # level. For instance "-I a -X a" 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)
-        matchargs = ','.join(('%r' % p) for p in matchargs)
-        opts['_matchfiles'] = matchargs
-    else:
-        if follow:
-            fpats = ('_patsfollow', '_patsfollowfirst')
-            fnopats = (('_ancestors', '_fancestors'),
-                       ('_descendants', '_fdescendants'))
-            if pats:
-                # follow() revset inteprets its file argument as a
-                # manifest entry, so use match.files(), not pats.
-                opts[fpats[followfirst]] = list(match.files())
-            else:
-                opts[fnopats[followdescendants][followfirst]] = str(startrev)
-        else:
-            opts['_patslog'] = list(pats)
-
-    filematcher = None
-    if opts.get('patch') or opts.get('stat'):
-        if follow:
-            filematcher = _makefilematcher(repo, pats, followfirst)
-        else:
-            filematcher = lambda rev: match
-
-    expr = []
-    for op, val in opts.iteritems():
-        if not val:
-            continue
-        if op not in opt2revset:
-            continue
-        revop, andor = opt2revset[op]
-        if '%(val)' not in revop:
-            expr.append(revop)
-        else:
-            if not isinstance(val, list):
-                e = revop % {'val': val}
-            else:
-                e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
-            expr.append(e)
-
-    if expr:
-        expr = '(' + ' and '.join(expr) + ')'
-    else:
-        expr = None
-    return expr, filematcher
-
-def getlogrevs(repo, pats, opts):
-    """Return (revs, expr, filematcher) where revs is an iterable of
-    revision numbers, expr is a revset string built from log options
-    and file patterns or None, and used to filter 'revs'. If --stat or
-    --patch are not passed filematcher is None. Otherwise it is a
-    callable taking a revision number and returning a match objects
-    filtering the files to be detailed when displaying the revision.
-    """
-    def increasingrevs(repo, revs, matcher):
-        # The sorted input rev sequence is chopped in sub-sequences
-        # which are sorted in ascending order and passed to the
-        # matcher. The filtered revs are sorted again as they were in
-        # the original sub-sequence. This achieve several things:
-        #
-        # - getlogrevs() now returns a generator which behaviour is
-        #   adapted to log need. First results come fast, last ones
-        #   are batched for performances.
-        #
-        # - revset matchers often operate faster on revision in
-        #   changelog order, because most filters deal with the
-        #   changelog.
-        #
-        # - revset matchers can reorder revisions. "A or B" typically
-        #   returns returns the revision matching A then the revision
-        #   matching B. We want to hide this internal implementation
-        #   detail from the caller, and sorting the filtered revision
-        #   again achieves this.
-        for i, window in cmdutil.increasingwindows(0, len(revs), windowsize=1):
-            orevs = revs[i:i + window]
-            nrevs = set(matcher(repo, sorted(orevs)))
-            for rev in orevs:
-                if rev in nrevs:
-                    yield rev
-
-    if not len(repo):
-        return iter([]), None, None
-    # Default --rev value depends on --follow but --follow behaviour
-    # depends on revisions resolved from --rev...
-    follow = opts.get('follow') or opts.get('follow_first')
-    if opts.get('rev'):
-        revs = scmutil.revrange(repo, opts['rev'])
-    else:
-        if follow and len(repo) > 0:
-            revs = scmutil.revrange(repo, ['.:0'])
-        else:
-            revs = range(len(repo) - 1, -1, -1)
-    if not revs:
-        return iter([]), None, None
-    expr, filematcher = _makelogrevset(repo, pats, opts, revs)
-    if expr:
-        matcher = revset.match(repo.ui, expr)
-        revs = increasingrevs(repo, revs, matcher)
-    if not opts.get('hidden'):
-        # --hidden is still experimental and not worth a dedicated revset
-        # yet. Fortunately, filtering revision number is fast.
-        revs = (r for r in revs if r not in repo.changelog.hiddenrevs)
-    else:
-        revs = iter(revs)
-    return revs, expr, filematcher
-
-def generate(ui, dag, displayer, showparents, edgefn, getrenamed=None,
-             filematcher=None):
-    seen, state = [], graphmod.asciistate()
-    for rev, type, ctx, parents in dag:
-        char = 'o'
-        if ctx.node() in showparents:
-            char = '@'
-        elif ctx.obsolete():
-            char = 'x'
-        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())
-        displayer.show(ctx, copies=copies, matchfn=revmatchfn)
-        lines = displayer.hunk.pop(rev).split('\n')
-        if not lines[-1]:
-            del lines[-1]
-        displayer.flush(rev)
-        edges = edgefn(type, char, lines, seen, rev, parents)
-        for type, char, lines, coldata in edges:
-            graphmod.ascii(ui, state, type, char, lines, coldata)
-    displayer.close()
-
 @command('glog',
     [('f', 'follow', None,
      _('follow changeset history, or file history across copies and renames')),
@@ -298,7 +60,7 @@
     directory.
     """
 
-    revs, expr, filematcher = getlogrevs(repo, pats, opts)
+    revs, expr, filematcher = cmdutil.getgraphlogrevs(repo, pats, opts)
     revs = sorted(revs, reverse=1)
     limit = cmdutil.loglimit(opts)
     if limit is not None:
@@ -313,8 +75,8 @@
         getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
     displayer = show_changeset(ui, repo, opts, buffered=True)
     showparents = [ctx.node() for ctx in repo[None].parents()]
-    generate(ui, revdag, displayer, showparents, graphmod.asciiedges,
-             getrenamed, filematcher)
+    cmdutil.displaygraph(ui, revdag, displayer, showparents,
+                         graphmod.asciiedges, getrenamed, filematcher)
 
 def graphrevs(repo, nodes, opts):
     limit = cmdutil.loglimit(opts)
@@ -341,7 +103,8 @@
     revdag = graphrevs(repo, o, opts)
     displayer = show_changeset(ui, repo, opts, buffered=True)
     showparents = [ctx.node() for ctx in repo[None].parents()]
-    generate(ui, revdag, displayer, showparents, graphmod.asciiedges)
+    cmdutil.displaygraph(ui, revdag, displayer, showparents,
+                         graphmod.asciiedges)
 
 def gincoming(ui, repo, source="default", **opts):
     """show the incoming changesets alongside an ASCII revision graph
@@ -359,7 +122,8 @@
     def display(other, chlist, displayer):
         revdag = graphrevs(other, chlist, opts)
         showparents = [ctx.node() for ctx in repo[None].parents()]
-        generate(ui, revdag, displayer, showparents, graphmod.asciiedges)
+        cmdutil.displaygraph(ui, revdag, displayer, showparents,
+                             graphmod.asciiedges)
 
     hg._incoming(display, subreporecurse, ui, repo, source, opts, buffered=True)
 
--- a/mercurial/cmdutil.py	Wed Jul 11 17:13:39 2012 +0200
+++ b/mercurial/cmdutil.py	Sat Jul 14 18:55:21 2012 +0200
@@ -10,7 +10,7 @@
 import os, sys, errno, re, tempfile
 import util, scmutil, templater, patch, error, templatekw, revlog, copies
 import match as matchmod
-import subrepo, context, repair, bookmarks
+import subrepo, context, repair, bookmarks, graphmod, revset
 
 def parsealiases(cmd):
     return cmd.lstrip("^").split("|")
@@ -1192,6 +1192,244 @@
                 yield change(rev)
     return iterate()
 
+def _makegraphfilematcher(repo, pats, followfirst):
+    # 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 by
+    # reproducing the graph traversal already done by --follow revset
+    # and relating linkrevs to file names (which is not "correct" but
+    # good enough).
+    fcache = {}
+    fcacheready = [False]
+    pctx = repo['.']
+    wctx = repo[None]
+
+    def populate():
+        for fn in pats:
+            for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
+                for c in i:
+                    fcache.setdefault(c.linkrev(), set()).add(c.path())
+
+    def filematcher(rev):
+        if not fcacheready[0]:
+            # Lazy initialization
+            fcacheready[0] = True
+            populate()
+        return scmutil.match(wctx, fcache.get(rev, []), default='path')
+
+    return filematcher
+
+def _makegraphlogrevset(repo, pats, opts, revs):
+    """Return (expr, filematcher) where expr is a revset string built
+    from log options and file patterns or None. If --stat or --patch
+    are not passed filematcher is None. Otherwise it is a callable
+    taking a revision number and returning a match objects filtering
+    the files to be detailed when displaying the revision.
+    """
+    opt2revset = {
+        'no_merges':        ('not merge()', None),
+        'only_merges':      ('merge()', None),
+        '_ancestors':       ('ancestors(%(val)s)', None),
+        '_fancestors':      ('_firstancestors(%(val)s)', None),
+        '_descendants':     ('descendants(%(val)s)', None),
+        '_fdescendants':    ('_firstdescendants(%(val)s)', None),
+        '_matchfiles':      ('_matchfiles(%(val)s)', None),
+        'date':             ('date(%(val)r)', None),
+        'branch':           ('branch(%(val)r)', ' or '),
+        '_patslog':         ('filelog(%(val)r)', ' or '),
+        '_patsfollow':      ('follow(%(val)r)', ' or '),
+        '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
+        'keyword':          ('keyword(%(val)r)', ' or '),
+        'prune':            ('not (%(val)r or ancestors(%(val)r))', ' and '),
+        'user':             ('user(%(val)r)', ' or '),
+        }
+
+    opts = dict(opts)
+    # follow or not follow?
+    follow = opts.get('follow') or opts.get('follow_first')
+    followfirst = opts.get('follow_first') and 1 or 0
+    # --follow with FILE behaviour depends on revs...
+    startrev = revs[0]
+    followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
+
+    # 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']]
+    # pats/include/exclude are passed to match.match() directly in
+    # _matchfile() revset but walkchangerevs() builds its matcher with
+    # scmutil.match(). The difference is input pats are globbed on
+    # platforms without shell expansion (windows).
+    pctx = repo[None]
+    match, pats = scmutil.matchandpats(pctx, pats, opts)
+    slowpath = match.anypats() or (match.files() and opts.get('removed'))
+    if not slowpath:
+        for f in match.files():
+            if follow and f not in pctx:
+                raise util.Abort(_('cannot follow file not in parent '
+                                   'revision: "%s"') % f)
+            filelog = repo.file(f)
+            if not len(filelog):
+                # A zero count may be a directory or deleted file, so
+                # try to find matching entries on the slow path.
+                if follow:
+                    raise util.Abort(
+                        _('cannot follow nonexistent file: "%s"') % f)
+                slowpath = True
+    if slowpath:
+        # See walkchangerevs() slow path.
+        #
+        if follow:
+            raise util.Abort(_('can only follow copies/renames for explicit '
+                               'filenames'))
+        # pats/include/exclude cannot be represented as separate
+        # revset expressions as their filtering logic applies at file
+        # level. For instance "-I a -X a" 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)
+        matchargs = ','.join(('%r' % p) for p in matchargs)
+        opts['_matchfiles'] = matchargs
+    else:
+        if follow:
+            fpats = ('_patsfollow', '_patsfollowfirst')
+            fnopats = (('_ancestors', '_fancestors'),
+                       ('_descendants', '_fdescendants'))
+            if pats:
+                # follow() revset inteprets its file argument as a
+                # manifest entry, so use match.files(), not pats.
+                opts[fpats[followfirst]] = list(match.files())
+            else:
+                opts[fnopats[followdescendants][followfirst]] = str(startrev)
+        else:
+            opts['_patslog'] = list(pats)
+
+    filematcher = None
+    if opts.get('patch') or opts.get('stat'):
+        if follow:
+            filematcher = _makegraphfilematcher(repo, pats, followfirst)
+        else:
+            filematcher = lambda rev: match
+
+    expr = []
+    for op, val in opts.iteritems():
+        if not val:
+            continue
+        if op not in opt2revset:
+            continue
+        revop, andor = opt2revset[op]
+        if '%(val)' not in revop:
+            expr.append(revop)
+        else:
+            if not isinstance(val, list):
+                e = revop % {'val': val}
+            else:
+                e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
+            expr.append(e)
+
+    if expr:
+        expr = '(' + ' and '.join(expr) + ')'
+    else:
+        expr = None
+    return expr, filematcher
+
+def getgraphlogrevs(repo, pats, opts):
+    """Return (revs, expr, filematcher) where revs is an iterable of
+    revision numbers, expr is a revset string built from log options
+    and file patterns or None, and used to filter 'revs'. If --stat or
+    --patch are not passed filematcher is None. Otherwise it is a
+    callable taking a revision number and returning a match objects
+    filtering the files to be detailed when displaying the revision.
+    """
+    def increasingrevs(repo, revs, matcher):
+        # The sorted input rev sequence is chopped in sub-sequences
+        # which are sorted in ascending order and passed to the
+        # matcher. The filtered revs are sorted again as they were in
+        # the original sub-sequence. This achieve several things:
+        #
+        # - getlogrevs() now returns a generator which behaviour is
+        #   adapted to log need. First results come fast, last ones
+        #   are batched for performances.
+        #
+        # - revset matchers often operate faster on revision in
+        #   changelog order, because most filters deal with the
+        #   changelog.
+        #
+        # - revset matchers can reorder revisions. "A or B" typically
+        #   returns returns the revision matching A then the revision
+        #   matching B. We want to hide this internal implementation
+        #   detail from the caller, and sorting the filtered revision
+        #   again achieves this.
+        for i, window in increasingwindows(0, len(revs), windowsize=1):
+            orevs = revs[i:i + window]
+            nrevs = set(matcher(repo, sorted(orevs)))
+            for rev in orevs:
+                if rev in nrevs:
+                    yield rev
+
+    if not len(repo):
+        return iter([]), None, None
+    # Default --rev value depends on --follow but --follow behaviour
+    # depends on revisions resolved from --rev...
+    follow = opts.get('follow') or opts.get('follow_first')
+    if opts.get('rev'):
+        revs = scmutil.revrange(repo, opts['rev'])
+    else:
+        if follow and len(repo) > 0:
+            revs = scmutil.revrange(repo, ['.:0'])
+        else:
+            revs = range(len(repo) - 1, -1, -1)
+    if not revs:
+        return iter([]), None, None
+    expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs)
+    if expr:
+        matcher = revset.match(repo.ui, expr)
+        revs = increasingrevs(repo, revs, matcher)
+    if not opts.get('hidden'):
+        # --hidden is still experimental and not worth a dedicated revset
+        # yet. Fortunately, filtering revision number is fast.
+        revs = (r for r in revs if r not in repo.changelog.hiddenrevs)
+    else:
+        revs = iter(revs)
+    return revs, expr, filematcher
+
+def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
+                 filematcher=None):
+    seen, state = [], graphmod.asciistate()
+    for rev, type, ctx, parents in dag:
+        char = 'o'
+        if ctx.node() in showparents:
+            char = '@'
+        elif ctx.obsolete():
+            char = 'x'
+        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())
+        displayer.show(ctx, copies=copies, matchfn=revmatchfn)
+        lines = displayer.hunk.pop(rev).split('\n')
+        if not lines[-1]:
+            del lines[-1]
+        displayer.flush(rev)
+        edges = edgefn(type, char, lines, seen, rev, parents)
+        for type, char, lines, coldata in edges:
+            graphmod.ascii(ui, state, type, char, lines, coldata)
+    displayer.close()
+
 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
     join = lambda f: os.path.join(prefix, f)
     bad = []
--- a/tests/test-glog.t	Wed Jul 11 17:13:39 2012 +0200
+++ b/tests/test-glog.t	Sat Jul 14 18:55:21 2012 +0200
@@ -82,13 +82,12 @@
   > }
 
   $ cat > printrevset.py <<EOF
-  > from mercurial import extensions, revset, commands
-  > from hgext import graphlog
+  > from mercurial import extensions, revset, commands, cmdutil
   >  
   > def uisetup(ui):
   >     def printrevset(orig, ui, repo, *pats, **opts):
   >         if opts.get('print_revset'):
-  >             expr = graphlog.getlogrevs(repo, pats, opts)[1]
+  >             expr = cmdutil.getgraphlogrevs(repo, pats, opts)[1]
   >             if expr:
   >                 tree = revset.parse(expr)[0]
   >             else: