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.
--- 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