evolvecmd: move more functions from __init__.py to evolvecmd.py
If things are looking ugly, hold on.
# Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
# Logilab SA <contact@logilab.fr>
# Pierre-Yves David <pierre-yves.david@ens-lyon.org>
# Patrick Mezard <patrick@mezard.eu>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
"""logic related to hg evolve command"""
from mercurial import (
cmdutil,
context,
copies,
error,
hg,
lock as lockmod,
merge,
node,
obsolete,
phases,
)
from mercurial.i18n import _
from . import (
cmdrewrite,
compat,
rewriteutil,
state,
utility,
)
TROUBLES = compat.TROUBLES
shorttemplate = utility.shorttemplate
_bookmarksupdater = rewriteutil.bookmarksupdater
from . import relocate, divergentdata, MergeFailure
def _solveone(ui, repo, ctx, dryrun, confirm, progresscb, category):
"""Resolve the troubles affecting one revision
returns a tuple (bool, newnode) where,
bool: a boolean value indicating whether the instability was solved
newnode: if bool is True, then the newnode of the resultant commit
formed. newnode can be node, when resolution led to no new
commit. If bool is False, this is ''.
"""
wlock = lock = tr = None
try:
wlock = repo.wlock()
lock = repo.lock()
tr = repo.transaction("evolve")
if 'orphan' == category:
result = _solveunstable(ui, repo, ctx, dryrun, confirm, progresscb)
elif 'phasedivergent' == category:
result = _solvebumped(ui, repo, ctx, dryrun, confirm, progresscb)
elif 'contentdivergent' == category:
result = _solvedivergent(ui, repo, ctx, dryrun, confirm,
progresscb)
else:
assert False, "unknown trouble category: %s" % (category)
tr.close()
return result
finally:
lockmod.release(tr, lock, wlock)
def _solveunstable(ui, repo, orig, dryrun=False, confirm=False,
progresscb=None):
""" Tries to stabilize the changeset orig which is orphan.
returns a tuple (bool, newnode) where,
bool: a boolean value indicating whether the instability was solved
newnode: if bool is True, then the newnode of the resultant commit
formed. newnode can be node, when resolution led to no new
commit. If bool is False, this is ''.
"""
pctx = orig.p1()
keepbranch = orig.p1().branch() != orig.branch()
if len(orig.parents()) == 2:
if not pctx.obsolete():
pctx = orig.p2() # second parent is obsolete ?
keepbranch = orig.p2().branch() != orig.branch()
elif orig.p2().obsolete():
hint = _("Redo the merge (%s) and use `hg prune <old> "
"--succ <new>` to obsolete the old one") % orig.hex()[:12]
ui.warn(_("warning: no support for evolving merge changesets "
"with two obsolete parents yet\n") +
_("(%s)\n") % hint)
return (False, '')
if not pctx.obsolete():
ui.warn(_("cannot solve instability of %s, skipping\n") % orig)
return (False, '')
obs = pctx
newer = compat.successorssets(repo, obs.node())
# search of a parent which is not killed
while not newer or newer == [()]:
ui.debug("stabilize target %s is plain dead,"
" trying to stabilize on its parent\n" %
obs)
obs = obs.parents()[0]
newer = compat.successorssets(repo, obs.node())
if len(newer) > 1:
msg = _("skipping %s: divergent rewriting. can't choose "
"destination\n") % obs
ui.write_err(msg)
return (False, '')
targets = newer[0]
assert targets
if len(targets) > 1:
# split target, figure out which one to pick, are they all in line?
targetrevs = [repo[r].rev() for r in targets]
roots = repo.revs('roots(%ld)', targetrevs)
heads = repo.revs('heads(%ld)', targetrevs)
if len(roots) > 1 or len(heads) > 1:
msg = "cannot solve split across two branches\n"
ui.write_err(msg)
return (False, '')
target = repo[heads.first()]
else:
target = targets[0]
displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
target = repo[target]
if not ui.quiet or confirm:
repo.ui.write(_('move:'))
displayer.show(orig)
repo.ui.write(_('atop:'))
displayer.show(target)
if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y':
raise error.Abort(_('evolve aborted by user'))
if progresscb:
progresscb()
todo = 'hg rebase -r %s -d %s\n' % (orig, target)
if dryrun:
repo.ui.write(todo)
return (False, '')
else:
repo.ui.note(todo)
if progresscb:
progresscb()
try:
newid = relocate(repo, orig, target, pctx, keepbranch)
return (True, newid)
except MergeFailure:
ops = {'current': orig.node()}
evolvestate = state.cmdstate(repo, opts=ops)
evolvestate.save()
repo.ui.write_err(_('evolve failed!\n'))
repo.ui.write_err(
_("fix conflict and run 'hg evolve --continue'"
" or use 'hg update -C .' to abort\n"))
raise
def _solvebumped(ui, repo, bumped, dryrun=False, confirm=False,
progresscb=None):
"""Stabilize a bumped changeset
returns a tuple (bool, newnode) where,
bool: a boolean value indicating whether the instability was solved
newnode: if bool is True, then the newnode of the resultant commit
formed. newnode can be node, when resolution led to no new
commit. If bool is False, this is ''.
"""
repo = repo.unfiltered()
bumped = repo[bumped.rev()]
# For now we deny bumped merge
if len(bumped.parents()) > 1:
msg = _('skipping %s : we do not handle merge yet\n') % bumped
ui.write_err(msg)
return (False, '')
prec = repo.set('last(allprecursors(%d) and public())', bumped).next()
# For now we deny target merge
if len(prec.parents()) > 1:
msg = _('skipping: %s: public version is a merge, '
'this is not handled yet\n') % prec
ui.write_err(msg)
return (False, '')
displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
if not ui.quiet or confirm:
repo.ui.write(_('recreate:'))
displayer.show(bumped)
repo.ui.write(_('atop:'))
displayer.show(prec)
if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y':
raise error.Abort(_('evolve aborted by user'))
if dryrun:
todo = 'hg rebase --rev %s --dest %s;\n' % (bumped, prec.p1())
repo.ui.write(todo)
repo.ui.write(('hg update %s;\n' % prec))
repo.ui.write(('hg revert --all --rev %s;\n' % bumped))
repo.ui.write(('hg commit --msg "%s update to %s"\n' %
(TROUBLES['PHASEDIVERGENT'], bumped)))
return (False, '')
if progresscb:
progresscb()
newid = tmpctx = None
tmpctx = bumped
# Basic check for common parent. Far too complicated and fragile
tr = repo.currenttransaction()
assert tr is not None
bmupdate = _bookmarksupdater(repo, bumped.node(), tr)
if not list(repo.set('parents(%d) and parents(%d)', bumped, prec)):
# Need to rebase the changeset at the right place
repo.ui.status(
_('rebasing to destination parent: %s\n') % prec.p1())
try:
tmpid = relocate(repo, bumped, prec.p1())
if tmpid is not None:
tmpctx = repo[tmpid]
obsolete.createmarkers(repo, [(bumped, (tmpctx,))])
except MergeFailure:
repo.vfs.write('graftstate', bumped.hex() + '\n')
repo.ui.write_err(_('evolution failed!\n'))
msg = _("fix conflict and run 'hg evolve --continue'\n")
repo.ui.write_err(msg)
raise
# Create the new commit context
repo.ui.status(_('computing new diff\n'))
files = set()
copied = copies.pathcopies(prec, bumped)
precmanifest = prec.manifest().copy()
# 3.3.2 needs a list.
# future 3.4 don't detect the size change during iteration
# this is fishy
for key, val in list(bumped.manifest().iteritems()):
precvalue = precmanifest.get(key, None)
if precvalue is not None:
del precmanifest[key]
if precvalue != val:
files.add(key)
files.update(precmanifest) # add missing files
# commit it
if files: # something to commit!
def filectxfn(repo, ctx, path):
if path in bumped:
fctx = bumped[path]
flags = fctx.flags()
mctx = compat.memfilectx(repo, ctx, fctx, flags, copied, path)
return mctx
return None
text = '%s update to %s:\n\n' % (TROUBLES['PHASEDIVERGENT'], prec)
text += bumped.description()
new = context.memctx(repo,
parents=[prec.node(), node.nullid],
text=text,
files=files,
filectxfn=filectxfn,
user=bumped.user(),
date=bumped.date(),
extra=bumped.extra())
newid = repo.commitctx(new)
if newid is None:
obsolete.createmarkers(repo, [(tmpctx, ())])
newid = prec.node()
else:
phases.retractboundary(repo, tr, bumped.phase(), [newid])
obsolete.createmarkers(repo, [(tmpctx, (repo[newid],))],
flag=obsolete.bumpedfix)
bmupdate(newid)
repo.ui.status(_('committed as %s\n') % node.short(newid))
# reroute the working copy parent to the new changeset
with repo.dirstate.parentchange():
repo.dirstate.setparents(newid, node.nullid)
return (True, newid)
def _solvedivergent(ui, repo, divergent, dryrun=False, confirm=False,
progresscb=None):
"""tries to solve content-divergence of a changeset
returns a tuple (bool, newnode) where,
bool: a boolean value indicating whether the instability was solved
newnode: if bool is True, then the newnode of the resultant commit
formed. newnode can be node, when resolution led to no new
commit. If bool is False, this is ''.
"""
repo = repo.unfiltered()
divergent = repo[divergent.rev()]
base, others = divergentdata(divergent)
if len(others) > 1:
othersstr = "[%s]" % (','.join([str(i) for i in others]))
msg = _("skipping %d:%s with a changeset that got split"
" into multiple ones:\n"
"|[%s]\n"
"| This is not handled by automatic evolution yet\n"
"| You have to fallback to manual handling with commands "
"such as:\n"
"| - hg touch -D\n"
"| - hg prune\n"
"| \n"
"| You should contact your local evolution Guru for help.\n"
) % (divergent, TROUBLES['CONTENTDIVERGENT'], othersstr)
ui.write_err(msg)
return (False, '')
other = others[0]
if len(other.parents()) > 1:
msg = _("skipping %s: %s changeset can't be "
"a merge (yet)\n") % (divergent, TROUBLES['CONTENTDIVERGENT'])
ui.write_err(msg)
hint = _("You have to fallback to solving this by hand...\n"
"| This probably means redoing the merge and using \n"
"| `hg prune` to kill older version.\n")
ui.write_err(hint)
return (False, '')
if other.p1() not in divergent.parents():
msg = _("skipping %s: have a different parent than %s "
"(not handled yet)\n") % (divergent, other)
hint = _("| %(d)s, %(o)s are not based on the same changeset.\n"
"| With the current state of its implementation, \n"
"| evolve does not work in that case.\n"
"| rebase one of them next to the other and run \n"
"| this command again.\n"
"| - either: hg rebase --dest 'p1(%(d)s)' -r %(o)s\n"
"| - or: hg rebase --dest 'p1(%(o)s)' -r %(d)s\n"
) % {'d': divergent, 'o': other}
ui.write_err(msg)
ui.write_err(hint)
return (False, '')
displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
if not ui.quiet or confirm:
ui.write(_('merge:'))
displayer.show(divergent)
ui.write(_('with: '))
displayer.show(other)
ui.write(_('base: '))
displayer.show(base)
if confirm and ui.prompt(_('perform evolve? [Ny]'), 'n') != 'y':
raise error.Abort(_('evolve aborted by user'))
if dryrun:
ui.write(('hg update -c %s &&\n' % divergent))
ui.write(('hg merge %s &&\n' % other))
ui.write(('hg commit -m "auto merge resolving conflict between '
'%s and %s"&&\n' % (divergent, other)))
ui.write(('hg up -C %s &&\n' % base))
ui.write(('hg revert --all --rev tip &&\n'))
ui.write(('hg commit -m "`hg log -r %s --template={desc}`";\n'
% divergent))
return (False, '')
if divergent not in repo[None].parents():
repo.ui.status(_('updating to "local" conflict\n'))
hg.update(repo, divergent.rev())
repo.ui.note(_('merging %s changeset\n') % TROUBLES['CONTENTDIVERGENT'])
if progresscb:
progresscb()
stats = merge.update(repo,
other.node(),
branchmerge=True,
force=False,
ancestor=base.node(),
mergeancestor=True)
hg._showstats(repo, stats)
if stats[3]:
repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
"or 'hg update -C .' to abort\n"))
if stats[3] > 0:
raise error.Abort('merge conflict between several amendments '
'(this is not automated yet)',
hint="""/!\ You can try:
/!\ * manual merge + resolve => new cset X
/!\ * hg up to the parent of the amended changeset (which are named W and Z)
/!\ * hg revert --all -r X
/!\ * hg ci -m "same message as the amended changeset" => new cset Y
/!\ * hg prune -n Y W Z
""")
if progresscb:
progresscb()
emtpycommitallowed = repo.ui.backupconfig('ui', 'allowemptycommit')
tr = repo.currenttransaction()
assert tr is not None
try:
repo.ui.setconfig('ui', 'allowemptycommit', True, 'evolve')
with repo.dirstate.parentchange():
repo.dirstate.setparents(divergent.node(), node.nullid)
oldlen = len(repo)
cmdrewrite.amend(ui, repo, message='', logfile='')
if oldlen == len(repo):
new = divergent
# no changes
else:
new = repo['.']
obsolete.createmarkers(repo, [(other, (new,))])
phases.retractboundary(repo, tr, other.phase(), [new.node()])
return (True, new.node())
finally:
repo.ui.restoreconfig(emtpycommitallowed)