diff hgext3rd/pullbundle.py @ 4129:bc4e62a1cb82

pullbundle: slice pull into multiple ranges We subdivide the set of missing revisions into multiple "range" using the "stable range" data structure. This slicing aims at maximizing the capability of the resulting ranges.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Sun, 23 Sep 2018 23:41:08 +0200
parents 4e5ec9ae682e
children a1f6b8211016
line wrap: on
line diff
--- a/hgext3rd/pullbundle.py	Sun Sep 23 23:53:23 2018 +0200
+++ b/hgext3rd/pullbundle.py	Sun Sep 23 23:41:08 2018 +0200
@@ -5,14 +5,22 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
+import errno
+import os
+
 from mercurial import (
     changegroup,
+    discovery,
     exchange,
     narrowspec,
+    node as nodemod,
+    util,
 )
 
 from mercurial.i18n import _
 
+# generic wrapping
+
 def uisetup(ui):
     exchange.getbundle2partsmapping['changegroup'] = _getbundlechangegrouppart
 
@@ -59,14 +67,156 @@
 
 def makeallcgpart(newpart, repo, outgoing, version, source,
                   bundlecaps, filematcher, cgversions):
-    makeonecgpart(newpart, repo, outgoing, version, source, bundlecaps,
-                  filematcher, cgversions)
+
+    pullbundle = not filematcher
+    if pullbundle and not util.safehasattr(repo, 'stablerange'):
+        repo.ui.warn('pullbundle: required extension "evolve" are missing, skipping pullbundle\n')
+        pullbundle = False
+    if filematcher:
+        makeonecgpart(newpart, repo, None, outgoing, version, source, bundlecaps,
+                      filematcher, cgversions)
+    else:
+        for sliceid, sliceout in sliceoutgoing(repo, outgoing):
+            makeonecgpart(newpart, repo, sliceid, sliceout, version, source, bundlecaps,
+                          filematcher, cgversions)
+
+# stable range slicing
+
+def sliceoutgoing(repo, outgoing):
+    cl = repo.changelog
+    rev = cl.nodemap.get
+    node = cl.node
+    revsort = repo.stablesort
+
+    missingrevs = set(rev(n) for n in outgoing.missing)
+    allslices = []
+    missingheads = [rev(n) for n in outgoing.missingheads]
+    for head in missingheads:
+        localslices = []
+        localmissing = set(repo.revs('%ld and ::%d', missingrevs, head))
+        while localmissing:
+            slicerevs = []
+            for r in revsort.walkfrom(repo, head):
+                if r not in missingrevs:
+                    break
+                slicerevs.append(r)
+            slicenodes = [node(r) for r in slicerevs]
+            localslices.extend(canonicalslices(repo, slicenodes))
+            missingrevs.difference_update(slicerevs)
+            localmissing.difference_update(slicerevs)
+            if localmissing:
+                head = max(localmissing)
+
+        allslices.extend(localslices)
+    return [(rangeid, outgoingfromnodes(repo, nodes))
+            for rangeid, nodes in allslices]
+
+def canonicalslices(repo, nodes):
+    depth = repo.depthcache.get
+    stablerange = repo.stablerange
+    rangelength = lambda x: stablerange.rangelength(repo, x)
+    headrev = repo.changelog.rev(nodes[0])
+    nbrevs = len(nodes)
+    headdepth = depth(headrev)
+    skipped = headdepth - nbrevs
+    rangeid = (headrev, skipped)
+
+    subranges = canonicalsubranges(repo, stablerange, rangeid)
+    idx = 0
+    slices =[]
+    nodes.reverse()
+    for rangeid in subranges:
+        size = rangelength(rangeid)
+        slices.append((rangeid, nodes[idx:idx + size]))
+        idx += size
+    return slices
+
+def canonicalsubranges(repo, stablerange, rangeid):
+    """slice a size of nodes into most reusable subranges
+
+    We try to slice a range into a set of "largest" and "canonical" stable
+    range.
+
+    It might make sense to move this function as a 'stablerange' method.
+    """
+    headrev, skip = rangeid
+    rangedepth = stablerange.depthrev(repo, rangeid[0])
+    canonicals = []
+
+    # 0. find the first power of 2 higher than this range depth
+    cursor = 1
+    while cursor <= rangedepth:
+        cursor *= 2
+
+    # 1. find first cupt
+    precut = cut = 0
+    while True:
+        if skip <= cut:
+            break
+        if cut + cursor < rangedepth:
+            precut = cut
+            cut += cursor
+        if cursor == 1:
+            break
+        cursor //=2
+
+    # 2. optimise, bottom part
+    if skip != cut:
+        tailranges = []
+        tailsize = cut - skip
+        assert 0 < tailsize, tailsize
+        prerange = (headrev, precut)
+        size = stablerange.rangelength(repo, prerange)
+        sub = stablerange.subranges(repo, prerange)[:-1]
+        while not poweroftwo(tailsize):
+            for prerange in reversed(sub):
+                if tailsize <= 0:
+                    break
+
+                assert stablerange.depthrev(repo, prerange[0]) != prerange[1], prerange
+                tailrev, tailskip = prerange
+                size = stablerange.rangelength(repo, (tailrev, tailskip))
+                if tailsize < size:
+                    tailskip += size - tailsize
+                    size = tailsize
+                tailranges.append((tailrev, tailskip))
+                tailsize -= size
+            else:
+                # size of the last block
+                tailsize = stablerange.rangelength(repo, tailranges[-1])
+                if poweroftwo(tailsize):
+                    continue # exit the loop
+                prerange = tailranges.pop()
+                sub = stablerange.subranges(repo, prerange)
+
+        tailranges.reverse()
+        canonicals.extend(tailranges)
+
+    # 3. take recursive subrange until we get to a power of two size?
+    current = (headrev, cut)
+    while not poweroftwo(stablerange.rangelength(repo, current)):
+        sub = stablerange.subranges(repo, current)
+        canonicals.extend(sub[:-1])
+        current = sub[-1]
+    canonicals.append(current)
+
+    return canonicals
+
+def poweroftwo(num):
+    return num and not num & (num - 1)
+
+def outgoingfromnodes(repo, nodes):
+    return discovery.outgoing(repo,
+                              missingroots=nodes,
+                              missingheads=nodes)
+
+# changegroup part construction
 
 def _changegroupinfo(repo, nodes, source):
     if repo.ui.verbose or source == 'bundle':
         repo.ui.status(_("%d changesets found\n") % len(nodes))
 
-def makeonecgpart(newpart, repo, outgoing, version, source,
+def makeonecgpart(newpart, repo, rangeid, outgoing, version, source,
                   bundlecaps, filematcher, cgversions):
     # same as upstream code