Mercurial > evolve
view src/topic/__init__.py @ 1885:d49f75eab6a3
topic: take topic in account for all branch head computation
This changeset introduce a "topicmap" that is tracking not just the head of all
branches, but the heads of all branch+topic pair. Including the head of the part
of the branch without any topic. In practice this means that BRANCHNAME now
resolve to the tipmost part for the branch without topic and impact various
other logic like head checking during push and default destination for update and
merge (these aspect will need adjustment in later changesets).
The on-the-fly-temporary-monkey-patching process is pretty horrible, but allow
to move forward without waiting on having core patched.
We use 'branch:topic' as the branchmap key, this is a small and easy hack that
help use a lot for (future) support of heads discovery/checking and on disc
cache. I'm not sure it is worthwhile to improve this until an implementation
into core.
Note that this changeset change the branchmap in all cases, including during
exchange, see next changeset for improved behavior.
We also currently have the on-disk cache disabled because the core branchmap is
lacking phase information in its cache key. This will get done in a later
changesets
author | Pierre-Yves David <pierre-yves.david@fb.com> |
---|---|
date | Sat, 12 Mar 2016 15:36:17 +0000 |
parents | 8a53f99d9061 |
children | 0504e76bfbd9 |
line wrap: on
line source
# __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 error from mercurial import extensions 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 branchmap from . import constants from . import revset as topicrevset from . import destination from . import topicmap 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() 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 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] 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() oldbranchcache = branchmap.branchcache oldfilename = branchmap._filename oldcaches = getattr(self, '_branchcaches', {}) try: branchmap.branchcache = topicmap.topiccache branchmap._filename = topicmap._filename self._branchcaches = getattr(self, '_topiccaches', {}) branchmap.updatecache(self) self._topiccaches = self._branchcaches return self._topiccaches[self.filtername] finally: self._branchcaches = oldcaches branchmap.branchcache = oldbranchcache branchmap._filename = oldfilename def invalidatecaches(self): super(topicrepo, self).invalidatecaches() if '_topiccaches' in vars(self.unfiltered()): self.unfiltered()._topiccaches.clear() 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()) 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) 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'))