changeset 60:14a4499d2cd6

small refactoring and big doc update. Sorry for the big commit crecord one so much diff seems to confuse my powerbook to death :-/
author Pierre-Yves David <pierre-yves.david@ens-lyon.org>
date Mon, 12 Sep 2011 14:05:32 +0200
parents 02fba620d139
children 0dfe459c7b1c
files hgext/states.py
diffstat 1 files changed, 200 insertions(+), 35 deletions(-) [+]
line wrap: on
line diff
--- a/hgext/states.py	Fri Sep 09 15:56:50 2011 +0200
+++ b/hgext/states.py	Mon Sep 12 14:05:32 2011 +0200
@@ -176,7 +176,7 @@
 .. note:
 
     As Repository without any specific state have all their changeset
-    ``published``, Pushing to such repo will ``publish`` all common changeset. 
+    ``published``, Pushing to such repo will ``publish`` all common changeset.
 
 2. Tagged changeset get automatically Published. The tagging changeset is
 tagged too... This doesn't apply to local tag.
@@ -215,6 +215,7 @@
 :outgoing:  Exclude ``draft`` changeset of local repository.
 :pull:      As :hg:`in`  + change state of local changeset according to remote side.
 :push:      As :hg:`out` + sync state of common changeset on both side
+:rollback:  rollback restore states heads as before the last transaction (see bookmark)
 
 Template
 ........
@@ -234,17 +235,85 @@
 
     - add ``<state>heads()`` directives to that return the currently in used heads
 
-    - add ``<state>()`` directives that 
+    - add ``<state>()`` directives that match all node in a state.
+
+Implementation
+==============
+
+State definition
+................
+
+Conceptually:
+
+The set of node in the states are defined by the set of the state heads. This allow
+easy storage, exchange and consistency.
+
+.. note: A cache of the complete set of node that belong to a states will
+         probably be need for performance.
+
+Code wise:
 
-implementation
-=========================
+There is a ``state`` class that hold the state property and several useful
+logic (name, revset entry etc).
+
+All defined states are accessible thought the STATES tuple at the ROOT of the
+module. Or the STATESMAP dictionary that allow to fetch a state from it's
+name.
+
+You can get and edit the list head node that define a state with two methods on
+repo.
+
+:stateheads(<state>):        Returns the list of heads node that define a states
+:setstate(<state>, [nodes]): Move states boundary forward to include the given
+                             nodes in the given states.
+
+Those methods handle ``node`` and not rev as it seems more resilient to me that
+rev in a mutable world. Maybe it' would make more sens to have ``node`` store
+on disk but revision in the code.
+
+Storage
+.......
 
-To be completed
+States related data are stored in the ``.hg/states/`` directory.
+
+The ``.hg/states/Enabled`` file list the states enabled in this
+repository. This data is *not* stored in the .hg/hgrc because the .hg/hgrc
+might be ignored for trust reason. As missing und with states can be pretty
+annoying. (publishing unfinalized changeset, pulling draft one etc) we don't
+want trust issue to interfer with enabled states information.
+
+``.hg/states/<state>-heads`` file list the nodes that define a states.
+
+_NOSHARE filtering
+..................
+
+Any changeset in a state with a _NOSHARE property will be exclude from pull,
+push, clone, incoming, outgoing and bundle. It is done through three mechanism:
+
+1. Wrapping the findcommonincoming and findcommonoutgoing code with (not very
+   efficient) logic that recompute the exchanged heads.
 
-Why to you store activate state outside ``.hg/hgrc``? :
+2. Altering ``heads`` wireprotocol command to return sharead heads.
+
+3. Disabling hardlink cloning when there is _NOSHARE changeset available.
+
+Internal plumbery
+-----------------
+
+sum up of what we do:
+
+* state are object
 
-    ``.hg/hgrc`` might be ignored for trust reason. We don't want the trust
-    issue to interfer with enabled state information.
+* repo.__class__ is extended
+
+* discovery is wrapped up
+
+* wire protocol is patched
+
+* transaction and rollback mechanism are wrapped up.
+
+* XXX we write new version of the boundard whenever something happen. We need a
+  smarter and faster way to do this.
 
 
 '''
@@ -268,10 +337,23 @@
 from mercurial.lock import release
 
 
+# states property constante
 _NOSHARE=2
 _MUTABLE=1
 
 class state(object):
+    """State of changeset
+
+    An utility object that handle several behaviour and containts useful code
+
+    A state is defined by:
+        - It's name
+        - It's property (defined right above)
+
+        - It's next state.
+
+    XXX maybe we could stick description of the state semantic here.
+    """
 
     def __init__(self, name, properties=0, next=None):
         self.name = name
@@ -289,10 +371,16 @@
     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"""
+        We don't need to track heads for the last state as this is repo heads"""
         return self.next is not None
 
     def __cmp__(self, other):
+        """Use property to compare states.
+
+        This is a naiv approach that assume the  the next state are strictly
+        more property than the one before
+        # assert min(self, other).properties = self.properties & other.properties
+        """
         return cmp(self.properties, other.properties)
 
     @util.propertycache
@@ -319,15 +407,22 @@
         else:
             return 'heads'
 
+# Actual state definition
+
 ST2 = state('draft', _NOSHARE | _MUTABLE)
 ST1 = state('ready', _MUTABLE, next=ST2)
 ST0 = state('published', next=ST1)
 
+# all available state
 STATES = (ST0, ST1, ST2)
+# all available state by name
 STATESMAP =dict([(st.name, st) for st in STATES])
 
 @util.cachefunc
 def laststatewithout(prop):
+    """Find the states with the most property but <prop>
+
+    (This function is necessary because the whole state stuff are abstracted)"""
     for state in STATES:
         if not state.properties & prop:
             candidate = state
@@ -337,6 +432,7 @@
 # util function
 #############################
 def noderange(repo, revsets):
+    """The same as revrange but return node"""
     return map(repo.changelog.node,
                scmutil.revrange(repo, revsets))
 
@@ -344,6 +440,7 @@
 #############################
 
 def state(ctx):
+    """return the state objet associated to the context"""
     if ctx.node()is None:
         return STATES[-1]
     return ctx._repo.nodestate(ctx.node())
@@ -353,6 +450,7 @@
 #############################
 
 def showstate(ctx, **args):
+    """Show the name of the state associated with the context"""
     return ctx.state()
 
 
@@ -391,11 +489,14 @@
     return 0
 
 cmdtable = {'states': (cmdstates, [ ('', 'off', False, _('desactivate the state') )], '<state>')}
-#cmdtable = {'states': (cmdstates, [], '<state>')}
 
+# automatic generation of command that set state
 def makecmd(state):
     def cmdmoveheads(ui, repo, *changesets):
-        """set a revision in %s state""" % state
+        """set revisions in %s state
+
+        This command also alter state of ancestors if necessary.
+        """ % state
         revs = scmutil.revrange(repo, changesets)
         repo.setstate(state, [repo.changelog.node(rev) for rev in revs])
         return 0
@@ -410,6 +511,12 @@
 #########################################
 
 def pushstatesheads(repo, key, old, new):
+    """receive a new state for a revision via pushkey
+
+    It only move revision from a state to a <= one
+
+    Return True if the <key> revision exist in the repository
+    Return False otherwise. (and doesn't alter any state)"""
     st = STATESMAP[new]
     w = repo.wlock()
     try:
@@ -424,6 +531,10 @@
         w.release()
 
 def liststatesheads(repo):
+    """List the boundary of all states.
+
+    {"node-hex" -> "comma separated list of state",}
+    """
     keys = {}
     for state in [st for st in STATES if st.trackheads]:
         for head in repo.stateheads(state):
@@ -437,43 +548,57 @@
 pushkey.register('states-heads', pushstatesheads, liststatesheads)
 
 
+# Wrap discovery
+####################
+def filterprivateout(orig, repo, *args,**kwargs):
+    """wrapper for findcommonoutgoing that remove _NOSHARE"""
+    common, heads = orig(repo, *args, **kwargs)
+    if getattr(repo, '_reducehead', None) is not None:
+        return common, repo._reducehead(heads)
+def filterprivatein(orig, repo, remote, *args, **kwargs):
+    """wrapper for findcommonincoming that remove _NOSHARE"""
+    common, anyinc, heads = orig(repo, remote, *args, **kwargs)
+    if getattr(remote, '_reducehead', None) is not None:
+        heads = remote._reducehead(heads)
+    return common, anyinc, heads
 
+# WireProtocols
+####################
+def wireheads(repo, proto):
+    """Altered head command that doesn't include _NOSHARE
 
+    This is a write protocol command"""
+    st = laststatewithout(_NOSHARE)
+    h = repo.stateheads(st)
+    return wireproto.encodelist(h) + "\n"
 
 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
-
+    """
+    * patch stuff for the _NOSHARE property
+    * add template keyword
+    """
+    # patch discovery
     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"
+    # patch wireprotocol
+    wireproto.commands['heads'] = (wireheads, '')
 
-    def _reducehead(wirerepo, heads):
-        """heads filtering is done repo side"""
-        return heads
-
-    wireproto.wirerepository._reducehead = _reducehead
-    wireproto.commands['heads'] = (heads, '')
-
+    # add template keyword
     templatekw.keywords['state'] = showstate
 
 def extsetup(ui):
+    """Extension setup
+
+    * add revset entry"""
     for state in STATES:
         if state.trackheads:
             revset.symbols[state.headssymbol] = state._revsetheads
 
 def reposetup(ui, repo):
+    """Repository setup
+
+    * extend repo class with states logic"""
 
     if not repo.local():
         return
@@ -485,12 +610,18 @@
     orollback = repo.rollback
     o_writejournal = repo._writejournal
     class statefulrepo(repo.__class__):
+        """An extension of repo class that handle state logic
+
+        - nodestate
+        - stateheads
+        """
 
         def nodestate(self, node):
+            """return the state object associated to the given node"""
             rev = self.changelog.rev(node)
 
             for state in STATES:
-                # XXX avoid for untracked heads
+                # avoid for untracked heads
                 if state.next is not None:
                     ancestors = map(self.changelog.rev, self.stateheads(state))
                     ancestors.extend(self.changelog.ancestors(*ancestors))
@@ -501,6 +632,7 @@
 
 
         def stateheads(self, state):
+            """Return the set of head that define the state"""
             # look for a relevant state
             while state.trackheads and state.next not in self._enabledstates:
                 state = state.next
@@ -511,10 +643,14 @@
 
         @util.propertycache
         def _statesheads(self):
+            """{ state-object -> set(defining head)} mapping"""
             return self._readstatesheads()
 
 
         def _readheadsfile(self, filename):
+            """read head from the given file
+
+            XXX move me elsewhere"""
             heads = [nullid]
             try:
                 f = self.opener(filename)
@@ -527,6 +663,9 @@
             return heads
 
         def _readstatesheads(self, undo=False):
+            """read all state heads
+
+            XXX move me elsewhere"""
             statesheads = {}
             for state in STATES:
                 if state.trackheads:
@@ -536,6 +675,9 @@
             return statesheads
 
         def _writeheadsfile(self, filename, heads):
+            """write given <heads> in the file with at <filename>
+
+            XXX move me elsewhere"""
             f = self.opener(filename, 'w', atomictemp=True)
             try:
                 for h in heads:
@@ -545,7 +687,10 @@
                 f.close()
 
         def _writestateshead(self):
-            # transaction!
+            """write all heads
+
+            XXX move me elsewhere"""
+            # XXX transaction!
             for state in STATES:
                 if state.trackheads:
                     filename = 'states/%s-heads' % state.name
@@ -570,6 +715,11 @@
                 self.setstate(state.next, nodes) # cascading
 
         def _reducehead(self, candidates):
+            """recompute a set of heads so it doesn't include _NOSHARE changeset
+
+            This is basically a complicated method that compute
+            heads(::candidates - _NOSHARE)
+            """
             selected = set()
             st = laststatewithout(_NOSHARE)
             candidates = set(map(self.changelog.rev, candidates))
@@ -588,9 +738,11 @@
 
         @util.propertycache
         def _enabledstates(self):
+            """The set of state enabled in this repository"""
             return self._readenabledstates()
 
         def _readenabledstates(self):
+            """read enabled state from disk"""
             states = set()
             states.add(ST0)
             mapping = dict([(st.name, st) for st in STATES])
@@ -604,6 +756,7 @@
                 return states
 
         def _writeenabledstates(self):
+            """read enabled state to disk"""
             f = self.opener('states/Enabled', 'w', atomictemp=True)
             try:
                 for st in self._enabledstates:
@@ -615,20 +768,22 @@
         ### local clone support
 
         def cancopy(self):
+            """deny copy if there is _NOSHARE changeset"""
             st = laststatewithout(_NOSHARE)
             return ocancopy() and (self.stateheads(st) == self.heads())
 
         ### pull // push support
 
         def pull(self, remote, *args, **kwargs):
+            """altered pull that also update states heads on local repo"""
             result = opull(remote, *args, **kwargs)
             remoteheads = self._pullstatesheads(remote)
-            #print [node.short(h) for h in remoteheads]
             for st, heads in remoteheads.iteritems():
                 self.setstate(st, heads)
             return result
 
         def push(self, remote, *args, **opts):
+            """altered push that also update states heads on local and remote"""
             result = opush(remote, *args, **opts)
             remoteheads = self._pullstatesheads(remote)
             for st, heads in remoteheads.iteritems():
@@ -638,6 +793,10 @@
             return result
 
         def _pushstatesheads(self, remote, state, remoteheads):
+            """push head of a given state for remote
+
+            This handle pushing boundary that does exist on remote host
+            This is done a very naive way"""
             local = set(self.stateheads(state))
             missing = local - set(remoteheads)
             while missing:
@@ -648,6 +807,9 @@
 
 
         def _pullstatesheads(self, remote):
+            """pull all remote states boundary locally
+
+            This can only make the boundary move on a newer changeset"""
             remoteheads = {}
             self.ui.debug('checking for states-heads on remote server')
             if 'states-heads' not in remote.listkeys('namespaces'):
@@ -664,6 +826,7 @@
         ### Tag support
 
         def _tag(self, names, node, *args, **kwargs):
+            """Altered version of _tag that make tag (and tagging) published"""
             tagnode = o_tag(names, node, *args, **kwargs)
             if tagnode is not None: # do nothing for local one
                 self.setstate(ST0, [node, tagnode])
@@ -672,6 +835,7 @@
         ### rollback support
 
         def _writejournal(self, desc):
+            """extended _writejournal that also save states"""
             entries = list(o_writejournal(desc))
             for state in STATES:
                 if state.trackheads:
@@ -685,6 +849,7 @@
             return tuple(entries)
 
         def rollback(self, dryrun=False):
+            """extended rollback that also restore states"""
             wlock = lock = None
             try:
                 wlock = self.wlock()