diff mercurial/wireproto.py @ 37498:aacfca6f9767

wireproto: support for pullbundles Pullbundles are similar to clonebundles, but served as normal inline bundle streams. They are almost transparent to the client -- the only visible effect is that the client might get less changes than what it asked for, i.e. not all requested head revisions are provided. The client announces support for the necessary retries with the partial-pull capability. After receiving a partial bundle, it updates the set of revisions shared with the server and drops all now-known heads from the request list. It will then rerun getbundle until no changes are received or all remote heads are present. Extend badserverext to support per-socket limit, i.e. don't assume that the same limits should be applied to all sockets. Differential Revision: https://phab.mercurial-scm.org/D1856
author Joerg Sonnenberger <joerg@bec.de>
date Thu, 18 Jan 2018 12:54:01 +0100
parents 3a91911c4343
children df4985497986
line wrap: on
line diff
--- a/mercurial/wireproto.py	Fri Apr 06 22:39:58 2018 -0700
+++ b/mercurial/wireproto.py	Thu Jan 18 12:54:01 2018 +0100
@@ -938,6 +938,70 @@
     return wireprototypes.bytesresponse(repo.debugwireargs(
         one, two, **pycompat.strkwargs(opts)))
 
+def find_pullbundle(repo, proto, opts, clheads, heads, common):
+    """Return a file object for the first matching pullbundle.
+
+    Pullbundles are specified in .hg/pullbundles.manifest similar to
+    clonebundles.
+    For each entry, the bundle specification is checked for compatibility:
+    - Client features vs the BUNDLESPEC.
+    - Revisions shared with the clients vs base revisions of the bundle.
+      A bundle can be applied only if all its base revisions are known by
+      the client.
+    - At least one leaf of the bundle's DAG is missing on the client.
+    - Every leaf of the bundle's DAG is part of node set the client wants.
+      E.g. do not send a bundle of all changes if the client wants only
+      one specific branch of many.
+    """
+    def decodehexstring(s):
+        return set([h.decode('hex') for h in s.split(';')])
+
+    manifest = repo.vfs.tryread('pullbundles.manifest')
+    if not manifest:
+        return None
+    res = exchange.parseclonebundlesmanifest(repo, manifest)
+    res = exchange.filterclonebundleentries(repo, res)
+    if not res:
+        return None
+    cl = repo.changelog
+    heads_anc = cl.ancestors([cl.rev(rev) for rev in heads], inclusive=True)
+    common_anc = cl.ancestors([cl.rev(rev) for rev in common], inclusive=True)
+    compformats = clientcompressionsupport(proto)
+    for entry in res:
+        if 'COMPRESSION' in entry and entry['COMPRESSION'] not in compformats:
+            continue
+        # No test yet for VERSION, since V2 is supported by any client
+        # that advertises partial pulls
+        if 'heads' in entry:
+            try:
+                bundle_heads = decodehexstring(entry['heads'])
+            except TypeError:
+                # Bad heads entry
+                continue
+            if bundle_heads.issubset(common):
+                continue # Nothing new
+            if all(cl.rev(rev) in common_anc for rev in bundle_heads):
+                continue # Still nothing new
+            if any(cl.rev(rev) not in heads_anc and
+                   cl.rev(rev) not in common_anc for rev in bundle_heads):
+                continue
+        if 'bases' in entry:
+            try:
+                bundle_bases = decodehexstring(entry['bases'])
+            except TypeError:
+                # Bad bases entry
+                continue
+            if not all(cl.rev(rev) in common_anc for rev in bundle_bases):
+                continue
+        path = entry['URL']
+        repo.ui.debug('sending pullbundle "%s"\n' % path)
+        try:
+            return repo.vfs.open(path)
+        except IOError:
+            repo.ui.debug('pullbundle "%s" not accessible\n' % path)
+            continue
+    return None
+
 @wireprotocommand('getbundle', '*', permission='pull')
 def getbundle(repo, proto, others):
     opts = options('getbundle', gboptsmap.keys(), others)
@@ -970,13 +1034,21 @@
     prefercompressed = True
 
     try:
+        clheads = set(repo.changelog.heads())
+        heads = set(opts.get('heads', set()))
+        common = set(opts.get('common', set()))
+        common.discard(nullid)
+        if (repo.ui.configbool('server', 'pullbundle') and
+            'partial-pull' in proto.getprotocaps()):
+            # Check if a pre-built bundle covers this request.
+            bundle = find_pullbundle(repo, proto, opts, clheads, heads, common)
+            if bundle:
+                return wireprototypes.streamres(gen=util.filechunkiter(bundle),
+                                                prefer_uncompressed=True)
+
         if repo.ui.configbool('server', 'disablefullbundle'):
             # Check to see if this is a full clone.
-            clheads = set(repo.changelog.heads())
             changegroup = opts.get('cg', True)
-            heads = set(opts.get('heads', set()))
-            common = set(opts.get('common', set()))
-            common.discard(nullid)
             if changegroup and not common and clheads == heads:
                 raise error.Abort(
                     _('server has pull-based clones disabled'),