mercurial/wireprotov2server.py
changeset 39637 c7a7c7e844e5
parent 39636 399ddd3227a4
child 39639 0e03e6a44dee
--- a/mercurial/wireprotov2server.py	Wed Aug 22 14:51:11 2018 -0700
+++ b/mercurial/wireprotov2server.py	Wed Sep 05 09:09:52 2018 -0700
@@ -11,8 +11,11 @@
 from .i18n import _
 from .node import (
     nullid,
+    nullrev,
 )
 from . import (
+    changegroup,
+    dagop,
     discovery,
     encoding,
     error,
@@ -411,6 +414,67 @@
 
     return proto.addcapabilities(repo, caps)
 
+def builddeltarequests(store, nodes):
+    """Build a series of revision delta requests against a backend store.
+
+    Returns a list of revision numbers in the order they should be sent
+    and a list of ``irevisiondeltarequest`` instances to be made against
+    the backend store.
+    """
+    # We sort and send nodes in DAG order because this is optimal for
+    # storage emission.
+    # TODO we may want a better storage API here - one where we can throw
+    # a list of nodes and delta preconditions over a figurative wall and
+    # have the storage backend figure it out for us.
+    revs = dagop.linearize({store.rev(n) for n in nodes}, store.parentrevs)
+
+    requests = []
+
+    for rev in revs:
+        node = store.node(rev)
+        parents = store.parents(node)
+        deltaparent = store.node(store.deltaparent(rev))
+
+        # There is a delta in storage. That means we can send the delta
+        # efficiently.
+        #
+        # But, the delta may be against a revision the receiver doesn't
+        # have (e.g. shallow clone or when the delta isn't against a parent
+        # revision). For now, we ignore the problem of shallow clone. As
+        # long as a delta exists against a parent, we send it.
+        # TODO allow arguments to control this behavior, as the receiver
+        # may not have the base revision in some scenarios.
+        if deltaparent != nullid and deltaparent in parents:
+            basenode = deltaparent
+
+        # Else there is no delta parent in storage or the delta that is
+        # # there isn't suitable. Let's use a delta against a parent
+        # revision, if possible.
+        #
+        # There is room to check if the delta parent is in the ancestry of
+        # this node. But there isn't an API on the manifest storage object
+        # for that. So ignore this case for now.
+
+        elif parents[0] != nullid:
+            basenode = parents[0]
+        elif parents[1] != nullid:
+            basenode = parents[1]
+
+        # No potential bases to delta against. Send a full revision.
+        else:
+            basenode = nullid
+
+        requests.append(changegroup.revisiondeltarequest(
+            node=node,
+            p1node=parents[0],
+            p2node=parents[1],
+            # Receiver deals with linknode resolution.
+            linknode=nullid,
+            basenode=basenode,
+        ))
+
+    return revs, requests
+
 def wireprotocommand(name, args=None, permission='push'):
     """Decorator to declare a wire protocol command.
 
@@ -630,6 +694,87 @@
 
     yield node
 
+@wireprotocommand('manifestdata',
+                  args={
+                      'nodes': [b'0123456...'],
+                      'fields': [b'parents', b'revision'],
+                      'tree': b'',
+                  },
+                  permission='pull')
+def manifestdata(repo, proto, nodes=None, fields=None, tree=None):
+    fields = fields or set()
+
+    if nodes is None:
+        raise error.WireprotoCommandError(
+            'nodes argument must be defined')
+
+    if tree is None:
+        raise error.WireprotoCommandError(
+            'tree argument must be defined')
+
+    store = repo.manifestlog.getstorage(tree)
+
+    # Validate the node is known and abort on unknown revisions.
+    for node in nodes:
+        try:
+            store.rev(node)
+        except error.LookupError:
+            raise error.WireprotoCommandError(
+                'unknown node: %s', (node,))
+
+    revs, requests = builddeltarequests(store, nodes)
+
+    yield {
+        b'totalitems': len(revs),
+    }
+
+    if b'revision' in fields:
+        deltas = store.emitrevisiondeltas(requests)
+    else:
+        deltas = None
+
+    for rev in revs:
+        node = store.node(rev)
+
+        if deltas is not None:
+            delta = next(deltas)
+        else:
+            delta = None
+
+        d = {
+            b'node': node,
+        }
+
+        if b'parents' in fields:
+            d[b'parents'] = store.parents(node)
+
+        if b'revision' in fields:
+            assert delta is not None
+            assert delta.flags == 0
+            assert d[b'node'] == delta.node
+
+            if delta.revision is not None:
+                revisiondata = delta.revision
+                d[b'revisionsize'] = len(revisiondata)
+            else:
+                d[b'deltabasenode'] = delta.basenode
+                revisiondata = delta.delta
+                d[b'deltasize'] = len(revisiondata)
+        else:
+            revisiondata = None
+
+        yield d
+
+        if revisiondata is not None:
+            yield revisiondata
+
+    if deltas is not None:
+        try:
+            next(deltas)
+            raise error.ProgrammingError('should not have more deltas')
+        except GeneratorExit:
+            pass
+
 @wireprotocommand('pushkey',
                   args={
                       'namespace': b'ns',