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 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/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/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/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