# destutil.py - Mercurial utility function for command destination # # Copyright Olivia Mackall 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. from __future__ import annotations from .i18n import _ from . import bookmarks, error, obsutil, scmutil, stack 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 def _destupdateobs(repo, clean): """decide of an update destination from obsolescence markers""" node = None wc = repo[None] p1 = wc.p1() movemark = None if p1.obsolete() and not p1.children(): # allow updating to successors successors = obsutil.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: update to the closest non-obsolete ancestor, # similar to what 'hg prune' currently does 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(b'max(%ln)', successors).first() else: 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() return node, movemark, None def _destupdatebook(repo, clean): """decide on an update destination from active bookmark""" # we also move the active bookmark, if any node = None activemark, movemark = bookmarks.calculateupdate(repo.ui, repo) if activemark is not None: node = repo._bookmarks[activemark] return node, movemark, activemark def _destupdatebranch(repo, clean): """decide on an update destination from current branch This ignores closed branch heads. """ wc = repo[None] movemark = node = None currentbranch = wc.branch() if clean: currentbranch = repo[b'.'].branch() if currentbranch in repo.branchmap(): heads = repo.branchheads(currentbranch) if heads: node = repo.revs(b'max(.::(%ln))', heads).first() if bookmarks.isactivewdirparent(repo): movemark = repo[b'.'].node() elif currentbranch == b'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(b'max(head() and not closed())').first() else: node = repo[b'.'].node() return node, movemark, None def _destupdatebranchfallback(repo, clean): """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, b"any branch has at least one head" node = repo.revs(b'max(.::(%ln))', heads).first() assert ( node is not None ), b"any revision has at least one descendant branch head" if bookmarks.isactivewdirparent(repo): movemark = repo[b'.'].node() else: # here, no "default" branch, and all branches are closed node = repo.lookup(b'tip') assert node is not None, b"'tip' exists even in empty repository" return node, movemark, None # order in which each step should be evaluated # steps are run until one finds a destination destupdatesteps = [b'evolution', b'bookmark', b'branch', b'branchfallback'] # mapping to ease extension overriding steps. destupdatestepmap = { b'evolution': _destupdateobs, b'bookmark': _destupdatebook, b'branch': _destupdatebranch, b'branchfallback': _destupdatebranchfallback, } def destupdate(repo, clean=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. """ node = movemark = activemark = None for step in destupdatesteps: node, movemark, activemark = destupdatestepmap[step](repo, clean) if node is not None: break rev = repo[node].rev() return rev, movemark, activemark msgdestmerge = { # too many matching divergent bookmark b'toomanybookmarks': { b'merge': ( _( b"multiple matching bookmarks to merge -" b" please merge with an explicit rev or bookmark" ), _(b"run 'hg heads' to see all heads, specify rev with -r"), ), b'rebase': ( _( b"multiple matching bookmarks to rebase -" b" please rebase to an explicit rev or bookmark" ), _(b"run 'hg heads' to see all heads, specify destination with -d"), ), }, # no other matching divergent bookmark b'nootherbookmarks': { b'merge': ( _( b"no matching bookmark to merge - " b"please merge with an explicit rev or bookmark" ), _(b"run 'hg heads' to see all heads, specify rev with -r"), ), b'rebase': ( _( b"no matching bookmark to rebase - " b"please rebase to an explicit rev or bookmark" ), _(b"run 'hg heads' to see all heads, specify destination with -d"), ), }, # branch have too many unbookmarked heads, no obvious destination b'toomanyheads': { b'merge': ( _(b"branch '%s' has %d heads - please merge with an explicit rev"), _(b"run 'hg heads .' to see heads, specify rev with -r"), ), b'rebase': ( _(b"branch '%s' has %d heads - please rebase to an explicit rev"), _(b"run 'hg heads .' to see heads, specify destination with -d"), ), }, # branch have no other unbookmarked heads b'bookmarkedheads': { b'merge': ( _(b"heads are bookmarked - please merge with an explicit rev"), _(b"run 'hg heads' to see all heads, specify rev with -r"), ), b'rebase': ( _(b"heads are bookmarked - please rebase to an explicit rev"), _(b"run 'hg heads' to see all heads, specify destination with -d"), ), }, # branch have just a single heads, but there is other branches b'nootherbranchheads': { b'merge': ( _(b"branch '%s' has one head - please merge with an explicit rev"), _(b"run 'hg heads' to see all heads, specify rev with -r"), ), b'rebase': ( _(b"branch '%s' has one head - please rebase to an explicit rev"), _(b"run 'hg heads' to see all heads, specify destination with -d"), ), }, # repository have a single head b'nootherheads': { b'merge': (_(b'nothing to merge'), None), b'rebase': (_(b'nothing to rebase'), None), }, # repository have a single head and we are not on it b'nootherheadsbehind': { b'merge': (_(b'nothing to merge'), _(b"use 'hg update' instead")), b'rebase': (_(b'nothing to rebase'), _(b"use 'hg update' instead")), }, # We are not on a head b'notatheads': { b'merge': ( _(b'working directory not at a head revision'), _(b"use 'hg update' or merge with an explicit revision"), ), b'rebase': ( _(b'working directory not at a head revision'), _(b"use 'hg update' or rebase to an explicit revision"), ), }, b'emptysourceset': { b'merge': (_(b'source set is empty'), None), b'rebase': (_(b'source set is empty'), None), }, 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'), ), }, } def _destmergebook(repo, action=b'merge', sourceset=None, destspace=None): """find merge destination in the active bookmark case""" node = None bmheads = bookmarks.headsforactive(repo) curhead = repo._bookmarks[repo._activebookmark] if len(bmheads) == 2: if curhead == bmheads[0]: node = bmheads[1] else: node = bmheads[0] elif len(bmheads) > 2: msg, hint = msgdestmerge[b'toomanybookmarks'][action] raise error.ManyMergeDestAbort(msg, hint=hint) elif len(bmheads) <= 1: msg, hint = msgdestmerge[b'nootherbookmarks'][action] raise error.NoMergeDestAbort(msg, hint=hint) assert node is not None return node def _destmergebranch( repo, action=b'merge', sourceset=None, onheadcheck=True, destspace=None ): """find merge destination based on branch heads""" node = None if sourceset is None: sourceset = [repo[repo.dirstate.p1()].rev()] branch = repo.dirstate.branch() elif not sourceset: msg, hint = msgdestmerge[b'emptysourceset'][action] raise error.NoMergeDestAbort(msg, hint=hint) else: branch = None for ctx in repo.set(b'roots(%ld::%ld)', sourceset, sourceset): if branch is not None and ctx.branch() != branch: msg, hint = msgdestmerge[b'multiplebranchessourceset'][action] raise error.ManyMergeDestAbort(msg, hint=hint) branch = ctx.branch() bheads = repo.branchheads(branch) onhead = repo.revs(b'%ld and %ln', sourceset, bheads) if onheadcheck and not onhead: # Case A: working copy if not on a head. (merge only) # # This is probably a user mistake We bailout pointing at 'hg update' if len(repo.heads()) <= 1: msg, hint = msgdestmerge[b'nootherheadsbehind'][action] else: msg, hint = msgdestmerge[b'notatheads'][action] raise error.Abort(msg, hint=hint) # remove heads descendants of source from the set bheads = list(repo.revs(b'%ln - (%ld::)', bheads, sourceset)) # filters out bookmarked heads nbhs = list(repo.revs(b'%ld - bookmark()', bheads)) if destspace is not None: # restrict search space # used in the 'hg pull --rebase' case, see issue 5214. nbhs = list(repo.revs(b'%ld and %ld', destspace, nbhs)) if len(nbhs) > 1: # Case B: There is more than 1 other anonymous heads # # This means that there will be more than 1 candidate. This is # ambiguous. We abort asking the user to pick as explicit destination # instead. msg, hint = msgdestmerge[b'toomanyheads'][action] msg %= (branch, len(bheads) + 1) raise error.ManyMergeDestAbort(msg, hint=hint) elif not nbhs: # Case B: There is no other anonymous heads # # This means that there is no natural candidate to merge with. # We abort, with various messages for various cases. if bheads: msg, hint = msgdestmerge[b'bookmarkedheads'][action] elif len(repo.heads()) > 1: msg, hint = msgdestmerge[b'nootherbranchheads'][action] msg %= branch elif not onhead: # if 'onheadcheck == False' (rebase case), # this was not caught in Case A. msg, hint = msgdestmerge[b'nootherheadsbehind'][action] else: msg, hint = msgdestmerge[b'nootherheads'][action] raise error.NoMergeDestAbort(msg, hint=hint) else: node = nbhs[0] assert node is not None return node def destmerge( repo, action=b'merge', sourceset=None, onheadcheck=True, destspace=None ): """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 """ # destspace is here to work around issues with `hg pull --rebase` see # issue5214 for details if repo._activebookmark: node = _destmergebook( repo, action=action, sourceset=sourceset, destspace=destspace ) else: node = _destmergebranch( repo, action=action, sourceset=sourceset, onheadcheck=onheadcheck, destspace=destspace, ) return repo[node].rev() def desthistedit(ui, repo): """Default base revision to edit for `hg histedit`.""" default = ui.config(b'histedit', b'defaultrev') if default is None: revs = stack.getstack(repo) elif default: revs = scmutil.revrange(repo, [default]) else: raise error.ConfigError( _(b"config option histedit.defaultrev can't be empty") ) if revs: # Take the first revision of the revset as the root return revs.min() return None def stackbase(ui, repo): revs = stack.getstack(repo) return revs.first() if revs else None def _statusotherbook(ui, repo): bmheads = bookmarks.headsforactive(repo) curhead = repo._bookmarks[repo._activebookmark] if repo.revs(b'%n and parents()', curhead): # we are on the active bookmark bmheads = [b for b in bmheads if curhead != b] if bmheads: msg = _(b'%i other divergent bookmarks for "%s"\n') ui.status(msg % (len(bmheads), repo._activebookmark)) def _statusotherbranchheads(ui, repo): currentbranch = repo.dirstate.branch() allheads = repo.branchheads(currentbranch, closed=True) heads = repo.branchheads(currentbranch) if repo.revs(b'%ln and parents()', allheads): # we are on a head, even though it might be closed # # 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 # ========= ========== otherheads = repo.revs(b'%ln - parents()', heads) if repo[b'.'].closesbranch(): ui.warn( _( b'no open descendant heads on branch "%s", ' b'updating to a closed head\n' ) % currentbranch ) if otherheads: ui.warn( _( b"(committing will reopen the head, " b"use 'hg heads .' to see %i other heads)\n" ) % (len(otherheads)) ) else: ui.warn( _(b'(committing will reopen branch "%s")\n') % currentbranch ) elif otherheads: curhead = repo[b'.'] ui.status( _(b'updated to "%s: %s"\n') % (curhead, curhead.description().split(b'\n')[0]) ) ui.status( _(b'%i other heads for branch "%s"\n') % (len(otherheads), currentbranch) ) 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)