topic: introduce a minimal extensions to enable topic on the server
authorPierre-Yves David <pierre-yves.david@octobus.net>
Wed, 15 Nov 2017 08:00:17 +0100
changeset 3206 3ccde4699cf0
parent 3205 0b85da2e8e2a
child 3207 35c79686a635
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.
CHANGELOG
hgext3rd/serverminitopic.py
setup.py
tests/test-minitopic.t
--- a/CHANGELOG	Sat Nov 25 17:47:02 2017 -0500
+++ b/CHANGELOG	Wed Nov 15 08:00:17 2017 +0100
@@ -6,6 +6,11 @@
 
   * verbosity: respect --quiet for prev, next and summary
 
+topic (0.6.0)
+
+  * add a new 'serverminitopic' extension for minimal server support
+    (see `hg help -e serverminitopic` for details)
+
 7.0.1 -- 2017-11-14
 -------------------
 
--- /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)
--- a/setup.py	Sat Nov 25 17:47:02 2017 -0500
+++ b/setup.py	Wed Nov 15 08:00:17 2017 +0100
@@ -19,6 +19,7 @@
     return get_metadata()['minimumhgversion']
 
 py_modules = [
+    'hgext3rd.serverminitopic',
 ]
 py_packages = [
     'hgext3rd',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-minitopic.t	Wed Nov 15 08:00:17 2017 +0100
@@ -0,0 +1,238 @@
+  $ . $TESTDIR/testlib/common.sh
+
+setup
+  $ cat >> $HGRCPATH << EOF
+  > [extensions]
+  > share=
+  > blackbox=
+  > [web]
+  > allow_push = *
+  > push_ssl = no
+  > [phases]
+  > publish = False
+  > [paths]
+  > enabled = http://localhost:$HGPORT/
+  > disabled = http://localhost:$HGPORT2/
+  > EOF
+
+  $ hg init ./server-enabled
+  $ cat >> server-enabled/.hg/hgrc << EOF
+  > [extensions]
+  > serverminitopic=
+  > [experimental]
+  > server-mini-topic = yes
+  > EOF
+
+  $ hg share ./server-enabled ./server-disabled
+  updating working directory
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cat >> server-disabled/.hg/hgrc << EOF
+  > [extensions]
+  > serverminitopic=
+  > [experimental]
+  > server-mini-topic = no
+  > EOF
+
+  $ hg init client-disabled
+  $ hg init client-enabled
+  $ cat >> client-enabled/.hg/hgrc << EOF
+  > [extensions]
+  > topic=
+  > EOF
+
+  $ hg serve -R server-enabled -p $HGPORT -d --pid-file hg1.pid --errorlog hg1.error
+  $ cat hg1.pid > $DAEMON_PIDS
+  $ hg serve -R server-disabled -p $HGPORT2 -d --pid-file hg2.pid --errorlog hg2.error
+  $ cat hg2.pid >> $DAEMON_PIDS
+
+  $ curl --silent http://localhost:$HGPORT/?cmd=capabilities | grep -o topics
+  topics
+  $ curl --silent http://localhost:$HGPORT2/?cmd=capabilities | grep -o topics
+  [1]
+
+Pushing first changesets to the servers
+--------------------------------------
+
+  $ cd client-enabled
+  $ mkcommit c_A0
+  $ hg push enabled
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  $ mkcommit c_B0
+  $ hg push disabled
+  pushing to http://localhost:$HGPORT2/
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+
+  $ cat $TESTTMP/hg1.error
+  $ cat $TESTTMP/hg2.error
+
+Pushing new head
+----------------
+
+  $ hg up 'desc("c_A0")'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit c_C0
+  created new head
+  $ hg push enabled
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  abort: push creates new remote head 22c9514ed811!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+  $ hg push disabled
+  pushing to http://localhost:$HGPORT2/
+  searching for changes
+  abort: push creates new remote head 22c9514ed811!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+
+  $ curl --silent http://localhost:$HGPORT/?cmd=branchmap | sort
+  default 0ab6d544d0efd629fda056601cfe95e73d1af210
+  $ curl --silent http://localhost:$HGPORT2/?cmd=branchmap | sort
+  default 0ab6d544d0efd629fda056601cfe95e73d1af210
+  $ cat $TESTTMP/hg1.error
+  $ cat $TESTTMP/hg2.error
+
+Pushing new topic
+-----------------
+
+  $ hg merge
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ mkcommit c_D0
+  $ hg log -G
+  @    changeset:   3:9c660cf97499
+  |\   tag:         tip
+  | |  parent:      2:22c9514ed811
+  | |  parent:      1:0ab6d544d0ef
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     c_D0
+  | |
+  | o  changeset:   2:22c9514ed811
+  | |  parent:      0:14faebcf9752
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     c_C0
+  | |
+  o |  changeset:   1:0ab6d544d0ef
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     c_B0
+  |
+  o  changeset:   0:14faebcf9752
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     c_A0
+  
+  $ hg push enabled
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 2 changesets with 2 changes to 2 files
+  $ hg up 'desc("c_C0")'
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg topic topic_A
+  marked working directory as topic: topic_A
+  $ mkcommit c_E0
+  active topic 'topic_A' grew its first changeset
+  $ hg push disabled
+  pushing to http://localhost:$HGPORT2/
+  searching for changes
+  abort: push creates new remote head f31af349535e!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+  $ hg push enabled
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files (+1 heads)
+
+  $ curl --silent http://localhost:$HGPORT/?cmd=branchmap | sort
+  default 9c660cf97499ae01ccb6894880455c6ffa4b19cf
+  default%3Atopic_A f31af349535e413b6023f11b51a6afccf4139180
+  $ curl --silent http://localhost:$HGPORT2/?cmd=branchmap | sort
+  default 9c660cf97499ae01ccb6894880455c6ffa4b19cf f31af349535e413b6023f11b51a6afccf4139180
+  $ cat $TESTTMP/hg1.error
+  $ cat $TESTTMP/hg2.error
+
+Pushing new head to a topic
+---------------------------
+
+  $ hg up 'desc("c_D0")'
+  2 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg topic topic_A
+  marked working directory as topic: topic_A
+  $ mkcommit c_F0
+  $ hg log -G
+  @  changeset:   5:82c5842e0472
+  |  tag:         tip
+  |  topic:       topic_A
+  |  parent:      3:9c660cf97499
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     c_F0
+  |
+  | o  changeset:   4:f31af349535e
+  | |  topic:       topic_A
+  | |  parent:      2:22c9514ed811
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     c_E0
+  | |
+  o |  changeset:   3:9c660cf97499
+  |\|  parent:      2:22c9514ed811
+  | |  parent:      1:0ab6d544d0ef
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     c_D0
+  | |
+  | o  changeset:   2:22c9514ed811
+  | |  parent:      0:14faebcf9752
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     c_C0
+  | |
+  o |  changeset:   1:0ab6d544d0ef
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     c_B0
+  |
+  o  changeset:   0:14faebcf9752
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     c_A0
+  
+  $ hg push enabled
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  abort: push creates new remote head 82c5842e0472 on branch 'default:topic_A'!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+  $ hg push disabled
+  pushing to http://localhost:$HGPORT2/
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+
+  $ curl --silent http://localhost:$HGPORT/?cmd=branchmap | sort
+  default 9c660cf97499ae01ccb6894880455c6ffa4b19cf
+  default%3Atopic_A f31af349535e413b6023f11b51a6afccf4139180 82c5842e047215160763f81ae93ae42c65b20a63
+  $ curl --silent http://localhost:$HGPORT2/?cmd=branchmap | sort
+  default f31af349535e413b6023f11b51a6afccf4139180 82c5842e047215160763f81ae93ae42c65b20a63
+  $ cat $TESTTMP/hg1.error
+  $ cat $TESTTMP/hg2.error