mercurial/wireprotov2server.py
changeset 39639 0e03e6a44dee
parent 39637 c7a7c7e844e5
child 39641 aa7e312375cf
--- 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,