small refactoring and big doc update.
authorPierre-Yves David <pierre-yves.david@ens-lyon.org>
Mon, 12 Sep 2011 14:05:32 +0200
changeset 60 14a4499d2cd6
parent 59 02fba620d139
child 61 0dfe459c7b1c
small refactoring and big doc update. Sorry for the big commit crecord one so much diff seems to confuse my powerbook to death :-/
hgext/states.py
--- 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()