topic: merge the topic extension in the evolve repository
authorPierre-Yves David <pierre-yves.david@ens-lyon.org>
Thu, 02 Mar 2017 18:07:46 +0100
changeset 2020 143c8e4dc22d
parent 2019 996a562b6c9f (diff)
parent 1838 6942750831bb (current diff)
child 2021 e6db5d48ebc5
topic: merge the topic extension in the evolve repository There is a lot of synergy between the two concepts. Topic is expected to be able to smooth multiple of evolution sharp edge. Having them both in the same repository will make this collaboration easier.
.hgignore
COPYING
Makefile
README
README.md
hgext3rd/__init__.py
setup.cfg
setup.py
tests/test-check-pyflakes.t
tests/test-evolve-topic.t
--- a/COPYING	Tue Feb 28 18:21:23 2017 +0100
+++ b/COPYING	Thu Mar 02 18:07:46 2017 +0100
@@ -1,12 +1,12 @@
-		    GNU GENERAL PUBLIC LICENSE
-		       Version 2, June 1991
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
 
  Copyright (C) 1989, 1991 Free Software Foundation, Inc.
-	51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+        51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  Everyone is permitted to copy and distribute verbatim copies
  of this license document, but changing it is not allowed.
 
-			    Preamble
+                            Preamble
 
   The licenses for most software are designed to take away your
 freedom to share and change it.  By contrast, the GNU General Public
@@ -55,8 +55,8 @@
 
   The precise terms and conditions for copying, distribution and
 modification follow.
-
-		    GNU GENERAL PUBLIC LICENSE
+
+                    GNU GENERAL PUBLIC LICENSE
    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 
   0. This License applies to any program or other work which contains
@@ -110,7 +110,7 @@
     License.  (Exception: if the Program itself is interactive but
     does not normally print such an announcement, your work based on
     the Program is not required to print an announcement.)
-
+
 These requirements apply to the modified work as a whole.  If
 identifiable sections of that work are not derived from the Program,
 and can be reasonably considered independent and separate works in
@@ -168,7 +168,7 @@
 access to copy the source code from the same place counts as
 distribution of the source code, even though third parties are not
 compelled to copy the source along with the object code.
-
+
   4. You may not copy, modify, sublicense, or distribute the Program
 except as expressly provided under this License.  Any attempt
 otherwise to copy, modify, sublicense or distribute the Program is
@@ -225,7 +225,7 @@
 
 This section is intended to make thoroughly clear what is believed to
 be a consequence of the rest of this License.
-
+
   8. If the distribution and/or use of the Program is restricted in
 certain countries either by patents or by copyrighted interfaces, the
 original copyright holder who places the Program under this License
@@ -255,7 +255,7 @@
 of preserving the free status of all derivatives of our free software and
 of promoting the sharing and reuse of software generally.
 
-			    NO WARRANTY
+                            NO WARRANTY
 
   11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
@@ -277,9 +277,9 @@
 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGES.
 
-		     END OF TERMS AND CONDITIONS
-
-	    How to Apply These Terms to Your New Programs
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
 
   If you develop a new program, and you want it to be of the greatest
 possible use to the public, the best way to achieve this is to make it
@@ -313,7 +313,7 @@
 If the program is interactive, make it output a short notice like this
 when it starts in an interactive mode:
 
-    Gnomovision version 69, Copyright (C) year  name of author
+    Gnomovision version 69, Copyright (C) year name of author
     Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
     This is free software, and you are welcome to redistribute it
     under certain conditions; type `show c' for details.
--- a/Makefile	Tue Feb 28 18:21:23 2017 +0100
+++ b/Makefile	Thu Mar 02 18:07:46 2017 +0100
@@ -15,3 +15,39 @@
 	mv hg-evolve-$(VERSION) ../mercurial-evolve_$(VERSION).orig
 	cp -r debian/ ../mercurial-evolve_$(VERSION).orig/
 	@cd ../mercurial-evolve_$(VERSION).orig && echo 'debian build directory ready at' `pwd`
+
+# test targets
+
+PYTHON=python
+ifeq ($(HGROOT),)
+  $(error HGROOT is not set to the root of the hg source tree)
+endif
+TESTFLAGS ?= $(shell echo $$HGTESTFLAGS)
+
+HGTESTS=$(HGROOT)/tests
+
+help:
+	@echo 'Commonly used make targets:'
+	@echo '  tests              - run all tests in the automatic test suite'
+	@echo '  all-version-tests - run all tests against many hg versions'
+	@echo '  tests-%s           - run all tests in the specified hg version'
+
+all: help
+
+tests:
+	cd tests && $(PYTHON) $(HGTESTS)/run-tests.py $(TESTFLAGS)
+
+# /!\ run outside of the compatibility branch output test will likely fails
+
+test-%:
+	cd tests && $(PYTHON) $(HGTESTS)/run-tests.py $(TESTFLAGS) $@
+
+tests-%:
+	hg -R $(HGROOT) checkout $$(echo $@ | sed s/tests-//) && \
+	(cd $(HGROOT) ; $(MAKE) clean ) && \
+	cd tests && $(PYTHON) $(HGTESTS)/run-tests.py $(TESTFLAGS)
+
+# build a script to extract declared version
+all-version-tests: tests-@
+
+.PHONY: tests all-version-tests
--- a/README	Tue Feb 28 18:21:23 2017 +0100
+++ b/README	Thu Mar 02 18:07:46 2017 +0100
@@ -25,6 +25,29 @@
 
 Or see the ``doc/`` directory for a local copy.
 
+topic
+=====
+
+Topics are an experiment to see if maybe the workflow defined by git
+branches and hg bookmarks is only partially what users want - perhaps
+something that feels more like a traditional VCS branch is right, but
+that it should "dissolve" upon being finished. This extension exists
+to be a sandbox for that experimentation.
+
+# install
+
+Enable topics like any mercurial extension: download the source code to a
+local directory, and add that directory to your `.hgrc`:
+
+    [extensions]
+    topics=PATH/TO/evolve-main/hgext3rd/topic/
+
+# help
+
+See 'hg help -e topic' for a generic help.
+See 'hg help topics' and 'hg help stack' for help on specific commands.
+See the 'tests/test-topic-tutorial.t' file for a quick tutorial.
+
 Contribute
 ==========
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/__init__.py	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,475 @@
+# __init__.py - topic extension
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+"""support for topic branches
+
+Topic branches are lightweight branches which disappear when changes are
+finalized (move to the public phase).
+
+Compared to bookmark, topic is reference carried by each changesets of the
+series instead of just the single head revision.  Topic are quite similar to
+the way named branch work, except they eventualy fade away when the changeset
+becomes part of the immutable history.  Changeset can below to both a topic and
+a named branch, but as long as it is mutable, its topic identity will prevail.
+As a result, default destination for 'update', 'merge', etc...  will take topic
+into account. When a topic is active these operations will only consider other
+changesets on that topic (and, in some occurence, bare changeset on same
+branch).  When no topic is active, changeset with topic will be ignored and
+only bare one on the same branch will be taken in account.
+
+There is currently two commands to be used with that extension: 'topics' and
+'stack'.
+
+The 'hg topics' command is used to set the current topic and list existing one.
+'hg topics --verbose' will list various information related to each topic.
+
+The 'stack' will show you in formation about the stack of commit belonging to
+your current topic.
+
+Topic is offering you aliases reference to changeset in your current topic
+stack as 't#'. For example, 't1' refers to the root of your stack, 't2' to the
+second commits, etc. The 'hg stack' command show these number.
+
+Push behavior will change a bit with topic. When pushing to a publishing
+repository the changesets will turn public and the topic data on them will fade
+away. The logic regarding pushing new heads will behave has before, ignore any
+topic related data. When pushing to a non-publishing repository (supporting
+topic), the head checking will be done taking topic data into account.
+Push will complain about multiple heads on a branch if you push multiple heads
+with no topic information on them (or multiple public heads). But pushing a new
+topic will not requires any specific flag. However, pushing multiple heads on a
+topic will be met with the usual warning.
+
+The 'evolve' extension takes 'topic' into account. 'hg evolve --all'
+will evolve all changesets in the active topic. In addition, by default. 'hg
+next' and 'hg prev' will stick to the current topic.
+
+Be aware that this extension is still an experiment, commands and other features
+are likely to be change/adjusted/dropped over time as we refine the concept.
+"""
+
+from __future__ import absolute_import
+
+import re
+
+from mercurial.i18n import _
+from mercurial import (
+    branchmap,
+    cmdutil,
+    commands,
+    context,
+    error,
+    extensions,
+    localrepo,
+    lock,
+    merge,
+    namespaces,
+    node,
+    obsolete,
+    patch,
+    phases,
+    util,
+)
+
+from . import (
+    constants,
+    revset as topicrevset,
+    destination,
+    stack,
+    topicmap,
+    discovery,
+)
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+colortable = {'topic.active': 'green',
+              'topic.list.troubledcount': 'red',
+              'topic.list.headcount.multiple': 'yellow',
+              'topic.list.behindcount': 'cyan',
+              'topic.list.behinderror': 'red',
+              'topic.stack.index': 'yellow',
+              'topic.stack.index.base': 'none dim',
+              'topic.stack.desc.base': 'none dim',
+              'topic.stack.state.base': 'dim',
+              'topic.stack.state.clean': 'green',
+              'topic.stack.index.current': 'cyan',       # random pick
+              'topic.stack.state.current': 'cyan bold',  # random pick
+              'topic.stack.desc.current': 'cyan',        # random pick
+              'topic.stack.state.unstable': 'red',
+              'topic.stack.summary.behindcount': 'cyan',
+              'topic.stack.summary.behinderror': 'red',
+              'topic.stack.summary.headcount.multiple': 'yellow',
+             }
+
+testedwith = '3.9'
+
+def _contexttopic(self):
+    return self.extra().get(constants.extrakey, '')
+context.basectx.topic = _contexttopic
+
+topicrev = re.compile(r'^t\d+$')
+
+def _namemap(repo, name):
+    if topicrev.match(name):
+        idx = int(name[1:])
+        topic = repo.currenttopic
+        if not topic:
+            raise error.Abort(_('cannot resolve "%s": no active topic') % name)
+        revs = list(stack.getstack(repo, topic))
+        try:
+            r = revs[idx - 1]
+        except IndexError:
+            msg = _('cannot resolve "%s": topic "%s" has only %d changesets')
+            raise error.Abort(msg % (name, topic, len(revs)))
+        return [repo[r].node()]
+    if name not in repo.topics:
+        return []
+    return [ctx.node() for ctx in
+            repo.set('not public() and extra(topic, %s)', name)]
+
+def _nodemap(repo, node):
+    ctx = repo[node]
+    t = ctx.topic()
+    if t and ctx.phase() > phases.public:
+        return [t]
+    return []
+
+def uisetup(ui):
+    destination.modsetup(ui)
+    topicrevset.modsetup(ui)
+    discovery.modsetup(ui)
+    topicmap.modsetup(ui)
+    setupimportexport(ui)
+
+    extensions.afterloaded('rebase', _fixrebase)
+
+    entry = extensions.wrapcommand(commands.table, 'commit', commitwrap)
+    entry[1].append(('t', 'topic', '',
+                     _("use specified topic"), _('TOPIC')))
+
+    extensions.wrapfunction(cmdutil, 'buildcommittext', committextwrap)
+    extensions.wrapfunction(merge, 'update', mergeupdatewrap)
+    cmdutil.summaryhooks.add('topic', summaryhook)
+
+
+def reposetup(ui, repo):
+    orig = repo.__class__
+    if not isinstance(repo, localrepo.localrepository):
+        return # this can be a peer in the ssh case (puzzling)
+
+    class topicrepo(repo.__class__):
+
+        def _restrictcapabilities(self, caps):
+            caps = super(topicrepo, self)._restrictcapabilities(caps)
+            caps.add('topics')
+            return caps
+
+        def commit(self, *args, **kwargs):
+            backup = self.ui.backupconfig('ui', 'allowemptycommit')
+            try:
+                if repo.currenttopic != repo['.'].topic():
+                    # bypass the core "nothing changed" logic
+                    self.ui.setconfig('ui', 'allowemptycommit', True)
+                return orig.commit(self, *args, **kwargs)
+            finally:
+                self.ui.restoreconfig(backup)
+
+        def commitctx(self, ctx, error=None):
+            if isinstance(ctx, context.workingcommitctx):
+                current = self.currenttopic
+                if current:
+                    ctx.extra()[constants.extrakey] = current
+            if (isinstance(ctx, context.memctx) and
+                ctx.extra().get('amend_source') and
+                ctx.topic() and
+                not self.currenttopic):
+                # we are amending and need to remove a topic
+                del ctx.extra()[constants.extrakey]
+            with topicmap.usetopicmap(self):
+                return orig.commitctx(self, ctx, error=error)
+
+        @property
+        def topics(self):
+            if self._topics is not None:
+                return self._topics
+            topics = set(['', self.currenttopic])
+            for c in self.set('not public()'):
+                topics.add(c.topic())
+            topics.remove('')
+            self._topics = topics
+            return topics
+
+        @property
+        def currenttopic(self):
+            return self.vfs.tryread('topic')
+
+        def branchmap(self, topic=True):
+            if not topic:
+                super(topicrepo, self).branchmap()
+            with topicmap.usetopicmap(self):
+                branchmap.updatecache(self)
+            return self._topiccaches[self.filtername]
+
+        def destroyed(self, *args, **kwargs):
+            with topicmap.usetopicmap(self):
+                return super(topicrepo, self).destroyed(*args, **kwargs)
+
+        def invalidatevolatilesets(self):
+            # XXX we might be able to move this to something invalidated less often
+            super(topicrepo, self).invalidatevolatilesets()
+            self._topics = None
+            if '_topiccaches' in vars(self.unfiltered()):
+                self.unfiltered()._topiccaches.clear()
+
+        def peer(self):
+            peer = super(topicrepo, self).peer()
+            if getattr(peer, '_repo', None) is not None: # localpeer
+                class topicpeer(peer.__class__):
+                    def branchmap(self):
+                        usetopic = not self._repo.publishing()
+                        return self._repo.branchmap(topic=usetopic)
+                peer.__class__ = topicpeer
+            return peer
+
+    repo.__class__ = topicrepo
+    repo._topics = None
+    if util.safehasattr(repo, 'names'):
+        repo.names.addnamespace(namespaces.namespace(
+            'topics', 'topic', namemap=_namemap, nodemap=_nodemap,
+            listnames=lambda repo: repo.topics))
+
+@command('topics [TOPIC]', [
+        ('', 'clear', False, 'clear active topic if any'),
+        ('', 'change', '', 'revset of existing revisions to change topic'),
+        ('l', 'list', False, 'show the stack of changeset in the topic'),
+    ] + commands.formatteropts)
+def topics(ui, repo, topic='', clear=False, change=None, list=False, **opts):
+    """View current topic, set current topic, or see all topics.
+
+    The --verbose version of this command display various information on the state of each topic."""
+    if list:
+        if clear or change:
+            raise error.Abort(_("cannot use --clear or --change with --list"))
+        if not topic:
+            topic = repo.currenttopic
+        if not topic:
+            raise error.Abort(_('no active topic to list'))
+        return stack.showstack(ui, repo, topic, opts)
+
+    if change:
+        if not obsolete.isenabled(repo, obsolete.createmarkersopt):
+            raise error.Abort(_('must have obsolete enabled to use --change'))
+        if not topic and not clear:
+            raise error.Abort('changing topic requires a topic name or --clear')
+        if any(not c.mutable() for c in repo.set('%r and public()', change)):
+            raise error.Abort("can't change topic of a public change")
+        rewrote = 0
+        needevolve = False
+        l = repo.lock()
+        txn = repo.transaction('rewrite-topics')
+        try:
+            for c in repo.set('%r', change):
+                def filectxfn(repo, ctx, path):
+                    try:
+                        return c[path]
+                    except error.ManifestLookupError:
+                        return None
+                fixedextra = dict(c.extra())
+                ui.debug('old node id is %s\n' % node.hex(c.node()))
+                ui.debug('origextra: %r\n' % fixedextra)
+                newtopic = None if clear else topic
+                oldtopic = fixedextra.get(constants.extrakey, None)
+                if oldtopic == newtopic:
+                    continue
+                if clear:
+                    del fixedextra[constants.extrakey]
+                else:
+                    fixedextra[constants.extrakey] = topic
+                if 'amend_source' in fixedextra:
+                    # TODO: right now the commitctx wrapper in
+                    # topicrepo overwrites the topic in extra if
+                    # amend_source is set to support 'hg commit
+                    # --amend'. Support for amend should be adjusted
+                    # to not be so invasive.
+                    del fixedextra['amend_source']
+                ui.debug('changing topic of %s from %s to %s\n' % (
+                    c, oldtopic, newtopic))
+                ui.debug('fixedextra: %r\n' % fixedextra)
+                mc = context.memctx(
+                    repo, (c.p1().node(), c.p2().node()), c.description(),
+                    c.files(), filectxfn,
+                    user=c.user(), date=c.date(), extra=fixedextra)
+                newnode = repo.commitctx(mc)
+                ui.debug('new node id is %s\n' % node.hex(newnode))
+                needevolve = needevolve or (len(c.children()) > 0)
+                obsolete.createmarkers(repo, [(c, (repo[newnode],))])
+                rewrote += 1
+            txn.close()
+        except:
+            try:
+                txn.abort()
+            finally:
+                repo.invalidate()
+            raise
+        finally:
+            lock.release(txn, l)
+        ui.status('changed topic on %d changes\n' % rewrote)
+        if needevolve:
+            evolvetarget = 'topic(%s)' % topic if topic else 'not topic()'
+            ui.status('please run hg evolve --rev "%s" now\n' % evolvetarget)
+    if clear:
+        if repo.vfs.exists('topic'):
+            repo.vfs.unlink('topic')
+        return
+    if topic:
+        with repo.wlock():
+            with repo.vfs.open('topic', 'w') as f:
+                f.write(topic)
+        return
+    _listtopics(ui, repo, opts)
+
+@command('stack [TOPIC]', [] + commands.formatteropts)
+def cmdstack(ui, repo, topic='', **opts):
+    """list all changesets in a topic and other information
+
+    List the current topic by default."""
+    if not topic:
+        topic = repo.currenttopic
+    if not topic:
+        raise error.Abort(_('no active topic to list'))
+    return stack.showstack(ui, repo, topic, opts)
+
+def _listtopics(ui, repo, opts):
+    fm = ui.formatter('bookmarks', opts)
+    activetopic = repo.currenttopic
+    namemask = '%s'
+    if repo.topics and ui.verbose:
+        maxwidth = max(len(t) for t in repo.topics)
+        namemask = '%%-%is' % maxwidth
+    for topic in sorted(repo.topics):
+        fm.startitem()
+        marker = ' '
+        label = 'topic'
+        active = (topic == activetopic)
+        if active:
+            marker = '*'
+            label = 'topic.active'
+        if not ui.quiet:
+            # registering the active data is made explicitly later
+            fm.plain(' %s ' % marker, label=label)
+        fm.write('topic', namemask, topic, label=label)
+        fm.data(active=active)
+        if ui.verbose:
+            # XXX we should include the data even when not verbose
+            data = stack.stackdata(repo, topic)
+            fm.plain(' (')
+            fm.write('branches+', 'on branch: %s',
+                     '+'.join(data['branches']), # XXX use list directly after 4.0 is released
+                     label='topic.list.branches')
+            fm.plain(', ')
+            fm.write('changesetcount', '%d changesets', data['changesetcount'],
+                     label='topic.list.changesetcount')
+            if data['troubledcount']:
+                fm.plain(', ')
+                fm.write('troubledcount', '%d troubled',
+                         data['troubledcount'],
+                         label='topic.list.troubledcount')
+            if 1 < data['headcount']:
+                fm.plain(', ')
+                fm.write('headcount', '%d heads',
+                         data['headcount'],
+                         label='topic.list.headcount.multiple')
+            if 0 < data['behindcount']:
+                fm.plain(', ')
+                fm.write('behindcount', '%d behind',
+                         data['behindcount'],
+                         label='topic.list.behindcount')
+            elif -1 == data['behindcount']:
+                fm.plain(', ')
+                fm.write('behinderror', '%s',
+                         _('ambiguous destination'),
+                         label='topic.list.behinderror')
+            fm.plain(')')
+        fm.plain('\n')
+    fm.end()
+
+def summaryhook(ui, repo):
+    t = repo.currenttopic
+    if not t:
+        return
+    # i18n: column positioning for "hg summary"
+    ui.write(_("topic:  %s\n") % ui.label(t, 'topic.active'))
+
+def commitwrap(orig, ui, repo, *args, **opts):
+    with repo.wlock():
+        if opts.get('topic'):
+            t = opts['topic']
+            with repo.vfs.open('topic', 'w') as f:
+                f.write(t)
+        return orig(ui, repo, *args, **opts)
+
+def committextwrap(orig, repo, ctx, subs, extramsg):
+    ret = orig(repo, ctx, subs, extramsg)
+    t = repo.currenttopic
+    if t:
+        ret = ret.replace("\nHG: branch",
+                          "\nHG: topic '%s'\nHG: branch" % t)
+    return ret
+
+def mergeupdatewrap(orig, repo, node, branchmerge, force, *args, **kwargs):
+    matcher = kwargs.get('matcher')
+    partial = not (matcher is None or matcher.always())
+    wlock = repo.wlock()
+    try:
+        ret = orig(repo, node, branchmerge, force, *args, **kwargs)
+        if not partial and not branchmerge:
+            ot = repo.currenttopic
+            t = ''
+            pctx = repo[node]
+            if pctx.phase() > phases.public:
+                t = pctx.topic()
+            with repo.vfs.open('topic', 'w') as f:
+                f.write(t)
+            if t and t != ot:
+                repo.ui.status(_("switching to topic %s\n") % t)
+        return ret
+    finally:
+        wlock.release()
+
+def _fixrebase(loaded):
+    if not loaded:
+        return
+
+    def savetopic(ctx, extra):
+        if ctx.topic():
+            extra[constants.extrakey] = ctx.topic()
+
+    def newmakeextrafn(orig, copiers):
+        return orig(copiers + [savetopic])
+
+    try:
+        rebase = extensions.find("rebase")
+        extensions.wrapfunction(rebase, '_makeextrafn', newmakeextrafn)
+    except KeyError:
+        pass
+
+## preserve topic during import/export
+
+def _exporttopic(seq, ctx):
+    topic = ctx.topic()
+    if topic:
+        return 'EXP-Topic %s' % topic
+    return None
+
+def _importtopic(repo, patchdata, extra, opts):
+    if 'topic' in patchdata:
+        extra['topic'] = patchdata['topic']
+
+def setupimportexport(ui):
+    """run at ui setup time to install import/export logic"""
+    cmdutil.extraexport.append('topic')
+    cmdutil.extraexportmap['topic'] = _exporttopic
+    cmdutil.extrapreimport.append('topic')
+    cmdutil.extrapreimportmap['topic'] = _importtopic
+    patch.patchheadermap.append(('EXP-Topic', 'topic'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/constants.py	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,1 @@
+extrakey = 'topic'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/destination.py	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,118 @@
+from __future__ import absolute_import
+
+from mercurial.i18n import _
+from mercurial import (
+    bookmarks,
+    destutil,
+    error,
+    extensions,
+    util,
+)
+from . import topicmap
+from .evolvebits import builddependencies
+
+def _destmergebranch(orig, repo, action='merge', sourceset=None,
+                     onheadcheck=True, destspace=None):
+    # XXX: take destspace into account
+    if sourceset is None:
+        p1 = repo['.']
+    else:
+        # XXX: using only the max here is flacky. That code should eventually
+        # be updated to take care of the whole sourceset.
+        p1 = repo[max(sourceset)]
+    top = p1.topic()
+    if top:
+        revs = repo.revs('topic(%s) - obsolete()', top)
+        deps, rdeps = builddependencies(repo, revs)
+        heads = [r for r in revs if not rdeps[r]]
+        if onheadcheck and p1.rev() not in heads:
+            raise error.Abort(_("not at topic head, update or explicit"))
+
+        # prune heads above the source
+        otherheads = set(heads)
+        pool = set([p1.rev()])
+        while pool:
+            current = pool.pop()
+            otherheads.discard(current)
+            pool.update(rdeps[current])
+        if not otherheads:
+            # nothing to do at the topic level
+            bhead = ngtip(repo, p1.branch(), all=True)
+            if not bhead:
+                raise error.NoMergeDestAbort(_("nothing to merge"))
+            elif 1 == len(bhead):
+                return bhead[0]
+            else:
+                msg = _("branch '%s' has %d heads "
+                        "- please merge with an explicit rev")
+                hint = _("run 'hg heads .' to see heads")
+                raise error.ManyMergeDestAbort(msg % (p1.branch(), len(bhead)),
+                                               hint=hint)
+        elif len(otherheads) == 1:
+            return otherheads.pop()
+        else:
+            msg = _("topic '%s' has %d heads "
+                    "- please merge with an explicit rev") % (top, len(heads))
+            raise error.ManyMergeDestAbort(msg)
+    if len(getattr(orig, 'func_defaults', ())) == 3: # version hg-3.7
+        return orig(repo, action, sourceset, onheadcheck)
+    if 3 < len(getattr(orig, 'func_defaults', ())): # version hg-3.8 and above
+        return orig(repo, action, sourceset, onheadcheck, destspace=destspace)
+    else:
+        return orig(repo)
+
+def _destupdatetopic(repo, clean, check=None):
+    """decide on an update destination from current topic"""
+    movemark = node = None
+    topic = repo.currenttopic
+    revs = repo.revs('.::topic("%s")' % topic)
+    if not revs:
+        return None, None, None
+    node = revs.last()
+    if bookmarks.isactivewdirparent(repo):
+        movemark = repo['.'].node()
+    return node, movemark, None
+
+def desthistedit(orig, ui, repo):
+    if not (ui.config('histedit', 'defaultrev', None) is None
+            and repo.currenttopic):
+        return orig(ui, repo)
+    revs = repo.revs('::. and stack()')
+    if revs:
+        return revs.min()
+    return None
+
+def ngtip(repo, branch, all=False):
+    """tip new generation"""
+    ## search for untopiced heads of branch
+    # could be heads((::branch(x) - topic()))
+    # but that is expensive
+    #
+    # we should write plain code instead
+    with topicmap.usetopicmap(repo):
+        tmap = repo.branchmap()
+        if branch not in tmap:
+            return []
+        elif all:
+            return tmap.branchheads(branch)
+        else:
+            return [tmap.branchtip(branch)]
+
+def modsetup(ui):
+    """run a uisetup time to install all destinations wrapping"""
+    if util.safehasattr(destutil, '_destmergebranch'):
+        extensions.wrapfunction(destutil, '_destmergebranch', _destmergebranch)
+    try:
+        rebase = extensions.find('rebase')
+    except KeyError:
+        rebase = None
+    if (util.safehasattr(rebase, '_destrebase')
+            # logic not shared with merge yet < hg-3.8
+            and not util.safehasattr(rebase, '_definesets')):
+        extensions.wrapfunction(rebase, '_destrebase', _destmergebranch)
+    if util.safehasattr(destutil, 'destupdatesteps'):
+        bridx = destutil.destupdatesteps.index('branch')
+        destutil.destupdatesteps.insert(bridx, 'topic')
+        destutil.destupdatestepmap['topic'] = _destupdatetopic
+    if util.safehasattr(destutil, 'desthistedit'):
+        extensions.wrapfunction(destutil, 'desthistedit', desthistedit)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/discovery.py	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,135 @@
+from __future__ import absolute_import
+
+import weakref
+
+from mercurial.i18n import _
+from mercurial import (
+    branchmap,
+    bundle2,
+    discovery,
+    error,
+    exchange,
+    extensions,
+    wireproto,
+)
+
+from . import topicmap
+
+def _headssummary(orig, repo, remote, outgoing):
+    publishing = ('phases' not in remote.listkeys('namespaces')
+                  or bool(remote.listkeys('phases').get('publishing', False)))
+    if publishing or not remote.capable('topics'):
+        return orig(repo, remote, outgoing)
+    oldrepo = repo.__class__
+    oldbranchcache = branchmap.branchcache
+    oldfilename = branchmap._filename
+    try:
+        class repocls(repo.__class__):
+            def __getitem__(self, key):
+                ctx = super(repocls, self).__getitem__(key)
+                oldbranch = ctx.branch
+
+                def branch():
+                    branch = oldbranch()
+                    topic = ctx.topic()
+                    if topic:
+                        branch = "%s:%s" % (branch, topic)
+                    return branch
+
+                ctx.branch = branch
+                return ctx
+
+        repo.__class__ = repocls
+        branchmap.branchcache = topicmap.topiccache
+        branchmap._filename = topicmap._filename
+        summary = orig(repo, remote, outgoing)
+        for key, value in summary.iteritems():
+            if ':' in key: # This is a topic
+                if value[0] is None and value[1]:
+                    summary[key] = ([value[1].pop(0)], ) + value[1:]
+        return summary
+    finally:
+        repo.__class__ = oldrepo
+        branchmap.branchcache = oldbranchcache
+        branchmap._filename = oldfilename
+
+def wireprotobranchmap(orig, repo, proto):
+    oldrepo = repo.__class__
+    try:
+        class repocls(repo.__class__):
+            def branchmap(self):
+                usetopic = not self.publishing()
+                return super(repocls, self).branchmap(topic=usetopic)
+        repo.__class__ = repocls
+        return orig(repo, proto)
+    finally:
+        repo.__class__ = oldrepo
+
+
+# Discovery have deficiency around phases, branch can get new heads with pure
+# phases change. This happened with a changeset was allowed to be pushed
+# because it had a topic, but it later become public and create a new branch
+# head.
+#
+# Handle this by doing an extra check for new head creation server side
+def _nbheads(repo):
+    data = {}
+    for b in repo.branchmap().iterbranches():
+        if ':' in b[0]:
+            continue
+        data[b[0]] = len(b[1])
+    return data
+
+def handlecheckheads(orig, op, inpart):
+    orig(op, inpart)
+    if op.repo.publishing():
+        return
+    tr = op.gettransaction()
+    if tr.hookargs['source'] not in ('push', 'serve'): # not a push
+        return
+    tr._prepushheads = _nbheads(op.repo)
+    reporef = weakref.ref(op.repo)
+    oldvalidator = tr.validator
+
+    def validator(tr):
+        repo = reporef()
+        if repo is not None:
+            repo.invalidatecaches()
+            finalheads = _nbheads(repo)
+            for branch, oldnb in tr._prepushheads.iteritems():
+                newnb = finalheads.pop(branch, 0)
+                if oldnb < newnb:
+                    msg = _('push create a new head on branch "%s"' % branch)
+                    raise error.Abort(msg)
+            for branch, newnb in finalheads.iteritems():
+                if 1 < newnb:
+                    msg = _('push create more than 1 head on new branch "%s"'
+                            % branch)
+                    raise error.Abort(msg)
+        return oldvalidator(tr)
+    tr.validator = validator
+handlecheckheads.params = frozenset()
+
+def _pushb2phases(orig, pushop, bundler):
+    hascheck = any(p.type == 'check:heads' for p in bundler._parts)
+    if pushop.outdatedphases and not hascheck:
+        exchange._pushb2ctxcheckheads(pushop, bundler)
+    return orig(pushop, bundler)
+
+def wireprotocaps(orig, repo, proto):
+    caps = orig(repo, proto)
+    if repo.peer().capable('topics'):
+        caps.append('topics')
+    return caps
+
+def modsetup(ui):
+    """run at uisetup time to install all destinations wrapping"""
+    extensions.wrapfunction(discovery, '_headssummary', _headssummary)
+    extensions.wrapfunction(wireproto, 'branchmap', wireprotobranchmap)
+    extensions.wrapfunction(wireproto, '_capabilities', wireprotocaps)
+    extensions.wrapfunction(bundle2, 'handlecheckheads', handlecheckheads)
+    # we need a proper wrap b2 part stuff
+    bundle2.handlecheckheads.params = frozenset()
+    bundle2.parthandlermapping['check:heads'] = bundle2.handlecheckheads
+    extensions.wrapfunction(exchange, '_pushb2phases', _pushb2phases)
+    exchange.b2partsgenmapping['phase'] = exchange._pushb2phases
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/evolvebits.py	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,96 @@
+import collections
+from mercurial import obsolete
+
+# Copied from evolve 081605c2e9b6
+
+def _orderrevs(repo, revs):
+    """Compute an ordering to solve instability for the given revs
+
+    revs is a list of unstable revisions.
+
+    Returns the same revisions ordered to solve their instability from the
+    bottom to the top of the stack that the stabilization process will produce
+    eventually.
+
+    This ensures the minimal number of stabilizations, as we can stabilize each
+    revision on its final stabilized destination.
+    """
+    # Step 1: Build the dependency graph
+    dependencies, rdependencies = builddependencies(repo, revs)
+    # Step 2: Build the ordering
+    # Remove the revisions with no dependency(A) and add them to the ordering.
+    # Removing these revisions leads to new revisions with no dependency (the
+    # one depending on A) that we can remove from the dependency graph and add
+    # to the ordering. We progress in a similar fashion until the ordering is
+    # built
+    solvablerevs = [r for r in sorted(dependencies.keys())
+                    if not dependencies[r]]
+    ordering = []
+    while solvablerevs:
+        rev = solvablerevs.pop()
+        for dependent in rdependencies[rev]:
+            dependencies[dependent].remove(rev)
+            if not dependencies[dependent]:
+                solvablerevs.append(dependent)
+        del dependencies[rev]
+        ordering.append(rev)
+
+    ordering.extend(sorted(dependencies))
+    return ordering
+
+def builddependencies(repo, revs):
+    """returns dependency graphs giving an order to solve instability of revs
+    (see _orderrevs for more information on usage)"""
+
+    # For each troubled revision we keep track of what instability if any should
+    # be resolved in order to resolve it. Example:
+    # dependencies = {3: [6], 6:[]}
+    # Means that: 6 has no dependency, 3 depends on 6 to be solved
+    dependencies = {}
+    # rdependencies is the inverted dict of dependencies
+    rdependencies = collections.defaultdict(set)
+
+    for r in revs:
+        dependencies[r] = set()
+        for p in repo[r].parents():
+            try:
+                succ = _singlesuccessor(repo, p)
+            except MultipleSuccessorsError as exc:
+                dependencies[r] = exc.successorssets
+                continue
+            if succ in revs:
+                dependencies[r].add(succ)
+                rdependencies[succ].add(r)
+    return dependencies, rdependencies
+
+def _singlesuccessor(repo, p):
+    """returns p (as rev) if not obsolete or its unique latest successors
+
+    fail if there are no such successor"""
+
+    if not p.obsolete():
+        return p.rev()
+    obs = repo[p]
+    ui = repo.ui
+    newer = obsolete.successorssets(repo, obs.node())
+    # search of a parent which is not killed
+    while not newer:
+        ui.debug("stabilize target %s is plain dead,"
+                 " trying to stabilize on its parent\n" %
+                 obs)
+        obs = obs.parents()[0]
+        newer = obsolete.successorssets(repo, obs.node())
+    if len(newer) > 1 or len(newer[0]) > 1:
+        raise MultipleSuccessorsError(newer)
+
+    return repo[newer[0][0]].rev()
+
+class MultipleSuccessorsError(RuntimeError):
+    """Exception raised by _singlesuccessor when multiple successor sets exists
+
+    The object contains the list of successorssets in its 'successorssets'
+    attribute to call to easily recover.
+    """
+
+    def __init__(self, successorssets):
+        self.successorssets = successorssets
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/revset.py	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,75 @@
+from __future__ import absolute_import
+
+from mercurial.i18n import _
+from mercurial import (
+    error,
+    revset,
+    util,
+)
+
+from . import (
+    constants,
+    destination,
+    stack,
+)
+
+try:
+    mkmatcher = revset._stringmatcher
+except AttributeError:
+    mkmatcher = util.stringmatcher
+
+
+def topicset(repo, subset, x):
+    """`topic([topic])`
+    Specified topic or all changes with any topic specified.
+
+    If `topic` starts with `re:` the remainder of the name is treated
+    as a regular expression.
+
+    TODO: make `topic(revset)` work the same as `branch(revset)`.
+    """
+    args = revset.getargs(x, 0, 1, 'topic takes one or no arguments')
+    if args:
+        # match a specific topic
+        topic = revset.getstring(args[0], 'topic() argument must be a string')
+        if topic == '.':
+            topic = repo['.'].extra().get('topic', '')
+        _kind, _pattern, matcher = mkmatcher(topic)
+    else:
+        matcher = lambda t: bool(t)
+    drafts = subset.filter(lambda r: repo[r].mutable())
+    return drafts.filter(
+        lambda r: matcher(repo[r].extra().get(constants.extrakey, '')))
+
+def ngtipset(repo, subset, x):
+    """`ngtip([branch])`
+
+    The untopiced tip.
+
+    Name is horrible so that people change it.
+    """
+    args = revset.getargs(x, 1, 1, 'topic takes one')
+    # match a specific topic
+    branch = revset.getstring(args[0], 'ngtip() argument must be a string')
+    if branch == '.':
+        branch = repo['.'].branch()
+    return subset & revset.baseset(destination.ngtip(repo, branch))
+
+def stackset(repo, subset, x):
+    """`stack()`
+    All relevant changes in the current topic,
+
+    This is roughly equivalent to 'topic(.) - obsolete' with a sorting moving
+    unstable changeset after there future parent (as if evolve where already
+    run)."""
+    topic = repo.currenttopic
+    if not topic:
+        raise error.Abort(_('no active topic to list'))
+    # ordering hack, boo
+    return revset.baseset(stack.getstack(repo, topic)) & subset
+
+
+def modsetup(ui):
+    revset.symbols.update({'topic': topicset})
+    revset.symbols.update({'ngtip': ngtipset})
+    revset.symbols.update({'stack': stackset})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/stack.py	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,121 @@
+# stack.py - code related to stack workflow
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+from mercurial.i18n import _
+from mercurial import (
+    destutil,
+    error,
+    node,
+)
+from .evolvebits import builddependencies, _orderrevs, _singlesuccessor
+
+def getstack(repo, topic):
+    # XXX need sorting
+    trevs = repo.revs("topic(%s) - obsolete()", topic)
+    return _orderrevs(repo, trevs)
+
+def showstack(ui, repo, topic, opts):
+    fm = ui.formatter('topicstack', opts)
+    prev = None
+    entries = []
+    idxmap = {}
+
+    label = 'topic'
+    if topic == repo.currenttopic:
+        label = 'topic.active'
+
+    data = stackdata(repo, topic)
+    fm.plain(_('### topic: %s') % ui.label(topic, label),
+             label='topic.stack.summary.topic')
+
+    if 1 < data['headcount']:
+        fm.plain(' (')
+        fm.plain('%d heads' % data['headcount'],
+                 label='topic.stack.summary.headcount.multiple')
+        fm.plain(')')
+    fm.plain('\n')
+    fm.plain(_('### branch: %s')
+             % '+'.join(data['branches']), # XXX handle multi branches
+             label='topic.stack.summary.branches')
+    if data['behindcount'] == -1:
+        fm.plain(', ')
+        fm.plain('ambigious rebase destination', label='topic.stack.summary.behinderror')
+    elif data['behindcount']:
+        fm.plain(', ')
+        fm.plain('%d behind' % data['behindcount'], label='topic.stack.summary.behindcount')
+    fm.plain('\n')
+
+    for idx, r in enumerate(getstack(repo, topic), 1):
+        ctx = repo[r]
+        p1 = ctx.p1()
+        if p1.obsolete():
+            p1 = repo[_singlesuccessor(repo, p1)]
+        if p1.rev() != prev and p1.node() != node.nullid:
+            entries.append((idxmap.get(p1.rev()), False, p1))
+        entries.append((idx, True, ctx))
+        idxmap[ctx.rev()] = idx
+        prev = r
+
+    # super crude initial version
+    for idx, isentry, ctx in entries[::-1]:
+        if not isentry:
+            symbol = '^'
+            state = 'base'
+        elif repo.revs('%d and parents()', ctx.rev()):
+            symbol = '@'
+            state = 'current'
+        elif repo.revs('%d and unstable()', ctx.rev()):
+            symbol = '$'
+            state = 'unstable'
+        else:
+            symbol = ':'
+            state = 'clean'
+        fm.startitem()
+        fm.data(isentry=isentry)
+        if idx is None:
+            fm.plain('  ')
+        else:
+            fm.write('topic.stack.index', 't%d', idx,
+                     label='topic.stack.index topic.stack.index.%s' % state)
+        fm.write('topic.stack.state.symbol', '%s', symbol,
+                 label='topic.stack.state topic.stack.state.%s' % state)
+        fm.plain(' ')
+        fm.write('topic.stack.desc', '%s', ctx.description().splitlines()[0],
+                 label='topic.stack.desc topic.stack.desc.%s' % state)
+        fm.condwrite(state != 'clean' and idx is not None, 'topic.stack.state',
+                     ' (%s)', state,
+                     label='topic.stack.state topic.stack.state.%s' % state)
+        fm.plain('\n')
+        fm.end()
+
+def stackdata(repo, topic):
+    """get various data about a stack
+
+    :changesetcount: number of non-obsolete changesets in the stack
+    :troubledcount: number on troubled changesets
+    :headcount: number of heads on the topic
+    :behindcount: number of changeset on rebase destination
+    """
+    data = {}
+    revs = repo.revs("topic(%s) - obsolete()", topic)
+    data['changesetcount'] = len(revs)
+    data['troubledcount'] = len([r for r in revs if repo[r].troubled()])
+    deps, rdeps = builddependencies(repo, revs)
+    data['headcount'] = len([r for r in revs if not rdeps[r]])
+    data['behindcount'] = 0
+    if revs:
+        minroot = [min(r for r in revs if not deps[r])]
+        try:
+            dest = destutil.destmerge(repo, action='rebase',
+                                      sourceset=minroot,
+                                      onheadcheck=False)
+            data['behindcount'] = len(repo.revs("only(%d, %ld)", dest,
+                                                minroot))
+        except error.NoMergeDestAbort:
+            data['behindcount'] = 0
+        except error.ManyMergeDestAbort:
+            data['behindcount'] = -1
+    data['branches'] = sorted(set(repo[r].branch() for r in revs))
+
+    return data
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/topicmap.py	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,247 @@
+import contextlib
+import hashlib
+
+from mercurial.node import hex, bin, nullid
+from mercurial import (
+    branchmap,
+    changegroup,
+    cmdutil,
+    encoding,
+    error,
+    extensions,
+    scmutil,
+)
+
+def _filename(repo):
+    """name of a branchcache file for a given repo or repoview"""
+    filename = "cache/topicmap"
+    if repo.filtername:
+        filename = '%s-%s' % (filename, repo.filtername)
+    return filename
+
+oldbranchcache = branchmap.branchcache
+
+def _phaseshash(repo, maxrev):
+    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 = nullid
+    revs = sorted(revs)
+    if revs:
+        s = hashlib.sha1()
+        for rev in revs:
+            s.update('%s;' % rev)
+        key = s.digest()
+    return key
+
+@contextlib.contextmanager
+def usetopicmap(repo):
+    """use awful monkey patching to ensure topic map usage
+
+    During the extend of the context block, The topicmap should be used and
+    updated instead of the branchmap."""
+    oldbranchcache = branchmap.branchcache
+    oldfilename = branchmap._filename
+    oldread = branchmap.read
+    oldcaches = getattr(repo, '_branchcaches', {})
+    try:
+        branchmap.branchcache = topiccache
+        branchmap._filename = _filename
+        branchmap.read = readtopicmap
+        repo._branchcaches = getattr(repo, '_topiccaches', {})
+        yield
+        repo._topiccaches = repo._branchcaches
+    finally:
+        repo._branchcaches = oldcaches
+        branchmap.branchcache = oldbranchcache
+        branchmap._filename = oldfilename
+        branchmap.read = oldread
+
+def cgapply(orig, repo, *args, **kwargs):
+    """make sure a topicmap is used when applying a changegroup"""
+    with usetopicmap(repo):
+        return orig(repo, *args, **kwargs)
+
+def commitstatus(orig, repo, node, branch, bheads=None, opts=None):
+    # wrap commit status use the topic branch heads
+    ctx = repo[node]
+    if ctx.topic() and ctx.branch() == branch:
+        bheads = repo.branchheads("%s:%s" % (branch, ctx.topic()))
+    return orig(repo, node, branch, bheads=bheads, opts=opts)
+
+class topiccache(oldbranchcache):
+
+    def __init__(self, *args, **kwargs):
+        otherbranchcache = branchmap.branchcache
+        try:
+            # super() call may fail otherwise
+            branchmap.branchcache = oldbranchcache
+            super(topiccache, self).__init__(*args, **kwargs)
+            if self.filteredhash is None:
+                self.filteredhash = nullid
+            self.phaseshash = nullid
+        finally:
+            branchmap.branchcache = otherbranchcache
+
+    def copy(self):
+        """return an deep copy of the branchcache object"""
+        new = topiccache(self, self.tipnode, self.tiprev, self.filteredhash,
+                         self._closednodes)
+        if self.filteredhash is None:
+            self.filteredhash = nullid
+        new.phaseshash = self.phaseshash
+        return new
+
+    def branchtip(self, branch, topic=''):
+        '''Return the tipmost open head on branch head, otherwise return the
+        tipmost closed head on branch.
+        Raise KeyError for unknown branch.'''
+        if topic:
+            branch = '%s:%s' % (branch, topic)
+        return super(topiccache, self).branchtip(branch)
+
+    def branchheads(self, branch, closed=False, topic=''):
+        if topic:
+            branch = '%s:%s' % (branch, topic)
+        return super(topiccache, self).branchheads(branch, closed=closed)
+
+    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."""
+        # This is copy paste of mercurial.branchmap.branchcache.validfor in
+        # 69077c65919d With a small changes to the cache key handling to
+        # include phase information that impact the topic cache.
+        #
+        # All code changes should be flagged on site.
+        try:
+            if (self.tipnode == repo.changelog.node(self.tiprev)):
+                fh = scmutil.filteredhash(repo, self.tiprev)
+                if fh is None:
+                    fh = nullid
+                if ((self.filteredhash == fh)
+                    and (self.phaseshash == _phaseshash(repo, self.tiprev))):
+                    return True
+            return False
+        except IndexError:
+            return False
+
+    def write(self, repo):
+        # This is copy paste of mercurial.branchmap.branchcache.write in
+        # 69077c65919d With a small changes to the cache key handling to
+        # include phase information that impact the topic cache.
+        #
+        # All code changes should be flagged on site.
+        try:
+            f = repo.vfs(_filename(repo), "w", atomictemp=True)
+            cachekey = [hex(self.tipnode), str(self.tiprev)]
+            # [CHANGE] we need a hash in all cases
+            assert self.filteredhash is not None
+            cachekey.append(hex(self.filteredhash))
+            cachekey.append(hex(self.phaseshash))
+            f.write(" ".join(cachekey) + '\n')
+            nodecount = 0
+            for label, nodes in sorted(self.iteritems()):
+                for node in nodes:
+                    nodecount += 1
+                    if node in self._closednodes:
+                        state = 'c'
+                    else:
+                        state = 'o'
+                    f.write("%s %s %s\n" % (hex(node), state,
+                                            encoding.fromlocal(label)))
+            f.close()
+            repo.ui.log('branchcache',
+                        'wrote %s branch cache with %d labels and %d nodes\n',
+                        repo.filtername, len(self), nodecount)
+        except (IOError, OSError, error.Abort) as inst:
+            repo.ui.debug("couldn't write branch cache: %s\n" % inst)
+            # Abort may be raise by read only opener
+            pass
+
+    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.
+        """
+        oldgetbranchinfo = repo.revbranchcache().branchinfo
+        try:
+            def branchinfo(r):
+                info = oldgetbranchinfo(r)
+                topic = ''
+                ctx = repo[r]
+                if ctx.mutable():
+                    topic = ctx.topic()
+                branch = info[0]
+                if topic:
+                    branch = '%s:%s' % (branch, topic)
+                return (branch, info[1])
+            repo.revbranchcache().branchinfo = branchinfo
+            super(topiccache, self).update(repo, revgen)
+            if self.filteredhash is None:
+                self.filteredhash = nullid
+            self.phaseshash = _phaseshash(repo, self.tiprev)
+        finally:
+            repo.revbranchcache().branchinfo = oldgetbranchinfo
+
+def readtopicmap(repo):
+    # This is copy paste of mercurial.branchmap.read in 69077c65919d
+    # With a small changes to the cache key handling to include phase
+    # information that impact the topic cache.
+    #
+    # All code changes should be flagged on site.
+    try:
+        f = repo.vfs(_filename(repo))
+        lines = f.read().split('\n')
+        f.close()
+    except (IOError, OSError):
+        return None
+
+    try:
+        cachekey = lines.pop(0).split(" ", 2)
+        last, lrev = cachekey[:2]
+        last, lrev = bin(last), int(lrev)
+        filteredhash = bin(cachekey[2]) # [CHANGE] unconditional filteredhash
+        partial = topiccache(tipnode=last, tiprev=lrev,
+                             filteredhash=filteredhash)
+        partial.phaseshash = bin(cachekey[3]) # [CHANGE] read phaseshash
+        if not partial.validfor(repo):
+            # invalidate the cache
+            raise ValueError('tip differs')
+        cl = repo.changelog
+        for l in lines:
+            if not l:
+                continue
+            node, state, label = l.split(" ", 2)
+            if state not in 'oc':
+                raise ValueError('invalid branch state')
+            label = encoding.tolocal(label.strip())
+            node = bin(node)
+            if not cl.hasnode(node):
+                raise ValueError('node %s does not exist' % hex(node))
+            partial.setdefault(label, []).append(node)
+            if state == 'c':
+                partial._closednodes.add(node)
+    except KeyboardInterrupt:
+        raise
+    except Exception as inst:
+        if repo.ui.debugflag:
+            msg = 'invalid branchheads cache'
+            if repo.filtername is not None:
+                msg += ' (%s)' % repo.filtername
+            msg += ': %s\n'
+            repo.ui.debug(msg % inst)
+        partial = None
+    return partial
+
+def modsetup(ui):
+    """call at uisetup time to install various wrappings"""
+    extensions.wrapfunction(changegroup.cg1unpacker, 'apply', cgapply)
+    extensions.wrapfunction(cmdutil, 'commitstatus', commitstatus)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.cfg	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,3 @@
+[flake8]
+ignore = E261, E266, E302, E305, E129, E731, E124, E501, E123, W503, N801
+
--- a/setup.py	Tue Feb 28 18:21:23 2017 +0100
+++ b/setup.py	Thu Mar 02 18:07:46 2017 +0100
@@ -1,6 +1,3 @@
-# Copied from histedit setup.py
-# Credit to Augie Fackler <durin42@gmail.com>
-
 import os
 from distutils.core import setup
 from os.path import dirname, join
@@ -15,20 +12,41 @@
           if "'" in line:
             return line.split("'")[1]
 
+def min_hg_version(relpath):
+    '''Read version info from a file without importing it'''
+    for line in open(join(dirname(__file__), relpath), 'rb'):
+        # Decode to a fail-safe string for PY3
+        # (gives unicode object in PY2)
+        line = line.decode('utf8')
+        if 'testedwith' in line:
+          if "'" in line:
+            return min(line.split("'")[1].split())
+
 py_modules = [
-    'hgext3rd.evolve.serveronly'
+    'hgext3rd.evolve.serveronly',
 ]
 py_packages = [
     'hgext3rd',
+    'hgext3rd.topic',
 ]
 
 if os.environ.get('INCLUDE_INHIBIT'):
     py_modules.append('hgext3rd.evolve.hack.inhibit')
     py_modules.append('hgext3rd.evolve.hack.directaccess')
 
+
+EVOLVE_PATH = 'hgext3rd/evolve/__init__.py'
+
+requires = []
+try:
+    import mercurial
+    mercurial.__all__
+except ImportError:
+    requires.append('mercurial>=%s' % min_hg_version(EVOLVE_PATH))
+
 setup(
     name='hg-evolve',
-    version=get_version('hgext3rd/evolve/__init__.py'),
+    version=get_version(EVOLVE_PATH),
     author='Pierre-Yves David',
     maintainer='Pierre-Yves David',
     maintainer_email='pierre-yves.david@ens-lyon.org',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-check-pyflakes.t	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,15 @@
+#require test-repo pyflakes
+
+Copied from Mercurial core (60ee2593a270)
+
+  $ cd "`dirname "$TESTDIR"`"
+
+run pyflakes on all tracked files ending in .py or without a file ending
+(skipping binary file random-seed)
+
+  $ hg locate 'set:hgext3rd/topic/**.py or grep("^!#.*python")' 2>/dev/null \
+  > | xargs pyflakes 2>/dev/null  
+
+run flake8 if it exists; if it doesn't, then just skip
+
+  $ type flake8 >/dev/null 2>/dev/null && hg files -0 'glob:hgext3rd/topic/**.py' | xargs -0 flake8 || true
--- a/tests/test-evolve-topic.t	Tue Feb 28 18:21:23 2017 +0100
+++ b/tests/test-evolve-topic.t	Thu Mar 02 18:07:46 2017 +0100
@@ -1,10 +1,6 @@
 
 Check we can find the topic extensions
 
-  $ [ -z "$HGTEST_TOPICROOT" ] && echo 'skipped: $HGTEST_TOPICROOT not set' >&2 && exit 80
-  [1]
-  $ [ ! -e $HGTEST_TOPICROOT/hgext3rd/topic/__init__.py ] && echo 'skipped: no topic repo found at $HGTEST_TOPICROOT' >&2 && exit 80
-  [1]
   $ cat >> $HGRCPATH <<EOF
   > [defaults]
   > amend=-d "0 0"
@@ -18,9 +14,9 @@
   > unified = 0
   > [extensions]
   > rebase = 
-  > topic = $HGTEST_TOPICROOT/hgext3rd/topic/
   > EOF
   $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
+  $ echo "topic=$(echo $(dirname $TESTDIR))/hgext3rd/topic/" >> $HGRCPATH
 
   $ mkcommit() {
   >    echo "$1" > "$1"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-topic-dest.t	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,492 @@
+  $ . "$TESTDIR/testlib"
+
+  $ hg init jungle
+  $ cd jungle
+  $ cat <<EOF >> .hg/hgrc
+  > [extensions]
+  > rebase=
+  > histedit=
+  > [phases]
+  > publish=false
+  > EOF
+  $ cat <<EOF >> $HGRCPATH
+  > [ui]
+  > logtemplate = '{rev} ({topics}) {desc}\n'
+  > EOF
+
+  $ for x in alpha beta gamma delta ; do
+  >   echo file $x >> $x
+  >   hg add $x
+  >   hg ci -m "c_$x"
+  > done
+
+Test NGTip feature
+==================
+
+Simple linear case
+
+  $ echo babar >> jungle
+  $ hg add jungle
+  $ hg ci -t elephant -m babar
+
+  $ hg log -G
+  @  4 (elephant) babar
+  |
+  o  3 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+  $ hg log -r 'ngtip(.)'
+  3 () c_delta
+  $ hg log -r 'default'
+  3 () c_delta
+
+
+multiple heads with topic
+
+  $ hg up "desc('c_beta')"
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ echo zephir >> jungle
+  $ hg add jungle
+  $ hg ci -t monkey -m zephir
+  $ hg log -G
+  @  5 (monkey) zephir
+  |
+  | o  4 (elephant) babar
+  | |
+  | o  3 () c_delta
+  | |
+  | o  2 () c_gamma
+  |/
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+  $ hg log -r 'ngtip(.)'
+  3 () c_delta
+  $ hg log -r 'default'
+  3 () c_delta
+
+one of the head is a valid tip
+
+  $ hg up "desc('c_delta')"
+  2 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ echo epsilon >> epsilon
+  $ hg add epsilon
+  $ hg ci -m "c_epsilon"
+  $ hg log -G
+  @  6 () c_epsilon
+  |
+  | o  5 (monkey) zephir
+  | |
+  +---o  4 (elephant) babar
+  | |
+  o |  3 () c_delta
+  | |
+  o |  2 () c_gamma
+  |/
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+  $ hg log -r 'ngtip(.)'
+  6 () c_epsilon
+  $ hg log -r 'default'
+  6 () c_epsilon
+
+rebase destination
+==================
+
+rebase on branch ngtip
+
+  $ hg up elephant
+  switching to topic elephant
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg rebase
+  rebasing 4:cb7ae72f4a80 "babar"
+  $ hg log -G
+  @  7 (elephant) babar
+  |
+  o  6 () c_epsilon
+  |
+  | o  5 (monkey) zephir
+  | |
+  o |  3 () c_delta
+  | |
+  o |  2 () c_gamma
+  |/
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+  $ hg up monkey
+  switching to topic monkey
+  1 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ hg rebase
+  rebasing 5:d832ddc604ec "zephir"
+  $ hg log -G
+  @  8 (monkey) zephir
+  |
+  | o  7 (elephant) babar
+  |/
+  o  6 () c_epsilon
+  |
+  o  3 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+
+Rebase on other topic heads if any
+
+  $ hg up 'desc(c_delta)'
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ echo "General Huc" >> monkeyville
+  $ hg add monkeyville
+  $ hg ci -t monkey -m Huc
+  $ hg log -G
+  @  9 (monkey) Huc
+  |
+  | o  8 (monkey) zephir
+  | |
+  | | o  7 (elephant) babar
+  | |/
+  | o  6 () c_epsilon
+  |/
+  o  3 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+  $ hg rebase
+  rebasing 9:d79a104e2902 "Huc" (tip)
+  $ hg log -G
+  @  10 (monkey) Huc
+  |
+  o  8 (monkey) zephir
+  |
+  | o  7 (elephant) babar
+  |/
+  o  6 () c_epsilon
+  |
+  o  3 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+
+merge destination
+=================
+
+  $ hg up 'ngtip(default)'
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg up default
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo zeta >> zeta
+  $ hg add zeta
+  $ hg ci -m "c_zeta"
+  $ hg log -G
+  @  11 () c_zeta
+  |
+  | o  10 (monkey) Huc
+  | |
+  | o  8 (monkey) zephir
+  |/
+  | o  7 (elephant) babar
+  |/
+  o  6 () c_epsilon
+  |
+  o  3 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+  $ hg up elephant
+  switching to topic elephant
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg rebase -d 'desc(c_zeta)' # make sure tip is elsewhere
+  rebasing 7:8d0b77140b05 "babar"
+  $ hg up monkey
+  switching to topic monkey
+  2 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg merge
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg topic
+     elephant
+   * monkey
+  $ hg ci -m 'merge with default'
+  $ hg topic
+     elephant
+   * monkey
+  $ hg log -G
+  @    13 (monkey) merge with default
+  |\
+  | | o  12 (elephant) babar
+  | |/
+  | o  11 () c_zeta
+  | |
+  o |  10 (monkey) Huc
+  | |
+  o |  8 (monkey) zephir
+  |/
+  o  6 () c_epsilon
+  |
+  o  3 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+
+
+Check pull --rebase
+-------------------
+
+(we broke it a some point)
+
+  $ cd ..
+  $ hg clone jungle other --rev '2'
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 3 files
+  updating to branch default
+  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd other
+  $ echo other > other
+  $ hg add other
+  $ hg ci -m 'c_other'
+  $ hg pull -r default --rebase
+  pulling from $TESTTMP/jungle
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 3 files (+1 heads)
+  rebasing 3:dbc48dd9e743 "c_other"
+  $ hg log -G
+  @  7 () c_other
+  |
+  o  6 () c_zeta
+  |
+  o  5 () c_epsilon
+  |
+  o  4 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+  $ cd ../jungle
+
+
+Default destination for update
+===============================
+
+initial setup
+
+  $ hg up elephant
+  switching to topic elephant
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ echo arthur >> jungle
+  $ hg ci -m arthur
+  $ echo pompadour >> jungle
+  $ hg ci -m pompadour
+  $ hg up 'roots(all())'
+  0 files updated, 0 files merged, 6 files removed, 0 files unresolved
+  $ hg log -G
+  o  15 (elephant) pompadour
+  |
+  o  14 (elephant) arthur
+  |
+  | o    13 (monkey) merge with default
+  | |\
+  o---+  12 (elephant) babar
+   / /
+  | o  11 () c_zeta
+  | |
+  o |  10 (monkey) Huc
+  | |
+  o |  8 (monkey) zephir
+  |/
+  o  6 () c_epsilon
+  |
+  o  3 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  @  0 () c_alpha
+  
+
+testing default destination on a branch
+
+  $ hg up
+  5 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg log -G
+  o  15 (elephant) pompadour
+  |
+  o  14 (elephant) arthur
+  |
+  | o    13 (monkey) merge with default
+  | |\
+  o---+  12 (elephant) babar
+   / /
+  | @  11 () c_zeta
+  | |
+  o |  10 (monkey) Huc
+  | |
+  o |  8 (monkey) zephir
+  |/
+  o  6 () c_epsilon
+  |
+  o  3 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+
+extra setup for topic
+(making sure tip is not the topic)
+
+  $ hg up 'desc(c_zeta)'
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo 'eta' >> 'eta'
+  $ hg add 'eta'
+  $ hg commit -m 'c_eta'
+  $ hg log -G
+  @  16 () c_eta
+  |
+  | o  15 (elephant) pompadour
+  | |
+  | o  14 (elephant) arthur
+  | |
+  +---o  13 (monkey) merge with default
+  | | |
+  | o |  12 (elephant) babar
+  |/ /
+  o |  11 () c_zeta
+  | |
+  | o  10 (monkey) Huc
+  | |
+  | o  8 (monkey) zephir
+  |/
+  o  6 () c_epsilon
+  |
+  o  3 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+
+Testing default destination for topic
+
+  $ hg up 'roots(topic(elephant))'
+  switching to topic elephant
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg up
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg log -G
+  o  16 () c_eta
+  |
+  | @  15 (elephant) pompadour
+  | |
+  | o  14 (elephant) arthur
+  | |
+  +---o  13 (monkey) merge with default
+  | | |
+  | o |  12 (elephant) babar
+  |/ /
+  o |  11 () c_zeta
+  | |
+  | o  10 (monkey) Huc
+  | |
+  | o  8 (monkey) zephir
+  |/
+  o  6 () c_epsilon
+  |
+  o  3 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+
+Testing default destination for topic
+
+  $ hg up 'p1(roots(topic(elephant)))'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg topic elephant
+  $ hg up
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg log -G
+  o  16 () c_eta
+  |
+  | @  15 (elephant) pompadour
+  | |
+  | o  14 (elephant) arthur
+  | |
+  +---o  13 (monkey) merge with default
+  | | |
+  | o |  12 (elephant) babar
+  |/ /
+  o |  11 () c_zeta
+  | |
+  | o  10 (monkey) Huc
+  | |
+  | o  8 (monkey) zephir
+  |/
+  o  6 () c_epsilon
+  |
+  o  3 () c_delta
+  |
+  o  2 () c_gamma
+  |
+  o  1 () c_beta
+  |
+  o  0 () c_alpha
+  
+
+Default destination for histedit
+================================
+
+By default histedit should edit with the current topic only
+(even when based on other draft
+
+  $ hg phase 'desc(c_zeta)'
+  11: draft
+  $ HGEDITOR=cat hg histedit | grep pick
+  pick e44744d9ad73 12 babar
+  pick 38eea8439aee 14 arthur
+  pick 411315c48bdc 15 pompadour
+  #  p, pick = use commit
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-topic-push.t	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,414 @@
+  $ . "$TESTDIR/testlib"
+
+  $ cat << EOF >> $HGRCPATH
+  > [ui]
+  > logtemplate = {rev} {branch} {get(namespaces, "topics")} {phase} {desc|firstline}\n
+  > [ui]
+  > ssh =python "$RUNTESTDIR/dummyssh"
+  > EOF
+
+  $ hg init main
+  $ hg init draft
+  $ cat << EOF >> draft/.hg/hgrc
+  > [phases]
+  > publish=False
+  > EOF
+  $ hg clone main client
+  updating to branch default
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cat << EOF >> client/.hg/hgrc
+  > [paths]
+  > draft=../draft
+  > EOF
+
+
+Testing core behavior to make sure we did not break anything
+============================================================
+
+Pushing a first changeset
+
+  $ cd client
+  $ echo aaa > aaa
+  $ hg add aaa
+  $ hg commit -m 'CA'
+  $ hg outgoing -G
+  comparing with $TESTTMP/main
+  searching for changes
+  @  0 default  draft CA
+  
+  $ hg push
+  pushing to $TESTTMP/main
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+
+Pushing two heads
+
+  $ echo aaa > bbb
+  $ hg add bbb
+  $ hg commit -m 'CB'
+  $ echo aaa > ccc
+  $ hg up 'desc(CA)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg add ccc
+  $ hg commit -m 'CC'
+  created new head
+  $ hg outgoing -G
+  comparing with $TESTTMP/main
+  searching for changes
+  @  2 default  draft CC
+  
+  o  1 default  draft CB
+  
+  $ hg push
+  pushing to $TESTTMP/main
+  searching for changes
+  abort: push creates new remote head 9fe81b7f425d!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+  $ hg outgoing -r 'desc(CB)' -G
+  comparing with $TESTTMP/main
+  searching for changes
+  o  1 default  draft CB
+  
+  $ hg push -r 'desc(CB)'
+  pushing to $TESTTMP/main
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+
+Pushing a new branch
+
+  $ hg branch mountain
+  marked working directory as branch mountain
+  (branches are permanent and global, did you want a bookmark?)
+  $ hg commit --amend
+  $ hg outgoing -G
+  comparing with $TESTTMP/main
+  searching for changes
+  @  4 mountain  draft CC
+  
+  $ hg push 
+  pushing to $TESTTMP/main
+  searching for changes
+  abort: push creates new remote branches: mountain!
+  (use 'hg push --new-branch' to create new remote branches)
+  [255]
+  $ hg push --new-branch
+  pushing to $TESTTMP/main
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  2 new obsolescence markers
+
+Including on non-publishing
+
+  $ hg push --new-branch draft
+  pushing to $TESTTMP/draft
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 3 files (+1 heads)
+  2 new obsolescence markers
+
+Testing topic behavior
+======================
+
+Local peer tests
+----------------
+
+  $ hg up -r 'desc(CA)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg topic babar
+  $ echo aaa > ddd
+  $ hg add ddd
+  $ hg commit -m 'CD'
+  $ hg log -G # keep track of phase because I saw some strange bug during developement
+  @  5 default babar draft CD
+  |
+  | o  4 mountain  public CC
+  |/
+  | o  1 default  public CB
+  |/
+  o  0 default  public CA
+  
+
+Pushing a new topic to a non publishing server should not be seen as a new head
+
+  $ hg push draft
+  pushing to $TESTTMP/draft
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  $ hg log -G
+  @  5 default babar draft CD
+  |
+  | o  4 mountain  public CC
+  |/
+  | o  1 default  public CB
+  |/
+  o  0 default  public CA
+  
+
+Pushing a new topic to a publishing server should be seen as a new head
+
+  $ hg push
+  pushing to $TESTTMP/main
+  searching for changes
+  abort: push creates new remote head 67f579af159d!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+  $ hg log -G
+  @  5 default babar draft CD
+  |
+  | o  4 mountain  public CC
+  |/
+  | o  1 default  public CB
+  |/
+  o  0 default  public CA
+  
+
+wireprotocol tests
+------------------
+
+  $ hg up -r 'desc(CA)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg topic celeste
+  $ echo aaa > eee
+  $ hg add eee
+  $ hg commit -m 'CE'
+  $ hg log -G # keep track of phase because I saw some strange bug during developement
+  @  6 default celeste draft CE
+  |
+  | o  5 default babar draft CD
+  |/
+  | o  4 mountain  public CC
+  |/
+  | o  1 default  public CB
+  |/
+  o  0 default  public CA
+  
+
+Pushing a new topic to a non publishing server without topic -> new head
+
+  $ cat << EOF >> ../draft/.hg/hgrc
+  > [extensions]
+  > topic=!
+  > EOF
+  $ hg push ssh://user@dummy/draft
+  pushing to ssh://user@dummy/draft
+  searching for changes
+  abort: push creates new remote head 84eaf32db6c3!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+  $ hg log -G
+  @  6 default celeste draft CE
+  |
+  | o  5 default babar draft CD
+  |/
+  | o  4 mountain  public CC
+  |/
+  | o  1 default  public CB
+  |/
+  o  0 default  public CA
+  
+
+Pushing a new topic to a non publishing server should not be seen as a new head
+
+  $ printf "topic=" >> ../draft/.hg/hgrc
+  $ hg config extensions.topic >> ../draft/.hg/hgrc
+  $ hg push ssh://user@dummy/draft
+  pushing to ssh://user@dummy/draft
+  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)
+  $ hg log -G
+  @  6 default celeste draft CE
+  |
+  | o  5 default babar draft CD
+  |/
+  | o  4 mountain  public CC
+  |/
+  | o  1 default  public CB
+  |/
+  o  0 default  public CA
+  
+
+Pushing a new topic to a publishing server should be seen as a new head
+
+  $ hg push ssh://user@dummy/main
+  pushing to ssh://user@dummy/main
+  searching for changes
+  abort: push creates new remote head 67f579af159d!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+  $ hg log -G
+  @  6 default celeste draft CE
+  |
+  | o  5 default babar draft CD
+  |/
+  | o  4 mountain  public CC
+  |/
+  | o  1 default  public CB
+  |/
+  o  0 default  public CA
+  
+
+Check that we reject multiple head on the same topic
+----------------------------------------------------
+
+  $ hg up 'desc(CB)'
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg topic babar
+  $ echo aaa > fff
+  $ hg add fff
+  $ hg commit -m 'CF'
+  $ hg log -G
+  @  7 default babar draft CF
+  |
+  | o  6 default celeste draft CE
+  | |
+  | | o  5 default babar draft CD
+  | |/
+  | | o  4 mountain  public CC
+  | |/
+  o |  1 default  public CB
+  |/
+  o  0 default  public CA
+  
+
+  $ hg push draft
+  pushing to $TESTTMP/draft
+  searching for changes
+  abort: push creates new remote head f0bc62a661be on branch 'default:babar'!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+
+Multiple head on a branch merged in a topic changesets
+------------------------------------------------------------------------
+
+
+  $ hg up 'desc(CA)'
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ echo aaa > ggg
+  $ hg add ggg
+  $ hg commit -m 'CG'
+  created new head
+  $ hg up 'desc(CF)'
+  switching to topic babar
+  2 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg merge 'desc(CG)'
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg commit -m 'CM'
+  $ hg log -G
+  @    9 default babar draft CM
+  |\
+  | o  8 default  draft CG
+  | |
+  o |  7 default babar draft CF
+  | |
+  | | o  6 default celeste draft CE
+  | |/
+  | | o  5 default babar draft CD
+  | |/
+  | | o  4 mountain  public CC
+  | |/
+  o |  1 default  public CB
+  |/
+  o  0 default  public CA
+  
+
+Reject when pushing to draft
+
+  $ hg push draft -r .
+  pushing to $TESTTMP/draft
+  searching for changes
+  abort: push creates new remote head 4937c4cad39e!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+
+
+Reject when pushing to publishing
+
+  $ hg push -r .
+  pushing to $TESTTMP/main
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 2 changes to 2 files
+
+  $ cd ..
+
+Test phase move
+==================================
+
+setup, two repo knowns about two small topic branch
+
+  $ hg init repoA
+  $ hg clone repoA repoB
+  updating to branch default
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cat << EOF >> repoA/.hg/hgrc
+  > [phases]
+  > publish=False
+  > EOF
+  $ cat << EOF >> repoB/.hg/hgrc
+  > [phases]
+  > publish=False
+  > EOF
+  $ cd repoA
+  $ echo aaa > base
+  $ hg add base
+  $ hg commit -m 'CBASE'
+  $ echo aaa > aaa
+  $ hg add aaa
+  $ hg topic topicA
+  $ hg commit -m 'CA'
+  $ hg up 'desc(CBASE)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ echo aaa > bbb
+  $ hg add bbb
+  $ hg topic topicB
+  $ hg commit -m 'CB'
+  $ cd ..
+  $ hg push -R repoA repoB
+  pushing to repoB
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 3 files (+1 heads)
+  $ hg log -G -R repoA
+  @  2 default topicB draft CB
+  |
+  | o  1 default topicA draft CA
+  |/
+  o  0 default  draft CBASE
+  
+
+We turn different topic to public on each side,
+
+  $ hg -R repoA phase --public topicA
+  $ hg -R repoB phase --public topicB
+
+Pushing should complain because it create to heads on default
+
+  $ hg push -R repoA repoB
+  pushing to repoB
+  searching for changes
+  no changes found
+  abort: push create a new head on branch "default"
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-topic-stack-data.t	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,270 @@
+Setup
+=====
+
+  $ . "$TESTDIR/testlib"
+
+  $ hg init test-list
+  $ cd test-list
+  $ cat <<EOF >> .hg/hgrc
+  > [phases]
+  > publish=false
+  > EOF
+  $ cat <<EOF >> $HGRCPATH
+  > [experimental]
+  > # disable the new graph style until we drop 3.7 support
+  > graphstyle.missing = |
+  > # turn evolution on
+  > evolution=all
+  > EOF
+
+
+  $ mkcommit() {
+  >    echo "$1" > "$1"
+  >    hg add "$1"
+  >    hg ci -m "add $1"
+  > }
+
+Build some basic graph
+----------------------
+
+  $ for x in base_a base_b base_c base_d base_e ; do
+  >   mkcommit $x
+  > done
+
+Add another branch with two heads
+
+  $ hg up 'desc(base_a)'
+  0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  $ hg branch lake
+  marked working directory as branch lake
+  (branches are permanent and global, did you want a bookmark?)
+  $ mkcommit lake_a
+  $ mkcommit lake_b
+  $ hg up 'desc(lake_a)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit lake_c
+  created new head
+
+
+Add some topics
+---------------
+
+A simple topic that need rebasing
+
+  $ hg up 'desc(base_c)'
+  2 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg topic baz
+  $ mkcommit baz_a
+  $ mkcommit baz_b
+
+A simple topic with unstability
+
+  $ hg up 'desc(base_d)'
+  1 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg topic fuz
+  $ mkcommit fuz_a
+  $ mkcommit fuz_b
+  $ mkcommit fuz_c
+  $ hg up 'desc(fuz_a)'
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg commit --amend --message 'fuz1_a'
+
+A topic with multiple heads
+
+  $ hg up 'desc(base_e)'
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg topic bar
+  $ mkcommit bar_a
+  $ mkcommit bar_b
+  $ mkcommit bar_c
+  $ hg up 'desc(bar_b)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit bar_d
+  $ mkcommit bar_e
+  $ hg up 'desc(bar_d)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg commit --amend --message 'bar1_d'
+
+topic 'foo' on the multi headed branch
+
+  $ hg up 'desc(lake_a)'
+  1 files updated, 0 files merged, 7 files removed, 0 files unresolved
+  $ hg topic foo
+  $ mkcommit foo_a
+  $ mkcommit foo_b
+
+Summary
+-------
+
+  $ hg summary
+  parent: 21:3e54b49a3113 tip
+   add foo_b
+  branch: lake
+  commit: (clean)
+  update: 2 new changesets (update)
+  phases: 22 draft
+  unstable: 3 changesets
+  topic:  foo
+  $ hg log --graph -T '{desc} ({branch}) [{topic}]'
+  @  add foo_b (lake) []
+  |
+  o  add foo_a (lake) []
+  |
+  | o  bar1_d (default) []
+  | |
+  | | o  add bar_e (default) []
+  | | |
+  | | x  add bar_d (default) []
+  | |/
+  | | o  add bar_c (default) []
+  | |/
+  | o  add bar_b (default) []
+  | |
+  | o  add bar_a (default) []
+  | |
+  | | o  fuz1_a (default) []
+  | | |
+  | | | o  add fuz_c (default) []
+  | | | |
+  | | | o  add fuz_b (default) []
+  | | | |
+  | | | x  add fuz_a (default) []
+  | | |/
+  | | | o  add baz_b (default) []
+  | | | |
+  | | | o  add baz_a (default) []
+  | | | |
+  +-------o  add lake_c (lake) []
+  | | | |
+  +-------o  add lake_b (lake) []
+  | | | |
+  o | | |  add lake_a (lake) []
+  | | | |
+  | o | |  add base_e (default) []
+  | |/ /
+  | o /  add base_d (default) []
+  | |/
+  | o  add base_c (default) []
+  | |
+  | o  add base_b (default) []
+  |/
+  o  add base_a (default) []
+  
+
+Actual Testing
+==============
+
+basic output
+
+  $ hg topic
+     bar
+     baz
+   * foo
+     fuz
+
+quiet version
+
+  $ hg topic --quiet
+  bar
+  baz
+  foo
+  fuz
+
+verbose
+
+  $ hg topic --verbose
+     bar (on branch: default, 5 changesets, 1 troubled, 2 heads)
+     baz (on branch: default, 2 changesets, 2 behind)
+   * foo (on branch: lake, 2 changesets, ambiguous destination)
+     fuz (on branch: default, 3 changesets, 2 troubled, 1 behind)
+
+json
+
+  $ hg topic -T json
+  [
+   {
+    "active": false,
+    "topic": "bar"
+   },
+   {
+    "active": false,
+    "topic": "baz"
+   },
+   {
+    "active": true,
+    "topic": "foo"
+   },
+   {
+    "active": false,
+    "topic": "fuz"
+   }
+  ]
+
+json --verbose
+
+  $ hg topic -T json --verbose
+  [
+   {
+    "active": false,
+    "branches+": "default",
+    "changesetcount": 5,
+    "headcount": 2,
+    "topic": "bar",
+    "troubledcount": 1
+   },
+   {
+    "active": false,
+    "behindcount": 2,
+    "branches+": "default",
+    "changesetcount": 2,
+    "topic": "baz"
+   },
+   {
+    "active": true,
+    "behinderror": "ambiguous destination",
+    "branches+": "lake",
+    "changesetcount": 2,
+    "topic": "foo"
+   },
+   {
+    "active": false,
+    "behindcount": 1,
+    "branches+": "default",
+    "changesetcount": 3,
+    "topic": "fuz",
+    "troubledcount": 2
+   }
+  ]
+
+Also test this situation with 'hg stack'
+=======================================
+
+  $ hg stack bar
+  ### topic: bar (2 heads)
+  ### branch: default
+  t5: add bar_c
+  t2^ add bar_b (base)
+  t4$ add bar_e (unstable)
+  t3: bar1_d
+  t2: add bar_b
+  t1: add bar_a
+    ^ add base_e
+  $ hg stack baz
+  ### topic: baz
+  ### branch: default, 2 behind
+  t2: add baz_b
+  t1: add baz_a
+    ^ add base_c
+  $ hg stack foo
+  ### topic: foo
+  ### branch: lake, ambigious rebase destination
+  t2@ add foo_b (current)
+  t1: add foo_a
+    ^ add lake_a
+  $ hg stack fuz
+  ### topic: fuz
+  ### branch: default, 1 behind
+  t3$ add fuz_c (unstable)
+  t2$ add fuz_b (unstable)
+  t1: fuz1_a
+    ^ add base_d
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-topic-stack.t	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,254 @@
+  $ . "$TESTDIR/testlib"
+
+Initial setup
+
+
+  $ cat << EOF >> $HGRCPATH
+  > [ui]
+  > logtemplate = {rev} {branch} \{{get(namespaces, "topics")}} {phase} {desc|firstline}\n
+  > [experimental]
+  > evolution=createmarkers,exchange,allowunstable
+  > EOF
+
+  $ hg init main
+  $ cd main
+  $ hg topic other
+  $ echo aaa > aaa
+  $ hg add aaa
+  $ hg commit -m c_a
+  $ echo aaa > bbb
+  $ hg add bbb
+  $ hg commit -m c_b
+  $ hg topic foo
+  $ echo aaa > ccc
+  $ hg add ccc
+  $ hg commit -m c_c
+  $ echo aaa > ddd
+  $ hg add ddd
+  $ hg commit -m c_d
+  $ echo aaa > eee
+  $ hg add eee
+  $ hg commit -m c_e
+  $ echo aaa > fff
+  $ hg add fff
+  $ hg commit -m c_f
+  $ hg log -G
+  @  5 default {foo} draft c_f
+  |
+  o  4 default {foo} draft c_e
+  |
+  o  3 default {foo} draft c_d
+  |
+  o  2 default {foo} draft c_c
+  |
+  o  1 default {other} draft c_b
+  |
+  o  0 default {other} draft c_a
+  
+
+Check that topic without any parent does not crash --list
+---------------------------------------------------------
+
+  $ hg up other
+  switching to topic other
+  0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  $ hg topic --list
+  ### topic: other
+  ### branch: default
+  t2@ c_b (current)
+  t1: c_a
+  $ hg phase --public 'topic("other")'
+  $ hg up foo
+  switching to topic foo
+  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+Simple test
+-----------
+
+'hg stack' list all changeset in the topic
+
+  $ hg topic
+   * foo
+  $ hg stack
+  ### topic: foo
+  ### branch: default
+  t4@ c_f (current)
+  t3: c_e
+  t2: c_d
+  t1: c_c
+    ^ c_b
+
+error case, nothing to list
+
+  $ hg topic --clear
+  $ hg stack
+  abort: no active topic to list
+  [255]
+
+Test "t#" reference
+-------------------
+
+
+  $ hg up t2
+  abort: cannot resolve "t2": no active topic
+  [255]
+  $ hg topic foo
+  $ hg up t42
+  abort: cannot resolve "t42": topic "foo" has only 4 changesets
+  [255]
+  $ hg up t2
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg summary
+  parent: 3:e629654d7050 
+   c_d
+  branch: default
+  commit: (clean)
+  update: (current)
+  phases: 4 draft
+  topic:  foo
+
+Case with some of the topic unstable
+------------------------------------
+
+  $ echo bbb > ddd
+  $ hg commit --amend
+  $ hg log -G
+  @  7 default {foo} draft c_d
+  |
+  | o  5 default {foo} draft c_f
+  | |
+  | o  4 default {foo} draft c_e
+  | |
+  | x  3 default {foo} draft c_d
+  |/
+  o  2 default {foo} draft c_c
+  |
+  o  1 default {} public c_b
+  |
+  o  0 default {} public c_a
+  
+  $ hg topic --list
+  ### topic: foo
+  ### branch: default
+  t4$ c_f (unstable)
+  t3$ c_e (unstable)
+  t2@ c_d (current)
+  t1: c_c
+    ^ c_b
+
+Also test the revset:
+
+  $ hg log -r 'stack()'
+  2 default {foo} draft c_c
+  7 default {foo} draft c_d
+  4 default {foo} draft c_e
+  5 default {foo} draft c_f
+
+Case with multiple heads on the topic
+-------------------------------------
+
+Make things linear again
+
+  $ hg rebase -s 'desc(c_e)' -d 'desc(c_d) - obsolete()'
+  rebasing 4:0f9ac936c87d "c_e"
+  rebasing 5:6559e6d93aea "c_f"
+  $ hg log -G
+  o  9 default {foo} draft c_f
+  |
+  o  8 default {foo} draft c_e
+  |
+  @  7 default {foo} draft c_d
+  |
+  o  2 default {foo} draft c_c
+  |
+  o  1 default {} public c_b
+  |
+  o  0 default {} public c_a
+  
+
+
+Create the second branch
+
+  $ hg up 'desc(c_d)'
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo aaa > ggg
+  $ hg add ggg
+  $ hg commit -m c_g
+  $ echo aaa > hhh
+  $ hg add hhh
+  $ hg commit -m c_h
+  $ hg log -G
+  @  11 default {foo} draft c_h
+  |
+  o  10 default {foo} draft c_g
+  |
+  | o  9 default {foo} draft c_f
+  | |
+  | o  8 default {foo} draft c_e
+  |/
+  o  7 default {foo} draft c_d
+  |
+  o  2 default {foo} draft c_c
+  |
+  o  1 default {} public c_b
+  |
+  o  0 default {} public c_a
+  
+
+Test output
+
+  $ hg top -l
+  ### topic: foo (2 heads)
+  ### branch: default
+  t6: c_f
+  t5: c_e
+  t2^ c_d (base)
+  t4@ c_h (current)
+  t3: c_g
+  t2: c_d
+  t1: c_c
+    ^ c_b
+
+Case with multiple heads on the topic with unstability involved
+---------------------------------------------------------------
+
+We amend the message to make sure the display base pick the right changeset
+
+  $ hg up 'desc(c_d)'
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ echo ccc > ddd
+  $ hg commit --amend -m 'c_D' 
+  $ hg rebase -d . -s 'desc(c_g)'
+  rebasing 10:81264ae8a36a "c_g"
+  rebasing 11:fde5f5941642 "c_h"
+  $ hg log -G
+  o  15 default {foo} draft c_h
+  |
+  o  14 default {foo} draft c_g
+  |
+  @  13 default {foo} draft c_D
+  |
+  | o  9 default {foo} draft c_f
+  | |
+  | o  8 default {foo} draft c_e
+  | |
+  | x  7 default {foo} draft c_d
+  |/
+  o  2 default {foo} draft c_c
+  |
+  o  1 default {} public c_b
+  |
+  o  0 default {} public c_a
+  
+
+  $ hg topic --list
+  ### topic: foo (2 heads)
+  ### branch: default
+  t6$ c_f (unstable)
+  t5$ c_e (unstable)
+  t2^ c_D (base)
+  t4: c_h
+  t3: c_g
+  t2@ c_D (current)
+  t1: c_c
+    ^ c_b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-topic-tutorial.t	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,468 @@
+==============
+Topic Tutorial
+==============
+
+.. This test file is also supposed to be able to compile as a rest file.
+
+
+.. Some Setup::
+
+  $ . "$TESTDIR/testlib"
+  $ hg init server
+  $ cd server
+  $ cat >> .hg/hgrc << EOF
+  > [ui]
+  > user= Shopping Master
+  > EOF
+  $ cat >> shopping << EOF
+  > Spam
+  > Whizzo butter
+  > Albatross
+  > Rat (rather a lot)
+  > Jugged fish
+  > Blancmange
+  > Salmon mousse
+  > EOF
+  $ hg commit -A -m "Shopping list"
+  adding shopping
+  $ cd ..
+  $ hg clone server client
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ cat >> .hg/hgrc << EOF
+  > [ui]
+  > user= Tutorial User
+  > EOF
+
+Topic branches are lightweight branches which disappear when changes are
+finalized (move to the public phase). They can help users to organise and share
+their unfinished work.
+
+Topic Basics
+============
+
+Let's says we use Mercurial to manage our shopping list::
+
+  $ hg log --graph
+  @  changeset:   0:38da43f0a2ea
+     tag:         tip
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     Shopping list
+  
+
+We are about to do some edition to this list and would like to do them within
+a topic. Creating a new topic is done using the ``topic`` command::
+
+  $ hg topic food
+
+As for named branch, our topic is active but it does not contains any changesets yet::
+
+  $ hg topic
+   * food
+  $ hg summary
+  parent: 0:38da43f0a2ea tip
+   Shopping list
+  branch: default
+  commit: (clean)
+  update: (current)
+  topic:  food
+  $ hg log --graph
+  @  changeset:   0:38da43f0a2ea
+     tag:         tip
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     Shopping list
+  
+
+Our next commit will be part of the active topic::
+
+  $ cat >> shopping << EOF
+  > Egg
+  > Suggar
+  > Vinegar
+  > Oil
+  > EOF
+  $ hg commit -m "adding condiments"
+  $ hg log --graph --rev 'topic("food")'
+  @  changeset:   1:13900241408b
+  |  tag:         tip
+  ~  topic:       food
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     adding condiments
+  
+
+And future commit will be part of that topic too::
+
+  $ cat >> shopping << EOF
+  > Bananas
+  > Pear
+  > Apple
+  > EOF
+  $ hg commit -m "adding fruits"
+  $ hg log --graph --rev 'topic("food")'
+  @  changeset:   2:287de11b401f
+  |  tag:         tip
+  |  topic:       food
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     adding fruits
+  |
+  o  changeset:   1:13900241408b
+  |  topic:       food
+  ~  user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     adding condiments
+  
+
+We can get a compact view of the content of our topic using the ``stack`` command::
+
+  $ hg stack
+  ### topic: food
+  ### branch: default
+  t2@ adding fruits (current)
+  t1: adding condiments
+    ^ Shopping list
+
+The topic desactivate when we update away from it::
+
+  $ hg up default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg topic
+     food
+
+Note that ``default`` (name of the branch) now refers to the tipmost changeset of default without a topic::
+
+  $ hg log --graph
+  o  changeset:   2:287de11b401f
+  |  tag:         tip
+  |  topic:       food
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     adding fruits
+  |
+  o  changeset:   1:13900241408b
+  |  topic:       food
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     adding condiments
+  |
+  @  changeset:   0:38da43f0a2ea
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     Shopping list
+  
+
+And updating back to the topic reactivate it::
+
+  $ hg up food
+  switching to topic food
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg topic
+   * food
+
+The name used for updating does not affect the activation of the topic, updating to a revision part of a topic will activate it in all cases::
+
+  $ hg up default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg up --rev 'desc("condiments")'
+  switching to topic food
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg topic
+   * food
+
+.. server side activity::
+
+  $ cd ../server/
+  $ cat > shopping << EOF
+  > T-Shirt
+  > Trousers
+  > Spam
+  > Whizzo butter
+  > Albatross
+  > Rat (rather a lot)
+  > Jugged fish
+  > Blancmange
+  > Salmon mousse
+  > EOF
+  $ hg commit -A -m "Adding clothes"
+  $ cd ../client
+
+Topic will also affect rebase and merge destination. Let's pull the latest update from the main server::
+
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  (run 'hg heads' to see heads)
+  $ hg log -G
+  o  changeset:   3:6104862e8b84
+  |  tag:         tip
+  |  parent:      0:38da43f0a2ea
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Adding clothes
+  |
+  | o  changeset:   2:287de11b401f
+  | |  topic:       food
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     adding fruits
+  | |
+  | @  changeset:   1:13900241408b
+  |/   topic:       food
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     adding condiments
+  |
+  o  changeset:   0:38da43f0a2ea
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     Shopping list
+  
+
+The topic head will not be considered when merge from the new head of the branch::
+
+  $ hg up default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg merge
+  abort: branch 'default' has one head - please merge with an explicit rev
+  (run 'hg heads' to see all heads)
+  [255]
+
+But the topic will see that branch head as a valid destination::
+
+  $ hg up food
+  switching to topic food
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg rebase
+  rebasing 1:13900241408b "adding condiments"
+  merging shopping
+  rebasing 2:287de11b401f "adding fruits"
+  merging shopping
+  $ hg log --graph
+  @  changeset:   5:2d50db8b5b4c
+  |  tag:         tip
+  |  topic:       food
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     adding fruits
+  |
+  o  changeset:   4:4011b46eeb33
+  |  topic:       food
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     adding condiments
+  |
+  o  changeset:   3:6104862e8b84
+  |  parent:      0:38da43f0a2ea
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Adding clothes
+  |
+  o  changeset:   0:38da43f0a2ea
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     Shopping list
+  
+
+The topic information will fade out when we publish the changesets::
+
+  $ hg topic
+     food
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 1 files
+  2 new obsolescence markers
+  $ hg topic
+  $ hg log --graph
+  @  changeset:   5:2d50db8b5b4c
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     adding fruits
+  |
+  o  changeset:   4:4011b46eeb33
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     adding condiments
+  |
+  o  changeset:   3:6104862e8b84
+  |  parent:      0:38da43f0a2ea
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Adding clothes
+  |
+  o  changeset:   0:38da43f0a2ea
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     Shopping list
+  
+  $ hg up default
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+Working with Multiple Topics
+============================
+
+In the above example, topic are not bring much benefit since you only have one
+line of developement. Topic start to be more useful when you have to work on
+multiple features are the same time.
+
+We might go shopping in a hardware store in the same go, so let's add some
+tools to the shopping list withing a new topic::
+
+  $ hg topic tools
+  $ echo hammer >> shopping
+  $ hg ci -m 'Adding hammer'
+  $ echo saw >> shopping
+  $ hg ci -m 'Adding saw'
+  $ echo drill >> shopping
+  $ hg ci -m 'Adding drill'
+
+But are not sure to actually go in the hardward store, so in the meantime, we
+want to extend the list with drinks. We go back to the official default branch
+and start a new topic::
+
+  $ hg up default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg topic drinks
+  $ echo 'apple juice' >> shopping
+  $ hg ci -m 'Adding apple juice'
+  $ echo 'orange juice' >> shopping
+  $ hg ci -m 'Adding orange juice'
+
+We now have two topics::
+
+  $ hg topic
+   * drinks
+     tools
+
+The information ``hg stack`` command adapt to the active topic::
+
+  $ hg stack
+  ### topic: drinks
+  ### branch: default
+  t2@ Adding orange juice (current)
+  t1: Adding apple juice
+    ^ adding fruits
+  $ hg up tools
+  switching to topic tools
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg stack
+  ### topic: tools
+  ### branch: default
+  t3@ Adding drill (current)
+  t2: Adding saw
+  t1: Adding hammer
+    ^ adding fruits
+
+They are seen as independant branch by Mercurial. No rebase or merge betwen them will be attempted by default::
+
+  $ hg rebase
+  nothing to rebase
+  [1]
+
+.. server activity::
+
+  $ cd ../server
+  $ hg up
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ mv shopping foo
+  $ echo 'Coat' > shopping
+  $ cat foo >> shopping
+  $ hg ci -m 'add a coat'
+  $ echo 'Coat' > shopping
+  $ echo 'Shoes' >> shopping
+  $ cat foo >> shopping
+  $ hg rm foo
+  not removing foo: file is untracked
+  [1]
+  $ hg ci -m 'add a pair of shoes'
+  $ cd ../client
+
+Lets see what other people did in the mean time::
+
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 1 files (+1 heads)
+  (run 'hg heads' to see heads)
+
+There is new changes! We can simply use ``hg rebase`` to update our changeset on top of the latest::
+
+  $ hg rebase
+  rebasing 6:183984ef46d1 "Adding hammer"
+  merging shopping
+  rebasing 7:cffff85af537 "Adding saw"
+  merging shopping
+  rebasing 8:34255b455dac "Adding drill"
+  merging shopping
+
+But what about the other topic? You can use 'hg topic --verbose' to see information about them::
+
+  $ hg topic --verbose
+     drinks (on branch: default, 2 changesets, 2 behind)
+     tools  (on branch: default, 3 changesets)
+
+The "2 behind" is telling you that there is 2 new changesets on the named branch of the topic. You need to merge or rebase to incorporate them.
+
+Pushing that topic would create a new heads will be prevented::
+
+  $ hg push --rev drinks
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 70dfa201ed73!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+
+
+Even after a rebase Pushing all active topics at the same time will complains about the multiple heads it would create on that branch::
+
+  $ hg rebase -b drinks
+  rebasing 9:8dfa45bd5e0c "Adding apple juice"
+  merging shopping
+  rebasing 10:70dfa201ed73 "Adding orange juice"
+  merging shopping
+  switching to topic tools
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 4cd7c1591a67!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+
+Publishing only one of them is allowed (as long as it does not create a new branch head has we just saw in the previous case)::
+
+  $ hg push -r drinks
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 1 files
+  2 new obsolescence markers
+
+The publishing topic has now vanished, and the one still draft is now marked as "behind"::
+
+  $ hg topic --verbose
+   * tools (on branch: default, 3 changesets, 2 behind)
+  $ hg stack
+  ### topic: tools
+  ### branch: default, 2 behind
+  t3@ Adding drill (current)
+  t2: Adding saw
+  t1: Adding hammer
+    ^ add a pair of shoes
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-topic.t	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,656 @@
+  $ . "$TESTDIR/testlib"
+
+  $ hg init pinky
+  $ cd pinky
+  $ cat <<EOF >> .hg/hgrc
+  > [phases]
+  > publish=false
+  > EOF
+  $ cat <<EOF >> $HGRCPATH
+  > [experimental]
+  > # disable the new graph style until we drop 3.7 support
+  > graphstyle.missing = |
+  > EOF
+
+  $ hg help topics
+  hg topics [TOPIC]
+  
+  View current topic, set current topic, or see all topics.
+  
+      The --verbose version of this command display various information on the
+      state of each topic.
+  
+  options:
+  
+      --clear        clear active topic if any
+      --change VALUE revset of existing revisions to change topic
+   -l --list         show the stack of changeset in the topic
+  
+  (some details hidden, use --verbose to show complete help)
+  $ hg topics
+
+Test topics interaction with evolution:
+
+  $ hg topics --config experimental.evolution=
+  $ hg topics --config experimental.evolution= --change . bob
+  abort: must have obsolete enabled to use --change
+  [255]
+
+Create some changes:
+
+  $ for x in alpha beta gamma delta ; do
+  >   echo file $x >> $x
+  >   hg addremove
+  >   hg ci -m "Add file $x"
+  > done
+  adding alpha
+  adding beta
+  adding gamma
+  adding delta
+
+Still no topics
+  $ hg topics
+
+Test commit flag and help text
+
+  $ echo stuff >> alpha
+  $ HGEDITOR=cat hg ci -t topicflag
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: topic 'topicflag'
+  HG: branch 'default'
+  HG: changed alpha
+  abort: empty commit message
+  [255]
+  $ hg revert alpha
+  $ hg topic
+   * topicflag
+
+Make a topic
+  $ hg topic narf
+  $ hg topics
+   * narf
+  $ echo topic work >> alpha
+  $ hg ci -m 'start on narf'
+  $ hg co .^
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg topic fran
+  $ hg topics
+   * fran
+     narf
+  $ echo >> fran work >> beta
+  $ hg ci -m 'start on fran'
+  $ hg co narf
+  switching to topic narf
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg topic
+     fran
+   * narf
+  $ hg log -r . -T '{topics}\n'
+  narf
+  $ echo 'narf!!!' >> alpha
+  $ hg ci -m 'narf!'
+  $ hg log -G
+  @  changeset:   6:7c34953036d6
+  |  tag:         tip
+  |  topic:       narf
+  |  parent:      4:fb147b0b417c
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     narf!
+  |
+  | o  changeset:   5:0469d521db49
+  | |  topic:       fran
+  | |  parent:      3:a53952faf762
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     start on fran
+  | |
+  o |  changeset:   4:fb147b0b417c
+  |/   topic:       narf
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     start on narf
+  |
+  o  changeset:   3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Add file delta
+  |
+  o  changeset:   2:15d1eb11d2fa
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Add file gamma
+  |
+  o  changeset:   1:c692ea2c9224
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Add file beta
+  |
+  o  changeset:   0:c2b7d2f7d14b
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     Add file alpha
+  
+
+Exchanging of topics:
+  $ cd ..
+  $ hg init brain
+  $ hg -R pinky push -r 4 brain
+  pushing to brain
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 5 changesets with 5 changes to 4 files
+
+Export
+
+  $ hg -R pinky export
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 7c34953036d6a36eae468c550d0592b89ee8bffc
+  # Parent  fb147b0b417c25ca15547cd945acf51cf8dcaf02
+  # EXP-Topic narf
+  narf!
+  
+  diff -r fb147b0b417c -r 7c34953036d6 alpha
+  --- a/alpha	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/alpha	Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,2 +1,3 @@
+   file alpha
+   topic work
+  +narf!!!
+
+Import
+
+  $ hg -R pinky export > narf.diff
+  $ hg -R pinky --config extensions.strip= strip .
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  saved backup bundle to $TESTTMP/pinky/.hg/strip-backup/7c34953036d6-1ff3bae2-backup.hg (glob)
+  $ hg -R pinky import narf.diff
+  applying narf.diff
+  $ hg -R pinky log -r .
+  changeset:   6:7c34953036d6
+  tag:         tip
+  topic:       narf
+  parent:      4:fb147b0b417c
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     narf!
+  
+Now that we've pushed to brain, the work done on narf is no longer a
+draft, so we won't see that topic name anymore:
+
+  $ hg log -R pinky -G
+  @  changeset:   6:7c34953036d6
+  |  tag:         tip
+  |  topic:       narf
+  |  parent:      4:fb147b0b417c
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     narf!
+  |
+  | o  changeset:   5:0469d521db49
+  | |  topic:       fran
+  | |  parent:      3:a53952faf762
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     start on fran
+  | |
+  o |  changeset:   4:fb147b0b417c
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     start on narf
+  |
+  o  changeset:   3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Add file delta
+  |
+  o  changeset:   2:15d1eb11d2fa
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Add file gamma
+  |
+  o  changeset:   1:c692ea2c9224
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Add file beta
+  |
+  o  changeset:   0:c2b7d2f7d14b
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     Add file alpha
+  
+  $ cd brain
+  $ hg co tip
+  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+Because the change is public, we won't inherit the topic from narf.
+
+  $ hg topic
+  $ echo what >> alpha
+  $ hg topic query
+  $ hg ci -m 'what is narf, pinky?'
+  $ hg log -Gl2
+  @  changeset:   5:c01515cfc331
+  |  tag:         tip
+  |  topic:       query
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     what is narf, pinky?
+  |
+  o  changeset:   4:fb147b0b417c
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     start on narf
+  |
+  $ hg push -f ../pinky -r query
+  pushing to ../pinky
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  $ hg -R ../pinky log -Gl 4
+  o  changeset:   7:c01515cfc331
+  |  tag:         tip
+  |  topic:       query
+  |  parent:      4:fb147b0b417c
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     what is narf, pinky?
+  |
+  | @  changeset:   6:7c34953036d6
+  |/   topic:       narf
+  |    parent:      4:fb147b0b417c
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     narf!
+  |
+  | o  changeset:   5:0469d521db49
+  | |  topic:       fran
+  | |  parent:      3:a53952faf762
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     start on fran
+  | |
+  o |  changeset:   4:fb147b0b417c
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     start on narf
+  |
+  $ hg topics
+   * query
+  $ cd ../pinky
+  $ hg co query
+  switching to topic query
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo answer >> alpha
+  $ hg ci -m 'Narf is like `zort` or `poit`!'
+  $ hg merge narf
+  merging alpha
+  warning: conflicts while merging alpha! (edit, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
+  $ hg revert -r narf alpha
+  $ hg resolve -m alpha
+  (no more unresolved files)
+  $ hg topic narf
+  $ hg ci -m 'Finish narf'
+  $ hg topics
+     fran
+   * narf
+     query
+  $ hg debugnamecomplete # branch:topic here is a buggy side effect
+  default
+  default:fran
+  default:narf
+  default:query
+  fran
+  narf
+  query
+  tip
+  $ hg phase --public narf
+
+POSSIBLE BUG: narf topic stays alive even though we just made all
+narf commits public:
+
+  $ hg topics
+     fran
+   * narf
+  $ hg log -Gl 6
+  @    changeset:   9:ae074045b7a7
+  |\   tag:         tip
+  | |  parent:      8:54c943c1c167
+  | |  parent:      6:7c34953036d6
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     Finish narf
+  | |
+  | o  changeset:   8:54c943c1c167
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     Narf is like `zort` or `poit`!
+  | |
+  | o  changeset:   7:c01515cfc331
+  | |  parent:      4:fb147b0b417c
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     what is narf, pinky?
+  | |
+  o |  changeset:   6:7c34953036d6
+  |/   parent:      4:fb147b0b417c
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     narf!
+  |
+  | o  changeset:   5:0469d521db49
+  | |  topic:       fran
+  | |  parent:      3:a53952faf762
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     start on fran
+  | |
+  o |  changeset:   4:fb147b0b417c
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     start on narf
+  |
+  $ cd ../brain
+  $ hg topics
+   * query
+  $ hg pull ../pinky -r narf
+  pulling from ../pinky
+  abort: unknown revision 'narf'!
+  [255]
+  $ hg pull ../pinky -r default
+  pulling from ../pinky
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg topics
+   * query
+
+We can pull in the draft-phase change and we get the new topic
+
+  $ hg pull ../pinky
+  pulling from ../pinky
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  (run 'hg heads' to see heads)
+  $ hg topics
+     fran
+   * query
+  $ hg log -Gr 'draft()'
+  o  changeset:   9:0469d521db49
+  |  tag:         tip
+  |  topic:       fran
+  |  parent:      3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     start on fran
+  |
+
+query is not an open topic, so when we clear the current topic it'll
+disappear:
+
+  $ hg topics --clear
+  $ hg topics
+     fran
+
+--clear when we don't have an active topic isn't an error:
+
+  $ hg topics --clear
+
+Topic revset
+  $ hg log -r 'topic()' -G
+  o  changeset:   9:0469d521db49
+  |  tag:         tip
+  |  topic:       fran
+  |  parent:      3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     start on fran
+  |
+  $ hg log -r 'not topic()' -G
+  o    changeset:   8:ae074045b7a7
+  |\   parent:      7:54c943c1c167
+  | |  parent:      6:7c34953036d6
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     Finish narf
+  | |
+  | o  changeset:   7:54c943c1c167
+  | |  parent:      5:c01515cfc331
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     Narf is like `zort` or `poit`!
+  | |
+  o |  changeset:   6:7c34953036d6
+  | |  parent:      4:fb147b0b417c
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     narf!
+  | |
+  | @  changeset:   5:c01515cfc331
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     what is narf, pinky?
+  |
+  o  changeset:   4:fb147b0b417c
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     start on narf
+  |
+  o  changeset:   3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Add file delta
+  |
+  o  changeset:   2:15d1eb11d2fa
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Add file gamma
+  |
+  o  changeset:   1:c692ea2c9224
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     Add file beta
+  |
+  o  changeset:   0:c2b7d2f7d14b
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     Add file alpha
+  
+No matches because narf is already closed:
+  $ hg log -r 'topic(narf)' -G
+This regexp should match the topic `fran`:
+  $ hg log -r 'topic("re:.ra.")' -G
+  o  changeset:   9:0469d521db49
+  |  tag:         tip
+  |  topic:       fran
+  |  parent:      3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     start on fran
+  |
+Exact match on fran:
+  $ hg log -r 'topic(fran)' -G
+  o  changeset:   9:0469d521db49
+  |  tag:         tip
+  |  topic:       fran
+  |  parent:      3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     start on fran
+  |
+
+Match current topic:
+  $ hg topic
+     fran
+  $ hg log -r 'topic(.)'
+(no output is expected)
+  $ hg co fran
+  switching to topic fran
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg log -r 'topic(.)'
+  changeset:   9:0469d521db49
+  tag:         tip
+  topic:       fran
+  parent:      3:a53952faf762
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     start on fran
+  
+
+Deactivate the topic.
+  $ hg topics
+   * fran
+  $ hg topics --clear
+  $ echo fran? >> beta
+  $ hg ci -m 'fran?'
+  created new head
+  $ hg log -Gr 'draft()'
+  @  changeset:   10:4073470c35e1
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     fran?
+  |
+  o  changeset:   9:0469d521db49
+  |  topic:       fran
+  |  parent:      3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     start on fran
+  |
+  $ hg topics
+     fran
+Changing topic fails if we don't give a topic
+  $ hg topic --change 9
+  abort: changing topic requires a topic name or --clear
+  [255]
+
+Can't change topic of a public change
+  $ hg topic --change 1:: --clear
+  abort: can't change topic of a public change
+  [255]
+
+Can clear topics
+  $ hg topic --change 9 --clear
+  changed topic on 1 changes
+  please run hg evolve --rev "not topic()" now
+  $ hg log -Gr 'draft() and not obsolete()'
+  o  changeset:   11:783930e1d79e
+  |  tag:         tip
+  |  parent:      3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     start on fran
+  |
+  | @  changeset:   10:4073470c35e1
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  trouble:     unstable
+  | |  summary:     fran?
+  | |
+
+Normally you'd do this with evolve, but we'll use rebase to avoid
+bonus deps in the testsuite.
+
+  $ hg rebase -d tip -s .
+  rebasing 10:4073470c35e1 "fran?"
+
+Can add a topic to an existing change
+  $ hg topic --change 11 wat
+  changed topic on 1 changes
+  please run hg evolve --rev "topic(wat)" now
+  $ hg log -Gr 'draft() and not obsolete()'
+  o  changeset:   13:d91cd8fd490e
+  |  tag:         tip
+  |  topic:       wat
+  |  parent:      3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     start on fran
+  |
+  | @  changeset:   12:d9e32f4c4806
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  trouble:     unstable
+  | |  summary:     fran?
+  | |
+
+Normally you'd do this with evolve, but we'll use rebase to avoid
+bonus deps in the testsuite.
+
+  $ hg rebase -d tip -s .
+  rebasing 12:d9e32f4c4806 "fran?"
+
+  $ hg log -Gr 'draft()'
+  @  changeset:   14:cf24ad8bbef5
+  |  tag:         tip
+  |  topic:       wat
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     fran?
+  |
+  o  changeset:   13:d91cd8fd490e
+  |  topic:       wat
+  |  parent:      3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     start on fran
+  |
+
+Amend a topic
+
+  $ hg topic watwat
+  $ hg ci --amend
+  $ hg log -Gr 'draft()'
+  @  changeset:   16:893ffcf66c1f
+  |  tag:         tip
+  |  topic:       watwat
+  |  parent:      13:d91cd8fd490e
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     fran?
+  |
+  o  changeset:   13:d91cd8fd490e
+  |  topic:       wat
+  |  parent:      3:a53952faf762
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     start on fran
+  |
+
+Clear and amend:
+
+  $ hg topic --clear
+  $ hg ci --amend
+  $ hg log -r .
+  changeset:   18:a13639e22b65
+  tag:         tip
+  parent:      13:d91cd8fd490e
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     fran?
+  
+Readding the same topic with topic --change should work:
+  $ hg topic --change . watwat
+  changed topic on 1 changes
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/testlib	Thu Mar 02 18:07:46 2017 +0100
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+# This file holds logic that is used in many tests.
+# It can be called in a test like this:
+#  $ . "$TESTDIR/testlib"
+
+# Enable obsolete markers and enable extensions
+cat >> $HGRCPATH << EOF
+[experimental]
+evolution=createmarkers,exchange
+
+[extensions]
+rebase=
+EOF
+echo "topic=$(echo $(dirname $TESTDIR))/hgext3rd/topic" >> $HGRCPATH