states.py
author Pierre-Yves David <pierre-yves.david@logilab.fr>
Mon, 23 May 2011 17:15:47 +0200
changeset 9 1f84a74df837
parent 7 cc592295900f
child 10 91169d2d7f1b
permissions -rw-r--r--
introduce a third state

# states.py - introduce the state concept for mercurial changeset
#
# Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
#                Logilab SA        <contact@logilab.fr>
#                Augie Fackler     <durin42@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

'''introduce the state concept for mercurial changeset

Change can be in the following state:

0 immutable
1 mutable
2 private

name are not fixed yet.
'''
from functools import partial
from mercurial.i18n import _
from mercurial import cmdutil
from mercurial import scmutil
from mercurial import context
from mercurial import revset
from mercurial import templatekw
from mercurial import util
from mercurial import node
from mercurial.node import nullid, hex, short
from mercurial import discovery
from mercurial import extensions
from mercurial import wireproto


_NOPULLPUSH=2
_MUTABLE=1

STATES = (0, _MUTABLE, _NOPULLPUSH | _MUTABLE)
def statename(state):
    return str(STATES)


# util function
#############################
def noderange(repo, revsets):
    return map(repo.changelog.node,
               scmutil.revrange(repo, revsets))

# Patch changectx
#############################

def state(ctx):
    return ctx._repo.nodestate(ctx.node())
context.changectx.state = state

# improve template
#############################

def showstate(ctx, **args):
    return ctx.state()

# New revset predicate
#############################

def revsetstatehead(state):
    def revsetpublicheads(repo, subset, x):
        args = revset.getargs(x, 0, 0, 'publicheads takes no arguments')
        heads = map(repo.changelog.rev, repo._statesheads[state])
        heads.sort()
        return heads
    return revsetpublicheads

def extsetup(ui):
    revset.symbols['frozenheads'] = revsetstatehead(0)
    revset.symbols['publicheads'] = revsetstatehead(1)

REVSETHEADS = {0: 'frozenheads()',
               1: 'publicheads()'}

# New commands
#############################

def cmdsetstate(ui, repo, state, *changesets):
    """turn private changesets into public ones"""
    #assert repo.ui.configbool('private', 'enable', False)
    state = int(state) #for now
    revs = scmutil.revrange(repo, changesets)
    repo.setstate(state, [repo.changelog.node(rev) for rev in revs])
    return 0

cmdtable = {
    'setstate':  (cmdsetstate,   [], _('state <revset>')),
    }


templatekw.keywords['state'] = showstate






def uisetup(ui):
    def filterprivateout(orig, repo, *args,**kwargs):
        common, heads = orig(repo, *args, **kwargs)
        return common, repo._reducehead(heads)
    def filterprivatein(orig, repo, remote, *args, **kwargs):
        common, anyinc, heads = orig(repo, remote, *args, **kwargs)
        heads = remote._reducehead(heads)
        return common, anyinc, heads

    extensions.wrapfunction(discovery, 'findcommonoutgoing', filterprivateout)
    extensions.wrapfunction(discovery, 'findcommonincoming', filterprivatein)

    # Write protocols
    ####################
    def heads(repo, proto):
        h = repo._publicheads
        return wireproto.encodelist(h) + "\n"

    def _reducehead(wirerepo, heads):
        """heads filtering is done repo side"""
        return heads

    wireproto.wirerepository._reducehead = _reducehead
    wireproto.commands['heads'] = (heads, '')

def reposetup(ui, repo):

    if not repo.local():
        return

    o_cancopy =repo.cancopy
    class statefulrepo(repo.__class__):

        def nodestate(self, node):
            rev = self.changelog.rev(node)
            for head in self._publicheads:
                revhead = self.changelog.rev(head)
                if self.changelog.descendant(revhead, rev):
                    return STATES[2]
            for head in self._frozenheads:
                revhead = self.changelog.rev(head)
                if self.changelog.descendant(revhead, rev):
                    return STATES[1]
            return STATES[0]

        @property
        def _publicheads(self):
            if self.ui.configbool('states', 'private', False):
                return self._statesheads[1]
            return self.heads()

        @property
        def _frozenheads(self):
            if self.ui.configbool('states', 'liquid', False):
                return self._statesheads[O]
            return self.heads()

        @util.propertycache
        def _statesheads(self):
            return self._readstatesheads()


        def _readheadsfile(self, filename):
            heads = [nullid]
            try:
                f = self.opener(filename)
                try:
                    heads = sorted([node.bin(n) for n in f.read().split() if n])
                finally:
                    f.close()
            except IOError:
                pass
            return heads
        def _readstatesheads(self):
            statesheads = {}
            statesheads[0] = self._readheadsfile('frozenheads')
            statesheads[1] = self._readheadsfile('publicheads')
            return statesheads

        def _writeheadsfile(self, filename, heads):
            f = self.opener(filename, 'w', atomictemp=True)
            try:
                for h in heads:
                    f.write(hex(h) + '\n')
                f.rename()
            finally:
                f.close()

        def _writestateshead(self):
            # transaction!
            self._writeheadsfile('frozenheads', self._statesheads[0])
            self._writeheadsfile('publicheads', self._statesheads[1])

        def setstate(self, state, nodes):
            """freeze targets changeset and it's ancestors.

            Simplify the list of head."""
            heads = self._statesheads[state]
            olds = heads[:]
            heads.extend(nodes)
            heads[:] = set(heads)
            heads.sort()
            if olds != heads:
                heads[:] = noderange(repo, ["heads(::%s)" % REVSETHEADS[state]])
                heads.sort()
            if olds != heads:
                self._writestateshead()
            if state < 1:
                self.setstate(1, nodes) # cascading

        def _reducehead(self, candidates):
            selected = set()
            for candidate in candidates:
                rev = self.changelog.rev(candidate)
                ok = True
                for h in self._publicheads:
                    revh = self.changelog.rev(h)
                    if self.changelog.descendant(revh, rev):
                        ok = False
                        selected.add(h)
                if ok:
                    selected.add(candidate)
            return sorted(selected)

        def cancopy(self):
            return o_cancopy() and (self._publicheads == self.heads())

    repo.__class__ = statefulrepo