--- /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'))