diff mercurial/wireprotov2server.py @ 39655:0e03e6a44dee

wireprotov2: define and implement "filedata" command Continuing our trend of implementing *data commands for retrieving information about specific repository data primitives, this commit implements a command for retrieving data about an individual tracked file. The command is very similar to "manifestdata." The only significant difference is that we have a standalone function for obtaining storage for a tracked file. This is to provide a monkeypatch point for extensions to implement path-based access control. With this API available, wire protocol version 2 now exposes all data primitives necessary to implement a full clone. Of course, since "filedata" can only resolve data for a single path at a time, clients would need to issue N commands to perform a full clone. On the Firefox repository, this would be ~461k commands. We'll likely need to implement a file data retrieval command that supports multiple paths. But that can be implemented later. Differential Revision: https://phab.mercurial-scm.org/D4490
author Gregory Szorc <gregory.szorc@gmail.com>
date Wed, 05 Sep 2018 09:10:17 -0700
parents c7a7c7e844e5
children aa7e312375cf
line wrap: on
line diff
--- a/mercurial/wireprotov2server.py	Wed Sep 05 09:09:57 2018 -0700
+++ b/mercurial/wireprotov2server.py	Wed Sep 05 09:10:17 2018 -0700
@@ -10,6 +10,7 @@
 
 from .i18n import _
 from .node import (
+    hex,
     nullid,
     nullrev,
 )
@@ -648,6 +649,112 @@
                 b'bookmarks': sorted(marks),
             }
 
+class FileAccessError(Exception):
+    """Represents an error accessing a specific file."""
+
+    def __init__(self, path, msg, args):
+        self.path = path
+        self.msg = msg
+        self.args = args
+
+def getfilestore(repo, proto, path):
+    """Obtain a file storage object for use with wire protocol.
+
+    Exists as a standalone function so extensions can monkeypatch to add
+    access control.
+    """
+    # This seems to work even if the file doesn't exist. So catch
+    # "empty" files and return an error.
+    fl = repo.file(path)
+
+    if not len(fl):
+        raise FileAccessError(path, 'unknown file: %s', (path,))
+
+    return fl
+
+@wireprotocommand('filedata',
+                  args={
+                      'nodes': [b'0123456...'],
+                      'fields': [b'parents', b'revision'],
+                      'path': b'foo.txt',
+                  },
+                  permission='pull')
+def filedata(repo, proto, nodes=None, fields=None, path=None):
+    fields = fields or set()
+
+    if nodes is None:
+        raise error.WireprotoCommandError('nodes argument must be defined')
+
+    if path is None:
+        raise error.WireprotoCommandError('path argument must be defined')
+
+    try:
+        # Extensions may wish to access the protocol handler.
+        store = getfilestore(repo, proto, path)
+    except FileAccessError as e:
+        raise error.WireprotoCommandError(e.msg, e.args)
+
+    # Validate requested nodes.
+    for node in nodes:
+        try:
+            store.rev(node)
+        except error.LookupError:
+            raise error.WireprotoCommandError('unknown file node: %s',
+                                              (hex(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('heads',
                   args={
                       'publiconly': False,