move extensions in a hgext directory
authorPierre-Yves David <pierre-yves.david@logilab.fr>
Thu, 08 Sep 2011 17:15:20 +0200
changeset 51 d98e06ab8320
parent 50 19b22ad56b32
child 52 62bdc2567099
move extensions in a hgext directory
hgext/obsolete.py
hgext/states.py
obsolete.py
states.py
tests/test-draft.t
tests/test-obsolete.t
tests/test-ready.t
tests/test-states.t
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/obsolete.py	Thu Sep 08 17:15:20 2011 +0200
@@ -0,0 +1,237 @@
+# obsolete.py - introduce the obsolete concept in mercurial.
+#
+# Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
+#                Logilab SA        <contact@logilab.fr>
+#
+# 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 import util
+from mercurial import context
+from mercurial import revset
+from mercurial import scmutil
+from mercurial import extensions
+from mercurial import pushkey
+from mercurial import discovery
+from mercurial import error
+from mercurial.node import hex, bin
+
+# Patch changectx
+#############################
+
+def obsolete(ctx):
+    """is the changeset obsolete by other"""
+    if ctx.node()is None:
+        return False
+    return bool(ctx._repo.obsoletedby(ctx.node()))
+
+context.changectx.obsolete = obsolete
+
+ohidden = context.changectx.hidden
+def hidden(ctx):
+    # hack to fill hiddenrevs
+    # compute hidden (XXX should move elsewhere)
+    if not getattr(ctx._repo.changelog, 'hiddeninit', False):
+        basicquery = 'obsolete() - (ancestors(not obsolete() or . or bookmark()))'
+        for rev in scmutil.revrange(ctx._repo, [basicquery]):
+            ctx._repo.changelog.hiddenrevs.add(rev)
+        ctx._repo.changelog.hiddeninit = True
+
+    return ohidden(ctx)
+context.changectx.hidden = hidden
+
+# revset
+#############################
+
+def revsetobsolete(repo, subset, x):
+    args = revset.getargs(x, 0, 0, 'publicheads takes no arguments')
+    return [r for r in subset if repo[r].obsolete()] # XXX slow
+
+def extsetup(ui):
+    revset.symbols["obsolete"] = revsetobsolete
+
+    def filterobsoleteout(orig, repo, remote, *args,**kwargs):
+        common, heads = orig(repo, remote, *args, **kwargs)
+
+        # filter obsolete
+        heads = set(map(repo.changelog.rev, heads))
+        obsoletes = set()
+        for obj in repo._obsobjrels:
+            try:
+                obsoletes.add(repo.changelog.rev(obj))
+            except error.LookupError:
+                pass # we don't have this node locally
+
+        outgoing = set(repo.changelog.ancestors(*heads))
+        outgoing.update(heads)
+
+        selected = outgoing - obsoletes
+        heads = sorted(map(repo.changelog.node, selected))
+
+        return common, heads
+
+    extensions.wrapfunction(discovery, 'findcommonoutgoing', filterobsoleteout)
+    
+    try:
+        rebase = extensions.find('rebase')
+        if rebase:
+            extensions.wrapfunction(rebase, 'concludenode', concludenode)
+    except KeyError:
+        pass # rebase not found
+
+# Pushkey mechanism for mutable
+#########################################
+
+def pushobsolete(repo, key, old, relations):
+    assert key == "relations"
+    w = repo.wlock()
+    try:
+        for sub, objs in relations.iteritems():
+            for obj in objs:
+                repo.addobsolete(sub, obj)
+    finally:
+        w.release()
+
+def listobsolete(repo):
+    return {'relations': repo._obssubrels}
+
+pushkey.register('obsolete', pushobsolete, listobsolete)
+
+# New commands
+#############################
+
+
+def cmddebugobsolete(ui, repo, subject, object):
+    """Add an obsolete relation between a too node
+    
+    The subject is expected to be a newer version of the object"""
+    sub = repo[subject]
+    obj = repo[object]
+    repo.addobsolete(sub.node(), obj.node())
+    return 0
+
+cmdtable = {'debugobsolete': (cmddebugobsolete, [], '<subject> <object>')}
+
+def reposetup(ui, repo):
+
+    if not repo.local():
+        return
+
+    opull = repo.pull
+    opush = repo.push
+
+    class obsoletingrepo(repo.__class__):
+
+
+        ### Hidden revision support
+        @util.propertycache
+        def hiddenrevs(self):
+            # It's a property because It simpler that to handle the __init__
+            revs = set()
+            return revs
+
+        ### obsolete storage
+        @util.propertycache
+        def _obsobjrels(self):
+            """{<old-node> -> set(<new-node>)}
+
+            also compute hidden revision"""
+            #reverse sub -> objs mapping
+            objrels = {}
+            for sub, objs in self._obssubrels.iteritems():
+                for obj in objs:
+                    objrels.setdefault(obj, set()).add(sub)
+            return objrels
+
+        @util.propertycache
+        def _obssubrels(self):
+            """{<new-node> -> set(<old-node>)}"""
+            return self._readobsrels()
+
+
+        ### Disk IO
+        def _readobsrels(self):
+            """Write obsolete relation on disk"""
+            # XXX handle lock
+            rels = {}
+            try:
+                f = self.opener('obsolete-relations')
+                try:
+                    for line in f:
+                        subhex, objhex = line.split()
+                        rels.setdefault(bin(subhex), set()).add(bin(objhex))
+                finally:
+                    f.close()
+            except IOError:
+                pass
+            return rels
+
+        def _writeobsrels(self):
+            """Write obsolete relation on disk"""
+            # XXX handle lock
+            f = self.opener('obsolete-relations', 'w', atomictemp=True)
+            try:
+                for sub, objs in self._obssubrels.iteritems():
+                    for obj in objs:
+                        f.write('%s %s\n' % (hex(sub), hex(obj)))
+                f.rename()
+            finally:
+                f.close()
+
+        ### local clone support
+
+        def cancopy(self):
+            return not bool(self._obsobjrels) # you can't copy if there is obsolete
+
+        ### pull // push support
+
+        def pull(self, remote, *args, **kwargs):
+            obskey = remote.listkeys('obsolete')
+            obsrels = obskey.get('relations', {})
+            result = opull(remote, *args, **kwargs)
+            for sub, objs in obsrels.iteritems():
+                for obj in objs:
+                    self.addobsolete(sub, obj)
+            return result
+
+        def push(self, remote, *args, **opts):
+            obskey = remote.listkeys('obsolete')
+            obssupport = 'relations' in obskey
+            result = opush(remote, *args, **opts)
+            if obssupport:
+                remote.pushkey('obsolete', 'relations', {}, self._obssubrels)
+            return result
+
+
+        ### Public method
+        def obsoletedby(self, node):
+            """return the set of node that make <node> obsolete (obj)"""
+            return self._obsobjrels.get(node, set())
+
+        def obsolete(self, node):
+            """return the set of node that <node> make obsolete (sub)"""
+            return self._obssubrels.get(node, set())
+
+        def addobsolete(self, sub, obj):
+            """Add a relation marking that node <sub> is a new version of <obj>"""
+            self._obssubrels.setdefault(sub, set()).add(obj)
+            self._obsobjrels.setdefault(obj, set()).add(sub)
+            try:
+                self.changelog.hiddenrevs.add(repo[obj].rev())
+            except error.RepoLookupError:
+                pass #unknow revision (but keep propagating the data
+            self._writeobsrels()
+
+    repo.__class__ = obsoletingrepo
+
+
+### Other Extension compat
+############################
+
+def concludenode(orig, repo, rev, *args, **kwargs):
+    newrev = orig(repo, rev, *args, **kwargs)
+    oldnode = repo[rev].node()
+    newnode = repo[newrev].node()
+    repo.addobsolete(newnode, oldnode)
+    return newrev
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/states.py	Thu Sep 08 17:15:20 2011 +0200
@@ -0,0 +1,452 @@
+# 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.
+'''
+import os
+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
+from mercurial import pushkey
+from mercurial.lock import release
+
+
+_NOSHARE=2
+_MUTABLE=1
+
+class state(object):
+
+    def __init__(self, name, properties=0, next=None):
+        self.name = name
+        self.properties = properties
+        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.properties, other.properties)
+
+    @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.cachefunc
+def laststatewithout(prop):
+    for state in STATES:
+        if not state.properties & prop:
+            candidate = state
+        else:
+            return candidate
+
+# util function
+#############################
+def noderange(repo, revsets):
+    return map(repo.changelog.node,
+               scmutil.revrange(repo, revsets))
+
+# Patch changectx
+#############################
+
+def state(ctx):
+    if ctx.node()is None:
+        return STATES[-1]
+    return ctx._repo.nodestate(ctx.node())
+context.changectx.state = state
+
+# improve template
+#############################
+
+def showstate(ctx, **args):
+    return ctx.state()
+
+
+# New commands
+#############################
+
+
+def cmdstates(ui, repo, *states, **opt):
+    """view and modify activated states.
+
+    With no argument, list activated state.
+
+    With argument, activate the state in argument.
+
+    With argument plus the --off switch, deactivate the state in argument.
+
+    note: published state are alway activated."""
+
+    if not states:
+        for st in sorted(repo._enabledstates):
+            ui.write('%s\n' % st)
+    else:
+        off = opt.get('off', False)
+        for state_name in states:
+            for st in STATES:
+                if st.name == state_name:
+                    break
+            else:
+                ui.write_err(_('no state named %s\n') % state_name)
+                return 1
+            if off and st in repo._enabledstates:
+                repo._enabledstates.remove(st)
+            else:
+                repo._enabledstates.add(st)
+        repo._writeenabledstates()
+    return 0
+
+cmdtable = {'states': (cmdstates, [ ('', 'off', False, _('desactivate the state') )], '<state>')}
+#cmdtable = {'states': (cmdstates, [], '<state>')}
+
+def makecmd(state):
+    def cmdmoveheads(ui, repo, *changesets):
+        """set a revision in %s state""" % state
+        revs = scmutil.revrange(repo, changesets)
+        repo.setstate(state, [repo.changelog.node(rev) for rev in revs])
+        return 0
+    return cmdmoveheads
+
+for state in STATES:
+    if state.trackheads:
+        cmdmoveheads = makecmd(state)
+        cmdtable[state.name] = (cmdmoveheads, [], '<revset>')
+
+# Pushkey mechanism for mutable
+#########################################
+
+def pushimmutableheads(repo, key, old, new):
+    st = ST0
+    w = repo.wlock()
+    try:
+        #print 'pushing', key
+        repo.setstate(ST0, [node.bin(key)])
+    finally:
+        w.release()
+
+def listimmutableheads(repo):
+    return dict.fromkeys(map(node.hex, repo.stateheads(ST0)), '1')
+
+pushkey.register('immutableheads', pushimmutableheads, listimmutableheads)
+
+
+
+
+
+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):
+        st = laststatewithout(_NOSHARE)
+        h = repo.stateheads(st)
+        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
+
+    ocancopy =repo.cancopy
+    opull = repo.pull
+    opush = repo.push
+    o_tag = repo._tag
+    orollback = repo.rollback
+    o_writejournal = repo._writejournal
+    class statefulrepo(repo.__class__):
+
+        def nodestate(self, node):
+            rev = self.changelog.rev(node)
+
+            for state in STATES:
+                # XXX avoid for untracked heads
+                if state.next is not None:
+                    ancestors = map(self.changelog.rev, self.stateheads(state))
+                    ancestors.extend(self.changelog.ancestors(*ancestors))
+                    if rev in ancestors:
+                        break
+            return state
+
+
+
+        def stateheads(self, state):
+            # look for a relevant state
+            while state.trackheads and state.next not in self._enabledstates:
+                state = state.next
+            # last state have no cached head.
+            if state.trackheads:
+                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, undo=False):
+            statesheads = {}
+            for state in STATES:
+                if state.trackheads:
+                    filemask = 'states/%s-heads'
+                    filename = filemask % 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):
+            """change state of targets changeset and it's ancestors.
+
+            Simplify the list of head."""
+            assert not isinstance(nodes, basestring)
+            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()
+            st = laststatewithout(_NOSHARE)
+            candidates = set(map(self.changelog.rev, candidates))
+            heads = set(map(self.changelog.rev, self.stateheads(st)))
+            shareable = set(self.changelog.ancestors(*heads))
+            shareable.update(heads)
+            selected = candidates & shareable
+            unselected = candidates - shareable
+            for rev in unselected:
+                for revh in heads:
+                    if self.changelog.descendant(revh, rev):
+                        selected.add(revh)
+            return sorted(map(self.changelog.node, selected))
+
+        ### enable // disable logic
+
+        @util.propertycache
+        def _enabledstates(self):
+            return self._readenabledstates()
+
+        def _readenabledstates(self):
+            states = set()
+            states.add(ST0)
+            mapping = dict([(st.name, st) for st in STATES])
+            try:
+                f = self.opener('states/Enabled')
+                for line in f:
+                    st =  mapping.get(line.strip())
+                    if st is not None:
+                        states.add(st)
+            finally:
+                return states
+
+        def _writeenabledstates(self):
+            f = self.opener('states/Enabled', 'w', atomictemp=True)
+            try:
+                for st in self._enabledstates:
+                    f.write(st.name + '\n')
+                f.rename()
+            finally:
+                f.close()
+
+        ### local clone support
+
+        def cancopy(self):
+            st = laststatewithout(_NOSHARE)
+            return ocancopy() and (self.stateheads(st) == self.heads())
+
+        ### pull // push support
+
+        def pull(self, remote, *args, **kwargs):
+            result = opull(remote, *args, **kwargs)
+            remoteheads = self._pullimmutableheads(remote)
+            #print [node.short(h) for h in remoteheads]
+            self.setstate(ST0, remoteheads)
+            return result
+
+        def push(self, remote, *args, **opts):
+            result = opush(remote, *args, **opts)
+            remoteheads = self._pullimmutableheads(remote)
+            self.setstate(ST0, remoteheads)
+            if remoteheads != self.stateheads(ST0):
+                #print 'stuff to push'
+                #print 'remote', [node.short(h) for h in remoteheads]
+                #print 'local',  [node.short(h) for h in self._statesheads[ST0]]
+                self._pushimmutableheads(remote, remoteheads)
+            return result
+
+        def _pushimmutableheads(self, remote, remoteheads):
+            missing = set(self.stateheads(ST0)) - set(remoteheads)
+            for h in missing:
+                #print 'missing', node.short(h)
+                remote.pushkey('immutableheads', node.hex(h), '', '1')
+
+
+        def _pullimmutableheads(self, remote):
+            self.ui.debug('checking for immutableheadshg on server')
+            if 'immutableheads' not in remote.listkeys('namespaces'):
+                self.ui.debug('immutableheads not enabled on the remote server, '
+                              'marking everything as frozen')
+                remote = remote.heads()
+            else:
+                self.ui.debug('server has immutableheads enabled, merging lists')
+                remote = map(node.bin, remote.listkeys('immutableheads'))
+            return remote
+
+        ### Tag support
+
+        def _tag(self, names, node, *args, **kwargs):
+            tagnode = o_tag(names, node, *args, **kwargs)
+            if tagnode is not None: # do nothing for local one
+                self.setstate(ST0, [node, tagnode])
+            return tagnode
+
+        ### rollback support
+
+        def _writejournal(self, desc):
+            entries = list(o_writejournal(desc))
+            for state in STATES:
+                if state.trackheads:
+                    filename = 'states/%s-heads' % state.name
+                    filepath = self.join(filename)
+                    if  os.path.exists(filepath):
+                        journalname = 'states/journal.%s-heads' % state.name
+                        journalpath = self.join(journalname)
+                        util.copyfile(filepath, journalpath)
+                        entries.append(journalpath)
+            return tuple(entries)
+
+        def rollback(self, dryrun=False):
+            wlock = lock = None
+            try:
+                wlock = self.wlock()
+                lock = self.lock()
+                ret = orollback(dryrun)
+                if not (ret or dryrun): #rollback did not failed
+                    for state in STATES:
+                        if state.trackheads:
+                            src  = self.join('states/undo.%s-heads') % state.name
+                            dest = self.join('states/%s-heads') % state.name
+                            if os.path.exists(src):
+                                util.rename(src, dest)
+                            elif os.path.exists(dest): #unlink in any case
+                                os.unlink(dest)
+                    self.__dict__.pop('_statesheads', None)
+                return ret
+            finally:
+                release(lock, wlock)
+
+    repo.__class__ = statefulrepo
+
--- a/obsolete.py	Thu Sep 08 17:11:31 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,237 +0,0 @@
-# obsolete.py - introduce the obsolete concept in mercurial.
-#
-# Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
-#                Logilab SA        <contact@logilab.fr>
-#
-# 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 import util
-from mercurial import context
-from mercurial import revset
-from mercurial import scmutil
-from mercurial import extensions
-from mercurial import pushkey
-from mercurial import discovery
-from mercurial import error
-from mercurial.node import hex, bin
-
-# Patch changectx
-#############################
-
-def obsolete(ctx):
-    """is the changeset obsolete by other"""
-    if ctx.node()is None:
-        return False
-    return bool(ctx._repo.obsoletedby(ctx.node()))
-
-context.changectx.obsolete = obsolete
-
-ohidden = context.changectx.hidden
-def hidden(ctx):
-    # hack to fill hiddenrevs
-    # compute hidden (XXX should move elsewhere)
-    if not getattr(ctx._repo.changelog, 'hiddeninit', False):
-        basicquery = 'obsolete() - (ancestors(not obsolete() or . or bookmark()))'
-        for rev in scmutil.revrange(ctx._repo, [basicquery]):
-            ctx._repo.changelog.hiddenrevs.add(rev)
-        ctx._repo.changelog.hiddeninit = True
-
-    return ohidden(ctx)
-context.changectx.hidden = hidden
-
-# revset
-#############################
-
-def revsetobsolete(repo, subset, x):
-    args = revset.getargs(x, 0, 0, 'publicheads takes no arguments')
-    return [r for r in subset if repo[r].obsolete()] # XXX slow
-
-def extsetup(ui):
-    revset.symbols["obsolete"] = revsetobsolete
-
-    def filterobsoleteout(orig, repo, remote, *args,**kwargs):
-        common, heads = orig(repo, remote, *args, **kwargs)
-
-        # filter obsolete
-        heads = set(map(repo.changelog.rev, heads))
-        obsoletes = set()
-        for obj in repo._obsobjrels:
-            try:
-                obsoletes.add(repo.changelog.rev(obj))
-            except error.LookupError:
-                pass # we don't have this node locally
-
-        outgoing = set(repo.changelog.ancestors(*heads))
-        outgoing.update(heads)
-
-        selected = outgoing - obsoletes
-        heads = sorted(map(repo.changelog.node, selected))
-
-        return common, heads
-
-    extensions.wrapfunction(discovery, 'findcommonoutgoing', filterobsoleteout)
-    
-    try:
-        rebase = extensions.find('rebase')
-        if rebase:
-            extensions.wrapfunction(rebase, 'concludenode', concludenode)
-    except KeyError:
-        pass # rebase not found
-
-# Pushkey mechanism for mutable
-#########################################
-
-def pushobsolete(repo, key, old, relations):
-    assert key == "relations"
-    w = repo.wlock()
-    try:
-        for sub, objs in relations.iteritems():
-            for obj in objs:
-                repo.addobsolete(sub, obj)
-    finally:
-        w.release()
-
-def listobsolete(repo):
-    return {'relations': repo._obssubrels}
-
-pushkey.register('obsolete', pushobsolete, listobsolete)
-
-# New commands
-#############################
-
-
-def cmddebugobsolete(ui, repo, subject, object):
-    """Add an obsolete relation between a too node
-    
-    The subject is expected to be a newer version of the object"""
-    sub = repo[subject]
-    obj = repo[object]
-    repo.addobsolete(sub.node(), obj.node())
-    return 0
-
-cmdtable = {'debugobsolete': (cmddebugobsolete, [], '<subject> <object>')}
-
-def reposetup(ui, repo):
-
-    if not repo.local():
-        return
-
-    opull = repo.pull
-    opush = repo.push
-
-    class obsoletingrepo(repo.__class__):
-
-
-        ### Hidden revision support
-        @util.propertycache
-        def hiddenrevs(self):
-            # It's a property because It simpler that to handle the __init__
-            revs = set()
-            return revs
-
-        ### obsolete storage
-        @util.propertycache
-        def _obsobjrels(self):
-            """{<old-node> -> set(<new-node>)}
-
-            also compute hidden revision"""
-            #reverse sub -> objs mapping
-            objrels = {}
-            for sub, objs in self._obssubrels.iteritems():
-                for obj in objs:
-                    objrels.setdefault(obj, set()).add(sub)
-            return objrels
-
-        @util.propertycache
-        def _obssubrels(self):
-            """{<new-node> -> set(<old-node>)}"""
-            return self._readobsrels()
-
-
-        ### Disk IO
-        def _readobsrels(self):
-            """Write obsolete relation on disk"""
-            # XXX handle lock
-            rels = {}
-            try:
-                f = self.opener('obsolete-relations')
-                try:
-                    for line in f:
-                        subhex, objhex = line.split()
-                        rels.setdefault(bin(subhex), set()).add(bin(objhex))
-                finally:
-                    f.close()
-            except IOError:
-                pass
-            return rels
-
-        def _writeobsrels(self):
-            """Write obsolete relation on disk"""
-            # XXX handle lock
-            f = self.opener('obsolete-relations', 'w', atomictemp=True)
-            try:
-                for sub, objs in self._obssubrels.iteritems():
-                    for obj in objs:
-                        f.write('%s %s\n' % (hex(sub), hex(obj)))
-                f.rename()
-            finally:
-                f.close()
-
-        ### local clone support
-
-        def cancopy(self):
-            return not bool(self._obsobjrels) # you can't copy if there is obsolete
-
-        ### pull // push support
-
-        def pull(self, remote, *args, **kwargs):
-            obskey = remote.listkeys('obsolete')
-            obsrels = obskey.get('relations', {})
-            result = opull(remote, *args, **kwargs)
-            for sub, objs in obsrels.iteritems():
-                for obj in objs:
-                    self.addobsolete(sub, obj)
-            return result
-
-        def push(self, remote, *args, **opts):
-            obskey = remote.listkeys('obsolete')
-            obssupport = 'relations' in obskey
-            result = opush(remote, *args, **opts)
-            if obssupport:
-                remote.pushkey('obsolete', 'relations', {}, self._obssubrels)
-            return result
-
-
-        ### Public method
-        def obsoletedby(self, node):
-            """return the set of node that make <node> obsolete (obj)"""
-            return self._obsobjrels.get(node, set())
-
-        def obsolete(self, node):
-            """return the set of node that <node> make obsolete (sub)"""
-            return self._obssubrels.get(node, set())
-
-        def addobsolete(self, sub, obj):
-            """Add a relation marking that node <sub> is a new version of <obj>"""
-            self._obssubrels.setdefault(sub, set()).add(obj)
-            self._obsobjrels.setdefault(obj, set()).add(sub)
-            try:
-                self.changelog.hiddenrevs.add(repo[obj].rev())
-            except error.RepoLookupError:
-                pass #unknow revision (but keep propagating the data
-            self._writeobsrels()
-
-    repo.__class__ = obsoletingrepo
-
-
-### Other Extension compat
-############################
-
-def concludenode(orig, repo, rev, *args, **kwargs):
-    newrev = orig(repo, rev, *args, **kwargs)
-    oldnode = repo[rev].node()
-    newnode = repo[newrev].node()
-    repo.addobsolete(newnode, oldnode)
-    return newrev
-
--- a/states.py	Thu Sep 08 17:11:31 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,452 +0,0 @@
-# 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.
-'''
-import os
-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
-from mercurial import pushkey
-from mercurial.lock import release
-
-
-_NOSHARE=2
-_MUTABLE=1
-
-class state(object):
-
-    def __init__(self, name, properties=0, next=None):
-        self.name = name
-        self.properties = properties
-        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.properties, other.properties)
-
-    @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.cachefunc
-def laststatewithout(prop):
-    for state in STATES:
-        if not state.properties & prop:
-            candidate = state
-        else:
-            return candidate
-
-# util function
-#############################
-def noderange(repo, revsets):
-    return map(repo.changelog.node,
-               scmutil.revrange(repo, revsets))
-
-# Patch changectx
-#############################
-
-def state(ctx):
-    if ctx.node()is None:
-        return STATES[-1]
-    return ctx._repo.nodestate(ctx.node())
-context.changectx.state = state
-
-# improve template
-#############################
-
-def showstate(ctx, **args):
-    return ctx.state()
-
-
-# New commands
-#############################
-
-
-def cmdstates(ui, repo, *states, **opt):
-    """view and modify activated states.
-
-    With no argument, list activated state.
-
-    With argument, activate the state in argument.
-
-    With argument plus the --off switch, deactivate the state in argument.
-
-    note: published state are alway activated."""
-
-    if not states:
-        for st in sorted(repo._enabledstates):
-            ui.write('%s\n' % st)
-    else:
-        off = opt.get('off', False)
-        for state_name in states:
-            for st in STATES:
-                if st.name == state_name:
-                    break
-            else:
-                ui.write_err(_('no state named %s\n') % state_name)
-                return 1
-            if off and st in repo._enabledstates:
-                repo._enabledstates.remove(st)
-            else:
-                repo._enabledstates.add(st)
-        repo._writeenabledstates()
-    return 0
-
-cmdtable = {'states': (cmdstates, [ ('', 'off', False, _('desactivate the state') )], '<state>')}
-#cmdtable = {'states': (cmdstates, [], '<state>')}
-
-def makecmd(state):
-    def cmdmoveheads(ui, repo, *changesets):
-        """set a revision in %s state""" % state
-        revs = scmutil.revrange(repo, changesets)
-        repo.setstate(state, [repo.changelog.node(rev) for rev in revs])
-        return 0
-    return cmdmoveheads
-
-for state in STATES:
-    if state.trackheads:
-        cmdmoveheads = makecmd(state)
-        cmdtable[state.name] = (cmdmoveheads, [], '<revset>')
-
-# Pushkey mechanism for mutable
-#########################################
-
-def pushimmutableheads(repo, key, old, new):
-    st = ST0
-    w = repo.wlock()
-    try:
-        #print 'pushing', key
-        repo.setstate(ST0, [node.bin(key)])
-    finally:
-        w.release()
-
-def listimmutableheads(repo):
-    return dict.fromkeys(map(node.hex, repo.stateheads(ST0)), '1')
-
-pushkey.register('immutableheads', pushimmutableheads, listimmutableheads)
-
-
-
-
-
-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):
-        st = laststatewithout(_NOSHARE)
-        h = repo.stateheads(st)
-        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
-
-    ocancopy =repo.cancopy
-    opull = repo.pull
-    opush = repo.push
-    o_tag = repo._tag
-    orollback = repo.rollback
-    o_writejournal = repo._writejournal
-    class statefulrepo(repo.__class__):
-
-        def nodestate(self, node):
-            rev = self.changelog.rev(node)
-
-            for state in STATES:
-                # XXX avoid for untracked heads
-                if state.next is not None:
-                    ancestors = map(self.changelog.rev, self.stateheads(state))
-                    ancestors.extend(self.changelog.ancestors(*ancestors))
-                    if rev in ancestors:
-                        break
-            return state
-
-
-
-        def stateheads(self, state):
-            # look for a relevant state
-            while state.trackheads and state.next not in self._enabledstates:
-                state = state.next
-            # last state have no cached head.
-            if state.trackheads:
-                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, undo=False):
-            statesheads = {}
-            for state in STATES:
-                if state.trackheads:
-                    filemask = 'states/%s-heads'
-                    filename = filemask % 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):
-            """change state of targets changeset and it's ancestors.
-
-            Simplify the list of head."""
-            assert not isinstance(nodes, basestring)
-            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()
-            st = laststatewithout(_NOSHARE)
-            candidates = set(map(self.changelog.rev, candidates))
-            heads = set(map(self.changelog.rev, self.stateheads(st)))
-            shareable = set(self.changelog.ancestors(*heads))
-            shareable.update(heads)
-            selected = candidates & shareable
-            unselected = candidates - shareable
-            for rev in unselected:
-                for revh in heads:
-                    if self.changelog.descendant(revh, rev):
-                        selected.add(revh)
-            return sorted(map(self.changelog.node, selected))
-
-        ### enable // disable logic
-
-        @util.propertycache
-        def _enabledstates(self):
-            return self._readenabledstates()
-
-        def _readenabledstates(self):
-            states = set()
-            states.add(ST0)
-            mapping = dict([(st.name, st) for st in STATES])
-            try:
-                f = self.opener('states/Enabled')
-                for line in f:
-                    st =  mapping.get(line.strip())
-                    if st is not None:
-                        states.add(st)
-            finally:
-                return states
-
-        def _writeenabledstates(self):
-            f = self.opener('states/Enabled', 'w', atomictemp=True)
-            try:
-                for st in self._enabledstates:
-                    f.write(st.name + '\n')
-                f.rename()
-            finally:
-                f.close()
-
-        ### local clone support
-
-        def cancopy(self):
-            st = laststatewithout(_NOSHARE)
-            return ocancopy() and (self.stateheads(st) == self.heads())
-
-        ### pull // push support
-
-        def pull(self, remote, *args, **kwargs):
-            result = opull(remote, *args, **kwargs)
-            remoteheads = self._pullimmutableheads(remote)
-            #print [node.short(h) for h in remoteheads]
-            self.setstate(ST0, remoteheads)
-            return result
-
-        def push(self, remote, *args, **opts):
-            result = opush(remote, *args, **opts)
-            remoteheads = self._pullimmutableheads(remote)
-            self.setstate(ST0, remoteheads)
-            if remoteheads != self.stateheads(ST0):
-                #print 'stuff to push'
-                #print 'remote', [node.short(h) for h in remoteheads]
-                #print 'local',  [node.short(h) for h in self._statesheads[ST0]]
-                self._pushimmutableheads(remote, remoteheads)
-            return result
-
-        def _pushimmutableheads(self, remote, remoteheads):
-            missing = set(self.stateheads(ST0)) - set(remoteheads)
-            for h in missing:
-                #print 'missing', node.short(h)
-                remote.pushkey('immutableheads', node.hex(h), '', '1')
-
-
-        def _pullimmutableheads(self, remote):
-            self.ui.debug('checking for immutableheadshg on server')
-            if 'immutableheads' not in remote.listkeys('namespaces'):
-                self.ui.debug('immutableheads not enabled on the remote server, '
-                              'marking everything as frozen')
-                remote = remote.heads()
-            else:
-                self.ui.debug('server has immutableheads enabled, merging lists')
-                remote = map(node.bin, remote.listkeys('immutableheads'))
-            return remote
-
-        ### Tag support
-
-        def _tag(self, names, node, *args, **kwargs):
-            tagnode = o_tag(names, node, *args, **kwargs)
-            if tagnode is not None: # do nothing for local one
-                self.setstate(ST0, [node, tagnode])
-            return tagnode
-
-        ### rollback support
-
-        def _writejournal(self, desc):
-            entries = list(o_writejournal(desc))
-            for state in STATES:
-                if state.trackheads:
-                    filename = 'states/%s-heads' % state.name
-                    filepath = self.join(filename)
-                    if  os.path.exists(filepath):
-                        journalname = 'states/journal.%s-heads' % state.name
-                        journalpath = self.join(journalname)
-                        util.copyfile(filepath, journalpath)
-                        entries.append(journalpath)
-            return tuple(entries)
-
-        def rollback(self, dryrun=False):
-            wlock = lock = None
-            try:
-                wlock = self.wlock()
-                lock = self.lock()
-                ret = orollback(dryrun)
-                if not (ret or dryrun): #rollback did not failed
-                    for state in STATES:
-                        if state.trackheads:
-                            src  = self.join('states/undo.%s-heads') % state.name
-                            dest = self.join('states/%s-heads') % state.name
-                            if os.path.exists(src):
-                                util.rename(src, dest)
-                            elif os.path.exists(dest): #unlink in any case
-                                os.unlink(dest)
-                    self.__dict__.pop('_statesheads', None)
-                return ret
-            finally:
-                release(lock, wlock)
-
-    repo.__class__ = statefulrepo
-
--- a/tests/test-draft.t	Thu Sep 08 17:11:31 2011 +0200
+++ b/tests/test-draft.t	Thu Sep 08 17:15:20 2011 +0200
@@ -4,7 +4,7 @@
   > allow_push = *
   > [extensions]
   > EOF
-  $ echo "states=$(echo $(dirname $TESTDIR))/states.py" >> $HGRCPATH
+  $ echo "states=$(echo $(dirname $TESTDIR))/hgext/states.py" >> $HGRCPATH
 
   $ hg init local
   $ hg init remote1
--- a/tests/test-obsolete.t	Thu Sep 08 17:11:31 2011 +0200
+++ b/tests/test-obsolete.t	Thu Sep 08 17:15:20 2011 +0200
@@ -4,7 +4,7 @@
   > allow_push = *
   > [extensions]
   > EOF
-  $ echo "obsolete=$(echo $(dirname $TESTDIR))/obsolete.py" >> $HGRCPATH
+  $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH
   $ mkcommit() {
   >    echo "$1" > "$1"
   >    hg add "$1"
--- a/tests/test-ready.t	Thu Sep 08 17:11:31 2011 +0200
+++ b/tests/test-ready.t	Thu Sep 08 17:15:20 2011 +0200
@@ -4,7 +4,7 @@
   > [extensions]
   > graphlog=
   > EOF
-  $ echo "states=$(echo $(dirname $TESTDIR))/states.py" >> $HGRCPATH
+  $ echo "states=$(echo $(dirname $TESTDIR))/hgext/states.py" >> $HGRCPATH
 
   $ mkcommit() {
   >    echo "$1" > "$1"
--- a/tests/test-states.t	Thu Sep 08 17:11:31 2011 +0200
+++ b/tests/test-states.t	Thu Sep 08 17:15:20 2011 +0200
@@ -5,7 +5,7 @@
   > allow_push = *
   > [extensions]
   > EOF
-  $ echo "states=$(echo $(dirname $TESTDIR))/states.py" >> $HGRCPATH
+  $ echo "states=$(echo $(dirname $TESTDIR))/hgext/states.py" >> $HGRCPATH
 
   $ hg init local
   $ hg init other