diff -r b65f39791f92 -r 85390446f8c1 hgext3rd/topic/__init__.py --- /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'))