# HG changeset patch # User Pierre-Yves David # Date 1315829132 -7200 # Node ID 14a4499d2cd65d47cefa27683bad9be1c395f5ba # Parent 02fba620d1390ed15ef4d94be0d4ac90b5a1918e small refactoring and big doc update. Sorry for the big commit crecord one so much diff seems to confuse my powerbook to death :-/ diff -r 02fba620d139 -r 14a4499d2cd6 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 ``heads()`` directives to that return the currently in used heads - - add ``()`` directives that + - add ``()`` 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(): Returns the list of heads node that define a states +:setstate(, [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/-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 + + (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') )], '')} -#cmdtable = {'states': (cmdstates, [], '')} +# 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 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 in the file with at + + 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()