Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/websdk/mercurial/setdiscovery.py
diff options
context:
space:
mode:
Diffstat (limited to 'websdk/mercurial/setdiscovery.py')
-rw-r--r--websdk/mercurial/setdiscovery.py194
1 files changed, 194 insertions, 0 deletions
diff --git a/websdk/mercurial/setdiscovery.py b/websdk/mercurial/setdiscovery.py
new file mode 100644
index 0000000..bbb1b18
--- /dev/null
+++ b/websdk/mercurial/setdiscovery.py
@@ -0,0 +1,194 @@
+# setdiscovery.py - improved discovery of common nodeset for mercurial
+#
+# Copyright 2010 Benoit Boissinot <bboissin@gmail.com>
+# and Peter Arrenbrecht <peter@arrenbrecht.ch>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from node import nullid
+from i18n import _
+import random, collections, util, dagutil
+
+def _updatesample(dag, nodes, sample, always, quicksamplesize=0):
+ # if nodes is empty we scan the entire graph
+ if nodes:
+ heads = dag.headsetofconnecteds(nodes)
+ else:
+ heads = dag.heads()
+ dist = {}
+ visit = collections.deque(heads)
+ seen = set()
+ factor = 1
+ while visit:
+ curr = visit.popleft()
+ if curr in seen:
+ continue
+ d = dist.setdefault(curr, 1)
+ if d > factor:
+ factor *= 2
+ if d == factor:
+ if curr not in always: # need this check for the early exit below
+ sample.add(curr)
+ if quicksamplesize and (len(sample) >= quicksamplesize):
+ return
+ seen.add(curr)
+ for p in dag.parents(curr):
+ if not nodes or p in nodes:
+ dist.setdefault(p, d + 1)
+ visit.append(p)
+
+def _setupsample(dag, nodes, size):
+ if len(nodes) <= size:
+ return set(nodes), None, 0
+ always = dag.headsetofconnecteds(nodes)
+ desiredlen = size - len(always)
+ if desiredlen <= 0:
+ # This could be bad if there are very many heads, all unknown to the
+ # server. We're counting on long request support here.
+ return always, None, desiredlen
+ return always, set(), desiredlen
+
+def _takequicksample(dag, nodes, size, initial):
+ always, sample, desiredlen = _setupsample(dag, nodes, size)
+ if sample is None:
+ return always
+ if initial:
+ fromset = None
+ else:
+ fromset = nodes
+ _updatesample(dag, fromset, sample, always, quicksamplesize=desiredlen)
+ sample.update(always)
+ return sample
+
+def _takefullsample(dag, nodes, size):
+ always, sample, desiredlen = _setupsample(dag, nodes, size)
+ if sample is None:
+ return always
+ # update from heads
+ _updatesample(dag, nodes, sample, always)
+ # update from roots
+ _updatesample(dag.inverse(), nodes, sample, always)
+ assert sample
+ if len(sample) > desiredlen:
+ sample = set(random.sample(sample, desiredlen))
+ elif len(sample) < desiredlen:
+ more = desiredlen - len(sample)
+ sample.update(random.sample(list(nodes - sample - always), more))
+ sample.update(always)
+ return sample
+
+def findcommonheads(ui, local, remote,
+ initialsamplesize=100,
+ fullsamplesize=200,
+ abortwhenunrelated=True):
+ '''Return a tuple (common, anyincoming, remoteheads) used to identify
+ missing nodes from or in remote.
+
+ shortcutlocal determines whether we try use direct access to localrepo if
+ remote is actually local.
+ '''
+ roundtrips = 0
+ cl = local.changelog
+ dag = dagutil.revlogdag(cl)
+
+ # early exit if we know all the specified remote heads already
+ ui.debug("query 1; heads\n")
+ roundtrips += 1
+ ownheads = dag.heads()
+ sample = ownheads
+ if remote.local():
+ # stopgap until we have a proper localpeer that supports batch()
+ srvheadhashes = remote.heads()
+ yesno = remote.known(dag.externalizeall(sample))
+ elif remote.capable('batch'):
+ batch = remote.batch()
+ srvheadhashesref = batch.heads()
+ yesnoref = batch.known(dag.externalizeall(sample))
+ batch.submit()
+ srvheadhashes = srvheadhashesref.value
+ yesno = yesnoref.value
+ else:
+ # compatibitity with pre-batch, but post-known remotes during 1.9 devel
+ srvheadhashes = remote.heads()
+ sample = []
+
+ if cl.tip() == nullid:
+ if srvheadhashes != [nullid]:
+ return [nullid], True, srvheadhashes
+ return [nullid], False, []
+
+ # start actual discovery (we note this before the next "if" for
+ # compatibility reasons)
+ ui.status(_("searching for changes\n"))
+
+ srvheads = dag.internalizeall(srvheadhashes, filterunknown=True)
+ if len(srvheads) == len(srvheadhashes):
+ ui.debug("all remote heads known locally\n")
+ return (srvheadhashes, False, srvheadhashes,)
+
+ if sample and util.all(yesno):
+ ui.note(_("all local heads known remotely\n"))
+ ownheadhashes = dag.externalizeall(ownheads)
+ return (ownheadhashes, True, srvheadhashes,)
+
+ # full blown discovery
+ undecided = dag.nodeset() # own nodes where I don't know if remote knows them
+ common = set() # own nodes I know we both know
+ missing = set() # own nodes I know remote lacks
+
+ # treat remote heads (and maybe own heads) as a first implicit sample response
+ common.update(dag.ancestorset(srvheads))
+ undecided.difference_update(common)
+
+ full = False
+ while undecided:
+
+ if sample:
+ commoninsample = set(n for i, n in enumerate(sample) if yesno[i])
+ common.update(dag.ancestorset(commoninsample, common))
+
+ missinginsample = [n for i, n in enumerate(sample) if not yesno[i]]
+ missing.update(dag.descendantset(missinginsample, missing))
+
+ undecided.difference_update(missing)
+ undecided.difference_update(common)
+
+ if not undecided:
+ break
+
+ if full:
+ ui.note(_("sampling from both directions\n"))
+ sample = _takefullsample(dag, undecided, size=fullsamplesize)
+ elif common:
+ # use cheapish initial sample
+ ui.debug("taking initial sample\n")
+ sample = _takefullsample(dag, undecided, size=fullsamplesize)
+ else:
+ # use even cheaper initial sample
+ ui.debug("taking quick initial sample\n")
+ sample = _takequicksample(dag, undecided, size=initialsamplesize,
+ initial=True)
+
+ roundtrips += 1
+ ui.progress(_('searching'), roundtrips, unit=_('queries'))
+ ui.debug("query %i; still undecided: %i, sample size is: %i\n"
+ % (roundtrips, len(undecided), len(sample)))
+ # indices between sample and externalized version must match
+ sample = list(sample)
+ yesno = remote.known(dag.externalizeall(sample))
+ full = True
+
+ result = dag.headsetofconnecteds(common)
+ ui.progress(_('searching'), None)
+ ui.debug("%d total queries\n" % roundtrips)
+
+ if not result and srvheadhashes != [nullid]:
+ if abortwhenunrelated:
+ raise util.Abort(_("repository is unrelated"))
+ else:
+ ui.warn(_("warning: repository is unrelated\n"))
+ return (set([nullid]), True, srvheadhashes,)
+
+ anyincoming = (srvheadhashes != [nullid])
+ return dag.externalizeall(result), anyincoming, srvheadhashes