Mercurial > public > mercurial-scm > hg
diff mercurial/wireprotov2server.py @ 40176:41263df08109
wireprotov2: change how revisions are specified to changesetdata
Right now, we have a handful of arguments for specifying the revisions
whose data should be returned. Defining how all these arguments
interact when various combinations are present is difficult.
This commit establishes a new, generic mechanism for specifying
revisions. Instead of a hodgepodge of arguments defining things, we
have a list of dicts that specify revision selectors. The final set
of revisions is a union of all these selectors.
We implement support for specifying revisions based on:
* An explicit list of changeset revisions
* An explicit list of changeset revisions plus ancestry depth
* A DAG range between changeset roots and heads
If you squint hard enough, this problem has already been solved by
revsets. But I'm reluctant to expose revsets to the wire protocol
because that would require servers to implement a revset parser.
Plus there are security and performance implications: the set
of revision selectors needs to be narrowly and specifically tailored
for what is appropriate to be executing on a server. Perhaps there
would be a way for us to express the "parse tree" of a revset
query, for example. I'm not sure. We can explore this space another
time. For now, the new mechanism should bring sufficient flexibility
while remaining relatively simple.
The selector "types" are prefixed with "changeset" because I plan
to add manifest and file-flavored selectors as well. This will enable
us to e.g. select file revisions based on a range of changeset
revisions.
Differential Revision: https://phab.mercurial-scm.org/D4979
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Mon, 08 Oct 2018 18:17:12 -0700 |
parents | 6c42409691ec |
children | 41e2633bcd00 |
line wrap: on
line diff
--- a/mercurial/wireprotov2server.py Mon Oct 08 17:54:14 2018 -0700 +++ b/mercurial/wireprotov2server.py Mon Oct 08 18:17:12 2018 -0700 @@ -777,6 +777,78 @@ """ return None +def resolvenodes(repo, revisions): + """Resolve nodes from a revisions specifier data structure.""" + cl = repo.changelog + clhasnode = cl.hasnode + + seen = set() + nodes = [] + + if not isinstance(revisions, list): + raise error.WireprotoCommandError('revisions must be defined as an ' + 'array') + + for spec in revisions: + if b'type' not in spec: + raise error.WireprotoCommandError( + 'type key not present in revision specifier') + + typ = spec[b'type'] + + if typ == b'changesetexplicit': + if b'nodes' not in spec: + raise error.WireprotoCommandError( + 'nodes key not present in changesetexplicit revision ' + 'specifier') + + for node in spec[b'nodes']: + if node not in seen: + nodes.append(node) + seen.add(node) + + elif typ == b'changesetexplicitdepth': + for key in (b'nodes', b'depth'): + if key not in spec: + raise error.WireprotoCommandError( + '%s key not present in changesetexplicitdepth revision ' + 'specifier', (key,)) + + for rev in repo.revs(b'ancestors(%ln, %d)', spec[b'nodes'], + spec[b'depth'] - 1): + node = cl.node(rev) + + if node not in seen: + nodes.append(node) + seen.add(node) + + elif typ == b'changesetdagrange': + for key in (b'roots', b'heads'): + if key not in spec: + raise error.WireprotoCommandError( + '%s key not present in changesetdagrange revision ' + 'specifier', (key,)) + + if not spec[b'heads']: + raise error.WireprotoCommandError( + 'heads key in changesetdagrange cannot be empty') + + if spec[b'roots']: + common = [n for n in spec[b'roots'] if clhasnode(n)] + else: + common = [nullid] + + for n in discovery.outgoing(repo, common, spec[b'heads']).missing: + if n not in seen: + nodes.append(n) + seen.add(n) + + else: + raise error.WireprotoCommandError( + 'unknown revision specifier type: %s', (typ,)) + + return nodes + @wireprotocommand('branchmap', permission='pull') def branchmapv2(repo, proto): yield {encoding.fromlocal(k): v @@ -789,20 +861,12 @@ @wireprotocommand( 'changesetdata', args={ - 'noderange': { - 'type': 'list', - 'default': lambda: None, - 'example': [[b'0123456...'], [b'abcdef...']], - }, - 'nodes': { + 'revisions': { 'type': 'list', - 'default': lambda: None, - 'example': [b'0123456...'], - }, - 'nodesdepth': { - 'type': 'int', - 'default': lambda: None, - 'example': 10, + 'example': [{ + b'type': b'changesetexplicit', + b'nodes': [b'abcdef...'], + }], }, 'fields': { 'type': 'set', @@ -812,57 +876,12 @@ }, }, permission='pull') -def changesetdata(repo, proto, noderange, nodes, nodesdepth, fields): +def changesetdata(repo, proto, revisions, fields): # TODO look for unknown fields and abort when they can't be serviced. # This could probably be validated by dispatcher using validvalues. - if noderange is None and nodes is None: - raise error.WireprotoCommandError( - 'noderange or nodes must be defined') - - if nodesdepth is not None and nodes is None: - raise error.WireprotoCommandError( - 'nodesdepth requires the nodes argument') - - if noderange is not None: - if len(noderange) != 2: - raise error.WireprotoCommandError( - 'noderange must consist of 2 elements') - - if not noderange[1]: - raise error.WireprotoCommandError( - 'heads in noderange request cannot be empty') - cl = repo.changelog - hasnode = cl.hasnode - - seen = set() - outgoing = [] - - if nodes is not None: - outgoing = [n for n in nodes if hasnode(n)] - - if nodesdepth: - outgoing = [cl.node(r) for r in - repo.revs(b'ancestors(%ln, %d)', outgoing, - nodesdepth - 1)] - - seen |= set(outgoing) - - if noderange is not None: - if noderange[0]: - common = [n for n in noderange[0] if hasnode(n)] - else: - common = [nullid] - - for n in discovery.outgoing(repo, common, noderange[1]).missing: - if n not in seen: - outgoing.append(n) - # Don't need to add to seen here because this is the final - # source of nodes and there should be no duplicates in this - # list. - - seen.clear() + outgoing = resolvenodes(repo, revisions) publishing = repo.publishing() if outgoing: