[PATCH 1 of 4] templater: introduce wrapper for smartset (API)

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
6 messages Options
Reply | Threaded
Open this post in threaded view
|

[PATCH 1 of 4] templater: introduce wrapper for smartset (API)

Yuya Nishihara
# HG changeset patch
# User Yuya Nishihara <[hidden email]>
# Date 1584252764 -32400
#      Sun Mar 15 15:12:44 2020 +0900
# Node ID 22fa586e96c80e85f433565e39b14b17993310b6
# Parent  2a98b0cd4995d725104ea1e42b5009f7ee26ac4c
templater: introduce wrapper for smartset (API)

I want to add a template function which takes a revset as an argument:

  {somefunc(..., revset(...))}
                 ^^^^^^^^^^^
                 evaluates to a revslist

This wrapper will provide a method to get an underlying smartset. It should
also be good for performance since count(revset(...)) will no longer have to
fully consume the smartset for example, but that isn't the point of this
change.

diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py
--- a/mercurial/templatefuncs.py
+++ b/mercurial/templatefuncs.py
@@ -668,7 +668,7 @@ def revset(context, mapping, args):
         else:
             revs = query(raw)
             revsetcache[raw] = revs
-    return templatekw.showrevslist(context, mapping, b"revision", revs)
+    return templateutil.revslist(repo, revs, name=b'revision')
 
 
 @templatefunc(b'rstdoc(text, style)')
diff --git a/mercurial/templatekw.py b/mercurial/templatekw.py
--- a/mercurial/templatekw.py
+++ b/mercurial/templatekw.py
@@ -873,24 +873,6 @@ def showrev(context, mapping):
     return scmutil.intrev(ctx)
 
 
-def showrevslist(context, mapping, name, revs):
-    """helper to generate a list of revisions in which a mapped template will
-    be evaluated"""
-    repo = context.resource(mapping, b'repo')
-    # revs may be a smartset; don't compute it until f() has to be evaluated
-    def f():
-        srevs = [b'%d' % r for r in revs]
-        return _showcompatlist(context, mapping, name, srevs)
-
-    return _hybrid(
-        f,
-        revs,
-        lambda x: {name: x, b'ctx': repo[x]},
-        pycompat.identity,
-        keytype=int,
-    )
-
-
 @templatekeyword(b'subrepos', requires={b'ctx'})
 def showsubrepos(context, mapping):
     """List of strings. Updated subrepositories in the changeset."""
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -45,6 +45,9 @@ hybrid
 hybriditem
     represents a scalar printable value, also supports % operator.
 
+revslist
+    represents a list of revision numbers.
+
 mappinggenerator, mappinglist
     represents mappings (i.e. a list of dicts), which may have default
     output format.
diff --git a/mercurial/templateutil.py b/mercurial/templateutil.py
--- a/mercurial/templateutil.py
+++ b/mercurial/templateutil.py
@@ -15,6 +15,7 @@ from .pycompat import getattr
 from . import (
     error,
     pycompat,
+    smartset,
     util,
 )
 from .utils import (
@@ -408,6 +409,74 @@ class hybriditem(mappable, wrapped):
         return _unthunk(context, mapping, self._value)
 
 
+class revslist(wrapped):
+    """Wrapper for a smartset (a list/set of revision numbers)
+
+    If name specified, the revs will be rendered with the old-style list
+    template of the given name by default.
+    """
+
+    def __init__(self, repo, revs, name=None):
+        assert isinstance(revs, smartset.abstractsmartset)
+        self._repo = repo
+        self._revs = revs
+        self._name = name
+
+    def contains(self, context, mapping, item):
+        rev = unwrapinteger(context, mapping, item)
+        return rev in self._revs
+
+    def getmember(self, context, mapping, key):
+        raise error.ParseError(_(b'not a dictionary'))
+
+    def getmin(self, context, mapping):
+        makehybriditem = self._makehybriditemfunc()
+        return makehybriditem(self._revs.min())
+
+    def getmax(self, context, mapping):
+        makehybriditem = self._makehybriditemfunc()
+        return makehybriditem(self._revs.max())
+
+    def filter(self, context, mapping, select):
+        makehybriditem = self._makehybriditemfunc()
+        frevs = self._revs.filter(lambda r: select(makehybriditem(r)))
+        # once filtered, no need to support old-style list template
+        return revslist(self._repo, frevs, name=None)
+
+    def itermaps(self, context):
+        makemap = self._makemapfunc()
+        for r in self._revs:
+            yield makemap(r)
+
+    def _makehybriditemfunc(self):
+        makemap = self._makemapfunc()
+        return lambda r: hybriditem(None, r, r, makemap)
+
+    def _makemapfunc(self):
+        repo = self._repo
+        name = self._name
+        if name:
+            return lambda r: {name: r, b'ctx': repo[r]}
+        else:
+            return lambda r: {b'ctx': repo[r]}
+
+    def join(self, context, mapping, sep):
+        return joinitems(self._revs, sep)
+
+    def show(self, context, mapping):
+        if self._name:
+            srevs = [b'%d' % r for r in self._revs]
+            return _showcompatlist(context, mapping, self._name, srevs)
+        else:
+            return self.join(context, mapping, b' ')
+
+    def tobool(self, context, mapping):
+        return bool(self._revs)
+
+    def tovalue(self, context, mapping):
+        return list(self._revs)
+
+
 class _mappingsequence(wrapped):
     """Wrapper for sequence of template mappings
 
diff --git a/tests/test-template-functions.t b/tests/test-template-functions.t
--- a/tests/test-template-functions.t
+++ b/tests/test-template-functions.t
@@ -820,6 +820,8 @@ Test json filter applied to wrapped obje
   {"branch": "default"}
   $ hg log -r0 -T '{date|json}\n'
   [0, 0]
+  $ hg log -r0 -T '{revset(":")|json}\n'
+  [0, 1]
 
 Test json filter applied to map result:
 
@@ -1263,6 +1265,28 @@ default. join() should agree with the de
   5:13207e5a10d9fd28ec424934298e176197f2c67f,
   4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
 
+for historical reasons, revset() supports old-style list template
+
+  $ hg log -T '{revset(":")}\n' -l1 \
+  >        --config templates.start_revisions='"["' \
+  >        --config templates.end_revisions='"]"' \
+  >        --config templates.revision='"{revision}, "' \
+  >        --config templates.last_revision='"{revision}"'
+  [0, 1, 2]
+  $ hg log -T '{revset(":") % " {revision}"}\n' -l1
+   0 1 2
+
+but a filtered one doesn't
+
+  $ hg log -T '{filter(revset(":"), ifeq(rev, 1, "", "y"))}\n' -l1 \
+  >        --config templates.start_revisions='"["' \
+  >        --config templates.end_revisions='"]"' \
+  >        --config templates.revision='"{revision}, "' \
+  >        --config templates.last_revision='"{revision}"'
+  0 2
+  $ hg log -T '{filter(revset(":"), ifeq(rev, 1, "", "y")) % "x{revision}"}\n' -l1
+  xx
+
 %d parameter handling:
 
   $ hg log -T '{revset("%d", rev)}\n' -r'wdir()'
@@ -1318,6 +1342,13 @@ Invalid arguments passed to revset()
   hg: parse error: invalid argument for revspec
   [255]
 
+Invalid operation on revset()
+
+  $ hg log -T '{get(revset(":"), "foo")}\n'
+  hg: parse error: not a dictionary
+  (get() expects a dict as first argument)
+  [255]
+
 Test files function
 
   $ hg log -T "{rev}\n{join(files('*'), '\n')}\n"
@@ -1568,6 +1599,23 @@ Test cbor filter:
    }
   ]
 
+  $ hg log -T "{revset(':')|cbor}" -R a -l1 | "$PYTHON" "$TESTTMP/decodecbor.py"
+  [
+   [
+    0,
+    1,
+    2,
+    3,
+    4,
+    5,
+    6,
+    7,
+    8,
+    9,
+    10
+   ]
+  ]
+
 json filter should escape HTML tags so that the output can be embedded in hgweb:
 
   $ hg log -T "{'<[hidden email]>'|json}\n" -R a -l1
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

[PATCH 2 of 4] templater: fix cbor() filter to accept smartset

Yuya Nishihara
# HG changeset patch
# User Yuya Nishihara <[hidden email]>
# Date 1584277298 -32400
#      Sun Mar 15 22:01:38 2020 +0900
# Node ID 2149f8b5c2eeb0cab6cc6aafb06db7cb7370253d
# Parent  22fa586e96c80e85f433565e39b14b17993310b6
templater: fix cbor() filter to accept smartset

So the wrapper type can return a bare smartset.

diff --git a/mercurial/templatefilters.py b/mercurial/templatefilters.py
--- a/mercurial/templatefilters.py
+++ b/mercurial/templatefilters.py
@@ -18,6 +18,7 @@ from . import (
     node,
     pycompat,
     registrar,
+    smartset,
     templateutil,
     url,
     util,
@@ -108,6 +109,9 @@ def basename(path):
 @templatefilter(b'cbor')
 def cbor(obj):
     """Any object. Serializes the object to CBOR bytes."""
+    if isinstance(obj, smartset.abstractsmartset):
+        # cborutil is stricter about type than json() filter
+        obj = list(obj)
     return b''.join(cborutil.streamencode(obj))
 
 
diff --git a/mercurial/templateutil.py b/mercurial/templateutil.py
--- a/mercurial/templateutil.py
+++ b/mercurial/templateutil.py
@@ -474,7 +474,7 @@ class revslist(wrapped):
         return bool(self._revs)
 
     def tovalue(self, context, mapping):
-        return list(self._revs)
+        return self._revs
 
 
 class _mappingsequence(wrapped):
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

[PATCH 3 of 4] templater: remember cache key of evaluated revset

Yuya Nishihara
In reply to this post by Yuya Nishihara
# HG changeset patch
# User Yuya Nishihara <[hidden email]>
# Date 1584255645 -32400
#      Sun Mar 15 16:00:45 2020 +0900
# Node ID d686edd2bf8868661260854dfbe37292fec79b4c
# Parent  2149f8b5c2eeb0cab6cc6aafb06db7cb7370253d
templater: remember cache key of evaluated revset

This provides a hint for caching further computation result of the given
revset. See the next patch for example.

diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py
--- a/mercurial/templatefuncs.py
+++ b/mercurial/templatefuncs.py
@@ -658,17 +658,19 @@ def revset(context, mapping, args):
         return m(repo)
 
     if len(args) > 1:
+        key = None  # dynamically-created revs shouldn't be cached
         formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
         revs = query(revsetlang.formatspec(raw, *formatargs))
     else:
         cache = context.resource(mapping, b'cache')
         revsetcache = cache.setdefault(b"revsetcache", {})
-        if raw in revsetcache:
-            revs = revsetcache[raw]
+        key = raw
+        if key in revsetcache:
+            revs = revsetcache[key]
         else:
             revs = query(raw)
-            revsetcache[raw] = revs
-    return templateutil.revslist(repo, revs, name=b'revision')
+            revsetcache[key] = revs
+    return templateutil.revslist(repo, revs, name=b'revision', cachekey=key)
 
 
 @templatefunc(b'rstdoc(text, style)')
diff --git a/mercurial/templateutil.py b/mercurial/templateutil.py
--- a/mercurial/templateutil.py
+++ b/mercurial/templateutil.py
@@ -414,13 +414,18 @@ class revslist(wrapped):
 
     If name specified, the revs will be rendered with the old-style list
     template of the given name by default.
+
+    The cachekey provides a hint to cache further computation on this
+    smartset. If the underlying smartset is dynamically created, the cachekey
+    should be None.
     """
 
-    def __init__(self, repo, revs, name=None):
+    def __init__(self, repo, revs, name=None, cachekey=None):
         assert isinstance(revs, smartset.abstractsmartset)
         self._repo = repo
         self._revs = revs
         self._name = name
+        self.cachekey = cachekey
 
     def contains(self, context, mapping, item):
         rev = unwrapinteger(context, mapping, item)
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

[PATCH 4 of 4] templater: add subsetparents(rev, revset) function

Yuya Nishihara
In reply to this post by Yuya Nishihara
# HG changeset patch
# User Yuya Nishihara <[hidden email]>
# Date 1584256318 -32400
#      Sun Mar 15 16:11:58 2020 +0900
# Node ID 700af5e971d4715b255782007f3ee967ed44fd56
# Parent  d686edd2bf8868661260854dfbe37292fec79b4c
templater: add subsetparents(rev, revset) function

Naming suggestions are welcome. And this could be flagged as an (ADVANCED)
function since the primary use case is to draw a graph.

This provides all data needed for drawing revisions graph filtered by revset,
and allows us to implement a GUI graph viewer in some languages better than
Python. A frontend grapher will be quite similar to our graphmod since
subsetparents() just returns parent-child relations in the filtered sub graph.

Frontend example:

  https://hg.sr.ht/~yuja/hgv/browse/default/core/hgchangesetgrapher.cpp

However, the resulting graph will be simpler than the one "hg log -G" would
generate because redundant edges are eliminated. This should be the same graph
rendering strategy as TortoiseHg.

This function could be implemented as a revset predicate, but that would mean
the scanning state couldn't be cached and thus slow.

Test cases are split to new file since test-template-functions.t is quite
big and we'll need a new branchy repository anyway.

diff --git a/mercurial/dagop.py b/mercurial/dagop.py
--- a/mercurial/dagop.py
+++ b/mercurial/dagop.py
@@ -274,6 +274,238 @@ def descendantrevs(revs, revsfn, parentr
                 break
 
 
+class subsetparentswalker(object):
+    r"""Scan adjacent ancestors in the graph given by the subset
+
+    This computes parent-child relations in the sub graph filtered by
+    a revset. Primary use case is to draw a revisions graph.
+
+    In the following example, we consider that the node 'f' has edges to all
+    ancestor nodes, but redundant paths are eliminated. The edge 'f'->'b'
+    is eliminated because there is a path 'f'->'c'->'b' for example.
+
+          - d - e -
+         /         \
+        a - b - c - f
+
+    If the node 'c' is filtered out, the edge 'f'->'b' is activated.
+
+          - d - e -
+         /         \
+        a - b -(c)- f
+
+    Likewise, if 'd' and 'e' are filtered out, this edge is fully eliminated
+    since there is a path 'f'->'c'->'b'->'a' for 'f'->'a'.
+
+           (d) (e)
+
+        a - b - c - f
+
+    Implementation-wise, 'f' is passed down to 'a' as unresolved through the
+    'f'->'e'->'d'->'a' path, whereas we do also remember that 'f' has already
+    been resolved while walking down the 'f'->'c'->'b'->'a' path. When
+    processing the node 'a', the unresolved 'f'->'a' path is eliminated as
+    the 'f' end is marked as resolved.
+
+    Ancestors are searched from the tipmost revision in the subset so the
+    results can be cached. You should specify startrev to narrow the search
+    space to ':startrev'.
+    """
+
+    def __init__(self, repo, subset, startrev=None):
+        if startrev is not None:
+            subset = repo.revs(b'%d:null', startrev) & subset
+
+        # equivalent to 'subset = subset.sorted(reverse=True)', but there's
+        # no such function.
+        fastdesc = subset.fastdesc
+        if fastdesc:
+            desciter = fastdesc()
+        else:
+            if not subset.isdescending() and not subset.istopo():
+                subset = smartset.baseset(subset)
+                subset.sort(reverse=True)
+            desciter = iter(subset)
+
+        self._repo = repo
+        self._changelog = repo.changelog
+        self._subset = subset
+
+        # scanning state (see _scanparents):
+        self._tovisit = []
+        self._pendingcnt = {}
+        self._pointers = {}
+        self._parents = {}
+        self._inputhead = nullrev  # reassigned by self._advanceinput()
+        self._inputtail = desciter
+        self._bottomrev = nullrev
+        self._advanceinput()
+
+    def parentsset(self, rev):
+        """Look up parents of the given revision in the subset, and returns
+        as a smartset"""
+        return smartset.baseset(self.parents(rev))
+
+    def parents(self, rev):
+        """Look up parents of the given revision in the subset
+
+        The returned revisions are sorted by parent index (p1/p2).
+        """
+        self._scanparents(rev)
+        return [r for _c, r in sorted(self._parents.get(rev, []))]
+
+    def _parentrevs(self, rev):
+        try:
+            revs = self._changelog.parentrevs(rev)
+            if revs[-1] == nullrev:
+                return revs[:-1]
+            return revs
+        except error.WdirUnsupported:
+            return tuple(pctx.rev() for pctx in self._repo[None].parents())
+
+    def _advanceinput(self):
+        """Advance the input iterator and set the next revision to _inputhead"""
+        if self._inputhead < nullrev:
+            return
+        try:
+            self._inputhead = next(self._inputtail)
+        except StopIteration:
+            self._bottomrev = self._inputhead
+            self._inputhead = nullrev - 1
+
+    def _scanparents(self, stoprev):
+        """Scan ancestors until the parents of the specified stoprev are
+        resolved"""
+
+        # 'tovisit' is the queue of the input revisions and their ancestors.
+        # It will be populated incrementally to minimize the initial cost
+        # of computing the given subset.
+        #
+        # For to-visit revisions, we keep track of
+        # - the number of the unresolved paths: pendingcnt[rev],
+        # - dict of the unresolved descendants and chains: pointers[rev][0],
+        # - set of the already resolved descendants: pointers[rev][1].
+        #
+        # When a revision is visited, 'pointers[rev]' should be popped and
+        # propagated to its parents accordingly.
+        #
+        # Once all pending paths have been resolved, 'pendingcnt[rev]' becomes
+        # 0 and 'parents[rev]' contains the unsorted list of parent revisions
+        # and p1/p2 chains (excluding linear paths.)
+
+        subset = self._subset
+        tovisit = self._tovisit  # heap queue of [-rev]
+        pendingcnt = self._pendingcnt  # {rev: count} for visited revisions
+        pointers = self._pointers  # {rev: [{unresolved_rev: chain}, resolved]}
+        parents = self._parents  # {rev: [(chain, rev)]}
+
+        while tovisit or self._inputhead >= nullrev:
+            if pendingcnt.get(stoprev) == 0:
+                return
+
+            # feed greater revisions from input set to queue
+            if not tovisit:
+                heapq.heappush(tovisit, -self._inputhead)
+                self._advanceinput()
+            while self._inputhead >= -tovisit[0]:
+                heapq.heappush(tovisit, -self._inputhead)
+                self._advanceinput()
+
+            rev = -heapq.heappop(tovisit)
+            if rev < self._bottomrev:
+                return
+            if rev in pendingcnt and rev not in pointers:
+                continue  # already visited
+
+            curactive = rev in subset
+            pendingcnt.setdefault(rev, 0)  # mark as visited
+            if curactive:
+                assert rev not in parents
+                parents[rev] = []
+            unresolved, resolved = pointers.pop(rev, ({}, set()))
+
+            if curactive:
+                # reached to active rev, resolve pending descendants' parents
+                for r, c in unresolved.items():
+                    pendingcnt[r] -= 1
+                    assert pendingcnt[r] >= 0
+                    if r in resolved:
+                        continue  # eliminate redundant path
+                    parents[r].append((c, rev))
+                    # mark the descendant 'r' as resolved through this path if
+                    # there are still pending pointers. the 'resolved' set may
+                    # be concatenated later at a fork revision.
+                    if pendingcnt[r] > 0:
+                        resolved.add(r)
+                unresolved.clear()
+                # occasionally clean resolved markers. otherwise the set
+                # would grow indefinitely.
+                resolved = {r for r in resolved if pendingcnt[r] > 0}
+
+            parentrevs = self._parentrevs(rev)
+            bothparentsactive = all(p in subset for p in parentrevs)
+
+            # set up or propagate tracking pointers if
+            # - one of the parents is not active,
+            # - or descendants' parents are unresolved.
+            if not bothparentsactive or unresolved or resolved:
+                if len(parentrevs) > 1:
+                    # 'rev' is a merge revision. increment the pending count
+                    # as the 'unresolved' dict will be duplicated.
+                    for r in unresolved:
+                        pendingcnt[r] += 1
+                reusable = True  # can we avoid copying the tracking pointer?
+                for i, p in enumerate(parentrevs):
+                    assert p < rev
+                    heapq.heappush(tovisit, -p)
+                    if p in pointers:
+                        # 'p' is a fork revision. concatenate tracking pointers
+                        # and decrement the pending count accordingly.
+                        knownunresolved, knownresolved = pointers[p]
+                        for r, c in unresolved.items():
+                            c += [b'1', b'2'][i]
+                            if r in knownunresolved:
+                                # unresolved at both paths
+                                pendingcnt[r] -= 1
+                                assert pendingcnt[r] > 0
+                                # take shorter chain
+                                knownunresolved[r] = min(c, knownunresolved[r])
+                            else:
+                                knownunresolved[r] = c
+                        # simply propagate the 'resolved' set as deduplicating
+                        # 'unresolved' here would be slightly complicated.
+                        knownresolved.update(resolved)
+                    elif reusable:
+                        pointers[p] = (unresolved, resolved)
+                        reusable = False
+                    else:
+                        pointers[p] = (unresolved.copy(), resolved.copy())
+
+            # then, populate the active parents directly and add the current
+            # 'rev' to the tracking pointers of the inactive parents.
+            # 'pointers[p]' may be optimized out if both parents are active.
+            if curactive and bothparentsactive:
+                for i, p in enumerate(parentrevs):
+                    c = [b'1', b'2'][i]
+                    parents[rev].append((c, p))
+                    # no need to mark 'rev' as resolved since the 'rev' should
+                    # be fully resolved (i.e. pendingcnt[rev] == 0)
+                assert pendingcnt[rev] == 0
+            elif curactive:
+                for i, p in enumerate(parentrevs):
+                    unresolved, resolved = pointers[p]
+                    assert rev not in unresolved
+                    c = [b'1', b'2'][i]
+                    if p in subset:
+                        parents[rev].append((c, p))
+                        # mark 'rev' as resolved through this path
+                        resolved.add(rev)
+                    else:
+                        pendingcnt[rev] += 1
+                        unresolved[rev] = c
+                assert 0 < pendingcnt[rev] <= 2
+
+
 def _reachablerootspure(pfunc, minroot, roots, heads, includepath):
     """See revlog.reachableroots"""
     if not roots:
diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py
--- a/mercurial/templatefuncs.py
+++ b/mercurial/templatefuncs.py
@@ -16,6 +16,7 @@ from .node import (
 )
 from . import (
     color,
+    dagop,
     diffutil,
     encoding,
     error,
@@ -842,6 +843,45 @@ def startswith(context, mapping, args):
     return b''
 
 
+@templatefunc(
+    b'subsetparents(rev, revset)',
+    argspec=b'rev revset',
+    requires={b'repo', b'cache'},
+)
+def subsetparents(context, mapping, args):
+    """Look up parents of the rev in the sub graph given by the revset."""
+    if b'rev' not in args or b'revset' not in args:
+        # i18n: "subsetparents" is a keyword
+        raise error.ParseError(_(b"subsetparents expects two arguments"))
+
+    repo = context.resource(mapping, b'repo')
+
+    rev = templateutil.evalinteger(context, mapping, args[b'rev'])
+
+    # TODO: maybe subsetparents(rev) should be allowed. the default revset
+    # will be the revisions specified by -rREV argument.
+    q = templateutil.evalwrapped(context, mapping, args[b'revset'])
+    if not isinstance(q, templateutil.revslist):
+        # i18n: "subsetparents" is a keyword
+        raise error.ParseError(_(b"subsetparents expects a queried revset"))
+    subset = q.tovalue(context, mapping)
+    key = q.cachekey
+
+    if key:
+        # cache only if revset query isn't dynamic
+        cache = context.resource(mapping, b'cache')
+        walkercache = cache.setdefault(b'subsetparentswalker', {})
+        if key in walkercache:
+            walker = walkercache[key]
+        else:
+            walker = dagop.subsetparentswalker(repo, subset)
+            walkercache[key] = walker
+    else:
+        # for one-shot use, specify startrev to limit the search space
+        walker = dagop.subsetparentswalker(repo, subset, startrev=rev)
+    return templateutil.revslist(repo, walker.parentsset(rev))
+
+
 @templatefunc(b'word(number, text[, separator])')
 def word(context, mapping, args):
     """Return the nth word from a string."""
diff --git a/tests/test-template-graph.t b/tests/test-template-graph.t
new file mode 100644
--- /dev/null
+++ b/tests/test-template-graph.t
@@ -0,0 +1,337 @@
+Test graph-related template functions
+=====================================
+
+  $ cat <<'EOF' >> $HGRCPATH
+  > [extensions]
+  > drawdag = $RUNTESTDIR/drawdag.py
+  > EOF
+
+  $ hg init a
+  $ cd a
+
+  $ hg debugdrawdag <<'EOF'
+  >   l
+  >  / \
+  > |   k
+  > |   |\
+  > |   | j
+  > |   | |
+  > i   | |
+  > |\  | |
+  > h | | |
+  > | | | |
+  > | g | |
+  > | | | |
+  > f | | |
+  > | |/ /
+  > | e |
+  > | |/
+  > | d
+  > |/|
+  > c |
+  > | |
+  > b |
+  >   |
+  >   a
+  > EOF
+
+  $ hg log -Gq -T'{rev} {tags}\n'
+  o    11 l tip
+  |\
+  | o    10 i
+  | |\
+  o \ \    9 k
+  |\ \ \
+  +-----o  8 g
+  | | |
+  | o |  7 j
+  | | |
+  | | o  6 h
+  | | |
+  o | |  5 e
+  |/ /
+  | o  4 f
+  | |
+  o |  3 d
+  |\|
+  | o  2 c
+  | |
+  | o  1 b
+  |
+  o  0 a
+  
+
+  $ cd ..
+
+subsetparents
+-------------
+
+  $ cd a
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+i"))}\n' -r 'c+i'
+  o  10 i: 2
+  :
+  o  2 c:
+  |
+  ~
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+h+i"))}\n' -r 'c+h+i'
+  o    10 i: 6
+  |\
+  o :  6 h: 2
+  :/
+  o  2 c:
+  |
+  ~
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+h+l"))}\n' -r 'c+h+l'
+  o    11 l tip: 6
+  :\
+  : o  6 h: 2
+  :/
+  o  2 c:
+  |
+  ~
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+f+l"))}\n' -r 'c+f+l'
+  o    11 l tip: 4
+  :\
+  : o  4 f: 2
+  :/
+  o  2 c:
+  |
+  ~
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+h+i+k"))}\n' -r 'c+h+i+k'
+  o    10 i: 6
+  |\
+  | : o  9 k: 2
+  | :/
+  o :  6 h: 2
+  :/
+  o  2 c:
+  |
+  ~
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+d+h+i+k"))}\n' -r 'c+d+h+i+k'
+  o    10 i: 6 3
+  |\
+  | : o  9 k: 3
+  | :/
+  o :  6 h: 2
+  : :
+  : o  3 d: 2
+  :/|
+  : ~
+  o  2 c:
+  |
+  ~
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+j+k+i"))}\n' -r 'c+j+k+i'
+  o  10 i: 2
+  :
+  : o  9 k: 7
+  :/|
+  : o  7 j: 2
+  :/
+  o  2 c:
+  |
+  ~
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+e+f+j"))}\n' -r 'c+e+f+j'
+  o  7 j: 2
+  :
+  : o  5 e: 2
+  :/
+  : o  4 f: 2
+  :/
+  o  2 c:
+  |
+  ~
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("b+e+f+j"))}\n' -r 'b+e+f+j'
+  o  7 j: 1
+  :
+  : o  5 e: 1
+  :/
+  : o  4 f: 1
+  :/
+  o  1 b:
+  
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("a+c+f+g+j+l"))}\n' -r 'a+c+f+g+j+l'
+  o    11 l tip: 4 8 7
+  :\
+  : \
+  : :\
+  : : \
+  : : :\
+  : : : \
+  : : : :\
+  : o---+ :  8 g: 0 2
+  : :/ / /
+  : +---o  7 j: 0 2
+  : : :/
+  o---+  4 f: 2
+   / /
+  : o  2 c:
+  : |
+  : ~
+  o  0 a:
+  
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("b+i+l"))}\n' -r 'b+i+l'
+  o    11 l tip: 10
+  |\
+  o :  10 i: 1
+  :/
+  o  1 b:
+  
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("b+i+j+l"))}\n' -r 'b+i+j+l'
+  o    11 l tip: 10 7
+  |\
+  | \
+  | :\
+  o : :  10 i: 1
+  :/ /
+  : o  7 j: 1
+  :/
+  o  1 b:
+  
+
+null in subset:
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("null+a+c+f"))}\n' -r 'null+a+c+f'
+  o  4 f: 2
+  |
+  o  2 c: -1
+  :
+  : o  0 a: -1
+  :/
+  @  -1 : -1
+  
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("null+a+b+c+f"))}\n' -r 'null+a+b+c+f'
+  o  4 f: 2
+  |
+  o  2 c: 1
+  |
+  o  1 b: -1
+  |
+  | o  0 a: -1
+  |/
+  @  -1 : -1
+  
+
+wdir in subset:
+
+  $ hg update -qC i
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("f+k+wdir()"))}\n' -r 'f+k+wdir()'
+  o  2147483647 : 4
+  :
+  : o    9 k:
+  : |\
+  : ~ ~
+  o  4 f:
+  |
+  ~
+
+  $ hg update -qC null
+
+Revisions not in subset:
+
+  $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("a+c+f+g+j+l"))}\n'
+  11 l tip: 4 8 7
+  10 i:
+  9 k:
+  8 g: 0 2
+  7 j: 0 2
+  6 h:
+  5 e:
+  4 f: 2
+  3 d:
+  2 c:
+  1 b:
+  0 a:
+
+  $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("b+c"))}\n'
+  11 l tip:
+  10 i:
+  9 k:
+  8 g:
+  7 j:
+  6 h:
+  5 e:
+  4 f:
+  3 d:
+  2 c: 1
+  1 b:
+  0 a:
+
+  $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("b+c"))}\n' -r'reverse(null:2)'
+  2 c: 1
+  1 b:
+  0 a:
+  -1 :
+
+Nothing excluded:
+
+  $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("null:wdir()"))}\n' -r'reverse(null:wdir())'
+  2147483647 : -1
+  11 l tip: 10 9
+  10 i: 6 8
+  9 k: 5 7
+  8 g: 5
+  7 j: 3
+  6 h: 4
+  5 e: 3
+  4 f: 2
+  3 d: 0 2
+  2 c: 1
+  1 b: -1
+  0 a: -1
+  -1 : -1
+
+Uncachable query:
+
+  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("%d:%d", rev, rev - 1))}\n'
+  o    11 l tip: 10
+  |\
+  | o    10 i:
+  | |\
+  o \ \    9 k:
+  |\ \ \
+  +-----o  8 g:
+  | | |
+  | o |  7 j:
+  | | |
+  | | o  6 h:
+  | | |
+  o | |  5 e:
+  |/ /
+  | o  4 f:
+  | |
+  o |  3 d: 2
+  |\|
+  | o  2 c: 1
+  | |
+  | o  1 b:
+  |
+  o  0 a: -1
+  
+
+Invalid arguments:
+
+  $ hg log -T '{subsetparents()}\n'
+  hg: parse error: subsetparents expects two arguments
+  [255]
+  $ hg log -T '{subsetparents("a")}\n'
+  hg: parse error: subsetparents expects two arguments
+  [255]
+  $ hg log -T '{subsetparents(rev, extras)}\n'
+  hg: parse error: subsetparents expects a queried revset
+  [255]
+
+  $ cd ..
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

Re: [PATCH 4 of 4] templater: add subsetparents(rev, revset) function

Augie Fackler-2
queued with enthusiasm

> On Mar 24, 2020, at 10:42, Yuya Nishihara <[hidden email]> wrote:
>
> # HG changeset patch
> # User Yuya Nishihara <[hidden email]>
> # Date 1584256318 -32400
> #      Sun Mar 15 16:11:58 2020 +0900
> # Node ID 700af5e971d4715b255782007f3ee967ed44fd56
> # Parent  d686edd2bf8868661260854dfbe37292fec79b4c
> templater: add subsetparents(rev, revset) function
>
> Naming suggestions are welcome. And this could be flagged as an (ADVANCED)
> function since the primary use case is to draw a graph.
>
> This provides all data needed for drawing revisions graph filtered by revset,
> and allows us to implement a GUI graph viewer in some languages better than
> Python. A frontend grapher will be quite similar to our graphmod since
> subsetparents() just returns parent-child relations in the filtered sub graph.
>
> Frontend example:
>
>  https://hg.sr.ht/~yuja/hgv/browse/default/core/hgchangesetgrapher.cpp
>
> However, the resulting graph will be simpler than the one "hg log -G" would
> generate because redundant edges are eliminated. This should be the same graph
> rendering strategy as TortoiseHg.
>
> This function could be implemented as a revset predicate, but that would mean
> the scanning state couldn't be cached and thus slow.
>
> Test cases are split to new file since test-template-functions.t is quite
> big and we'll need a new branchy repository anyway.
>
> diff --git a/mercurial/dagop.py b/mercurial/dagop.py
> --- a/mercurial/dagop.py
> +++ b/mercurial/dagop.py
> @@ -274,6 +274,238 @@ def descendantrevs(revs, revsfn, parentr
>                 break
>
>
> +class subsetparentswalker(object):
> +    r"""Scan adjacent ancestors in the graph given by the subset
> +
> +    This computes parent-child relations in the sub graph filtered by
> +    a revset. Primary use case is to draw a revisions graph.
> +
> +    In the following example, we consider that the node 'f' has edges to all
> +    ancestor nodes, but redundant paths are eliminated. The edge 'f'->'b'
> +    is eliminated because there is a path 'f'->'c'->'b' for example.
> +
> +          - d - e -
> +         /         \
> +        a - b - c - f
> +
> +    If the node 'c' is filtered out, the edge 'f'->'b' is activated.
> +
> +          - d - e -
> +         /         \
> +        a - b -(c)- f
> +
> +    Likewise, if 'd' and 'e' are filtered out, this edge is fully eliminated
> +    since there is a path 'f'->'c'->'b'->'a' for 'f'->'a'.
> +
> +           (d) (e)
> +
> +        a - b - c - f
> +
> +    Implementation-wise, 'f' is passed down to 'a' as unresolved through the
> +    'f'->'e'->'d'->'a' path, whereas we do also remember that 'f' has already
> +    been resolved while walking down the 'f'->'c'->'b'->'a' path. When
> +    processing the node 'a', the unresolved 'f'->'a' path is eliminated as
> +    the 'f' end is marked as resolved.
> +
> +    Ancestors are searched from the tipmost revision in the subset so the
> +    results can be cached. You should specify startrev to narrow the search
> +    space to ':startrev'.
> +    """
> +
> +    def __init__(self, repo, subset, startrev=None):
> +        if startrev is not None:
> +            subset = repo.revs(b'%d:null', startrev) & subset
> +
> +        # equivalent to 'subset = subset.sorted(reverse=True)', but there's
> +        # no such function.
> +        fastdesc = subset.fastdesc
> +        if fastdesc:
> +            desciter = fastdesc()
> +        else:
> +            if not subset.isdescending() and not subset.istopo():
> +                subset = smartset.baseset(subset)
> +                subset.sort(reverse=True)
> +            desciter = iter(subset)
> +
> +        self._repo = repo
> +        self._changelog = repo.changelog
> +        self._subset = subset
> +
> +        # scanning state (see _scanparents):
> +        self._tovisit = []
> +        self._pendingcnt = {}
> +        self._pointers = {}
> +        self._parents = {}
> +        self._inputhead = nullrev  # reassigned by self._advanceinput()
> +        self._inputtail = desciter
> +        self._bottomrev = nullrev
> +        self._advanceinput()
> +
> +    def parentsset(self, rev):
> +        """Look up parents of the given revision in the subset, and returns
> +        as a smartset"""
> +        return smartset.baseset(self.parents(rev))
> +
> +    def parents(self, rev):
> +        """Look up parents of the given revision in the subset
> +
> +        The returned revisions are sorted by parent index (p1/p2).
> +        """
> +        self._scanparents(rev)
> +        return [r for _c, r in sorted(self._parents.get(rev, []))]
> +
> +    def _parentrevs(self, rev):
> +        try:
> +            revs = self._changelog.parentrevs(rev)
> +            if revs[-1] == nullrev:
> +                return revs[:-1]
> +            return revs
> +        except error.WdirUnsupported:
> +            return tuple(pctx.rev() for pctx in self._repo[None].parents())
> +
> +    def _advanceinput(self):
> +        """Advance the input iterator and set the next revision to _inputhead"""
> +        if self._inputhead < nullrev:
> +            return
> +        try:
> +            self._inputhead = next(self._inputtail)
> +        except StopIteration:
> +            self._bottomrev = self._inputhead
> +            self._inputhead = nullrev - 1
> +
> +    def _scanparents(self, stoprev):
> +        """Scan ancestors until the parents of the specified stoprev are
> +        resolved"""
> +
> +        # 'tovisit' is the queue of the input revisions and their ancestors.
> +        # It will be populated incrementally to minimize the initial cost
> +        # of computing the given subset.
> +        #
> +        # For to-visit revisions, we keep track of
> +        # - the number of the unresolved paths: pendingcnt[rev],
> +        # - dict of the unresolved descendants and chains: pointers[rev][0],
> +        # - set of the already resolved descendants: pointers[rev][1].
> +        #
> +        # When a revision is visited, 'pointers[rev]' should be popped and
> +        # propagated to its parents accordingly.
> +        #
> +        # Once all pending paths have been resolved, 'pendingcnt[rev]' becomes
> +        # 0 and 'parents[rev]' contains the unsorted list of parent revisions
> +        # and p1/p2 chains (excluding linear paths.)
> +
> +        subset = self._subset
> +        tovisit = self._tovisit  # heap queue of [-rev]
> +        pendingcnt = self._pendingcnt  # {rev: count} for visited revisions
> +        pointers = self._pointers  # {rev: [{unresolved_rev: chain}, resolved]}
> +        parents = self._parents  # {rev: [(chain, rev)]}
> +
> +        while tovisit or self._inputhead >= nullrev:
> +            if pendingcnt.get(stoprev) == 0:
> +                return
> +
> +            # feed greater revisions from input set to queue
> +            if not tovisit:
> +                heapq.heappush(tovisit, -self._inputhead)
> +                self._advanceinput()
> +            while self._inputhead >= -tovisit[0]:
> +                heapq.heappush(tovisit, -self._inputhead)
> +                self._advanceinput()
> +
> +            rev = -heapq.heappop(tovisit)
> +            if rev < self._bottomrev:
> +                return
> +            if rev in pendingcnt and rev not in pointers:
> +                continue  # already visited
> +
> +            curactive = rev in subset
> +            pendingcnt.setdefault(rev, 0)  # mark as visited
> +            if curactive:
> +                assert rev not in parents
> +                parents[rev] = []
> +            unresolved, resolved = pointers.pop(rev, ({}, set()))
> +
> +            if curactive:
> +                # reached to active rev, resolve pending descendants' parents
> +                for r, c in unresolved.items():
> +                    pendingcnt[r] -= 1
> +                    assert pendingcnt[r] >= 0
> +                    if r in resolved:
> +                        continue  # eliminate redundant path
> +                    parents[r].append((c, rev))
> +                    # mark the descendant 'r' as resolved through this path if
> +                    # there are still pending pointers. the 'resolved' set may
> +                    # be concatenated later at a fork revision.
> +                    if pendingcnt[r] > 0:
> +                        resolved.add(r)
> +                unresolved.clear()
> +                # occasionally clean resolved markers. otherwise the set
> +                # would grow indefinitely.
> +                resolved = {r for r in resolved if pendingcnt[r] > 0}
> +
> +            parentrevs = self._parentrevs(rev)
> +            bothparentsactive = all(p in subset for p in parentrevs)
> +
> +            # set up or propagate tracking pointers if
> +            # - one of the parents is not active,
> +            # - or descendants' parents are unresolved.
> +            if not bothparentsactive or unresolved or resolved:
> +                if len(parentrevs) > 1:
> +                    # 'rev' is a merge revision. increment the pending count
> +                    # as the 'unresolved' dict will be duplicated.
> +                    for r in unresolved:
> +                        pendingcnt[r] += 1
> +                reusable = True  # can we avoid copying the tracking pointer?
> +                for i, p in enumerate(parentrevs):
> +                    assert p < rev
> +                    heapq.heappush(tovisit, -p)
> +                    if p in pointers:
> +                        # 'p' is a fork revision. concatenate tracking pointers
> +                        # and decrement the pending count accordingly.
> +                        knownunresolved, knownresolved = pointers[p]
> +                        for r, c in unresolved.items():
> +                            c += [b'1', b'2'][i]
> +                            if r in knownunresolved:
> +                                # unresolved at both paths
> +                                pendingcnt[r] -= 1
> +                                assert pendingcnt[r] > 0
> +                                # take shorter chain
> +                                knownunresolved[r] = min(c, knownunresolved[r])
> +                            else:
> +                                knownunresolved[r] = c
> +                        # simply propagate the 'resolved' set as deduplicating
> +                        # 'unresolved' here would be slightly complicated.
> +                        knownresolved.update(resolved)
> +                    elif reusable:
> +                        pointers[p] = (unresolved, resolved)
> +                        reusable = False
> +                    else:
> +                        pointers[p] = (unresolved.copy(), resolved.copy())
> +
> +            # then, populate the active parents directly and add the current
> +            # 'rev' to the tracking pointers of the inactive parents.
> +            # 'pointers[p]' may be optimized out if both parents are active.
> +            if curactive and bothparentsactive:
> +                for i, p in enumerate(parentrevs):
> +                    c = [b'1', b'2'][i]
> +                    parents[rev].append((c, p))
> +                    # no need to mark 'rev' as resolved since the 'rev' should
> +                    # be fully resolved (i.e. pendingcnt[rev] == 0)
> +                assert pendingcnt[rev] == 0
> +            elif curactive:
> +                for i, p in enumerate(parentrevs):
> +                    unresolved, resolved = pointers[p]
> +                    assert rev not in unresolved
> +                    c = [b'1', b'2'][i]
> +                    if p in subset:
> +                        parents[rev].append((c, p))
> +                        # mark 'rev' as resolved through this path
> +                        resolved.add(rev)
> +                    else:
> +                        pendingcnt[rev] += 1
> +                        unresolved[rev] = c
> +                assert 0 < pendingcnt[rev] <= 2
> +
> +
> def _reachablerootspure(pfunc, minroot, roots, heads, includepath):
>     """See revlog.reachableroots"""
>     if not roots:
> diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py
> --- a/mercurial/templatefuncs.py
> +++ b/mercurial/templatefuncs.py
> @@ -16,6 +16,7 @@ from .node import (
> )
> from . import (
>     color,
> +    dagop,
>     diffutil,
>     encoding,
>     error,
> @@ -842,6 +843,45 @@ def startswith(context, mapping, args):
>     return b''
>
>
> +@templatefunc(
> +    b'subsetparents(rev, revset)',
> +    argspec=b'rev revset',
> +    requires={b'repo', b'cache'},
> +)
> +def subsetparents(context, mapping, args):
> +    """Look up parents of the rev in the sub graph given by the revset."""
> +    if b'rev' not in args or b'revset' not in args:
> +        # i18n: "subsetparents" is a keyword
> +        raise error.ParseError(_(b"subsetparents expects two arguments"))
> +
> +    repo = context.resource(mapping, b'repo')
> +
> +    rev = templateutil.evalinteger(context, mapping, args[b'rev'])
> +
> +    # TODO: maybe subsetparents(rev) should be allowed. the default revset
> +    # will be the revisions specified by -rREV argument.
> +    q = templateutil.evalwrapped(context, mapping, args[b'revset'])
> +    if not isinstance(q, templateutil.revslist):
> +        # i18n: "subsetparents" is a keyword
> +        raise error.ParseError(_(b"subsetparents expects a queried revset"))
> +    subset = q.tovalue(context, mapping)
> +    key = q.cachekey
> +
> +    if key:
> +        # cache only if revset query isn't dynamic
> +        cache = context.resource(mapping, b'cache')
> +        walkercache = cache.setdefault(b'subsetparentswalker', {})
> +        if key in walkercache:
> +            walker = walkercache[key]
> +        else:
> +            walker = dagop.subsetparentswalker(repo, subset)
> +            walkercache[key] = walker
> +    else:
> +        # for one-shot use, specify startrev to limit the search space
> +        walker = dagop.subsetparentswalker(repo, subset, startrev=rev)
> +    return templateutil.revslist(repo, walker.parentsset(rev))
> +
> +
> @templatefunc(b'word(number, text[, separator])')
> def word(context, mapping, args):
>     """Return the nth word from a string."""
> diff --git a/tests/test-template-graph.t b/tests/test-template-graph.t
> new file mode 100644
> --- /dev/null
> +++ b/tests/test-template-graph.t
> @@ -0,0 +1,337 @@
> +Test graph-related template functions
> +=====================================
> +
> +  $ cat <<'EOF' >> $HGRCPATH
> +  > [extensions]
> +  > drawdag = $RUNTESTDIR/drawdag.py
> +  > EOF
> +
> +  $ hg init a
> +  $ cd a
> +
> +  $ hg debugdrawdag <<'EOF'
> +  >   l
> +  >  / \
> +  > |   k
> +  > |   |\
> +  > |   | j
> +  > |   | |
> +  > i   | |
> +  > |\  | |
> +  > h | | |
> +  > | | | |
> +  > | g | |
> +  > | | | |
> +  > f | | |
> +  > | |/ /
> +  > | e |
> +  > | |/
> +  > | d
> +  > |/|
> +  > c |
> +  > | |
> +  > b |
> +  >   |
> +  >   a
> +  > EOF
> +
> +  $ hg log -Gq -T'{rev} {tags}\n'
> +  o    11 l tip
> +  |\
> +  | o    10 i
> +  | |\
> +  o \ \    9 k
> +  |\ \ \
> +  +-----o  8 g
> +  | | |
> +  | o |  7 j
> +  | | |
> +  | | o  6 h
> +  | | |
> +  o | |  5 e
> +  |/ /
> +  | o  4 f
> +  | |
> +  o |  3 d
> +  |\|
> +  | o  2 c
> +  | |
> +  | o  1 b
> +  |
> +  o  0 a
> +  
> +
> +  $ cd ..
> +
> +subsetparents
> +-------------
> +
> +  $ cd a
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+i"))}\n' -r 'c+i'
> +  o  10 i: 2
> +  :
> +  o  2 c:
> +  |
> +  ~
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+h+i"))}\n' -r 'c+h+i'
> +  o    10 i: 6
> +  |\
> +  o :  6 h: 2
> +  :/
> +  o  2 c:
> +  |
> +  ~
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+h+l"))}\n' -r 'c+h+l'
> +  o    11 l tip: 6
> +  :\
> +  : o  6 h: 2
> +  :/
> +  o  2 c:
> +  |
> +  ~
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+f+l"))}\n' -r 'c+f+l'
> +  o    11 l tip: 4
> +  :\
> +  : o  4 f: 2
> +  :/
> +  o  2 c:
> +  |
> +  ~
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+h+i+k"))}\n' -r 'c+h+i+k'
> +  o    10 i: 6
> +  |\
> +  | : o  9 k: 2
> +  | :/
> +  o :  6 h: 2
> +  :/
> +  o  2 c:
> +  |
> +  ~
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+d+h+i+k"))}\n' -r 'c+d+h+i+k'
> +  o    10 i: 6 3
> +  |\
> +  | : o  9 k: 3
> +  | :/
> +  o :  6 h: 2
> +  : :
> +  : o  3 d: 2
> +  :/|
> +  : ~
> +  o  2 c:
> +  |
> +  ~
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+j+k+i"))}\n' -r 'c+j+k+i'
> +  o  10 i: 2
> +  :
> +  : o  9 k: 7
> +  :/|
> +  : o  7 j: 2
> +  :/
> +  o  2 c:
> +  |
> +  ~
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("c+e+f+j"))}\n' -r 'c+e+f+j'
> +  o  7 j: 2
> +  :
> +  : o  5 e: 2
> +  :/
> +  : o  4 f: 2
> +  :/
> +  o  2 c:
> +  |
> +  ~
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("b+e+f+j"))}\n' -r 'b+e+f+j'
> +  o  7 j: 1
> +  :
> +  : o  5 e: 1
> +  :/
> +  : o  4 f: 1
> +  :/
> +  o  1 b:
> +  
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("a+c+f+g+j+l"))}\n' -r 'a+c+f+g+j+l'
> +  o    11 l tip: 4 8 7
> +  :\
> +  : \
> +  : :\
> +  : : \
> +  : : :\
> +  : : : \
> +  : : : :\
> +  : o---+ :  8 g: 0 2
> +  : :/ / /
> +  : +---o  7 j: 0 2
> +  : : :/
> +  o---+  4 f: 2
> +   / /
> +  : o  2 c:
> +  : |
> +  : ~
> +  o  0 a:
> +  
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("b+i+l"))}\n' -r 'b+i+l'
> +  o    11 l tip: 10
> +  |\
> +  o :  10 i: 1
> +  :/
> +  o  1 b:
> +  
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("b+i+j+l"))}\n' -r 'b+i+j+l'
> +  o    11 l tip: 10 7
> +  |\
> +  | \
> +  | :\
> +  o : :  10 i: 1
> +  :/ /
> +  : o  7 j: 1
> +  :/
> +  o  1 b:
> +  
> +
> +null in subset:
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("null+a+c+f"))}\n' -r 'null+a+c+f'
> +  o  4 f: 2
> +  |
> +  o  2 c: -1
> +  :
> +  : o  0 a: -1
> +  :/
> +  @  -1 : -1
> +  
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("null+a+b+c+f"))}\n' -r 'null+a+b+c+f'
> +  o  4 f: 2
> +  |
> +  o  2 c: 1
> +  |
> +  o  1 b: -1
> +  |
> +  | o  0 a: -1
> +  |/
> +  @  -1 : -1
> +  
> +
> +wdir in subset:
> +
> +  $ hg update -qC i
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("f+k+wdir()"))}\n' -r 'f+k+wdir()'
> +  o  2147483647 : 4
> +  :
> +  : o    9 k:
> +  : |\
> +  : ~ ~
> +  o  4 f:
> +  |
> +  ~
> +
> +  $ hg update -qC null
> +
> +Revisions not in subset:
> +
> +  $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("a+c+f+g+j+l"))}\n'
> +  11 l tip: 4 8 7
> +  10 i:
> +  9 k:
> +  8 g: 0 2
> +  7 j: 0 2
> +  6 h:
> +  5 e:
> +  4 f: 2
> +  3 d:
> +  2 c:
> +  1 b:
> +  0 a:
> +
> +  $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("b+c"))}\n'
> +  11 l tip:
> +  10 i:
> +  9 k:
> +  8 g:
> +  7 j:
> +  6 h:
> +  5 e:
> +  4 f:
> +  3 d:
> +  2 c: 1
> +  1 b:
> +  0 a:
> +
> +  $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("b+c"))}\n' -r'reverse(null:2)'
> +  2 c: 1
> +  1 b:
> +  0 a:
> +  -1 :
> +
> +Nothing excluded:
> +
> +  $ hg log -T '{rev} {tags}: {subsetparents(rev, revset("null:wdir()"))}\n' -r'reverse(null:wdir())'
> +  2147483647 : -1
> +  11 l tip: 10 9
> +  10 i: 6 8
> +  9 k: 5 7
> +  8 g: 5
> +  7 j: 3
> +  6 h: 4
> +  5 e: 3
> +  4 f: 2
> +  3 d: 0 2
> +  2 c: 1
> +  1 b: -1
> +  0 a: -1
> +  -1 : -1
> +
> +Uncachable query:
> +
> +  $ hg log -Gq -T '{rev} {tags}: {subsetparents(rev, revset("%d:%d", rev, rev - 1))}\n'
> +  o    11 l tip: 10
> +  |\
> +  | o    10 i:
> +  | |\
> +  o \ \    9 k:
> +  |\ \ \
> +  +-----o  8 g:
> +  | | |
> +  | o |  7 j:
> +  | | |
> +  | | o  6 h:
> +  | | |
> +  o | |  5 e:
> +  |/ /
> +  | o  4 f:
> +  | |
> +  o |  3 d: 2
> +  |\|
> +  | o  2 c: 1
> +  | |
> +  | o  1 b:
> +  |
> +  o  0 a: -1
> +  
> +
> +Invalid arguments:
> +
> +  $ hg log -T '{subsetparents()}\n'
> +  hg: parse error: subsetparents expects two arguments
> +  [255]
> +  $ hg log -T '{subsetparents("a")}\n'
> +  hg: parse error: subsetparents expects two arguments
> +  [255]
> +  $ hg log -T '{subsetparents(rev, extras)}\n'
> +  hg: parse error: subsetparents expects a queried revset
> +  [255]
> +
> +  $ cd ..
> _______________________________________________
> Mercurial-devel mailing list
> [hidden email]
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel

_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

Re: [PATCH 4 of 4] templater: add subsetparents(rev, revset) function

Yuya Nishihara
On Tue, 24 Mar 2020 18:16:45 -0400, Augie Fackler wrote:

> > +            # set up or propagate tracking pointers if
> > +            # - one of the parents is not active,
> > +            # - or descendants' parents are unresolved.
> > +            if not bothparentsactive or unresolved or resolved:
> > +                if len(parentrevs) > 1:
> > +                    # 'rev' is a merge revision. increment the pending count
> > +                    # as the 'unresolved' dict will be duplicated.
> > +                    for r in unresolved:
> > +                        pendingcnt[r] += 1
> > +                reusable = True  # can we avoid copying the tracking pointer?
> > +                for i, p in enumerate(parentrevs):
> > +                    assert p < rev
> > +                    heapq.heappush(tovisit, -p)
> > +                    if p in pointers:
> > +                        # 'p' is a fork revision. concatenate tracking pointers
> > +                        # and decrement the pending count accordingly.
> > +                        knownunresolved, knownresolved = pointers[p]
> > +                        for r, c in unresolved.items():
> > +                            c += [b'1', b'2'][i]

I noticed tracking p1/p2 chain here isn't correct since the current rev
is not a merge point but a fork point. I'll send a follow up patch later.

Anyway, thanks for the review!
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel