view hgext3rd/serverminitopic.py @ 6217:4466989cee8d

precheck: fix false warning about divergence creation, in hg prune In case of pruning with no successors, there is no chance for new divergence. It was happening becuase current precheck logic assumes that every revision being rewritten will be replaced with at least one successor. Changes in test file demonstrate the fixed bug.
author Sushil khanchi <sushilkhanchi97@gmail.com>
date Fri, 23 Jul 2021 00:34:29 +0530
parents 1b57516a9a56
children 1d553662fc1b
line wrap: on
line source

"""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,
)

from mercurial import wireprotov1server

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

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

# nodemap.get and index.[has_node|rev|get_rev]
# hg <= 5.2 (02802fa87b74)
def getgetrev(cl):
    """Returns index.get_rev or nodemap.get (for pre-5.3 Mercurial)."""
    if util.safehasattr(cl.index, 'get_rev'):
        return cl.index.get_rev
    return cl.nodemap.get

# hg <= 5.4 (e2d17974a869)
def nonpublicphaseroots(repo):
    if util.safehasattr(repo._phasecache, 'nonpublicphaseroots'):
        return repo._phasecache.nonpublicphaseroots(repo)
    return set().union(
        *[roots for roots in repo._phasecache.phaseroots[1:] if roots]
    )

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
    getrev = getgetrev(cl)
    for n in nonpublicphaseroots(repo):
        r = getrev(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
        args = (_entries, self.tipnode, self.tiprev, self.filteredhash,
                self._closednodes)
        if util.safehasattr(self, '_repo'):
            # hg <= 5.7 (6266d19556ad)
            args = (self._repo,) + args
        new = self.__class__(*args)
        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:
        # hg <= 4.9 (3461814417f3)
        extensions.wrapfunction(branchmap, 'read', wrapread)
    except AttributeError:
        # Mercurial 5.0; branchcache.fromfile now takes care of this
        # which is alredy defined on _topiccache
        pass
    extensions.wrapfunction(wireprotov1server, '_capabilities', wireprotocaps)
    extensions.wrapfunction(context.changectx, 'branch', topicbranch)