mercurial/bookmarks.py
author Sean Farley <sean.michael.farley@gmail.com>
Wed, 01 May 2013 15:34:45 -0500
branchstable
changeset 19110 741d94aa92e4
parent 18984 efef056b1ae9
child 19523 f37b5a17e6a0
permissions -rw-r--r--
bookmarks: resolve divergent bookmarks when moving active bookmark forward This patch resolves divergent bookmarks between the current active bookmark MARK and the new destination. This situation can arise when pulling new changesets, abandoning your current changesets actively bookmarked with MARK via strip, and then doing a bare update. The non-divergent but active bookmark MARK is then moved to a common ancestor of the new changesets and the abandoned changesets. Test coverage is added.

# Mercurial bookmark support code
#
# Copyright 2008 David Soria Parra <dsp@php.net>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

from mercurial.i18n import _
from mercurial.node import hex
from mercurial import encoding, error, util, obsolete
import errno, os

class bmstore(dict):
    """Storage for bookmarks.

    This object should do all bookmark reads and writes, so that it's
    fairly simple to replace the storage underlying bookmarks without
    having to clone the logic surrounding bookmarks.

    This particular bmstore implementation stores bookmarks as
    {hash}\s{name}\n (the same format as localtags) in
    .hg/bookmarks. The mapping is stored as {name: nodeid}.

    This class does NOT handle the "current" bookmark state at this
    time.
    """

    def __init__(self, repo):
        dict.__init__(self)
        self._repo = repo
        try:
            for line in repo.vfs('bookmarks'):
                line = line.strip()
                if not line:
                    continue
                if ' ' not in line:
                    repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n')
                                 % line)
                    continue
                sha, refspec = line.split(' ', 1)
                refspec = encoding.tolocal(refspec)
                try:
                    self[refspec] = repo.changelog.lookup(sha)
                except LookupError:
                    pass
        except IOError, inst:
            if inst.errno != errno.ENOENT:
                raise

    def write(self):
        '''Write bookmarks

        Write the given bookmark => hash dictionary to the .hg/bookmarks file
        in a format equal to those of localtags.

        We also store a backup of the previous state in undo.bookmarks that
        can be copied back on rollback.
        '''
        repo = self._repo
        if repo._bookmarkcurrent not in self:
            setcurrent(repo, None)

        wlock = repo.wlock()
        try:

            file = repo.vfs('bookmarks', 'w', atomictemp=True)
            for name, node in self.iteritems():
                file.write("%s %s\n" % (hex(node), encoding.fromlocal(name)))
            file.close()

            # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
            try:
                os.utime(repo.sjoin('00changelog.i'), None)
            except OSError:
                pass

        finally:
            wlock.release()

def readcurrent(repo):
    '''Get the current bookmark

    If we use gittish branches we have a current bookmark that
    we are on. This function returns the name of the bookmark. It
    is stored in .hg/bookmarks.current
    '''
    mark = None
    try:
        file = repo.opener('bookmarks.current')
    except IOError, inst:
        if inst.errno != errno.ENOENT:
            raise
        return None
    try:
        # No readline() in osutil.posixfile, reading everything is cheap
        mark = encoding.tolocal((file.readlines() or [''])[0])
        if mark == '' or mark not in repo._bookmarks:
            mark = None
    finally:
        file.close()
    return mark

def setcurrent(repo, mark):
    '''Set the name of the bookmark that we are currently on

    Set the name of the bookmark that we are on (hg update <bookmark>).
    The name is recorded in .hg/bookmarks.current
    '''
    current = repo._bookmarkcurrent
    if current == mark:
        return

    if mark not in repo._bookmarks:
        mark = ''

    wlock = repo.wlock()
    try:
        file = repo.opener('bookmarks.current', 'w', atomictemp=True)
        file.write(encoding.fromlocal(mark))
        file.close()
    finally:
        wlock.release()
    repo._bookmarkcurrent = mark

def unsetcurrent(repo):
    wlock = repo.wlock()
    try:
        try:
            util.unlink(repo.join('bookmarks.current'))
            repo._bookmarkcurrent = None
        except OSError, inst:
            if inst.errno != errno.ENOENT:
                raise
    finally:
        wlock.release()

def iscurrent(repo, mark=None, parents=None):
    '''Tell whether the current bookmark is also active

    I.e., the bookmark listed in .hg/bookmarks.current also points to a
    parent of the working directory.
    '''
    if not mark:
        mark = repo._bookmarkcurrent
    if not parents:
        parents = [p.node() for p in repo[None].parents()]
    marks = repo._bookmarks
    return (mark in marks and marks[mark] in parents)

def updatecurrentbookmark(repo, oldnode, curbranch):
    try:
        return update(repo, oldnode, repo.branchtip(curbranch))
    except error.RepoLookupError:
        if curbranch == "default": # no default branch!
            return update(repo, oldnode, repo.lookup("tip"))
        else:
            raise util.Abort(_("branch %s not found") % curbranch)

def deletedivergent(repo, deletefrom, bm):
    '''Delete divergent versions of bm on nodes in deletefrom.

    Return True if at least one bookmark was deleted, False otherwise.'''
    deleted = False
    marks = repo._bookmarks
    divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]]
    for mark in divergent:
        if mark and marks[mark] in deletefrom:
            if mark != bm:
                del marks[mark]
                deleted = True
    return deleted

def update(repo, parents, node):
    deletefrom = parents
    marks = repo._bookmarks
    update = False
    cur = repo._bookmarkcurrent
    if not cur:
        return False

    if marks[cur] in parents:
        old = repo[marks[cur]]
        new = repo[node]
        divs = [repo[b] for b in marks
                if b.split('@', 1)[0] == cur.split('@', 1)[0]]
        anc = repo.changelog.ancestors([new.rev()])
        deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
        if old.descendant(new):
            marks[cur] = new.node()
            update = True

    if deletedivergent(repo, deletefrom, cur):
        update = True

    if update:
        marks.write()
    return update

def listbookmarks(repo):
    # We may try to list bookmarks on a repo type that does not
    # support it (e.g., statichttprepository).
    marks = getattr(repo, '_bookmarks', {})

    d = {}
    hasnode = repo.changelog.hasnode
    for k, v in marks.iteritems():
        # don't expose local divergent bookmarks
        if hasnode(v) and ('@' not in k or k.endswith('@')):
            d[k] = hex(v)
    return d

def pushbookmark(repo, key, old, new):
    w = repo.wlock()
    try:
        marks = repo._bookmarks
        if hex(marks.get(key, '')) != old:
            return False
        if new == '':
            del marks[key]
        else:
            if new not in repo:
                return False
            marks[key] = repo[new].node()
        marks.write()
        return True
    finally:
        w.release()

def updatefromremote(ui, repo, remotemarks, path):
    ui.debug("checking for updated bookmarks\n")
    changed = False
    localmarks = repo._bookmarks
    for k in sorted(remotemarks):
        if k in localmarks:
            nr, nl = remotemarks[k], localmarks[k]
            if nr in repo:
                cr = repo[nr]
                cl = repo[nl]
                if cl.rev() >= cr.rev():
                    continue
                if validdest(repo, cl, cr):
                    localmarks[k] = cr.node()
                    changed = True
                    ui.status(_("updating bookmark %s\n") % k)
                else:
                    if k == '@':
                        kd = ''
                    else:
                        kd = k
                    # find a unique @ suffix
                    for x in range(1, 100):
                        n = '%s@%d' % (kd, x)
                        if n not in localmarks:
                            break
                    # try to use an @pathalias suffix
                    # if an @pathalias already exists, we overwrite (update) it
                    for p, u in ui.configitems("paths"):
                        if path == u:
                            n = '%s@%s' % (kd, p)

                    localmarks[n] = cr.node()
                    changed = True
                    ui.warn(_("divergent bookmark %s stored as %s\n") % (k, n))
        elif remotemarks[k] in repo:
            # add remote bookmarks for changes we already have
            localmarks[k] = repo[remotemarks[k]].node()
            changed = True
            ui.status(_("adding remote bookmark %s\n") % k)

    if changed:
        localmarks.write()

def diff(ui, dst, src):
    ui.status(_("searching for changed bookmarks\n"))

    smarks = src.listkeys('bookmarks')
    dmarks = dst.listkeys('bookmarks')

    diff = sorted(set(smarks) - set(dmarks))
    for k in diff:
        mark = ui.debugflag and smarks[k] or smarks[k][:12]
        ui.write("   %-25s %s\n" % (k, mark))

    if len(diff) <= 0:
        ui.status(_("no changed bookmarks found\n"))
        return 1
    return 0

def validdest(repo, old, new):
    """Is the new bookmark destination a valid update from the old one"""
    repo = repo.unfiltered()
    if old == new:
        # Old == new -> nothing to update.
        return False
    elif not old:
        # old is nullrev, anything is valid.
        # (new != nullrev has been excluded by the previous check)
        return True
    elif repo.obsstore:
        return new.node() in obsolete.foreground(repo, [old.node()])
    else:
        # still an independant clause as it is lazyer (and therefore faster)
        return old.descendant(new)