hgext3rd/serverminitopic.py
author Sushil khanchi <sushilkhanchi97@gmail.com>
Sun, 29 Dec 2019 23:59:41 +0530
changeset 5239 13152b2fe8f7
parent 5042 51d0f2355215
child 5180 515d425c0a05
permissions -rw-r--r--
evolve: refactor content-divergence resolution logic > What is the case we are looking at? This is about refactoring the part of content-div resolution logic where it decides which cset should be relocated and where. > What is a "topologicial common ancestors" vs a "greatest common ancestors"? `tca` is an ancestor which we can decide/find by looking at the at graph visually for e.g ``` c3(*) c4(*) | | c2(x) c1(x) c5 | / \ | / c0 ``` (c5 is the successor of c2 and c1) now here, `tca` of c3 and c4 is: c0 `gca` of c3 and c4 is: c5 > What is the new top-level logic/behavior that makes it better? The old code had some unnecessary edge cases just because we were using `gca`, since it can point to a revision that is not a topological ancestor. For e.g see b779b40f996e Eventually, the code around this was getting messy unnecessarily. So I looked into it and found a simple and more robust approach. And in new code, it is simple and straightforward (and easy to understand), where we handle the following 4 cases when solving content-div: 1) when both are on the same parent => (no need to do anything special, and simply proceed) 2) both are on the different parent but a) `tca` is the parent of one of them or b) there is no non-obsolete revision between `tca` and one of the divergent cset. => (relocate one to the other side and proceed) 3) both are on different parents and `tca` is not the parent of any of them and there is at least one non-obsolete cset between tca and both the divergent cset i.e (tca::div1) and (tca::div2) both the ranges have at least one non-obs revision. => (this is the case which we don't handle yet, but the solution would be to prompt the user to choose an evolve destination.) 4) both are in the parent-child relation => (both are merged and new cset will be based on the successor of `tca`) Changes in test-evolve-issue5958.t demonstrate that new code also covered case4 because in a resolution of "two divergent csets with parent-child relation" there should be one cset as a result and no orphan revs (as you can see there was an orphan before this patch).

"""enable a minimal verison of topic for server

! This extensions is not actively maintained
! We recommand using the main topic extension instead

Non publishing repository will see topic as "branch:topic" in the branch field.

In addition to adding the extensions, the feature must be manually enabled in the config:

    [experimental]
    server-mini-topic = yes
"""
import hashlib
import contextlib

from mercurial import (
    branchmap,
    context,
    encoding,
    extensions,
    node,
    registrar,
    util,
)

# hg <= 4.5 (b4d85bc122bd)
try:
    from mercurial import wireproto  # pytype: disable=import-error
    wireproto.branchmap
except ImportError:
    from mercurial import wireprotov1server as wireproto

if util.safehasattr(registrar, 'configitem'):

    configtable = {}
    configitem = registrar.configitem(configtable)
    configitem(b'experimental', b'server-mini-topic',
               default=False,
    )

def hasminitopic(repo):
    """true if minitopic is enabled on the repository

    (The value is cached on the repository)
    """
    enabled = getattr(repo, '_hasminitopic', None)
    if enabled is None:
        enabled = (repo.ui.configbool(b'experimental', b'server-mini-topic')
                   and not repo.publishing())
        repo._hasminitopic = enabled
    return enabled

### make topic visible though "ctx.branch()"

def topicbranch(orig, self):
    branch = orig(self)
    if hasminitopic(self._repo) and self.phase():
        topic = self._changeset.extra.get(b'topic')
        if topic is not None:
            topic = encoding.tolocal(topic)
            branch = b'%s:%s' % (branch, topic)
    return branch

### avoid caching topic data in rev-branch-cache

class revbranchcacheoverlay(object):
    """revbranch mixin that don't use the cache for non public changeset"""

    def _init__(self, *args, **kwargs):
        super(revbranchcacheoverlay, self).__init__(*args, **kwargs)
        if r'branchinfo' in vars(self):
            del self.branchinfo

    def branchinfo(self, rev, changelog=None):
        """return branch name and close flag for rev, using and updating
        persistent cache."""
        phase = self._repo._phasecache.phase(self._repo, rev)
        if phase:
            ctx = self._repo[rev]
            return ctx.branch(), ctx.closesbranch()
        return super(revbranchcacheoverlay, self).branchinfo(rev)

def reposetup(ui, repo):
    """install a repo class with a special revbranchcache"""

    if hasminitopic(repo):
        repo = repo.unfiltered()

        class minitopicrepo(repo.__class__):
            """repository subclass that install the modified cache"""

            def revbranchcache(self):
                if self._revbranchcache is None:
                    cache = super(minitopicrepo, self).revbranchcache()

                    class topicawarerbc(revbranchcacheoverlay, cache.__class__):
                        pass
                    cache.__class__ = topicawarerbc
                    if r'branchinfo' in vars(cache):
                        del cache.branchinfo
                    self._revbranchcache = cache
                return self._revbranchcache

        repo.__class__ = minitopicrepo

### topic aware branch head cache

def _phaseshash(repo, maxrev):
    """uniq ID for a phase matching a set of rev"""
    revs = set()
    cl = repo.changelog
    fr = cl.filteredrevs
    nm = cl.nodemap
    for roots in repo._phasecache.phaseroots[1:]:
        for n in roots:
            r = nm.get(n)
            if r not in fr and r < maxrev:
                revs.add(r)
    key = node.nullid
    revs = sorted(revs)
    if revs:
        s = hashlib.sha1()
        for rev in revs:
            s.update(b'%d;' % rev)
        key = s.digest()
    return key

# needed to prevent reference used for 'super()' call using in branchmap.py to
# no go into cycle. (yes, URG)
_oldbranchmap = branchmap.branchcache

@contextlib.contextmanager
def oldbranchmap():
    previous = branchmap.branchcache
    try:
        branchmap.branchcache = _oldbranchmap
        yield
    finally:
        branchmap.branchcache = previous

_publiconly = set([
    b'base',
    b'immutable',
])

def mighttopic(repo):
    return hasminitopic(repo) and repo.filtername not in _publiconly

class _topiccache(branchmap.branchcache): # combine me with branchmap.branchcache
    @classmethod
    def fromfile(cls, repo):
        orig = super(_topiccache, cls).fromfile
        return wrapread(orig, repo)

    def __init__(self, *args, **kwargs):
        # super() call may fail otherwise
        with oldbranchmap():
            super(_topiccache, self).__init__(*args, **kwargs)
        self.phaseshash = None

    def copy(self):
        """return an deep copy of the branchcache object"""
        if util.safehasattr(self, '_entries'):
            _entries = self._entries
        else:
            # hg <= 4.9 (624d6683c705, b137a6793c51)
            _entries = self
        new = self.__class__(_entries, self.tipnode, self.tiprev,
                             self.filteredhash, self._closednodes)
        new.phaseshash = self.phaseshash
        return new

    def validfor(self, repo):
        """Is the cache content valid regarding a repo

        - False when cached tipnode is unknown or if we detect a strip.
        - True when cache is up to date or a subset of current repo."""
        valid = super(_topiccache, self).validfor(repo)
        if not valid:
            return False
        elif self.phaseshash is None:
            # phasehash at None means this is a branchmap
            # coming from a public only set
            return True
        else:
            try:
                valid = self.phaseshash == _phaseshash(repo, self.tiprev)
                return valid
            except IndexError:
                return False

    def write(self, repo):
        # we expect (hope) mutable set to be small enough to be that computing
        # it all the time will be fast enough
        if not mighttopic(repo):
            super(_topiccache, self).write(repo)

    def update(self, repo, revgen):
        """Given a branchhead cache, self, that may have extra nodes or be
        missing heads, and a generator of nodes that are strictly a superset of
        heads missing, this function updates self to be correct.
        """
        super(_topiccache, self).update(repo, revgen)
        if mighttopic(repo):
            self.phaseshash = _phaseshash(repo, self.tiprev)

def wrapread(orig, repo):
    # Avoiding to write cache for filter where topic applies is a good step,
    # but we need to also avoid reading it. Existing branchmap cache might
    # exists before the turned the feature on.
    if mighttopic(repo):
        return None
    return orig(repo)

# advertise topic capabilities

def wireprotocaps(orig, repo, proto):
    caps = orig(repo, proto)
    if hasminitopic(repo):
        caps.append(b'topics')
    return caps

# wrap the necessary bit

def wrapclass(container, oldname, new):
    old = getattr(container, oldname)
    if not issubclass(old, new):
        targetclass = new
        # check if someone else already wrapped the class and handle that
        if not issubclass(new, old):
            class targetclass(new, old):
                pass
        setattr(container, oldname, targetclass)
    current = getattr(container, oldname)
    assert issubclass(current, new), (current, new, targetclass)

def uisetup(ui):
    wrapclass(branchmap, 'branchcache', _topiccache)
    try:
        # Mercurial 4.8 and older
        extensions.wrapfunction(branchmap, 'read', wrapread)
    except AttributeError:
        # Mercurial 4.9; branchcache.fromfile now takes care of this
        # which is alredy defined on _topiccache
        pass
    extensions.wrapfunction(wireproto, '_capabilities', wireprotocaps)
    extensions.wrapfunction(context.changectx, 'branch', topicbranch)