--- 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()