destutil.py
496 lines
| 17.1 KiB
| text/x-python
|
PythonLexer
/ mercurial / destutil.py
|
r26569 | # destutil.py - Mercurial utility function for command destination | ||
# | ||||
|
r47575 | # Copyright Olivia Mackall <olivia@selenic.com> and other | ||
|
r26569 | # | ||
# 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 _ | ||
|
r43346 | from . import bookmarks, error, obsutil, scmutil, stack | ||
|
r26569 | |||
|
r37805 | def orphanpossibledestination(repo, rev): | ||
"""Return all changesets that may be a new parent for orphan `rev`. | ||||
This function works fine on non-orphan revisions, it's just silly | ||||
because there's no destination implied by obsolete markers, so | ||||
it'll return nothing. | ||||
""" | ||||
tonode = repo.changelog.node | ||||
parents = repo.changelog.parentrevs | ||||
torev = repo.changelog.rev | ||||
dest = set() | ||||
tovisit = list(parents(rev)) | ||||
while tovisit: | ||||
r = tovisit.pop() | ||||
succsets = obsutil.successorssets(repo, tonode(r)) | ||||
if not succsets: | ||||
# if there are no successors for r, r was probably pruned | ||||
# and we should walk up to r's parents to try and find | ||||
# some successors. | ||||
tovisit.extend(parents(r)) | ||||
else: | ||||
# We should probably pick only one destination from split | ||||
# (case where '1 < len(ss)'), This could be the currently | ||||
# tipmost, but the correct result is less clear when | ||||
# results of the split have been moved such that they | ||||
# reside on multiple branches. | ||||
for ss in succsets: | ||||
for n in ss: | ||||
dr = torev(n) | ||||
if dr != -1: | ||||
dest.add(dr) | ||||
return dest | ||||
|
r43346 | |||
|
r30962 | def _destupdateobs(repo, clean): | ||
|
r26723 | """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 | ||||
r33143 | successors = obsutil.successorssets(repo, p1.node()) | |||
|
r26569 | |||
# 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 | ||||
# | ||||
r49538 | # pruned changeset: update to the closest non-obsolete ancestor, | |||
# similar to what 'hg prune' currently does | ||||
|
r26569 | |||
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 | ||||
|
r43347 | node = repo.revs(b'max(%ln)', successors).first() | ||
r49536 | else: | |||
r49538 | p1 = p1.p1() | |||
while p1.obsolete(): | ||||
p1 = p1.p1() | ||||
node = p1.node() | ||||
if node is not None and bookmarks.isactivewdirparent(repo): | ||||
movemark = repo[b'.'].node() | ||||
|
r26723 | return node, movemark, None | ||
|
r43346 | |||
|
r30962 | def _destupdatebook(repo, clean): | ||
|
r26724 | """decide on an update destination from active bookmark""" | ||
# we also move the active bookmark, if any | ||||
|
r37377 | node = None | ||
|
r37393 | activemark, movemark = bookmarks.calculateupdate(repo.ui, repo) | ||
|
r37377 | if activemark is not None: | ||
|
r37470 | node = repo._bookmarks[activemark] | ||
|
r26724 | return node, movemark, activemark | ||
|
r43346 | |||
|
r30962 | def _destupdatebranch(repo, clean): | ||
|
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: | ||||
|
r43347 | currentbranch = repo[b'.'].branch() | ||
|
r29284 | |||
|
r28236 | if currentbranch in repo.branchmap(): | ||
|
r28385 | heads = repo.branchheads(currentbranch) | ||
|
r28236 | if heads: | ||
|
r43347 | node = repo.revs(b'max(.::(%ln))', heads).first() | ||
|
r26725 | if bookmarks.isactivewdirparent(repo): | ||
|
r43347 | movemark = repo[b'.'].node() | ||
elif currentbranch == b'default' and not wc.p1(): | ||||
|
r28924 | # "null" parent belongs to "default" branch, but it doesn't exist, so | ||
# update to the tipmost non-closed branch head | ||||
|
r43347 | node = repo.revs(b'max(head() and not closed())').first() | ||
|
r28236 | else: | ||
|
r43347 | node = repo[b'.'].node() | ||
|
r26725 | return node, movemark, None | ||
|
r43346 | |||
|
r30962 | def _destupdatebranchfallback(repo, clean): | ||
|
r28385 | """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) | ||||
|
r43347 | assert heads, b"any branch has at least one head" | ||
node = repo.revs(b'max(.::(%ln))', heads).first() | ||||
|
r43387 | assert ( | ||
node is not None | ||||
), b"any revision has at least one descendant branch head" | ||||
|
r28385 | if bookmarks.isactivewdirparent(repo): | ||
|
r43347 | movemark = repo[b'.'].node() | ||
|
r28385 | else: | ||
# here, no "default" branch, and all branches are closed | ||||
|
r43347 | node = repo.lookup(b'tip') | ||
assert node is not None, b"'tip' exists even in empty repository" | ||||
|
r28385 | return node, movemark, None | ||
|
r43346 | |||
|
r30332 | # order in which each step should be evaluated | ||
|
r26726 | # steps are run until one finds a destination | ||
|
r43347 | destupdatesteps = [b'evolution', b'bookmark', b'branch', b'branchfallback'] | ||
|
r26726 | # mapping to ease extension overriding steps. | ||
|
r43346 | destupdatestepmap = { | ||
|
r43347 | b'evolution': _destupdateobs, | ||
b'bookmark': _destupdatebook, | ||||
b'branch': _destupdatebranch, | ||||
b'branchfallback': _destupdatebranchfallback, | ||||
|
r43346 | } | ||
|
r26726 | |||
|
r30962 | def destupdate(repo, clean=False): | ||
|
r26723 | """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: | ||
|
r30962 | node, movemark, activemark = destupdatestepmap[step](repo, clean) | ||
|
r26726 | if node is not None: | ||
break | ||||
|
r26628 | rev = repo[node].rev() | ||
|
r26641 | return rev, movemark, activemark | ||
|
r26714 | |||
|
r43346 | |||
|
r28102 | msgdestmerge = { | ||
# too many matching divergent bookmark | ||||
|
r43347 | b'toomanybookmarks': { | ||
b'merge': ( | ||||
|
r43346 | _( | ||
|
r43347 | b"multiple matching bookmarks to merge -" | ||
b" please merge with an explicit rev or bookmark" | ||||
|
r43346 | ), | ||
|
r43384 | _(b"run 'hg heads' to see all heads, specify rev with -r"), | ||
|
r43346 | ), | ||
|
r43347 | b'rebase': ( | ||
|
r43346 | _( | ||
|
r43347 | b"multiple matching bookmarks to rebase -" | ||
b" please rebase to an explicit rev or bookmark" | ||||
|
r43346 | ), | ||
|
r43384 | _(b"run 'hg heads' to see all heads, specify destination with -d"), | ||
|
r43346 | ), | ||
}, | ||||
|
r28102 | # no other matching divergent bookmark | ||
|
r43347 | b'nootherbookmarks': { | ||
b'merge': ( | ||||
|
r43346 | _( | ||
|
r43347 | b"no matching bookmark to merge - " | ||
b"please merge with an explicit rev or bookmark" | ||||
|
r43346 | ), | ||
|
r43384 | _(b"run 'hg heads' to see all heads, specify rev with -r"), | ||
|
r43346 | ), | ||
|
r43347 | b'rebase': ( | ||
|
r43346 | _( | ||
|
r43347 | b"no matching bookmark to rebase - " | ||
b"please rebase to an explicit rev or bookmark" | ||||
|
r43346 | ), | ||
|
r43384 | _(b"run 'hg heads' to see all heads, specify destination with -d"), | ||
|
r43346 | ), | ||
}, | ||||
|
r28102 | # branch have too many unbookmarked heads, no obvious destination | ||
|
r43347 | b'toomanyheads': { | ||
b'merge': ( | ||||
_(b"branch '%s' has %d heads - please merge with an explicit rev"), | ||||
|
r43384 | _(b"run 'hg heads .' to see heads, specify rev with -r"), | ||
|
r43346 | ), | ||
|
r43347 | b'rebase': ( | ||
_(b"branch '%s' has %d heads - please rebase to an explicit rev"), | ||||
|
r43384 | _(b"run 'hg heads .' to see heads, specify destination with -d"), | ||
|
r43346 | ), | ||
}, | ||||
|
r28102 | # branch have no other unbookmarked heads | ||
|
r43347 | b'bookmarkedheads': { | ||
b'merge': ( | ||||
_(b"heads are bookmarked - please merge with an explicit rev"), | ||||
|
r43384 | _(b"run 'hg heads' to see all heads, specify rev with -r"), | ||
|
r43346 | ), | ||
|
r43347 | b'rebase': ( | ||
_(b"heads are bookmarked - please rebase to an explicit rev"), | ||||
|
r43384 | _(b"run 'hg heads' to see all heads, specify destination with -d"), | ||
|
r43346 | ), | ||
}, | ||||
|
r28102 | # branch have just a single heads, but there is other branches | ||
|
r43347 | b'nootherbranchheads': { | ||
b'merge': ( | ||||
_(b"branch '%s' has one head - please merge with an explicit rev"), | ||||
|
r43384 | _(b"run 'hg heads' to see all heads, specify rev with -r"), | ||
|
r43346 | ), | ||
|
r43347 | b'rebase': ( | ||
_(b"branch '%s' has one head - please rebase to an explicit rev"), | ||||
|
r43384 | _(b"run 'hg heads' to see all heads, specify destination with -d"), | ||
|
r43346 | ), | ||
}, | ||||
|
r28102 | # repository have a single head | ||
|
r43347 | b'nootherheads': { | ||
b'merge': (_(b'nothing to merge'), None), | ||||
b'rebase': (_(b'nothing to rebase'), None), | ||||
|
r43346 | }, | ||
|
r28102 | # repository have a single head and we are not on it | ||
|
r43347 | b'nootherheadsbehind': { | ||
b'merge': (_(b'nothing to merge'), _(b"use 'hg update' instead")), | ||||
b'rebase': (_(b'nothing to rebase'), _(b"use 'hg update' instead")), | ||||
|
r43346 | }, | ||
|
r28102 | # We are not on a head | ||
|
r43347 | b'notatheads': { | ||
b'merge': ( | ||||
_(b'working directory not at a head revision'), | ||||
_(b"use 'hg update' or merge with an explicit revision"), | ||||
|
r43346 | ), | ||
|
r43347 | b'rebase': ( | ||
_(b'working directory not at a head revision'), | ||||
_(b"use 'hg update' or rebase to an explicit revision"), | ||||
|
r43346 | ), | ||
}, | ||||
|
r43347 | b'emptysourceset': { | ||
b'merge': (_(b'source set is empty'), None), | ||||
b'rebase': (_(b'source set is empty'), None), | ||||
|
r43346 | }, | ||
|
r43347 | b'multiplebranchessourceset': { | ||
b'merge': (_(b'source set is rooted in multiple branches'), None), | ||||
b'rebase': ( | ||||
_(b'rebaseset is rooted in multiple named branches'), | ||||
_(b'specify an explicit destination with --dest'), | ||||
|
r43346 | ), | ||
}, | ||||
} | ||||
|
r28102 | |||
|
r43347 | def _destmergebook(repo, action=b'merge', sourceset=None, destspace=None): | ||
|
r26727 | """find merge destination in the active bookmark case""" | ||
node = None | ||||
|
r32381 | bmheads = bookmarks.headsforactive(repo) | ||
|
r37470 | curhead = repo._bookmarks[repo._activebookmark] | ||
|
r26727 | if len(bmheads) == 2: | ||
if curhead == bmheads[0]: | ||||
node = bmheads[1] | ||||
else: | ||||
node = bmheads[0] | ||||
elif len(bmheads) > 2: | ||||
|
r43347 | msg, hint = msgdestmerge[b'toomanybookmarks'][action] | ||
|
r28141 | raise error.ManyMergeDestAbort(msg, hint=hint) | ||
|
r26727 | elif len(bmheads) <= 1: | ||
|
r43347 | msg, hint = msgdestmerge[b'nootherbookmarks'][action] | ||
|
r28141 | raise error.NoMergeDestAbort(msg, hint=hint) | ||
|
r26727 | assert node is not None | ||
return node | ||||
|
r43346 | |||
def _destmergebranch( | ||||
|
r43347 | repo, action=b'merge', sourceset=None, onheadcheck=True, destspace=None | ||
|
r43346 | ): | ||
|
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: | ||||
|
r43347 | msg, hint = msgdestmerge[b'emptysourceset'][action] | ||
|
r28141 | raise error.NoMergeDestAbort(msg, hint=hint) | ||
|
r28139 | else: | ||
branch = None | ||||
|
r43347 | for ctx in repo.set(b'roots(%ld::%ld)', sourceset, sourceset): | ||
|
r28139 | if branch is not None and ctx.branch() != branch: | ||
|
r43347 | msg, hint = msgdestmerge[b'multiplebranchessourceset'][action] | ||
|
r28141 | raise error.ManyMergeDestAbort(msg, hint=hint) | ||
|
r28139 | branch = ctx.branch() | ||
|
r26728 | bheads = repo.branchheads(branch) | ||
|
r43347 | onhead = repo.revs(b'%ld and %ln', sourceset, bheads) | ||
|
r28161 | 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: | ||
|
r43347 | msg, hint = msgdestmerge[b'nootherheadsbehind'][action] | ||
|
r28103 | else: | ||
|
r43347 | msg, hint = msgdestmerge[b'notatheads'][action] | ||
|
r28103 | raise error.Abort(msg, hint=hint) | ||
|
r28139 | # remove heads descendants of source from the set | ||
|
r43347 | bheads = list(repo.revs(b'%ln - (%ld::)', bheads, sourceset)) | ||
|
r28138 | # filters out bookmarked heads | ||
|
r43347 | nbhs = list(repo.revs(b'%ld - bookmark()', bheads)) | ||
|
r29043 | |||
if destspace is not None: | ||||
# restrict search space | ||||
# used in the 'hg pull --rebase' case, see issue 5214. | ||||
|
r43347 | nbhs = list(repo.revs(b'%ld and %ld', destspace, nbhs)) | ||
|
r29043 | |||
|
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. | ||||
|
r43347 | msg, hint = msgdestmerge[b'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: | ||
|
r43347 | msg, hint = msgdestmerge[b'bookmarkedheads'][action] | ||
|
r28102 | elif len(repo.heads()) > 1: | ||
|
r43347 | msg, hint = msgdestmerge[b'nootherbranchheads'][action] | ||
|
r28102 | msg %= branch | ||
|
r28161 | elif not onhead: | ||
# if 'onheadcheck == False' (rebase case), | ||||
# this was not caught in Case A. | ||||
|
r43347 | msg, hint = msgdestmerge[b'nootherheadsbehind'][action] | ||
|
r28102 | else: | ||
|
r43347 | msg, hint = msgdestmerge[b'nootherheads'][action] | ||
|
r28141 | raise error.NoMergeDestAbort(msg, hint=hint) | ||
|
r26728 | else: | ||
node = nbhs[0] | ||||
assert node is not None | ||||
return node | ||||
|
r43346 | |||
def destmerge( | ||||
|
r43347 | repo, action=b'merge', sourceset=None, onheadcheck=True, destspace=None | ||
|
r43346 | ): | ||
|
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: | ||
|
r43346 | node = _destmergebook( | ||
repo, action=action, sourceset=sourceset, destspace=destspace | ||||
) | ||||
|
r26714 | else: | ||
|
r43346 | node = _destmergebranch( | ||
repo, | ||||
action=action, | ||||
sourceset=sourceset, | ||||
onheadcheck=onheadcheck, | ||||
destspace=destspace, | ||||
) | ||||
|
r26714 | return repo[node].rev() | ||
|
r27262 | |||
|
r43346 | |||
|
r27262 | def desthistedit(ui, repo): | ||
"""Default base revision to edit for `hg histedit`.""" | ||||
|
r43347 | default = ui.config(b'histedit', b'defaultrev') | ||
|
r37021 | |||
if default is None: | ||||
revs = stack.getstack(repo) | ||||
elif default: | ||||
|
r27559 | revs = scmutil.revrange(repo, [default]) | ||
|
r41202 | else: | ||
|
r46447 | raise error.ConfigError( | ||
|
r43347 | _(b"config option histedit.defaultrev can't be empty") | ||
) | ||||
|
r37021 | |||
if revs: | ||||
|
r37408 | # Take the first revision of the revset as the root | ||
return revs.min() | ||||
|
r27262 | |||
return None | ||||
|
r28029 | |||
|
r43346 | |||
|
r33194 | def stackbase(ui, repo): | ||
|
r37020 | revs = stack.getstack(repo) | ||
|
r37022 | return revs.first() if revs else None | ||
|
r33194 | |||
|
r43346 | |||
|
r28029 | def _statusotherbook(ui, repo): | ||
|
r32381 | bmheads = bookmarks.headsforactive(repo) | ||
|
r37470 | curhead = repo._bookmarks[repo._activebookmark] | ||
|
r43347 | if repo.revs(b'%n and parents()', curhead): | ||
|
r28029 | # we are on the active bookmark | ||
bmheads = [b for b in bmheads if curhead != b] | ||||
if bmheads: | ||||
|
r43347 | msg = _(b'%i other divergent bookmarks for "%s"\n') | ||
|
r28029 | ui.status(msg % (len(bmheads), repo._activebookmark)) | ||
|
r43346 | |||
|
r28029 | def _statusotherbranchheads(ui, repo): | ||
currentbranch = repo.dirstate.branch() | ||||
|
r28266 | allheads = repo.branchheads(currentbranch, closed=True) | ||
|
r28029 | heads = repo.branchheads(currentbranch) | ||
|
r43347 | if repo.revs(b'%ln and parents()', allheads): | ||
|
r28266 | # 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 | ||||
# ========= ========== | ||||
|
r43347 | otherheads = repo.revs(b'%ln - parents()', heads) | ||
if repo[b'.'].closesbranch(): | ||||
|
r43346 | ui.warn( | ||
_( | ||||
|
r43347 | b'no open descendant heads on branch "%s", ' | ||
b'updating to a closed head\n' | ||||
|
r43346 | ) | ||
% currentbranch | ||||
) | ||||
|
r28385 | if otherheads: | ||
|
r43346 | ui.warn( | ||
_( | ||||
|
r43347 | b"(committing will reopen the head, " | ||
b"use 'hg heads .' to see %i other heads)\n" | ||||
|
r43346 | ) | ||
% (len(otherheads)) | ||||
) | ||||
|
r28385 | else: | ||
|
r43346 | ui.warn( | ||
|
r43347 | _(b'(committing will reopen branch "%s")\n') % currentbranch | ||
|
r43346 | ) | ||
|
r28385 | elif otherheads: | ||
|
r43347 | curhead = repo[b'.'] | ||
|
r43346 | ui.status( | ||
|
r43347 | _(b'updated to "%s: %s"\n') | ||
% (curhead, curhead.description().split(b'\n')[0]) | ||||
|
r43346 | ) | ||
ui.status( | ||||
|
r43347 | _(b'%i other heads for branch "%s"\n') | ||
|
r43346 | % (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) | ||||