push: allow pushing new topic to non-publishing server by default
This improves and fix the behavior change introduced by the new "topicmap".
* Topics are properly ignored when pushing to a publishing server,
* pushing new topics is allowed without --force a non-publishing server,
* Pushing extra heads on a topic requires --force.
Create of new head on a branch by phase movement is not properly detected for
now. We'll improve that part in a later changesets.
There is more awful monkey patching going on. We'll have to refactor core to get
rid of them.
# __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 branchmap
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 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 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()
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]
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()
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'),
])
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)
extensions.wrapfunction(discoverymod, '_headssummary', discovery._headssummary)
extensions.wrapfunction(wireproto, 'branchmap', discovery.wireprotobranchmap)
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'))