evolve: handle relocation during divergence resolution producing no changes
When resolving divergence and the two divergent commits have different
parents, we start by rebasing one of them to have the same parent as
the other. That step can result in no changes to commit. When it does,
we would crash with a TypeError before this patch.
This patch fixes it by instead creating an empty commit in that
scenario. The existing code then continues to attempt to merge it,
which produces no changes, and no commit is created on top. The other
side of the divergence is marked as successor as usual, so orphans
from the side that became empty will be evolved to the right place
(see test).
from __future__ import absolute_import
from mercurial import (
commands,
error,
exchange,
extensions,
node,
phases,
util,
)
from mercurial.i18n import _
from . import (
compat,
)
def enforcesinglehead(repo, tr):
branchmap = repo.filtered(b'visible').branchmap()
for name, heads in compat.branchmapitems(branchmap):
if len(heads) > 1:
hexs = [node.short(n) for n in heads]
raise error.Abort(_(b'%d heads on "%s"') % (len(heads), name),
hint=(b', '.join(hexs)))
def publishbarebranch(repo, tr):
"""Publish changeset without topic"""
if b'node' not in tr.hookargs: # no new node
return
startnode = node.bin(tr.hookargs[b'node'])
topublish = repo.revs(b'not public() and (%n:) - hidden() - topic()', startnode)
if topublish:
cl = repo.changelog
nodes = [cl.node(r) for r in topublish]
repo._phasecache.advanceboundary(repo, tr, phases.public, nodes)
def rejectuntopicedchangeset(repo, tr):
"""Reject the push if there are changeset without topic"""
if b'node' not in tr.hookargs: # no new revs
return
startnode = node.bin(tr.hookargs[b'node'])
mode = repo.ui.config(b'experimental', b'topic-mode.server', b'ignore')
untopiced = repo.revs(b'not public() and (%n:) - hidden() - topic()', startnode)
if untopiced:
num = len(untopiced)
fnode = repo[untopiced.first()].hex()[:10]
if num == 1:
msg = _(b"%s") % fnode
else:
msg = _(b"%s and %d more") % (fnode, num - 1)
if mode == b'warning':
fullmsg = _(b"pushed draft changeset without topic: %s\n")
repo.ui.warn(fullmsg % msg)
elif mode == b'enforce':
fullmsg = _(b"rejecting draft changesets: %s")
raise error.Abort(fullmsg % msg)
else:
repo.ui.warn(_(b"unknown 'topic-mode.server': %s\n" % mode))
def reject_publish(repo, tr):
"""prevent a transaction to be publish anything"""
if util.safehasattr(tr.changes[b'phases'], 'items'):
# hg <= 5.3 (fdc802f29b2c)
published = {
r for r, (o, n) in tr.changes[b'phases'].items()
if n == phases.public
}
else:
revranges = [
r for r, (o, n) in tr.changes[b'phases']
if n == phases.public
]
published = {r for revrange in revranges for r in revrange}
if published:
r = min(published)
msg = b"rejecting publishing of changeset %s" % repo[r]
if len(published) > 1:
msg += b' and %d others' % (len(published) - 1)
raise error.Abort(msg)
def wrappush(orig, repo, remote, *args, **kwargs):
"""interpret the --publish flag and pass it to the push operation"""
newargs = kwargs.copy()
if kwargs.pop('publish', False):
opargs = kwargs.get('opargs')
if opargs is None:
opargs = {}
newargs[r'opargs'] = opargs.copy()
newargs[r'opargs'][b'publish'] = True
return orig(repo, remote, *args, **newargs)
def extendpushoperation(orig, self, *args, **kwargs):
publish = kwargs.pop('publish', False)
orig(self, *args, **kwargs)
self.publish = publish
def wrapphasediscovery(orig, pushop):
orig(pushop)
if getattr(pushop, 'publish', False):
if not pushop.remotephases.publishing:
unfi = pushop.repo.unfiltered()
droots = pushop.remotephases.draftroots
revset = b'%ln and (not public() or %ln::)'
future = list(unfi.set(revset, pushop.futureheads, droots))
pushop.outdatedphases = future
def installpushflag(ui):
entry = extensions.wrapcommand(commands.table, b'push', wrappush)
if not any(opt for opt in entry[1] if opt[1] == b'publish'):
# hg <= 4.8 (9b8d1ad851f8)
entry[1].append((b'', b'publish', False,
_(b'push the changeset as public')))
extensions.wrapfunction(exchange.pushoperation, '__init__',
extendpushoperation)
extensions.wrapfunction(exchange, '_pushdiscoveryphase', wrapphasediscovery)
exchange.pushdiscoverymapping[b'phase'] = exchange._pushdiscoveryphase
def replacecheckpublish(orig, pushop):
listkeys = exchange.listkeys
repo = pushop.repo
ui = repo.ui
behavior = ui.config(b'experimental', b'auto-publish')
if pushop.publish or behavior not in (b'warn', b'confirm', b'abort'):
return
# possible modes are:
#
# none -> nothing is published on push
# all -> everything is published on push
# auto -> only changeset without topic are published on push
#
# Unknown mode is assumed "all" for safety.
#
# TODO: do a wider brain storming about mode names.
mode = b'all'
remotephases = listkeys(pushop.remote, b'phases')
if not remotephases.get(b'publishing', False):
mode = b'none'
for c in pushop.remote.capabilities():
if c.startswith(b'ext-topics-publish'):
mode = c.split(b'=', 1)[1]
break
if mode == b'none':
return
if pushop.revs is None:
published = repo.filtered(b'served').revs(b'not public()')
else:
published = repo.revs(b'::%ln - public()', pushop.revs)
if mode == b'auto':
published = repo.revs(b'%ld::(%ld - topic())', published, published)
if published:
if behavior == b'warn':
ui.warn(
_(b'%i changesets about to be published\n') % len(published)
)
elif behavior == b'confirm':
if ui.promptchoice(
_(b'push and publish %i changesets (yn)?$$ &Yes $$ &No')
% len(published)
):
raise error.Abort(_(b'user quit'))
elif behavior == b'abort':
msg = _(b'push would publish %i changesets') % len(published)
hint = _(
b"use --publish or adjust 'experimental.auto-publish'"
b" config"
)
raise error.Abort(msg, hint=hint)