src/topic/__init__.py
author Matt Mackall <mpm@selenic.com>
Fri, 19 Jun 2015 16:16:42 -0500
changeset 1861 972d4e0c3d44
parent 1860 b7b9e5028c2a
child 1862 565f057bdc08
permissions -rw-r--r--
changectx: add topic method

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

from mercurial.i18n import _
from mercurial import cmdutil
from mercurial import commands
from mercurial import context
from mercurial import extensions
from mercurial import lock
from mercurial import merge
from mercurial import namespaces
from mercurial import obsolete
from mercurial import phases
from mercurial import util

from . import constants
from . import revset as topicrevset

cmdtable = {}
command = cmdutil.command(cmdtable)

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 reposetup(ui, repo):
    orig = repo.__class__
    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
            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')

    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'),
])
def topics(ui, repo, topic='', clear=False, change=None):
    """View current topic, set current topic, or see all topics."""
    if change:
        if not obsolete.isenabled(repo, obsolete.createmarkersopt):
            raise util.Abort(_('must have obsolete enabled to use --change'))
        if not topic and not clear:
            raise util.Abort('changing topic requires a topic name or --clear')
        if any(not c.mutable() for c in repo.set('%r and public()', change)):
            raise util.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())
               newtopic = None if clear else topic
               if fixedextra.get(constants.extrakey, '') == topic:
                   continue
               if clear and constants.extrakey in fixedextra:
                   del fixedextra[constants.extrakey]
               else:
                   fixedextra[constants.extrakey] = topic
               c.parents()
               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)
               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, partial,
                    ancestor=None, mergeancestor=False, labels=None):
    wlock = repo.wlock()
    try:
        ret = orig(repo, node, branchmerge, force, partial, ancestor=ancestor,
                   mergeancestor=mergeancestor, labels=labels)
        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)

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)
topicrevset.modsetup()
cmdutil.summaryhooks.add('topic', summaryhook)