changeset 1901:85390446f8c1

packaging: fix setup.py and install as hgext3rd.topic This changeset is doing two things (gasp): - It fixes various errors in the setup.py - It move the topic source and install into hgext3rd.topic. This last part (code source move) use hgext3rd as namespace package to prevent installation nightmare. This won't be officially supported until Mercurial 3.8, but in the meantime, 3.7 user can enable it using the full package name: [extensions] hgext3rd.topic= Thanks goes to Julien Cristau <julien.cristau@logilab.fr> for the initial version of this.
author Pierre-Yves David <pierre-yves.david@fb.com>
date Thu, 17 Mar 2016 09:12:18 -0700
parents b65f39791f92
children 93cf0ddb5234
files README.md hgext3rd/__init__.py hgext3rd/topic/__init__.py hgext3rd/topic/constants.py hgext3rd/topic/destination.py hgext3rd/topic/discovery.py hgext3rd/topic/revset.py hgext3rd/topic/stack.py hgext3rd/topic/topicmap.py setup.py src/topic/__init__.py src/topic/constants.py src/topic/destination.py src/topic/discovery.py src/topic/revset.py src/topic/stack.py src/topic/topicmap.py tests/testlib
diffstat 18 files changed, 916 insertions(+), 908 deletions(-) [+]
line wrap: on
line diff
--- a/README.md	Tue Mar 15 17:25:18 2016 +0000
+++ b/README.md	Thu Mar 17 09:12:18 2016 -0700
@@ -13,3 +13,8 @@
 
     [extensions]
     topics=path/to/hg-topics/src
+
+If you are using Mercurial 3.7 use:
+
+    [extensions]
+    hgext3rd.topics=path/to/hg-topics/src
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/__init__.py	Thu Mar 17 09:12:18 2016 -0700
@@ -0,0 +1,3 @@
+from __future__ import absolute_import
+import pkgutil
+__path__ = pkgutil.extend_path(__path__, __name__)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/__init__.py	Thu Mar 17 09:12:18 2016 -0700
@@ -0,0 +1,343 @@
+# __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.
+
+This is sort of similar to a bookmark, but it applies to a whole
+series instead of a single revision.
+"""
+import functools
+import contextlib
+
+from mercurial.i18n import _
+from mercurial import branchmap
+from mercurial import bundle2
+from mercurial import changegroup
+from mercurial import cmdutil
+from mercurial import commands
+from mercurial import context
+from mercurial import discovery as discoverymod
+from mercurial import error
+from mercurial import exchange
+from mercurial import extensions
+from mercurial import localrepo
+from mercurial import lock
+from mercurial import merge
+from mercurial import namespaces
+from mercurial import node
+from mercurial import obsolete
+from mercurial import patch
+from mercurial import phases
+from mercurial import util
+from mercurial import wireproto
+
+from . import constants
+from . import revset as topicrevset
+from . import destination
+from . import stack
+from . import topicmap
+from . import discovery
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+
+testedwith = '3.7'
+
+def _contexttopic(self):
+    return self.extra().get(constants.extrakey, '')
+context.basectx.topic = _contexttopic
+
+def _namemap(repo, name):
+    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.setupdest()
+
+@contextlib.contextmanager
+def usetopicmap(repo):
+    """use awful monkey patching to update the topic cache"""
+    oldbranchcache = branchmap.branchcache
+    oldfilename = branchmap._filename
+    oldread = branchmap.read
+    oldcaches =  getattr(repo, '_branchcaches', {})
+    try:
+        branchmap.branchcache = topicmap.topiccache
+        branchmap._filename = topicmap._filename
+        branchmap.read = topicmap.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):
+    with usetopicmap(repo):
+        return orig(repo, *args, **kwargs)
+
+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 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 usetopicmap(self):
+                return orig.commitctx(self, ctx, error=error)
+
+        @property
+        def topics(self):
+            topics = set(['', self.currenttopic])
+            for c in self.set('not public()'):
+                topics.add(c.topic())
+            topics.remove('')
+            return topics
+
+        @property
+        def currenttopic(self):
+            return self.vfs.tryread('topic')
+
+        def branchmap(self, topic=True):
+            if not topic:
+                super(topicrepo, self).branchmap()
+            with usetopicmap(self):
+                branchmap.updatecache(self)
+            return self._topiccaches[self.filtername]
+
+        def destroyed(self, *args, **kwargs):
+            with usetopicmap(self):
+                return super(topicrepo, self).destroyed(*args, **kwargs)
+
+        def invalidatecaches(self):
+            super(topicrepo, self).invalidatecaches()
+            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
+    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'),
+])
+def topics(ui, repo, topic='', clear=False, change=None, list=False):
+    """View current topic, set current topic, or see all topics."""
+    if list:
+        if clear or change:
+            raise error.Abort(_("cannot use --clear or --change with --list"))
+        return stack.showstack(ui, repo, topic)
+
+    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.vfs.open('topic', 'w') as f:
+            f.write(topic)
+        return
+    current = repo.currenttopic
+    for t in sorted(repo.topics):
+        marker = '*' if t == current else ' '
+        ui.write(' %s %s\n' % (marker, t))
+
+def summaryhook(ui, repo):
+    t = repo.currenttopic
+    if not t:
+        return
+    # i18n: column positioning for "hg summary"
+    ui.write(_("topic:  %s\n") % t)
+
+def commitwrap(orig, ui, repo, *args, **opts):
+    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):
+    partial = bool(len(args)) or 'matcher' in kwargs
+    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])
+
+    rebase = extensions.find("rebase")
+    extensions.wrapfunction(rebase, '_makeextrafn', newmakeextrafn)
+
+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']
+
+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)
+extensions.wrapfunction(discoverymod, '_headssummary', discovery._headssummary)
+extensions.wrapfunction(wireproto, 'branchmap', discovery.wireprotobranchmap)
+extensions.wrapfunction(bundle2, 'handlecheckheads', discovery.handlecheckheads)
+bundle2.handlecheckheads.params = frozenset() # we need a proper wrape b2 part stuff
+bundle2.parthandlermapping['check:heads'] = bundle2.handlecheckheads
+extensions.wrapfunction(exchange, '_pushb2phases', discovery._pushb2phases)
+extensions.wrapfunction(changegroup.cg1unpacker, 'apply', cgapply)
+exchange.b2partsgenmapping['phase'] = exchange._pushb2phases
+topicrevset.modsetup()
+cmdutil.summaryhooks.add('topic', summaryhook)
+
+if util.safehasattr(cmdutil, 'extraexport'):
+    cmdutil.extraexport.append('topic')
+    cmdutil.extraexportmap['topic'] = _exporttopic
+if util.safehasattr(cmdutil, 'extrapreimport'):
+    cmdutil.extrapreimport.append('topic')
+    cmdutil.extrapreimportmap['topic'] = _importtopic
+if util.safehasattr(patch, 'patchheadermap'):
+    patch.patchheadermap.append(('EXP-Topic', 'topic'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/constants.py	Thu Mar 17 09:12:18 2016 -0700
@@ -0,0 +1,1 @@
+extrakey = 'topic'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/destination.py	Thu Mar 17 09:12:18 2016 -0700
@@ -0,0 +1,87 @@
+from mercurial import error
+from mercurial import util
+from mercurial import destutil
+from mercurial import extensions
+from mercurial import bookmarks
+from mercurial.i18n import _
+
+def _destmergebranch(orig, repo, action='merge', sourceset=None, onheadcheck=True):
+    p1 = repo['.']
+    top = p1.topic()
+    if top:
+        heads = repo.revs('heads(topic(.)::topic(.))')
+        if p1.rev() not in heads:
+            raise error.Abort(_("not at topic head, update or explicit"))
+        elif 1 == len(heads):
+            # should look at all branch involved but... later
+            bhead = ngtip(repo, p1.branch(), all=True)
+            if not bhead:
+                raise error.Abort(_("nothing to merge"))
+            elif 1 == len(bhead):
+                return bhead.first()
+            else:
+                raise error.Abort(_("branch '%s' has %d heads - "
+                                   "please merge with an explicit rev")
+                                 % (p1.branch(), len(bhead)),
+                                 hint=_("run 'hg heads .' to see heads"))
+        elif 2 == len(heads):
+            heads = [r for r in heads if r != p1.rev()]
+            # XXX: bla bla bla bla bla
+            if 1 < len(heads):
+                raise error.Abort(_('working directory not at a head revision'),
+                                 hint=_("use 'hg update' or merge with an "
+                                        "explicit revision"))
+            return heads[0]
+        elif 2 < len(heads):
+            raise error.Abort(_("topic '%s' has %d heads - "
+                                "please merge with an explicit rev")
+                              % (top, len(heads)))
+        else:
+            assert False # that's impossible
+    if orig.func_default: # version above hg-3.7
+        return orig(repo, action, sourceset, onheadcheck)
+    else:
+        return orig(repo)
+
+def _destupdatetopic(repo, clean, check):
+    """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 setupdest():
+    if util.safehasattr(destutil, '_destmergebranch'):
+        extensions.wrapfunction(destutil, '_destmergebranch', _destmergebranch)
+    rebase = extensions.find('rebase')
+    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
+
+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
+    subquery = '''heads(
+                    parents(
+                       ancestor(
+                         (head() and branch(%s)
+                         or (topic() and branch(%s)))))
+                   ::(head() and branch(%s))
+                   - topic())'''
+    if not all:
+        subquery = 'max(%s)' % subquery
+    return repo.revs(subquery, branch, branch, branch)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/discovery.py	Thu Mar 17 09:12:18 2016 -0700
@@ -0,0 +1,106 @@
+import weakref
+from mercurial import branchmap
+from mercurial import error
+from mercurial import exchange
+from mercurial.i18n import _
+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:
+        return orig(repo, remote, outgoing)
+    oldgetitem = repo.__getitem__
+    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)
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/revset.py	Thu Mar 17 09:12:18 2016 -0700
@@ -0,0 +1,50 @@
+from mercurial import revset
+from mercurial import util
+
+from . import constants, destination
+
+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 & destination.ngtip(repo, branch)
+
+def modsetup():
+    revset.symbols.update({'topic': topicset})
+    revset.symbols.update({'ngtip': ngtipset})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/stack.py	Thu Mar 17 09:12:18 2016 -0700
@@ -0,0 +1,118 @@
+# 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.
+import collections
+from mercurial.i18n import _
+from mercurial import error
+from mercurial import extensions
+from mercurial import obsolete
+
+def _getstack(repo, topic):
+    # XXX need sorting
+    trevs = repo.revs("topic(%s) - obsolete()", topic)
+    return _orderrevs(repo, trevs)
+
+def showstack(ui, repo, topic):
+    if not topic:
+        topic = repo.currenttopic
+    if not topic:
+        raise error.Abort(_('no active topic to list'))
+    for idx, r in enumerate(_getstack(repo, topic)):
+        # super crude initial version
+        l = "%d: %s\n" % (idx, repo[r].description().splitlines()[0])
+        ui.write(l)
+
+# 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 = collections.deque([r for r in sorted(dependencies.keys())
+                                      if not dependencies[r]])
+    ordering = []
+    while solvablerevs:
+        rev = solvablerevs.popleft()
+        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/topicmap.py	Thu Mar 17 09:12:18 2016 -0700
@@ -0,0 +1,200 @@
+from mercurial import branchmap
+from mercurial import encoding
+from mercurial import error
+from mercurial import scmutil
+from mercurial import util
+from mercurial.node import hex, bin, nullid
+
+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 = util.sha1()
+        for rev in revs:
+            s.update('%s;' % rev)
+        key = s.digest()
+    return key
+
+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 = branchcache(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
--- a/setup.py	Tue Mar 15 17:25:18 2016 +0000
+++ b/setup.py	Thu Mar 17 09:12:18 2016 -0700
@@ -14,9 +14,9 @@
     maintainer_email='augie@google.com',
     url='http://bitbucket.org/durin42/hg-topics/',
     description='Experimental tinkering with workflow ideas for topic branches.',
-    long_description=open('README').read(),
+    long_description=open('README.md').read(),
     keywords='hg mercurial',
     license='GPLv2+',
-    py_modules=['src'],
+    packages=['hgext3rd.topic'],
     install_requires=requires,
 )
--- a/src/topic/__init__.py	Tue Mar 15 17:25:18 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,343 +0,0 @@
-# __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.
-
-This is sort of similar to a bookmark, but it applies to a whole
-series instead of a single revision.
-"""
-import functools
-import contextlib
-
-from mercurial.i18n import _
-from mercurial import branchmap
-from mercurial import bundle2
-from mercurial import changegroup
-from mercurial import cmdutil
-from mercurial import commands
-from mercurial import context
-from mercurial import discovery as discoverymod
-from mercurial import error
-from mercurial import exchange
-from mercurial import extensions
-from mercurial import localrepo
-from mercurial import lock
-from mercurial import merge
-from mercurial import namespaces
-from mercurial import node
-from mercurial import obsolete
-from mercurial import patch
-from mercurial import phases
-from mercurial import util
-from mercurial import wireproto
-
-from . import constants
-from . import revset as topicrevset
-from . import destination
-from . import stack
-from . import topicmap
-from . import discovery
-
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-
-testedwith = '3.7'
-
-def _contexttopic(self):
-    return self.extra().get(constants.extrakey, '')
-context.basectx.topic = _contexttopic
-
-def _namemap(repo, name):
-    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.setupdest()
-
-@contextlib.contextmanager
-def usetopicmap(repo):
-    """use awful monkey patching to update the topic cache"""
-    oldbranchcache = branchmap.branchcache
-    oldfilename = branchmap._filename
-    oldread = branchmap.read
-    oldcaches =  getattr(repo, '_branchcaches', {})
-    try:
-        branchmap.branchcache = topicmap.topiccache
-        branchmap._filename = topicmap._filename
-        branchmap.read = topicmap.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):
-    with usetopicmap(repo):
-        return orig(repo, *args, **kwargs)
-
-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 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 usetopicmap(self):
-                return orig.commitctx(self, ctx, error=error)
-
-        @property
-        def topics(self):
-            topics = set(['', self.currenttopic])
-            for c in self.set('not public()'):
-                topics.add(c.topic())
-            topics.remove('')
-            return topics
-
-        @property
-        def currenttopic(self):
-            return self.vfs.tryread('topic')
-
-        def branchmap(self, topic=True):
-            if not topic:
-                super(topicrepo, self).branchmap()
-            with usetopicmap(self):
-                branchmap.updatecache(self)
-            return self._topiccaches[self.filtername]
-
-        def destroyed(self, *args, **kwargs):
-            with usetopicmap(self):
-                return super(topicrepo, self).destroyed(*args, **kwargs)
-
-        def invalidatecaches(self):
-            super(topicrepo, self).invalidatecaches()
-            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
-    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'),
-])
-def topics(ui, repo, topic='', clear=False, change=None, list=False):
-    """View current topic, set current topic, or see all topics."""
-    if list:
-        if clear or change:
-            raise error.Abort(_("cannot use --clear or --change with --list"))
-        return stack.showstack(ui, repo, topic)
-
-    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.vfs.open('topic', 'w') as f:
-            f.write(topic)
-        return
-    current = repo.currenttopic
-    for t in sorted(repo.topics):
-        marker = '*' if t == current else ' '
-        ui.write(' %s %s\n' % (marker, t))
-
-def summaryhook(ui, repo):
-    t = repo.currenttopic
-    if not t:
-        return
-    # i18n: column positioning for "hg summary"
-    ui.write(_("topic:  %s\n") % t)
-
-def commitwrap(orig, ui, repo, *args, **opts):
-    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):
-    partial = bool(len(args)) or 'matcher' in kwargs
-    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])
-
-    rebase = extensions.find("rebase")
-    extensions.wrapfunction(rebase, '_makeextrafn', newmakeextrafn)
-
-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']
-
-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)
-extensions.wrapfunction(discoverymod, '_headssummary', discovery._headssummary)
-extensions.wrapfunction(wireproto, 'branchmap', discovery.wireprotobranchmap)
-extensions.wrapfunction(bundle2, 'handlecheckheads', discovery.handlecheckheads)
-bundle2.handlecheckheads.params = frozenset() # we need a proper wrape b2 part stuff
-bundle2.parthandlermapping['check:heads'] = bundle2.handlecheckheads
-extensions.wrapfunction(exchange, '_pushb2phases', discovery._pushb2phases)
-extensions.wrapfunction(changegroup.cg1unpacker, 'apply', cgapply)
-exchange.b2partsgenmapping['phase'] = exchange._pushb2phases
-topicrevset.modsetup()
-cmdutil.summaryhooks.add('topic', summaryhook)
-
-if util.safehasattr(cmdutil, 'extraexport'):
-    cmdutil.extraexport.append('topic')
-    cmdutil.extraexportmap['topic'] = _exporttopic
-if util.safehasattr(cmdutil, 'extrapreimport'):
-    cmdutil.extrapreimport.append('topic')
-    cmdutil.extrapreimportmap['topic'] = _importtopic
-if util.safehasattr(patch, 'patchheadermap'):
-    patch.patchheadermap.append(('EXP-Topic', 'topic'))
--- a/src/topic/constants.py	Tue Mar 15 17:25:18 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-extrakey = 'topic'
--- a/src/topic/destination.py	Tue Mar 15 17:25:18 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-from mercurial import error
-from mercurial import util
-from mercurial import destutil
-from mercurial import extensions
-from mercurial import bookmarks
-from mercurial.i18n import _
-
-def _destmergebranch(orig, repo, action='merge', sourceset=None, onheadcheck=True):
-    p1 = repo['.']
-    top = p1.topic()
-    if top:
-        heads = repo.revs('heads(topic(.)::topic(.))')
-        if p1.rev() not in heads:
-            raise error.Abort(_("not at topic head, update or explicit"))
-        elif 1 == len(heads):
-            # should look at all branch involved but... later
-            bhead = ngtip(repo, p1.branch(), all=True)
-            if not bhead:
-                raise error.Abort(_("nothing to merge"))
-            elif 1 == len(bhead):
-                return bhead.first()
-            else:
-                raise error.Abort(_("branch '%s' has %d heads - "
-                                   "please merge with an explicit rev")
-                                 % (p1.branch(), len(bhead)),
-                                 hint=_("run 'hg heads .' to see heads"))
-        elif 2 == len(heads):
-            heads = [r for r in heads if r != p1.rev()]
-            # XXX: bla bla bla bla bla
-            if 1 < len(heads):
-                raise error.Abort(_('working directory not at a head revision'),
-                                 hint=_("use 'hg update' or merge with an "
-                                        "explicit revision"))
-            return heads[0]
-        elif 2 < len(heads):
-            raise error.Abort(_("topic '%s' has %d heads - "
-                                "please merge with an explicit rev")
-                              % (top, len(heads)))
-        else:
-            assert False # that's impossible
-    if orig.func_default: # version above hg-3.7
-        return orig(repo, action, sourceset, onheadcheck)
-    else:
-        return orig(repo)
-
-def _destupdatetopic(repo, clean, check):
-    """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 setupdest():
-    if util.safehasattr(destutil, '_destmergebranch'):
-        extensions.wrapfunction(destutil, '_destmergebranch', _destmergebranch)
-    rebase = extensions.find('rebase')
-    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
-
-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
-    subquery = '''heads(
-                    parents(
-                       ancestor(
-                         (head() and branch(%s)
-                         or (topic() and branch(%s)))))
-                   ::(head() and branch(%s))
-                   - topic())'''
-    if not all:
-        subquery = 'max(%s)' % subquery
-    return repo.revs(subquery, branch, branch, branch)
--- a/src/topic/discovery.py	Tue Mar 15 17:25:18 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,106 +0,0 @@
-import weakref
-from mercurial import branchmap
-from mercurial import error
-from mercurial import exchange
-from mercurial.i18n import _
-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:
-        return orig(repo, remote, outgoing)
-    oldgetitem = repo.__getitem__
-    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)
-
-
-
--- a/src/topic/revset.py	Tue Mar 15 17:25:18 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-from mercurial import revset
-from mercurial import util
-
-from . import constants, destination
-
-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 & destination.ngtip(repo, branch)
-
-def modsetup():
-    revset.symbols.update({'topic': topicset})
-    revset.symbols.update({'ngtip': ngtipset})
--- a/src/topic/stack.py	Tue Mar 15 17:25:18 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
-# 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.
-import collections
-from mercurial.i18n import _
-from mercurial import error
-from mercurial import extensions
-from mercurial import obsolete
-
-def _getstack(repo, topic):
-    # XXX need sorting
-    trevs = repo.revs("topic(%s) - obsolete()", topic)
-    return _orderrevs(repo, trevs)
-
-def showstack(ui, repo, topic):
-    if not topic:
-        topic = repo.currenttopic
-    if not topic:
-        raise error.Abort(_('no active topic to list'))
-    for idx, r in enumerate(_getstack(repo, topic)):
-        # super crude initial version
-        l = "%d: %s\n" % (idx, repo[r].description().splitlines()[0])
-        ui.write(l)
-
-# 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 = collections.deque([r for r in sorted(dependencies.keys())
-                                      if not dependencies[r]])
-    ordering = []
-    while solvablerevs:
-        rev = solvablerevs.popleft()
-        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
--- a/src/topic/topicmap.py	Tue Mar 15 17:25:18 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,200 +0,0 @@
-from mercurial import branchmap
-from mercurial import encoding
-from mercurial import error
-from mercurial import scmutil
-from mercurial import util
-from mercurial.node import hex, bin, nullid
-
-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 = util.sha1()
-        for rev in revs:
-            s.update('%s;' % rev)
-        key = s.digest()
-    return key
-
-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 = branchcache(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
--- a/tests/testlib	Tue Mar 15 17:25:18 2016 +0000
+++ b/tests/testlib	Thu Mar 17 09:12:18 2016 -0700
@@ -12,4 +12,4 @@
 [extensions]
 rebase=
 EOF
-echo "topic=$(echo $(dirname $TESTDIR))/src/topic" >> $HGRCPATH
+echo "topic=$(echo $(dirname $TESTDIR))/hgext3rd/topic" >> $HGRCPATH