destutil.py
437 lines
| 16.4 KiB
| text/x-python
|
PythonLexer
/ mercurial / destutil.py
|
r26569 | # destutil.py - Mercurial utility function for command destination | ||
# | ||||
# Copyright Matt Mackall <mpm@selenic.com> and other | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
|
r27333 | from __future__ import absolute_import | ||
|
r26569 | from .i18n import _ | ||
from . import ( | ||||
|
r26641 | bookmarks, | ||
|
r26569 | error, | ||
obsolete, | ||||
) | ||||
|
r26720 | def _destupdatevalidate(repo, rev, clean, check): | ||
"""validate that the destination comply to various rules | ||||
This exists as its own function to help wrapping from extensions.""" | ||||
wc = repo[None] | ||||
p1 = wc.p1() | ||||
if not clean: | ||||
# Check that the update is linear. | ||||
# | ||||
# Mercurial do not allow update-merge for non linear pattern | ||||
# (that would be technically possible but was considered too confusing | ||||
# for user a long time ago) | ||||
# | ||||
# See mercurial.merge.update for details | ||||
if p1.rev() not in repo.changelog.ancestors([rev], inclusive=True): | ||||
dirty = wc.dirty(missing=True) | ||||
foreground = obsolete.foreground(repo, [p1.node()]) | ||||
if not repo[rev].node() in foreground: | ||||
if dirty: | ||||
msg = _("uncommitted changes") | ||||
hint = _("commit and merge, or update --clean to" | ||||
" discard changes") | ||||
raise error.UpdateAbort(msg, hint=hint) | ||||
elif not check: # destination is not a descendant. | ||||
msg = _("not a linear update") | ||||
hint = _("merge or update --check to force update") | ||||
raise error.UpdateAbort(msg, hint=hint) | ||||
|
r26723 | def _destupdateobs(repo, clean, check): | ||
"""decide of an update destination from obsolescence markers""" | ||||
|
r26569 | node = None | ||
wc = repo[None] | ||||
p1 = wc.p1() | ||||
|
r26723 | movemark = None | ||
|
r26569 | |||
if p1.obsolete() and not p1.children(): | ||||
# allow updating to successors | ||||
successors = obsolete.successorssets(repo, p1.node()) | ||||
# behavior of certain cases is as follows, | ||||
# | ||||
# divergent changesets: update to highest rev, similar to what | ||||
# is currently done when there are more than one head | ||||
# (i.e. 'tip') | ||||
# | ||||
# replaced changesets: same as divergent except we know there | ||||
# is no conflict | ||||
# | ||||
# pruned changeset: no update is done; though, we could | ||||
# consider updating to the first non-obsolete parent, | ||||
# similar to what is current done for 'hg prune' | ||||
if successors: | ||||
# flatten the list here handles both divergent (len > 1) | ||||
# and the usual case (len = 1) | ||||
successors = [n for sub in successors for n in sub] | ||||
# get the max revision for the given successors set, | ||||
# i.e. the 'tip' of a set | ||||
node = repo.revs('max(%ln)', successors).first() | ||||
|
r26722 | if bookmarks.isactivewdirparent(repo): | ||
movemark = repo['.'].node() | ||||
|
r26723 | return node, movemark, None | ||
|
r26724 | def _destupdatebook(repo, clean, check): | ||
"""decide on an update destination from active bookmark""" | ||||
# we also move the active bookmark, if any | ||||
activemark = None | ||||
node, movemark = bookmarks.calculateupdate(repo.ui, repo, None) | ||||
if node is not None: | ||||
activemark = node | ||||
return node, movemark, activemark | ||||
|
r26725 | def _destupdatebranch(repo, clean, check): | ||
|
r28385 | """decide on an update destination from current branch | ||
This ignores closed branch heads. | ||||
""" | ||||
|
r26725 | wc = repo[None] | ||
movemark = node = None | ||||
|
r28235 | currentbranch = wc.branch() | ||
|
r29284 | |||
if clean: | ||||
currentbranch = repo['.'].branch() | ||||
|
r28236 | if currentbranch in repo.branchmap(): | ||
|
r28385 | heads = repo.branchheads(currentbranch) | ||
|
r28236 | if heads: | ||
node = repo.revs('max(.::(%ln))', heads).first() | ||||
|
r26725 | if bookmarks.isactivewdirparent(repo): | ||
movemark = repo['.'].node() | ||||
|
r28924 | elif currentbranch == 'default' and not wc.p1(): | ||
# "null" parent belongs to "default" branch, but it doesn't exist, so | ||||
# update to the tipmost non-closed branch head | ||||
node = repo.revs('max(head() and not closed())').first() | ||||
|
r28236 | else: | ||
|
r28903 | node = repo['.'].node() | ||
|
r26725 | return node, movemark, None | ||
|
r28385 | def _destupdatebranchfallback(repo, clean, check): | ||
"""decide on an update destination from closed heads in current branch""" | ||||
wc = repo[None] | ||||
currentbranch = wc.branch() | ||||
movemark = None | ||||
if currentbranch in repo.branchmap(): | ||||
# here, all descendant branch heads are closed | ||||
heads = repo.branchheads(currentbranch, closed=True) | ||||
assert heads, "any branch has at least one head" | ||||
node = repo.revs('max(.::(%ln))', heads).first() | ||||
assert node is not None, ("any revision has at least " | ||||
"one descendant branch head") | ||||
if bookmarks.isactivewdirparent(repo): | ||||
movemark = repo['.'].node() | ||||
else: | ||||
# here, no "default" branch, and all branches are closed | ||||
node = repo.lookup('tip') | ||||
assert node is not None, "'tip' exists even in empty repository" | ||||
return node, movemark, None | ||||
|
r26726 | # order in which each step should be evalutated | ||
# steps are run until one finds a destination | ||||
|
r28385 | destupdatesteps = ['evolution', 'bookmark', 'branch', 'branchfallback'] | ||
|
r26726 | # mapping to ease extension overriding steps. | ||
destupdatestepmap = {'evolution': _destupdateobs, | ||||
'bookmark': _destupdatebook, | ||||
'branch': _destupdatebranch, | ||||
|
r28385 | 'branchfallback': _destupdatebranchfallback, | ||
|
r26726 | } | ||
|
r26723 | def destupdate(repo, clean=False, check=False): | ||
"""destination for bare update operation | ||||
return (rev, movemark, activemark) | ||||
- rev: the revision to update to, | ||||
- movemark: node to move the active bookmark from | ||||
(cf bookmark.calculate update), | ||||
- activemark: a bookmark to activate at the end of the update. | ||||
""" | ||||
|
r26726 | node = movemark = activemark = None | ||
|
r26723 | |||
|
r26726 | for step in destupdatesteps: | ||
node, movemark, activemark = destupdatestepmap[step](repo, clean, check) | ||||
if node is not None: | ||||
break | ||||
|
r26628 | rev = repo[node].rev() | ||
|
r26720 | _destupdatevalidate(repo, rev, clean, check) | ||
|
r26628 | |||
|
r26641 | return rev, movemark, activemark | ||
|
r26714 | |||
|
r28102 | msgdestmerge = { | ||
# too many matching divergent bookmark | ||||
'toomanybookmarks': | ||||
|
r28137 | {'merge': | ||
(_("multiple matching bookmarks to merge -" | ||||
" please merge with an explicit rev or bookmark"), | ||||
_("run 'hg heads' to see all heads")), | ||||
|
r28189 | 'rebase': | ||
(_("multiple matching bookmarks to rebase -" | ||||
" please rebase to an explicit rev or bookmark"), | ||||
_("run 'hg heads' to see all heads")), | ||||
|
r28137 | }, | ||
|
r28102 | # no other matching divergent bookmark | ||
'nootherbookmarks': | ||||
|
r28137 | {'merge': | ||
(_("no matching bookmark to merge - " | ||||
"please merge with an explicit rev or bookmark"), | ||||
_("run 'hg heads' to see all heads")), | ||||
|
r28189 | 'rebase': | ||
(_("no matching bookmark to rebase - " | ||||
"please rebase to an explicit rev or bookmark"), | ||||
_("run 'hg heads' to see all heads")), | ||||
|
r28137 | }, | ||
|
r28102 | # branch have too many unbookmarked heads, no obvious destination | ||
'toomanyheads': | ||||
|
r28137 | {'merge': | ||
(_("branch '%s' has %d heads - please merge with an explicit rev"), | ||||
_("run 'hg heads .' to see heads")), | ||||
|
r28189 | 'rebase': | ||
(_("branch '%s' has %d heads - please rebase to an explicit rev"), | ||||
_("run 'hg heads .' to see heads")), | ||||
|
r28137 | }, | ||
|
r28102 | # branch have no other unbookmarked heads | ||
'bookmarkedheads': | ||||
|
r28137 | {'merge': | ||
(_("heads are bookmarked - please merge with an explicit rev"), | ||||
_("run 'hg heads' to see all heads")), | ||||
|
r28189 | 'rebase': | ||
(_("heads are bookmarked - please rebase to an explicit rev"), | ||||
_("run 'hg heads' to see all heads")), | ||||
|
r28137 | }, | ||
|
r28102 | # branch have just a single heads, but there is other branches | ||
'nootherbranchheads': | ||||
|
r28137 | {'merge': | ||
(_("branch '%s' has one head - please merge with an explicit rev"), | ||||
_("run 'hg heads' to see all heads")), | ||||
|
r28189 | 'rebase': | ||
(_("branch '%s' has one head - please rebase to an explicit rev"), | ||||
_("run 'hg heads' to see all heads")), | ||||
|
r28137 | }, | ||
|
r28102 | # repository have a single head | ||
'nootherheads': | ||||
|
r28189 | {'merge': | ||
(_('nothing to merge'), | ||||
None), | ||||
'rebase': | ||||
(_('nothing to rebase'), | ||||
None), | ||||
|
r28137 | }, | ||
|
r28102 | # repository have a single head and we are not on it | ||
'nootherheadsbehind': | ||||
|
r28137 | {'merge': | ||
(_('nothing to merge'), | ||||
_("use 'hg update' instead")), | ||||
|
r28189 | 'rebase': | ||
(_('nothing to rebase'), | ||||
_("use 'hg update' instead")), | ||||
|
r28137 | }, | ||
|
r28102 | # We are not on a head | ||
'notatheads': | ||||
|
r28137 | {'merge': | ||
(_('working directory not at a head revision'), | ||||
|
r28189 | _("use 'hg update' or merge with an explicit revision")), | ||
'rebase': | ||||
(_('working directory not at a head revision'), | ||||
_("use 'hg update' or rebase to an explicit revision")) | ||||
|
r28137 | }, | ||
|
r28139 | 'emptysourceset': | ||
{'merge': | ||||
(_('source set is empty'), | ||||
|
r28189 | None), | ||
'rebase': | ||||
(_('source set is empty'), | ||||
None), | ||||
|
r28139 | }, | ||
'multiplebranchessourceset': | ||||
{'merge': | ||||
(_('source set is rooted in multiple branches'), | ||||
|
r28189 | None), | ||
'rebase': | ||||
(_('rebaseset is rooted in multiple named branches'), | ||||
_('specify an explicit destination with --dest')), | ||||
|
r28139 | }, | ||
|
r28137 | } | ||
|
r28102 | |||
|
r29043 | def _destmergebook(repo, action='merge', sourceset=None, destspace=None): | ||
|
r26727 | """find merge destination in the active bookmark case""" | ||
node = None | ||||
bmheads = repo.bookmarkheads(repo._activebookmark) | ||||
curhead = repo[repo._activebookmark].node() | ||||
if len(bmheads) == 2: | ||||
if curhead == bmheads[0]: | ||||
node = bmheads[1] | ||||
else: | ||||
node = bmheads[0] | ||||
elif len(bmheads) > 2: | ||||
|
r28137 | msg, hint = msgdestmerge['toomanybookmarks'][action] | ||
|
r28141 | raise error.ManyMergeDestAbort(msg, hint=hint) | ||
|
r26727 | elif len(bmheads) <= 1: | ||
|
r28137 | msg, hint = msgdestmerge['nootherbookmarks'][action] | ||
|
r28141 | raise error.NoMergeDestAbort(msg, hint=hint) | ||
|
r26727 | assert node is not None | ||
return node | ||||
|
r29043 | def _destmergebranch(repo, action='merge', sourceset=None, onheadcheck=True, | ||
destspace=None): | ||||
|
r26728 | """find merge destination based on branch heads""" | ||
node = None | ||||
|
r28139 | |||
if sourceset is None: | ||||
sourceset = [repo[repo.dirstate.p1()].rev()] | ||||
branch = repo.dirstate.branch() | ||||
elif not sourceset: | ||||
msg, hint = msgdestmerge['emptysourceset'][action] | ||||
|
r28141 | raise error.NoMergeDestAbort(msg, hint=hint) | ||
|
r28139 | else: | ||
branch = None | ||||
for ctx in repo.set('roots(%ld::%ld)', sourceset, sourceset): | ||||
if branch is not None and ctx.branch() != branch: | ||||
msg, hint = msgdestmerge['multiplebranchessourceset'][action] | ||||
|
r28141 | raise error.ManyMergeDestAbort(msg, hint=hint) | ||
|
r28139 | branch = ctx.branch() | ||
|
r26728 | bheads = repo.branchheads(branch) | ||
|
r28161 | onhead = repo.revs('%ld and %ln', sourceset, bheads) | ||
if onheadcheck and not onhead: | ||||
|
r28139 | # Case A: working copy if not on a head. (merge only) | ||
|
r28105 | # | ||
# This is probably a user mistake We bailout pointing at 'hg update' | ||||
|
r28103 | if len(repo.heads()) <= 1: | ||
|
r28137 | msg, hint = msgdestmerge['nootherheadsbehind'][action] | ||
|
r28103 | else: | ||
|
r28137 | msg, hint = msgdestmerge['notatheads'][action] | ||
|
r28103 | raise error.Abort(msg, hint=hint) | ||
|
r28139 | # remove heads descendants of source from the set | ||
bheads = list(repo.revs('%ln - (%ld::)', bheads, sourceset)) | ||||
|
r28138 | # filters out bookmarked heads | ||
|
r28139 | nbhs = list(repo.revs('%ld - bookmark()', bheads)) | ||
|
r29043 | |||
if destspace is not None: | ||||
# restrict search space | ||||
# used in the 'hg pull --rebase' case, see issue 5214. | ||||
nbhs = list(repo.revs('%ld and %ld', destspace, nbhs)) | ||||
|
r28138 | if len(nbhs) > 1: | ||
# Case B: There is more than 1 other anonymous heads | ||||
|
r28105 | # | ||
# This means that there will be more than 1 candidate. This is | ||||
# ambiguous. We abort asking the user to pick as explicit destination | ||||
# instead. | ||||
|
r28137 | msg, hint = msgdestmerge['toomanyheads'][action] | ||
|
r28138 | msg %= (branch, len(bheads) + 1) | ||
|
r28141 | raise error.ManyMergeDestAbort(msg, hint=hint) | ||
|
r28138 | elif not nbhs: | ||
# Case B: There is no other anonymous heads | ||||
|
r28105 | # | ||
# This means that there is no natural candidate to merge with. | ||||
# We abort, with various messages for various cases. | ||||
|
r28138 | if bheads: | ||
|
r28137 | msg, hint = msgdestmerge['bookmarkedheads'][action] | ||
|
r28102 | elif len(repo.heads()) > 1: | ||
|
r28137 | msg, hint = msgdestmerge['nootherbranchheads'][action] | ||
|
r28102 | msg %= branch | ||
|
r28161 | elif not onhead: | ||
# if 'onheadcheck == False' (rebase case), | ||||
# this was not caught in Case A. | ||||
msg, hint = msgdestmerge['nootherheadsbehind'][action] | ||||
|
r28102 | else: | ||
|
r28137 | msg, hint = msgdestmerge['nootherheads'][action] | ||
|
r28141 | raise error.NoMergeDestAbort(msg, hint=hint) | ||
|
r26728 | else: | ||
node = nbhs[0] | ||||
assert node is not None | ||||
return node | ||||
|
r29043 | def destmerge(repo, action='merge', sourceset=None, onheadcheck=True, | ||
destspace=None): | ||||
|
r28137 | """return the default destination for a merge | ||
(or raise exception about why it can't pick one) | ||||
:action: the action being performed, controls emitted error message | ||||
""" | ||||
|
r29043 | # destspace is here to work around issues with `hg pull --rebase` see | ||
# issue5214 for details | ||||
|
r26714 | if repo._activebookmark: | ||
|
r29043 | node = _destmergebook(repo, action=action, sourceset=sourceset, | ||
destspace=destspace) | ||||
|
r26714 | else: | ||
|
r28140 | node = _destmergebranch(repo, action=action, sourceset=sourceset, | ||
|
r29043 | onheadcheck=onheadcheck, destspace=destspace) | ||
|
r26714 | return repo[node].rev() | ||
|
r27262 | |||
histeditdefaultrevset = 'reverse(only(.) and not public() and not ::merge())' | ||||
def desthistedit(ui, repo): | ||||
"""Default base revision to edit for `hg histedit`.""" | ||||
|
r27559 | # Avoid cycle: scmutil -> revset -> destutil | ||
from . import scmutil | ||||
|
r27262 | default = ui.config('histedit', 'defaultrev', histeditdefaultrevset) | ||
if default: | ||||
|
r27559 | revs = scmutil.revrange(repo, [default]) | ||
|
r27262 | if revs: | ||
# The revset supplied by the user may not be in ascending order nor | ||||
# take the first revision. So do this manually. | ||||
revs.sort() | ||||
return revs.first() | ||||
return None | ||||
|
r28029 | |||
def _statusotherbook(ui, repo): | ||||
bmheads = repo.bookmarkheads(repo._activebookmark) | ||||
curhead = repo[repo._activebookmark].node() | ||||
if repo.revs('%n and parents()', curhead): | ||||
# we are on the active bookmark | ||||
bmheads = [b for b in bmheads if curhead != b] | ||||
if bmheads: | ||||
msg = _('%i other divergent bookmarks for "%s"\n') | ||||
ui.status(msg % (len(bmheads), repo._activebookmark)) | ||||
def _statusotherbranchheads(ui, repo): | ||||
currentbranch = repo.dirstate.branch() | ||||
|
r28266 | allheads = repo.branchheads(currentbranch, closed=True) | ||
|
r28029 | heads = repo.branchheads(currentbranch) | ||
|
r28266 | if repo.revs('%ln and parents()', allheads): | ||
# we are on a head, even though it might be closed | ||||
|
r28385 | # | ||
# on closed otherheads | ||||
# ========= ========== | ||||
# o 0 all heads for current branch are closed | ||||
# N only descendant branch heads are closed | ||||
# x 0 there is only one non-closed branch head | ||||
# N there are some non-closed branch heads | ||||
# ========= ========== | ||||
|
r28233 | otherheads = repo.revs('%ln - parents()', heads) | ||
|
r28385 | if repo['.'].closesbranch(): | ||
|
r28684 | ui.warn(_('no open descendant heads on branch "%s", ' | ||
|
r28683 | 'updating to a closed head\n') % | ||
(currentbranch)) | ||||
|
r28385 | if otherheads: | ||
|
r29964 | ui.warn(_("(committing will reopen the head, " | ||
"use 'hg heads .' to see %i other heads)\n") % | ||||
|
r28683 | (len(otherheads))) | ||
|
r28385 | else: | ||
|
r28684 | ui.warn(_('(committing will reopen branch "%s")\n') % | ||
|
r28683 | (currentbranch)) | ||
|
r28385 | elif otherheads: | ||
|
r28029 | ui.status(_('%i other heads for branch "%s"\n') % | ||
|
r28233 | (len(otherheads), currentbranch)) | ||
|
r28029 | |||
def statusotherdests(ui, repo): | ||||
"""Print message about other head""" | ||||
# XXX we should probably include a hint: | ||||
# - about what to do | ||||
# - how to see such heads | ||||
if repo._activebookmark: | ||||
_statusotherbook(ui, repo) | ||||
else: | ||||
_statusotherbranchheads(ui, repo) | ||||