diff hgext3rd/serverminitopic.py @ 3206:3ccde4699cf0

topic: introduce a minimal extensions to enable topic on the server This small extensions simply expose topic in the branch. This should help getting minimal support for topic from various hosting solution (eg: bitbucket maybe ?) See extensions help for details.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Wed, 15 Nov 2017 08:00:17 +0100
parents
children 35c79686a635
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/serverminitopic.py	Wed Nov 15 08:00:17 2017 +0100
@@ -0,0 +1,217 @@
+"""enable a minimal verison of topic for server
+
+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,
+    wireproto,
+)
+
+if util.safehasattr(registrar, 'configitem'):
+
+    configtable = {}
+    configitem = registrar.configitem(configtable)
+    configitem('experimental', '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('experimental', 'server-mini-topic')
+                   and not repo.publishing())
+        repo._hasminitopic = enabled
+    return enabled
+
+### make topic visible though "ctx.branch()"
+
+class topicchangectx(context.changectx):
+    """a sunclass of changectx that add topic to the branch name"""
+
+    def branch(self):
+        branch = super(topicchangectx, self).branch()
+        if hasminitopic(self._repo) and self.phase():
+            topic = self._changeset.extra.get('topic')
+            if topic is not None:
+                topic = encoding.tolocal(topic)
+                branch = '%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 'branchinfo' in vars(self):
+            del self.branchinfo
+
+    def branchinfo(self, rev):
+        """return branch name and close flag for rev, using and updating
+        persistent cache."""
+        phase = self._repo._phasecache.phase(self, 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 '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('%s;' % 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([
+    'base',
+    'immutable',
+])
+
+def mighttopic(repo):
+    return hasminitopic(repo) and repo.filtername not in _publiconly
+
+class _topiccache(branchmap.branchcache): # combine me with branchmap.branchcache
+
+    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"""
+        new = self.__class__(self, 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 not mighttopic(repo) and 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)
+
+# advertise topic capabilities
+
+def wireprotocaps(orig, repo, proto):
+    caps = orig(repo, proto)
+    if hasminitopic(repo):
+        caps.append('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(context, 'changectx', topicchangectx)
+    wrapclass(branchmap, 'branchcache', _topiccache)
+    extensions.wrapfunction(wireproto, '_capabilities', wireprotocaps)