hgext3rd/topic/__init__.py
changeset 1901 85390446f8c1
parent 1895 c8e4c6e03957
child 1903 58cdf061d49a
--- /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'))