# __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.
"""
from __future__ import absolute_import
import re
from mercurial.i18n import _
from mercurial import (
branchmap,
cmdutil,
commands,
context,
error,
extensions,
localrepo,
lock,
merge,
namespaces,
node,
obsolete,
patch,
phases,
util,
)
from . import (
constants,
revset as topicrevset,
destination,
stack,
topicmap,
discovery,
)
cmdtable = {}
command = cmdutil.command(cmdtable)
colortable = {'topic.stack.index': 'yellow',
'topic.stack.state.base': 'dim',
'topic.stack.state.clean': 'green',
'topic.stack.index.current': 'cyan', # random pick
'topic.stack.state.current': 'cyan bold', # random pick
'topic.stack.desc.current': 'cyan', # random pick
'topic.stack.state.unstable': 'red',
}
testedwith = '3.7'
def _contexttopic(self):
return self.extra().get(constants.extrakey, '')
context.basectx.topic = _contexttopic
topicrev = re.compile(r'^t\d+$')
def _namemap(repo, name):
if topicrev.match(name):
idx = int(name[1:])
topic = repo.currenttopic
if not topic:
raise error.Abort(_('cannot resolve "%s": no active topic') % name)
revs = list(stack.getstack(repo, topic))
try:
r = revs[idx - 1]
except IndexError:
msg = _('cannot resolve "%s": topic "%s" has only %d changesets')
raise error.Abort(msg % (name, topic, len(revs)))
return [repo[r].node()]
return [ctx.node() for ctx in
repo.set('not public() and extra(topic, %s)', name)]
def _nodemap(repo, node):
ctx = repo[node]
t = ctx.topic()
if t and ctx.phase() > phases.public:
return [t]
return []
def uisetup(ui):
destination.modsetup(ui)
topicrevset.modsetup(ui)
discovery.modsetup(ui)
topicmap.modsetup(ui)
setupimportexport(ui)
extensions.afterloaded('rebase', _fixrebase)
entry = extensions.wrapcommand(commands.table, 'commit', commitwrap)
entry[1].append(('t', 'topic', '',
_("use specified topic"), _('TOPIC')))
extensions.wrapfunction(cmdutil, 'buildcommittext', committextwrap)
extensions.wrapfunction(merge, 'update', mergeupdatewrap)
cmdutil.summaryhooks.add('topic', summaryhook)
def reposetup(ui, repo):
orig = repo.__class__
if not isinstance(repo, localrepo.localrepository):
return # this can be a peer in the ssh case (puzzling)
class topicrepo(repo.__class__):
def _restrictcapabilities(self, caps):
caps = super(topicrepo, self)._restrictcapabilities(caps)
caps.add('topics')
return caps
def commit(self, *args, **kwargs):
backup = self.ui.backupconfig('ui', 'allowemptycommit')
try:
if repo.currenttopic != repo['.'].topic():
# bypass the core "nothing changed" logic
self.ui.setconfig('ui', 'allowemptycommit', True)
return orig.commit(self, *args, **kwargs)
finally:
self.ui.restoreconfig(backup)
def commitctx(self, ctx, error=None):
if isinstance(ctx, context.workingcommitctx):
current = self.currenttopic
if current:
ctx.extra()[constants.extrakey] = current
if (isinstance(ctx, context.memctx) and
ctx.extra().get('amend_source') and
ctx.topic() and
not self.currenttopic):
# we are amending and need to remove a topic
del ctx.extra()[constants.extrakey]
with topicmap.usetopicmap(self):
return orig.commitctx(self, ctx, error=error)
@property
def topics(self):
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 topicmap.usetopicmap(self):
branchmap.updatecache(self)
return self._topiccaches[self.filtername]
def destroyed(self, *args, **kwargs):
with topicmap.usetopicmap(self):
return super(topicrepo, self).destroyed(*args, **kwargs)
def 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'),
] + commands.formatteropts)
def topics(ui, repo, topic='', clear=False, change=None, list=False, **opts):
"""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, opts)
if change:
if not obsolete.isenabled(repo, obsolete.createmarkersopt):
raise error.Abort(_('must have obsolete enabled to use --change'))
if not topic and not clear:
raise error.Abort('changing topic requires a topic name or --clear')
if any(not c.mutable() for c in repo.set('%r and public()', change)):
raise error.Abort("can't change topic of a public change")
rewrote = 0
needevolve = False
l = repo.lock()
txn = repo.transaction('rewrite-topics')
try:
for c in repo.set('%r', change):
def filectxfn(repo, ctx, path):
try:
return c[path]
except error.ManifestLookupError:
return None
fixedextra = dict(c.extra())
ui.debug('old node id is %s\n' % node.hex(c.node()))
ui.debug('origextra: %r\n' % fixedextra)
newtopic = None if clear else topic
oldtopic = fixedextra.get(constants.extrakey, None)
if oldtopic == newtopic:
continue
if clear:
del fixedextra[constants.extrakey]
else:
fixedextra[constants.extrakey] = topic
if 'amend_source' in fixedextra:
# TODO: right now the commitctx wrapper in
# topicrepo overwrites the topic in extra if
# amend_source is set to support 'hg commit
# --amend'. Support for amend should be adjusted
# to not be so invasive.
del fixedextra['amend_source']
ui.debug('changing topic of %s from %s to %s\n' % (
c, oldtopic, newtopic))
ui.debug('fixedextra: %r\n' % fixedextra)
mc = context.memctx(
repo, (c.p1().node(), c.p2().node()), c.description(),
c.files(), filectxfn,
user=c.user(), date=c.date(), extra=fixedextra)
newnode = repo.commitctx(mc)
ui.debug('new node id is %s\n' % node.hex(newnode))
needevolve = needevolve or (len(c.children()) > 0)
obsolete.createmarkers(repo, [(c, (repo[newnode],))])
rewrote += 1
txn.close()
except:
try:
txn.abort()
finally:
repo.invalidate()
raise
finally:
lock.release(txn, l)
ui.status('changed topic on %d changes\n' % rewrote)
if needevolve:
evolvetarget = 'topic(%s)' % topic if topic else 'not topic()'
ui.status('please run hg evolve --rev "%s" now\n' % evolvetarget)
if clear:
if repo.vfs.exists('topic'):
repo.vfs.unlink('topic')
return
if topic:
with repo.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):
matcher = kwargs.get('matcher')
partial = not (matcher is None or matcher.always())
wlock = repo.wlock()
try:
ret = orig(repo, node, branchmerge, force, *args, **kwargs)
if not partial and not branchmerge:
ot = repo.currenttopic
t = ''
pctx = repo[node]
if pctx.phase() > phases.public:
t = pctx.topic()
with repo.vfs.open('topic', 'w') as f:
f.write(t)
if t and t != ot:
repo.ui.status(_("switching to topic %s\n") % t)
return ret
finally:
wlock.release()
def _fixrebase(loaded):
if not loaded:
return
def savetopic(ctx, extra):
if ctx.topic():
extra[constants.extrakey] = ctx.topic()
def newmakeextrafn(orig, copiers):
return orig(copiers + [savetopic])
try:
rebase = extensions.find("rebase")
extensions.wrapfunction(rebase, '_makeextrafn', newmakeextrafn)
except KeyError:
pass
## preserve topic during import/export
def _exporttopic(seq, ctx):
topic = ctx.topic()
if topic:
return 'EXP-Topic %s' % topic
return None
def _importtopic(repo, patchdata, extra, opts):
if 'topic' in patchdata:
extra['topic'] = patchdata['topic']
def setupimportexport(ui):
"""run at ui setup time to install import/export logic"""
cmdutil.extraexport.append('topic')
cmdutil.extraexportmap['topic'] = _exporttopic
cmdutil.extrapreimport.append('topic')
cmdutil.extrapreimportmap['topic'] = _importtopic
patch.patchheadermap.append(('EXP-Topic', 'topic'))