D3715: namespaces: allow namespaces whose symbols resolve to many nodes (API)

martinvonz (Martin von Zweigbergk) phabricator at mercurial-scm.org
Mon Jun 11 23:11:04 UTC 2018


martinvonz created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  The goal of this commit is to allow namespaces that support multiple
  nodes per name to also have the name resolve to those multiple nodes
  in the revset. For example, the "topics" namespace seems to be a
  natural candidate for this (I don't know if the extension's maintainer
  agrees). I view a topic as having multiple nodes and I would therefore
  expect `hg log -r my-topic` to list all those nodes. Note that the
  topics extension already supports multiple nodes per name, but we
  don't expose that to the user in a consistent way (each namespace has
  to define its own revset).
  
  This commit adds an option to namespaces to indicate that `hg log -r
  <name in the namespace>` and similar should resolve to the nodes that
  the namespace says and not just the highest revnum among them.
  
  Marked (API) because I repurposed singlenode() to nodes().
  
  Note: I think branches should also resolve to multiple nodes (so
  e.g. `hg log -r stable` lists all nodes on stable), but it's obviously
  too late to change that now (and perhaps BC is the reason it even
  behaves the way it does). I also realize that any namespace that were
  to use the new mechanism would be inconsistent with how branches
  work. I think the convenience and intuitiveness outweighs the cost of
  that inconsistency.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D3715

AFFECTED FILES
  mercurial/namespaces.py
  mercurial/revset.py
  mercurial/scmutil.py
  tests/paritynamesext.py
  tests/test-revset2.t

CHANGE DETAILS

diff --git a/tests/test-revset2.t b/tests/test-revset2.t
--- a/tests/test-revset2.t
+++ b/tests/test-revset2.t
@@ -675,6 +675,12 @@
   6
   $ log 'named("tags")'
   6
+  $ hg --config extensions.revnamesext=$TESTDIR/paritynamesext.py log --template '{rev}\n' -r even
+  0
+  2
+  4
+  6
+  8
 
 issue2437
 
diff --git a/tests/paritynamesext.py b/tests/paritynamesext.py
new file mode 100644
--- /dev/null
+++ b/tests/paritynamesext.py
@@ -0,0 +1,26 @@
+# Defines a namespace with names 'even' and 'odd'
+
+from __future__ import absolute_import
+
+from mercurial import (
+    namespaces,
+)
+
+def reposetup(ui, repo):
+    def namemap(repo, name):
+        if name == 'even':
+            parity = 0
+        elif name == 'odd':
+            parity = 1
+        else:
+            return []
+        return [repo[rev].node() for rev in repo if rev % 2 == parity]
+    def nodemap(repo, node):
+        return [repo[node].rev() % 2]
+
+    ns = namespaces.namespace(b'parity', templatename=b'parity',
+                              logname=b'parity',
+                              listnames=lambda r: ['even', 'odd'],
+                              namemap=namemap, nodemap=nodemap,
+                              multinode=True)
+    repo.names.addnamespace(ns)
diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py
--- a/mercurial/scmutil.py
+++ b/mercurial/scmutil.py
@@ -488,7 +488,7 @@
     except error.RepoLookupError:
         return False
 
-def _revsymbol(repo, symbol):
+def _revsymbol(repo, symbol, allowmultiple):
     if not isinstance(symbol, bytes):
         msg = ("symbol (%s of type %s) was not a string, did you mean "
                "repo[symbol]?" % (symbol, type(symbol)))
@@ -524,9 +524,9 @@
 
         # look up bookmarks through the name interface
         try:
-            node = repo.names.singlenode(repo, symbol)
-            rev = repo.changelog.rev(node)
-            return [repo[rev]]
+            nodes = repo.names.nodes(repo, symbol, allowmultiple)
+            revs = [repo.changelog.rev(n) for n in nodes]
+            return [repo[r2] for r2 in revs] # "r2" to keep pyflakes happy
         except KeyError:
             pass
 
@@ -550,10 +550,19 @@
     i.e. things like ".", "tip", "1234", "deadbeef", "my-bookmark" work, but
     not "max(public())".
     """
-    ctxs = _revsymbol(repo, symbol)
+    ctxs = _revsymbol(repo, symbol, allowmultiple=False)
     assert len(ctxs) == 1
     return ctxs[0]
 
+def revsymbolmultiple(repo, symbol):
+    """Returns all contexts given a single revision symbol (as string).
+
+    This is similar to revsymbol(), but if "symbol" is a namespace symbol,
+    then this may return many context (if the namespace maps the symbol to
+    many revisions).
+    """
+    return _revsymbol(repo, symbol, allowmultiple=True)
+
 def _filterederror(repo, changeid):
     """build an exception to be raised about a filtered changeid
 
diff --git a/mercurial/revset.py b/mercurial/revset.py
--- a/mercurial/revset.py
+++ b/mercurial/revset.py
@@ -118,11 +118,14 @@
 def stringset(repo, subset, x, order):
     if not x:
         raise error.ParseError(_("empty string is not a valid revision"))
-    x = scmutil.intrev(scmutil.revsymbol(repo, x))
-    if (x in subset
-        or x == node.nullrev and isinstance(subset, fullreposet)):
-        return baseset([x])
-    return baseset()
+    ctxs = scmutil.revsymbolmultiple(repo, x)
+    if len(ctxs) == 1:
+        x = scmutil.intrev(ctxs[0])
+        if (x in subset
+            or x == node.nullrev and isinstance(subset, fullreposet)):
+            return baseset([x])
+        return baseset()
+    return subset & baseset(sorted(ctx.rev() for ctx in ctxs))
 
 def rangeset(repo, subset, x, y, order):
     m = getset(repo, fullreposet(repo), x)
diff --git a/mercurial/namespaces.py b/mercurial/namespaces.py
--- a/mercurial/namespaces.py
+++ b/mercurial/namespaces.py
@@ -93,7 +93,7 @@
             def generatekw(context, mapping):
                 return templatekw.shownames(context, mapping, namespace.name)
 
-    def singlenode(self, repo, name):
+    def nodes(self, repo, name, allowmultiple=False):
         """
         Return the 'best' node for the given name. Best means the first node
         in the first nonempty list returned by a name-to-nodes mapping function
@@ -104,12 +104,12 @@
         for ns, v in self._names.iteritems():
             n = v.namemap(repo, name)
             if n:
+                if (allowmultiple and v.multinode) or len(n) == 1:
+                    return n
                 # return max revision number
-                if len(n) > 1:
-                    cl = repo.changelog
-                    maxrev = max(cl.rev(node) for node in n)
-                    return cl.node(maxrev)
-                return n[0]
+                cl = repo.changelog
+                maxrev = max(cl.rev(node) for node in n)
+                return [cl.node(maxrev)]
         raise KeyError(_('no such name: %s') % name)
 
 class namespace(object):
@@ -142,7 +142,7 @@
 
     def __init__(self, name, templatename=None, logname=None, colorname=None,
                  logfmt=None, listnames=None, namemap=None, nodemap=None,
-                 deprecated=None, builtin=False):
+                 deprecated=None, builtin=False, multinode=False):
         """create a namespace
 
         name: the namespace to be registered (in plural form)
@@ -158,6 +158,9 @@
         nodemap: function that inputs a node, output name(s)
         deprecated: set of names to be masked for ordinary use
         builtin: whether namespace is implemented by core Mercurial
+        multinode: whether namespace can have multiple nodes associated with
+                   one name (`hg log -r that-name` will then list multiple
+                   nodes)
         """
         self.name = name
         self.templatename = templatename
@@ -187,6 +190,7 @@
             self.deprecated = deprecated
 
         self.builtin = builtin
+        self.multinode = multinode
 
     def names(self, repo, node):
         """method that returns a (sorted) list of names in a namespace that



To: martinvonz, #hg-reviewers
Cc: mercurial-devel


More information about the Mercurial-devel mailing list