hgext3rd/topic/__init__.py
author Pierre-Yves David <pierre-yves.david@fb.com>
Thu, 17 Mar 2016 09:12:18 -0700
changeset 1901 85390446f8c1
parent 1895 src/topic/__init__.py@c8e4c6e03957
child 1903 58cdf061d49a
permissions -rw-r--r--
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.

# __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'))