# HG changeset patch # User Peter Arrenbrecht # Date 2011-05-02 17:21:30 # Node ID cb98fed5249517b558fa07c7d0414c14cdfe4e4d # Parent 38184a72d793d2a42908b76db20f6702f4d1ab97 discovery: add new set-based discovery Adds a new discovery method based on repeatedly sampling the still undecided subset of the local node graph to determine the set of nodes common to both the client and the server. For small differences between client and server, it uses about the same or slightly fewer roundtrips than the old tree-based discovery. For larger differences, it typically reduces the number of roundtrips drastically (from 150 to 4, for instance). The old discovery code now lives in treediscovery.py, the new code is in setdiscovery.py. Still missing is a hook for extensions to contribute nodes to the initial sample. For instance, Augie's remotebranches could contribute the last known state of the server's heads. Credits for the actual sampler and computing common heads instead of bases go to Benoit Boissinot. diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -15,6 +15,7 @@ import archival, changegroup, cmdutil, s import merge as mergemod import minirst, revset, templatefilters import dagparser, context, simplemerge +import random, setdiscovery, treediscovery, dagutil # Commands start here, listed alphabetically @@ -1471,6 +1472,65 @@ def debugignore(ui, repo, *values, **opt else: raise util.Abort(_("no ignore patterns found")) +def debugdiscovery(ui, repo, remoteurl="default", **opts): + """runs the changeset discovery protocol in isolation""" + remoteurl, branches = hg.parseurl(ui.expandpath(remoteurl), opts.get('branch')) + remote = hg.repository(hg.remoteui(repo, opts), remoteurl) + ui.status(_('comparing with %s\n') % util.hidepassword(remoteurl)) + + # make sure tests are repeatable + random.seed(12323) + + def doit(localheads, remoteheads): + if opts.get('old'): + if localheads: + raise util.Abort('cannot use localheads with old style discovery') + common, _in, hds = treediscovery.findcommonincoming(repo, remote, + force=True) + common = set(common) + if not opts.get('nonheads'): + ui.write("unpruned common: %s\n" % " ".join([short(n) + for n in common])) + dag = dagutil.revlogdag(repo.changelog) + all = dag.ancestorset(dag.internalizeall(common)) + common = dag.externalizeall(dag.headsetofconnecteds(all)) + else: + common, any, hds = setdiscovery.findcommonheads(ui, repo, remote) + common = set(common) + rheads = set(hds) + lheads = set(repo.heads()) + ui.write("common heads: %s\n" % " ".join([short(n) for n in common])) + if lheads <= common: + ui.write("local is subset\n") + elif rheads <= common: + ui.write("remote is subset\n") + + serverlogs = opts.get('serverlog') + if serverlogs: + for filename in serverlogs: + logfile = open(filename, 'r') + try: + line = logfile.readline() + while line: + parts = line.strip().split(';') + op = parts[1] + if op == 'cg': + pass + elif op == 'cgss': + doit(parts[2].split(' '), parts[3].split(' ')) + elif op == 'unb': + doit(parts[3].split(' '), parts[2].split(' ')) + line = logfile.readline() + finally: + logfile.close() + + else: + remoterevs, _checkout = hg.addbranchrevs(repo, remote, branches, + opts.get('remote_head')) + localrevs = opts.get('local_head') + doit(localrevs, remoterevs) + + def debugindex(ui, repo, file_, **opts): """dump the contents of an index file""" r = None @@ -4513,6 +4573,14 @@ table = { [('e', 'extended', None, _('try extended date formats'))], _('[-e] DATE [RANGE]')), "debugdata": (debugdata, [], _('FILE REV')), + "debugdiscovery": (debugdiscovery, + [('', 'old', None, + _('use old-style discovery')), + ('', 'nonheads', None, + _('use old-style discovery with non-heads included')), + ] + remoteopts, + _('[-l REV] [-r REV] [-b BRANCH]...' + ' [OTHER]')), "debugfsinfo": (debugfsinfo, [], _('[PATH]')), "debuggetbundle": (debuggetbundle, diff --git a/mercurial/dagutil.py b/mercurial/dagutil.py new file mode 100644 --- /dev/null +++ b/mercurial/dagutil.py @@ -0,0 +1,242 @@ +# dagutil.py - dag utilities for mercurial +# +# Copyright 2010 Benoit Boissinot +# and Peter Arrenbrecht +# +# 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 nullrev + + +class basedag(object): + '''generic interface for DAGs + + terms: + "ix" (short for index) identifies a nodes internally, + "id" identifies one externally. + + All params are ixs unless explicitly suffixed otherwise. + Pluralized params are lists or sets. + ''' + + def __init__(self): + self._inverse = None + + def nodeset(self): + '''set of all node idxs''' + raise NotImplementedError() + + def heads(self): + '''list of head ixs''' + raise NotImplementedError() + + def parents(self, ix): + '''list of parents ixs of ix''' + raise NotImplementedError() + + def inverse(self): + '''inverse DAG, where parents becomes children, etc.''' + raise NotImplementedError() + + def ancestorset(self, starts, stops=None): + '''set of all ancestors of starts (incl), but stop walk at stops (excl)''' + raise NotImplementedError() + + def descendantset(self, starts, stops=None): + '''set of all descendants of starts (incl), but stop walk at stops (excl)''' + return self.inverse().ancestorset(starts, stops) + + def headsetofconnecteds(self, ixs): + '''subset of connected list of ixs so that no node has a descendant in it + + By "connected list" we mean that if an ancestor and a descendant are in + the list, then so is at least one path connecting them.''' + raise NotImplementedError() + + def externalize(self, ix): + '''return a list of (or set if given a set) of node ids''' + return self._externalize(ix) + + def externalizeall(self, ixs): + '''return a list of (or set if given a set) of node ids''' + ids = self._externalizeall(ixs) + if isinstance(ixs, set): + return set(ids) + return list(ids) + + def internalize(self, id): + '''return a list of (or set if given a set) of node ixs''' + return self._internalize(id) + + def internalizeall(self, ids, filterunknown=False): + '''return a list of (or set if given a set) of node ids''' + ixs = self._internalizeall(ids, filterunknown) + if isinstance(ids, set): + return set(ixs) + return list(ixs) + + +class genericdag(basedag): + '''generic implementations for DAGs''' + + def ancestorset(self, starts, stops=None): + stops = stops and set(stops) or set() + seen = set() + pending = list(starts) + while pending: + n = pending.pop() + if n not in seen and n not in stops: + seen.add(n) + pending.extend(self.parents(n)) + return seen + + def headsetofconnecteds(self, ixs): + hds = set(ixs) + if not hds: + return hds + for n in ixs: + for p in self.parents(n): + hds.discard(p) + assert hds + return hds + + +class revlogbaseddag(basedag): + '''generic dag interface to a revlog''' + + def __init__(self, revlog, nodeset): + basedag.__init__(self) + self._revlog = revlog + self._heads = None + self._nodeset = nodeset + + def nodeset(self): + return self._nodeset + + def heads(self): + if self._heads is None: + self._heads = self._getheads() + return self._heads + + def _externalize(self, ix): + return self._revlog.index[ix][7] + def _externalizeall(self, ixs): + idx = self._revlog.index + return [idx[i][7] for i in ixs] + + def _internalize(self, id): + ix = self._revlog.rev(id) + if ix == nullrev: + raise LookupError(id, self._revlog.indexfile, _('nullid')) + return ix + def _internalizeall(self, ids, filterunknown): + rl = self._revlog + if filterunknown: + return [r for r in map(rl.nodemap.get, ids) + if r is not None and r != nullrev] + return map(self._internalize, ids) + + +class revlogdag(revlogbaseddag): + '''dag interface to a revlog''' + + def __init__(self, revlog): + revlogbaseddag.__init__(self, revlog, set(xrange(len(revlog)))) + + def _getheads(self): + return [r for r in self._revlog.headrevs() if r != nullrev] + + def parents(self, ix): + rlog = self._revlog + idx = rlog.index + revdata = idx[ix] + prev = revdata[5] + if prev != nullrev: + prev2 = revdata[6] + if prev2 == nullrev: + return [prev] + return [prev, prev2] + prev2 = revdata[6] + if prev2 != nullrev: + return [prev2] + return [] + + def inverse(self): + if self._inverse is None: + self._inverse = inverserevlogdag(self) + return self._inverse + + def ancestorset(self, starts, stops=None): + rlog = self._revlog + idx = rlog.index + stops = stops and set(stops) or set() + seen = set() + pending = list(starts) + while pending: + rev = pending.pop() + if rev not in seen and rev not in stops: + seen.add(rev) + revdata = idx[rev] + for i in [5, 6]: + prev = revdata[i] + if prev != nullrev: + pending.append(prev) + return seen + + def headsetofconnecteds(self, ixs): + if not ixs: + return set() + rlog = self._revlog + idx = rlog.index + headrevs = set(ixs) + for rev in ixs: + revdata = idx[rev] + for i in [5, 6]: + prev = revdata[i] + if prev != nullrev: + headrevs.discard(prev) + assert headrevs + return headrevs + + +class inverserevlogdag(revlogbaseddag, genericdag): + '''inverse of an existing revlog dag; see revlogdag.inverse()''' + + def __init__(self, orig): + revlogbaseddag.__init__(self, orig._revlog, orig._nodeset) + self._orig = orig + self._children = {} + self._roots = [] + self._walkfrom = len(self._revlog) - 1 + + def _walkto(self, walkto): + rev = self._walkfrom + cs = self._children + roots = self._roots + idx = self._revlog.index + while rev >= walkto: + data = idx[rev] + isroot = True + for prev in [data[5], data[6]]: # parent revs + if prev != nullrev: + cs.setdefault(prev, []).append(rev) + isroot = False + if isroot: + roots.append(rev) + rev -= 1 + self._walkfrom = rev - 1 + + def _getheads(self): + self._walkto(nullrev) + return self._roots + + def parents(self, ix): + if ix is None: + return [] + if ix <= self._walkfrom: + self._walkto(ix) + return self._children.get(ix, []) + + def inverse(self): + return self._orig diff --git a/mercurial/discovery.py b/mercurial/discovery.py --- a/mercurial/discovery.py +++ b/mercurial/discovery.py @@ -7,7 +7,7 @@ from node import nullid, short from i18n import _ -import util, error +import util, error, setdiscovery, treediscovery def findcommonincoming(repo, remote, heads=None, force=False): """Return a tuple (common, anyincoming, heads) used to identify the common @@ -20,145 +20,28 @@ def findcommonincoming(repo, remote, hea changegroupsubset. No code except for pull should be relying on this fact any longer. "heads" is either the supplied heads, or else the remote's heads. + + If you pass heads and they are all known locally, the reponse lists justs + these heads in "common" and in "heads". """ - m = repo.changelog.nodemap - search = [] - fetch = set() - seen = set() - seenbranch = set() - base = set() - - if not heads: - heads = remote.heads() - - if repo.changelog.tip() == nullid: - base.add(nullid) - if heads != [nullid]: - return [nullid], [nullid], list(heads) - return [nullid], [], [] - - # assume we're closer to the tip than the root - # and start by examining the heads - repo.ui.status(_("searching for changes\n")) - - if remote.capable('getbundle'): - myheads = repo.heads() - known = remote.known(myheads) - if util.all(known): - hasincoming = set(heads).difference(set(myheads)) and True - return myheads, hasincoming, heads - - unknown = [] - for h in heads: - if h not in m: - unknown.append(h) - else: - base.add(h) - - heads = unknown - if not unknown: - return list(base), [], [] - - req = set(unknown) - reqcnt = 0 - - # search through remote branches - # a 'branch' here is a linear segment of history, with four parts: - # head, root, first parent, second parent - # (a branch always has two parents (or none) by definition) - unknown = remote.branches(unknown) - while unknown: - r = [] - while unknown: - n = unknown.pop(0) - if n[0] in seen: - continue + if not remote.capable('getbundle'): + return treediscovery.findcommonincoming(repo, remote, heads, force) - repo.ui.debug("examining %s:%s\n" - % (short(n[0]), short(n[1]))) - if n[0] == nullid: # found the end of the branch - pass - elif n in seenbranch: - repo.ui.debug("branch already found\n") - continue - elif n[1] and n[1] in m: # do we know the base? - repo.ui.debug("found incomplete branch %s:%s\n" - % (short(n[0]), short(n[1]))) - search.append(n[0:2]) # schedule branch range for scanning - seenbranch.add(n) - else: - if n[1] not in seen and n[1] not in fetch: - if n[2] in m and n[3] in m: - repo.ui.debug("found new changeset %s\n" % - short(n[1])) - fetch.add(n[1]) # earliest unknown - for p in n[2:4]: - if p in m: - base.add(p) # latest known - - for p in n[2:4]: - if p not in req and p not in m: - r.append(p) - req.add(p) - seen.add(n[0]) - - if r: - reqcnt += 1 - repo.ui.progress(_('searching'), reqcnt, unit=_('queries')) - repo.ui.debug("request %d: %s\n" % - (reqcnt, " ".join(map(short, r)))) - for p in xrange(0, len(r), 10): - for b in remote.branches(r[p:p + 10]): - repo.ui.debug("received %s:%s\n" % - (short(b[0]), short(b[1]))) - unknown.append(b) + if heads: + allknown = True + nm = repo.changelog.nodemap + for h in heads: + if nm.get(h) is None: + allknown = False + break + if allknown: + return (heads, False, heads) - # do binary search on the branches we found - while search: - newsearch = [] - reqcnt += 1 - repo.ui.progress(_('searching'), reqcnt, unit=_('queries')) - for n, l in zip(search, remote.between(search)): - l.append(n[1]) - p = n[0] - f = 1 - for i in l: - repo.ui.debug("narrowing %d:%d %s\n" % (f, len(l), short(i))) - if i in m: - if f <= 2: - repo.ui.debug("found new branch changeset %s\n" % - short(p)) - fetch.add(p) - base.add(i) - else: - repo.ui.debug("narrowed branch search to %s:%s\n" - % (short(p), short(i))) - newsearch.append((p, i)) - break - p, f = i, f * 2 - search = newsearch - - # sanity check our fetch list - for f in fetch: - if f in m: - raise error.RepoError(_("already have changeset ") - + short(f[:4])) - - base = list(base) - if base == [nullid]: - if force: - repo.ui.warn(_("warning: repository is unrelated\n")) - else: - raise util.Abort(_("repository is unrelated")) - - repo.ui.debug("found new changesets starting at " + - " ".join([short(f) for f in fetch]) + "\n") - - repo.ui.progress(_('searching'), None) - repo.ui.debug("%d total queries\n" % reqcnt) - - return base, list(fetch), heads + res = setdiscovery.findcommonheads(repo.ui, repo, remote, + abortwhenunrelated=not force) + common, anyinc, srvheads = res + return (list(common), anyinc, heads or list(srvheads)) def prepush(repo, remote, force, revs, newbranch): '''Analyze the local and remote repositories and determine which @@ -174,9 +57,7 @@ def prepush(repo, remote, force, revs, n changegroup is a readable file-like object whose read() returns successive changegroup chunks ready to be sent over the wire and remoteheads is the list of remote heads.''' - remoteheads = remote.heads() - common, inc, _rheads = findcommonincoming(repo, remote, heads=remoteheads, - force=force) + common, inc, remoteheads = findcommonincoming(repo, remote, force=force) cl = repo.changelog outg = cl.findmissing(common, revs) diff --git a/mercurial/revlog.py b/mercurial/revlog.py --- a/mercurial/revlog.py +++ b/mercurial/revlog.py @@ -617,6 +617,17 @@ class revlog(object): assert heads return (orderedout, roots, heads) + def headrevs(self): + count = len(self) + if not count: + return [nullrev] + ishead = [1] * (count + 1) + index = self.index + for r in xrange(count): + e = index[r] + ishead[e[5]] = ishead[e[6]] = 0 + return [r for r in xrange(count) if ishead[r]] + def heads(self, start=None, stop=None): """return the list of all nodes that have no children @@ -626,15 +637,9 @@ class revlog(object): as if they had no children """ if start is None and stop is None: - count = len(self) - if not count: + if not len(self): return [nullid] - ishead = [1] * (count + 1) - index = self.index - for r in xrange(count): - e = index[r] - ishead[e[5]] = ishead[e[6]] = 0 - return [self.node(r) for r in xrange(count) if ishead[r]] + return [self.node(r) for r in self.headrevs()] if start is None: start = nullid diff --git a/mercurial/setdiscovery.py b/mercurial/setdiscovery.py new file mode 100644 --- /dev/null +++ b/mercurial/setdiscovery.py @@ -0,0 +1,178 @@ +# setdiscovery.py - improved discovery of common nodeset for mercurial +# +# Copyright 2010 Benoit Boissinot +# and Peter Arrenbrecht +# +# 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 = set(dag.heads()) + 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) + nodes = dag.nodeset() + + # early exit if we know all the specified server heads already + ui.debug("query 1; heads\n") + roundtrips += 1 + srvheadhashes = remote.heads() + + ## TODO We might want to request an additional random sample of the server's + ## nodes batched with the heads query here. + + 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.note("all remote heads known locally\n") + return (srvheadhashes, False, srvheadhashes,) + + # full blown discovery + undecided = nodes # own nodes where I don't know if the server knows them + common = set() # own nodes I know we both know + missing = set() # own nodes I know the server lacks + + # treat remote heads as a first implicit sample response + common.update(dag.ancestorset(srvheads)) + undecided.difference_update(common) + # use cheapish initial sample + if common: + ui.debug("taking initial sample\n") + sample = _takefullsample(dag, undecided, size=fullsamplesize) + else: + ui.debug("taking quick initial sample\n") + sample = _takequicksample(dag, nodes, 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)) + + while undecided: + 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 + + ui.note("sampling from both directions\n") + sample = _takefullsample(dag, undecided, size=fullsamplesize) + + 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)) + + 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,) + + return (dag.externalizeall(result), True, srvheadhashes,) diff --git a/mercurial/discovery.py b/mercurial/treediscovery.py copy from mercurial/discovery.py copy to mercurial/treediscovery.py --- a/mercurial/discovery.py +++ b/mercurial/treediscovery.py @@ -10,15 +10,12 @@ from i18n import _ import util, error def findcommonincoming(repo, remote, heads=None, force=False): - """Return a tuple (common, anyincoming, heads) used to identify the common + """Return a tuple (common, fetch, heads) used to identify the common subset of nodes between repo and remote. "common" is a list of (at least) the heads of the common subset. - "anyincoming" is testable as a boolean indicating if any nodes are missing - locally. If remote does not support getbundle, this actually is a list of - roots of the nodes that would be incoming, to be supplied to - changegroupsubset. No code except for pull should be relying on this fact - any longer. + "fetch" is a list of roots of the nodes that would be incoming, to be + supplied to changegroupsubset. "heads" is either the supplied heads, or else the remote's heads. """ @@ -42,13 +39,6 @@ def findcommonincoming(repo, remote, hea # and start by examining the heads repo.ui.status(_("searching for changes\n")) - if remote.capable('getbundle'): - myheads = repo.heads() - known = remote.known(myheads) - if util.all(known): - hasincoming = set(heads).difference(set(myheads)) and True - return myheads, hasincoming, heads - unknown = [] for h in heads: if h not in m: @@ -159,130 +149,3 @@ def findcommonincoming(repo, remote, hea repo.ui.debug("%d total queries\n" % reqcnt) return base, list(fetch), heads - -def prepush(repo, remote, force, revs, newbranch): - '''Analyze the local and remote repositories and determine which - changesets need to be pushed to the remote. Return value depends - on circumstances: - - If we are not going to push anything, return a tuple (None, - outgoing) where outgoing is 0 if there are no outgoing - changesets and 1 if there are, but we refuse to push them - (e.g. would create new remote heads). - - Otherwise, return a tuple (changegroup, remoteheads), where - changegroup is a readable file-like object whose read() returns - successive changegroup chunks ready to be sent over the wire and - remoteheads is the list of remote heads.''' - remoteheads = remote.heads() - common, inc, _rheads = findcommonincoming(repo, remote, heads=remoteheads, - force=force) - - cl = repo.changelog - outg = cl.findmissing(common, revs) - - if not outg: - repo.ui.status(_("no changes found\n")) - return None, 1 - - if not force and remoteheads != [nullid]: - if remote.capable('branchmap'): - # Check for each named branch if we're creating new remote heads. - # To be a remote head after push, node must be either: - # - unknown locally - # - a local outgoing head descended from update - # - a remote head that's known locally and not - # ancestral to an outgoing head - - # 1. Create set of branches involved in the push. - branches = set(repo[n].branch() for n in outg) - - # 2. Check for new branches on the remote. - remotemap = remote.branchmap() - newbranches = branches - set(remotemap) - if newbranches and not newbranch: # new branch requires --new-branch - branchnames = ', '.join(sorted(newbranches)) - raise util.Abort(_("push creates new remote branches: %s!") - % branchnames, - hint=_("use 'hg push --new-branch' to create" - " new remote branches")) - branches.difference_update(newbranches) - - # 3. Construct the initial oldmap and newmap dicts. - # They contain information about the remote heads before and - # after the push, respectively. - # Heads not found locally are not included in either dict, - # since they won't be affected by the push. - # unsynced contains all branches with incoming changesets. - oldmap = {} - newmap = {} - unsynced = set() - for branch in branches: - remotebrheads = remotemap[branch] - prunedbrheads = [h for h in remotebrheads if h in cl.nodemap] - oldmap[branch] = prunedbrheads - newmap[branch] = list(prunedbrheads) - if len(remotebrheads) > len(prunedbrheads): - unsynced.add(branch) - - # 4. Update newmap with outgoing changes. - # This will possibly add new heads and remove existing ones. - ctxgen = (repo[n] for n in outg) - repo._updatebranchcache(newmap, ctxgen) - - else: - # 1-4b. old servers: Check for new topological heads. - # Construct {old,new}map with branch = None (topological branch). - # (code based on _updatebranchcache) - oldheads = set(h for h in remoteheads if h in cl.nodemap) - newheads = oldheads.union(outg) - if len(newheads) > 1: - for latest in reversed(outg): - if latest not in newheads: - continue - minhrev = min(cl.rev(h) for h in newheads) - reachable = cl.reachable(latest, cl.node(minhrev)) - reachable.remove(latest) - newheads.difference_update(reachable) - branches = set([None]) - newmap = {None: newheads} - oldmap = {None: oldheads} - unsynced = inc and branches or set() - - # 5. Check for new heads. - # If there are more heads after the push than before, a suitable - # error message, depending on unsynced status, is displayed. - error = None - for branch in branches: - newhs = set(newmap[branch]) - oldhs = set(oldmap[branch]) - if len(newhs) > len(oldhs): - if error is None: - if branch: - error = _("push creates new remote heads " - "on branch '%s'!") % branch - else: - error = _("push creates new remote heads!") - if branch in unsynced: - hint = _("you should pull and merge or " - "use push -f to force") - else: - hint = _("did you forget to merge? " - "use push -f to force") - if branch: - repo.ui.debug("new remote heads on branch '%s'\n" % branch) - for h in (newhs - oldhs): - repo.ui.debug("new remote head %s\n" % short(h)) - if error: - raise util.Abort(error, hint=hint) - - # 6. Check for unsynced changes on involved branches. - if unsynced: - repo.ui.warn(_("note: unsynced remote changes!\n")) - - if revs is None: - # use the fast path, no race possible on push - cg = repo._changegroup(outg, 'push') - else: - cg = repo.getbundle('push', heads=revs, common=common) - return cg, remoteheads diff --git a/tests/test-acl.t b/tests/test-acl.t --- a/tests/test-acl.t +++ b/tests/test-acl.t @@ -82,7 +82,9 @@ Extension disabled for lack of a hook hgrc = """ """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 3 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -135,7 +137,9 @@ Extension disabled for lack of acl.sourc pretxnchangegroup.acl = python:hgext.acl.hook """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally invalidating branch cache (tip differs) 3 changesets found list of changesets: @@ -192,7 +196,9 @@ No [acl.allow]/[acl.deny] sources = push """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally invalidating branch cache (tip differs) 3 changesets found list of changesets: @@ -258,7 +264,9 @@ Empty [acl.allow] [acl.allow] """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally invalidating branch cache (tip differs) 3 changesets found list of changesets: @@ -322,7 +330,9 @@ fred is allowed inside foo/ foo/** = fred """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 3 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -390,7 +400,9 @@ Empty [acl.deny] [acl.deny] """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 3 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -455,7 +467,9 @@ fred is allowed inside foo/, but not foo foo/bar/** = fred """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 3 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -525,7 +539,9 @@ fred is allowed inside foo/, but not foo foo/Bar/** = fred """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 3 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -592,7 +608,9 @@ fred is allowed inside foo/, but not foo foo/Bar/** = fred """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 3 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -661,7 +679,9 @@ barney is allowed everywhere ** = barney """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 3 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -733,7 +753,9 @@ wilma can change files with a .txt exten **/*.txt = wilma """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally invalidating branch cache (tip differs) 3 changesets found list of changesets: @@ -810,7 +832,9 @@ file specified by acl.config does not ex config = ../acl.config """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 3 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -880,7 +904,9 @@ betty is allowed inside foo/ by a acl.co foo/** = betty """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 3 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -962,7 +988,9 @@ acl.config can set only [acl.allow]/[acl changegroup.acl = false """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 3 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -1035,7 +1063,9 @@ fred is always allowed ** = fred """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally invalidating branch cache (tip differs) 3 changesets found list of changesets: @@ -1105,7 +1135,9 @@ no one is allowed inside foo/Bar/ foo/Bar/** = * """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally invalidating branch cache (tip differs) 3 changesets found list of changesets: @@ -1178,7 +1210,9 @@ OS-level groups ** = @group1 """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 3 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -1248,7 +1282,9 @@ OS-level groups foo/Bar/** = @group1 """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally invalidating branch cache (tip differs) 3 changesets found list of changesets: @@ -1359,7 +1395,9 @@ No branch acls specified [extensions] """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 4 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -1436,7 +1474,9 @@ Branch acl deny test foobar = * """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally invalidating branch cache (tip differs) 4 changesets found list of changesets: @@ -1512,7 +1552,9 @@ Branch acl empty allow test [acl.allow.branches] """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 4 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -1583,7 +1625,9 @@ Branch acl allow other * = george """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 4 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -1648,7 +1692,9 @@ Branch acl allow other * = george """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally 4 changesets found list of changesets: ef1ea85a6374b77d6da9dcda9541f498f2d17df7 @@ -1730,7 +1776,9 @@ push foobar into the remote * = george """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally invalidating branch cache (tip differs) 4 changesets found list of changesets: @@ -1812,7 +1860,9 @@ Branch acl conflicting deny * = george """ pushing to ../b + query 1; heads searching for changes + all remote heads known locally invalidating branch cache (tip differs) 4 changesets found list of changesets: diff --git a/tests/test-bookmarks-pushpull.t b/tests/test-bookmarks-pushpull.t --- a/tests/test-bookmarks-pushpull.t +++ b/tests/test-bookmarks-pushpull.t @@ -39,7 +39,6 @@ import bookmark by name Z 4e3505fd95835d721066b76e75dbb8cc554d7f77 $ hg pull -B X ../a pulling from ../a - searching for changes no changes found importing bookmark X $ hg bookmark @@ -173,7 +172,6 @@ hgweb foobar 000000000000 $ hg pull -B Z http://localhost:$HGPORT/ pulling from http://localhost:$HGPORT/ - searching for changes no changes found not updating divergent bookmark X importing bookmark Z diff --git a/tests/test-bundle.t b/tests/test-bundle.t --- a/tests/test-bundle.t +++ b/tests/test-bundle.t @@ -561,7 +561,9 @@ bundle single branch == bundling $ hg bundle bundle.hg part --debug + query 1; heads searching for changes + all remote heads known locally 2 changesets found list of changesets: d2ae7f538514cd87c17547b0de4cea71fe1af9fb diff --git a/tests/test-debugcomplete.t b/tests/test-debugcomplete.t --- a/tests/test-debugcomplete.t +++ b/tests/test-debugcomplete.t @@ -75,6 +75,7 @@ Show debug commands if there are no othe debugdag debugdata debugdate + debugdiscovery debugfsinfo debuggetbundle debugignore @@ -219,6 +220,7 @@ Show all commands + options debugdag: tags, branches, dots, spaces debugdata: debugdate: extended + debugdiscovery: old, nonheads, ssh, remotecmd, insecure debugfsinfo: debuggetbundle: head, common, type debugignore: diff --git a/tests/test-hook.t b/tests/test-hook.t --- a/tests/test-hook.t +++ b/tests/test-hook.t @@ -189,7 +189,7 @@ listkeys hook $ hg pull -B bar ../a pulling from ../a listkeys hook: HG_NAMESPACE=bookmarks HG_VALUES={'bar': '0000000000000000000000000000000000000000', 'foo': '0000000000000000000000000000000000000000'} - searching for changes + no changes found listkeys hook: HG_NAMESPACE=bookmarks HG_VALUES={'bar': '0000000000000000000000000000000000000000', 'foo': '0000000000000000000000000000000000000000'} importing bookmark bar $ cd ../a diff --git a/tests/test-push-warn.t b/tests/test-push-warn.t --- a/tests/test-push-warn.t +++ b/tests/test-push-warn.t @@ -31,14 +31,12 @@ $ hg push --debug ../a pushing to ../a + query 1; heads searching for changes - examining 1c9246a22a0a:d8d565842d04 - found incomplete branch 1c9246a22a0a:d8d565842d04 - searching: 1 queries - narrowing 1:1 d8d565842d04 - found new branch changeset 1c9246a22a0a - found new changesets starting at 1c9246a22a0a - 1 total queries + taking quick initial sample + searching: 2 queries + query 2; still undecided: 2, sample size is: 2 + 2 total queries new remote heads on branch 'default' new remote head 1e108cc5548c abort: push creates new remote heads on branch 'default'! diff --git a/tests/test-schemes.t b/tests/test-schemes.t --- a/tests/test-schemes.t +++ b/tests/test-schemes.t @@ -27,9 +27,10 @@ check that {1} syntax works using http://localhost:$HGPORT/ sending capabilities command comparing with parts://localhost/ + query 1; heads sending heads command searching for changes - sending known command + all remote heads known locally no changes found [1] diff --git a/tests/test-setdiscovery.t b/tests/test-setdiscovery.t new file mode 100644 --- /dev/null +++ b/tests/test-setdiscovery.t @@ -0,0 +1,271 @@ + +Function to test discovery between two repos in both directions, using both the local shortcut +(which is currently not activated by default) and the full remotable protocol: + + $ testdesc() { # revs_a, revs_b, dagdesc + > if [ -e foo ]; then rm -rf foo; fi + > hg init foo + > cd foo + > hg debugbuilddag "$3" + > hg clone . a $1 --quiet + > hg clone . b $2 --quiet + > echo + > echo "% -- a -> b tree" + > hg -R a debugdiscovery b --verbose --old + > echo + > echo "% -- a -> b set" + > hg -R a debugdiscovery b --verbose --debug + > echo + > echo "% -- b -> a tree" + > hg -R b debugdiscovery a --verbose --old + > echo + > echo "% -- b -> a set" + > hg -R b debugdiscovery a --verbose --debug + > cd .. + > } + + +Small superset: + + $ testdesc '-ra1 -ra2' '-rb1 -rb2 -rb3' ' + > +2:f +1:a1:b1 + > +5 :b2 + > b tree + comparing with b + searching for changes + unpruned common: b5714e113bc0 66f7d451a68b 01241442b3c2 + common heads: b5714e113bc0 01241442b3c2 + local is subset + + % -- a -> b set + comparing with b + query 1; heads + searching for changes + taking initial sample + searching: 2 queries + query 2; still undecided: 4, sample size is: 4 + 2 total queries + common heads: b5714e113bc0 01241442b3c2 + local is subset + + % -- b -> a tree + comparing with a + searching for changes + unpruned common: b5714e113bc0 01241442b3c2 + common heads: b5714e113bc0 01241442b3c2 + remote is subset + + % -- b -> a set + comparing with a + query 1; heads + searching for changes + all remote heads known locally + common heads: b5714e113bc0 01241442b3c2 + remote is subset + + +Many new: + + $ testdesc '-ra1 -ra2' '-rb' ' + > +2:f +3:a1 +3:b + > b tree + comparing with b + searching for changes + unpruned common: bebd167eb94d + common heads: bebd167eb94d + + % -- a -> b set + comparing with b + query 1; heads + searching for changes + taking quick initial sample + searching: 2 queries + query 2; still undecided: 35, sample size is: 35 + 2 total queries + common heads: bebd167eb94d + + % -- b -> a tree + comparing with a + searching for changes + unpruned common: bebd167eb94d 66f7d451a68b + common heads: bebd167eb94d + + % -- b -> a set + comparing with a + query 1; heads + searching for changes + taking initial sample + searching: 2 queries + query 2; still undecided: 3, sample size is: 3 + 2 total queries + common heads: bebd167eb94d + + +Both sides many new with stub: + + $ testdesc '-ra1 -ra2' '-rb' ' + > +2:f +2:a1 +30 :b + > b tree + comparing with b + searching for changes + unpruned common: 2dc09a01254d + common heads: 2dc09a01254d + + % -- a -> b set + comparing with b + query 1; heads + searching for changes + taking quick initial sample + searching: 2 queries + query 2; still undecided: 34, sample size is: 34 + 2 total queries + common heads: 2dc09a01254d + + % -- b -> a tree + comparing with a + searching for changes + unpruned common: 66f7d451a68b 2dc09a01254d + common heads: 2dc09a01254d + + % -- b -> a set + comparing with a + query 1; heads + searching for changes + taking initial sample + searching: 2 queries + query 2; still undecided: 30, sample size is: 30 + 2 total queries + common heads: 2dc09a01254d + + +Both many new: + + $ testdesc '-ra' '-rb' ' + > +2:f +30 :b + > b tree + comparing with b + searching for changes + unpruned common: 66f7d451a68b + common heads: 66f7d451a68b + + % -- a -> b set + comparing with b + query 1; heads + searching for changes + taking quick initial sample + searching: 2 queries + query 2; still undecided: 32, sample size is: 32 + 2 total queries + common heads: 66f7d451a68b + + % -- b -> a tree + comparing with a + searching for changes + unpruned common: 66f7d451a68b + common heads: 66f7d451a68b + + % -- b -> a set + comparing with a + query 1; heads + searching for changes + taking quick initial sample + searching: 2 queries + query 2; still undecided: 32, sample size is: 32 + 2 total queries + common heads: 66f7d451a68b + + +Both many new skewed: + + $ testdesc '-ra' '-rb' ' + > +2:f +30 :b + > b tree + comparing with b + searching for changes + unpruned common: 66f7d451a68b + common heads: 66f7d451a68b + + % -- a -> b set + comparing with b + query 1; heads + searching for changes + taking quick initial sample + searching: 2 queries + query 2; still undecided: 52, sample size is: 52 + 2 total queries + common heads: 66f7d451a68b + + % -- b -> a tree + comparing with a + searching for changes + unpruned common: 66f7d451a68b + common heads: 66f7d451a68b + + % -- b -> a set + comparing with a + query 1; heads + searching for changes + taking quick initial sample + searching: 2 queries + query 2; still undecided: 32, sample size is: 32 + 2 total queries + common heads: 66f7d451a68b + + +Both many new on top of long history: + + $ testdesc '-ra' '-rb' ' + > +1000:f +30 :b + > b tree + comparing with b + searching for changes + unpruned common: 7ead0cba2838 + common heads: 7ead0cba2838 + + % -- a -> b set + comparing with b + query 1; heads + searching for changes + taking quick initial sample + searching: 2 queries + query 2; still undecided: 1050, sample size is: 11 + sampling from both directions + searching: 3 queries + query 3; still undecided: 31, sample size is: 31 + 3 total queries + common heads: 7ead0cba2838 + + % -- b -> a tree + comparing with a + searching for changes + unpruned common: 7ead0cba2838 + common heads: 7ead0cba2838 + + % -- b -> a set + comparing with a + query 1; heads + searching for changes + taking quick initial sample + searching: 2 queries + query 2; still undecided: 1030, sample size is: 11 + sampling from both directions + searching: 3 queries + query 3; still undecided: 16, sample size is: 16 + 3 total queries + common heads: 7ead0cba2838 + + + diff --git a/tests/test-ssh.t b/tests/test-ssh.t --- a/tests/test-ssh.t +++ b/tests/test-ssh.t @@ -219,7 +219,6 @@ test pushkeys and bookmarks $ hg book -f -r 0 foo $ hg pull -B foo pulling from ssh://user@dummy/remote - searching for changes no changes found updating bookmark foo importing bookmark foo