mercurial/destutil.py
author Pierre-Yves David <pierre-yves.david@fb.com>
Mon, 08 Feb 2016 14:56:28 +0100
changeset 28104 96f8baddbd6a
parent 28103 7d852bb47b0a
child 28105 1fc7b5363871
permissions -rw-r--r--
destutil: consistently retrieve 'p1' and 'branch' We already read p1 from the dirstate so let's read the branch from it too.

# 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.

from __future__ import absolute_import

from .i18n import _
from . import (
    bookmarks,
    error,
    obsolete,
)

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)

def _destupdateobs(repo, clean, check):
    """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 = 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()
            if bookmarks.isactivewdirparent(repo):
                movemark = repo['.'].node()
    return node, movemark, None

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

def _destupdatebranch(repo, clean, check):
    """decide on an update destination from current branch"""
    wc = repo[None]
    movemark = node = None
    try:
        node = repo.revs('max(.::(head() and branch(%s)))'
                         , wc.branch()).first()
        if bookmarks.isactivewdirparent(repo):
            movemark = repo['.'].node()
    except error.RepoLookupError:
        if wc.branch() == 'default': # no default branch!
            node = repo.lookup('tip') # update to tip
        else:
            raise error.Abort(_("branch %s not found") % wc.branch())
    return node, movemark, None

# order in which each step should be evalutated
# steps are run until one finds a destination
destupdatesteps = ['evolution', 'bookmark', 'branch']
# mapping to ease extension overriding steps.
destupdatestepmap = {'evolution': _destupdateobs,
                     'bookmark': _destupdatebook,
                     'branch': _destupdatebranch,
                     }

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.
    """
    node = movemark = activemark = None

    for step in destupdatesteps:
        node, movemark, activemark = destupdatestepmap[step](repo, clean, check)
        if node is not None:
            break
    rev = repo[node].rev()

    _destupdatevalidate(repo, rev, clean, check)

    return rev, movemark, activemark

msgdestmerge = {
    # too many matching divergent bookmark
    'toomanybookmarks':
        (_("multiple matching bookmarks to merge -"
           " please merge with an explicit rev or bookmark"),
         _("run 'hg heads' to see all heads")),
    # no other matching divergent bookmark
    'nootherbookmarks':
        (_("no matching bookmark to merge - "
           "please merge with an explicit rev or bookmark"),
         _("run 'hg heads' to see all heads")),
    # branch have too many unbookmarked heads, no obvious destination
    'toomanyheads':
        (_("branch '%s' has %d heads - please merge with an explicit rev"),
         _("run 'hg heads .' to see heads")),
    # branch have no other unbookmarked heads
    'bookmarkedheads':
        (_("heads are bookmarked - please merge with an explicit rev"),
         _("run 'hg heads' to see all heads")),
    # branch have just a single heads, but there is other branches
    'nootherbranchheads':
        (_("branch '%s' has one head - please merge with an explicit rev"),
         _("run 'hg heads' to see all heads")),
    # repository have a single head
    'nootherheads':
        (_('nothing to merge'),
         None),
    # repository have a single head and we are not on it
    'nootherheadsbehind':
        (_('nothing to merge'),
         _("use 'hg update' instead")),
    # We are not on a head
    'notatheads':
        (_('working directory not at a head revision'),
         _("use 'hg update' or merge with an explicit revision"))
        }

def _destmergebook(repo):
    """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:
        msg, hint = msgdestmerge['toomanybookmarks']
        raise error.Abort(msg, hint=hint)
    elif len(bmheads) <= 1:
        msg, hint = msgdestmerge['nootherbookmarks']
        raise error.Abort(msg, hint=hint)
    assert node is not None
    return node

def _destmergebranch(repo):
    """find merge destination based on branch heads"""
    node = None
    parent = repo.dirstate.p1()
    branch = repo.dirstate.branch()
    bheads = repo.branchheads(branch)
    nbhs = [bh for bh in bheads if not repo[bh].bookmarks()]

    if parent not in bheads:
        if len(repo.heads()) <= 1:
            msg, hint = msgdestmerge['nootherheadsbehind']
        else:
            msg, hint = msgdestmerge['notatheads']
        raise error.Abort(msg, hint=hint)

    if len(nbhs) > 2:
        msg, hint = msgdestmerge['toomanyheads']
        msg %= (branch, len(bheads))
        raise error.Abort(msg, hint=hint)

    if len(nbhs) <= 1:
        if len(bheads) > 1:
            msg, hint = msgdestmerge['bookmarkedheads']
        elif len(repo.heads()) > 1:
            msg, hint = msgdestmerge['nootherbranchheads']
            msg %= branch
        else:
            msg, hint = msgdestmerge['nootherheads']
        raise error.Abort(msg, hint=hint)

    if parent == nbhs[0]:
        node = nbhs[-1]
    else:
        node = nbhs[0]
    assert node is not None
    return node

def destmerge(repo):
    if repo._activebookmark:
        node = _destmergebook(repo)
    else:
        node = _destmergebranch(repo)
    return repo[node].rev()

histeditdefaultrevset = 'reverse(only(.) and not public() and not ::merge())'

def desthistedit(ui, repo):
    """Default base revision to edit for `hg histedit`."""
    # Avoid cycle: scmutil -> revset -> destutil
    from . import scmutil

    default = ui.config('histedit', 'defaultrev', histeditdefaultrevset)
    if default:
        revs = scmutil.revrange(repo, [default])
        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

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()
    heads = repo.branchheads(currentbranch)
    l = len(heads)
    if repo.revs('%ln and parents()', heads):
        # we are on a head
        heads = repo.revs('%ln - parents()', heads)
        if heads and l != len(heads):
            ui.status(_('%i other heads for branch "%s"\n') %
                      (len(heads), 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)