view states.py @ 17:6aa349f47e7c

add templatekeyword in a proper location
author Pierre-Yves David <pierre-yves.david@ens-lyon.org>
date Wed, 25 May 2011 02:27:09 +0200
parents fd9f50406cb8
children 9ffe946febc0
line wrap: on
line source

# 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


_NOSHARE=2
_MUTABLE=1

class state(object):

    def __init__(self, name, order=0, next=None):
        self.name = name
        self.order = order
        assert next is None or self < next
        self.next = next

    def __repr__(self):
        return 'state(%s)' % self.name

    def __str__(self):
        return self.name

    @util.propertycache
    def trackheads(self):
        """Do we need to track heads of changeset in this state ?

        We don't need to track heads for the last state as this is repos heads"""
        return self.next is not None

    def __cmp__(self, other):
        return cmp(self.order, other.order)

    @util.propertycache
    def _revsetheads(self):
        """function to be used by revset to finds heads of this states"""
        assert self.trackheads
        def revsetheads(repo, subset, x):
            args = revset.getargs(x, 0, 0, 'publicheads takes no arguments')
            heads = map(repo.changelog.rev, repo._statesheads[self])
            heads.sort()
            return heads
        return revsetheads

    @util.propertycache
    def headssymbol(self):
        """name of the revset symbols"""
        if self.trackheads:
            return "%sheads" % self.name
        else:
            return 'heads'

ST2 = state('draft', _NOSHARE | _MUTABLE)
ST1 = state('ready', _MUTABLE, next=ST2)
ST0 = state('published', next=ST1)

STATES = (ST0, ST1, ST2)

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

def cmdpublished(ui, repo, *changesets):
    revs = scmutil.revrange(repo, changesets)
    repo.setstate(ST0, [repo.changelog.node(rev) for rev in revs])
    return 0

def cmdready(ui, repo, *changesets):
    revs = scmutil.revrange(repo, changesets)
    repo.setstate(ST1, [repo.changelog.node(rev) for rev in revs])
    return 0

#autogen this
cmdtable = {
    'published':  (cmdpublished,   [], _('<revset>')),
    'ready':  (cmdready,   [], _('<revset>')),
    }







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.stateheads(ST1)
        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, '')

    templatekw.keywords['state'] = showstate

def extsetup(ui):
    for state in STATES:
        if state.trackheads:
            revset.symbols[state.headssymbol] = state._revsetheads

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 state in STATES[::-1]:
                # XXX avoid for untracked heads
                if state.next is not None:
                    for head in self.stateheads(state):
                        revhead = self.changelog.rev(head)
                        if self.changelog.descendant(revhead, rev):
                            return state.next
            return state



        def stateheads(self, state):
            if state.trackheads:
                if self.ui.configbool('states', state.next.name, False):
                    return self._statesheads[state]
            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 = {}
            for state in STATES:
                if state.trackheads:
                    filename = 'states/%s-heads' % state.name
                    statesheads[state] = self._readheadsfile(filename)
            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!
            for state in STATES:
                if state.trackheads:
                    filename = 'states/%s-heads' % state.name
                    self._writeheadsfile(filename, self._statesheads[state])

        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())" % state.headssymbol])
                heads.sort()
            if olds != heads:
                self._writestateshead()
            if state.next is not None and state.next.trackheads:
                self.setstate(state.next, nodes) # cascading

        def _reducehead(self, candidates):
            selected = set()
            for candidate in candidates:
                rev = self.changelog.rev(candidate)
                ok = True
                for h in self.stateheads(ST1):
                    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.stateheads(ST1) == self.heads())

    repo.__class__ = statefulrepo