# HG changeset patch # User Pierre-Yves David # Date 1510729217 -3600 # Node ID 3ccde4699cf05a02d191f74fc50ba98750fb2c96 # Parent 0b85da2e8e2a26dcf9eac8371535d9f2f68ee1eb 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. diff -r 0b85da2e8e2a -r 3ccde4699cf0 CHANGELOG --- 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 ------------------- diff -r 0b85da2e8e2a -r 3ccde4699cf0 hgext3rd/serverminitopic.py --- /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) diff -r 0b85da2e8e2a -r 3ccde4699cf0 setup.py --- 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', diff -r 0b85da2e8e2a -r 3ccde4699cf0 tests/test-minitopic.t --- /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