hgext/states.py
changeset 60 14a4499d2cd6
parent 59 02fba620d139
child 61 0dfe459c7b1c
equal deleted inserted replaced
59:02fba620d139 60:14a4499d2cd6
   174 and remote repository.
   174 and remote repository.
   175 
   175 
   176 .. note:
   176 .. note:
   177 
   177 
   178     As Repository without any specific state have all their changeset
   178     As Repository without any specific state have all their changeset
   179     ``published``, Pushing to such repo will ``publish`` all common changeset. 
   179     ``published``, Pushing to such repo will ``publish`` all common changeset.
   180 
   180 
   181 2. Tagged changeset get automatically Published. The tagging changeset is
   181 2. Tagged changeset get automatically Published. The tagging changeset is
   182 tagged too... This doesn't apply to local tag.
   182 tagged too... This doesn't apply to local tag.
   183 
   183 
   184 
   184 
   213 :tag:       Move tagged and tagging changeset in the ``published`` state.
   213 :tag:       Move tagged and tagging changeset in the ``published`` state.
   214 :incoming:  Exclude ``draft`` changeset of remote repository.
   214 :incoming:  Exclude ``draft`` changeset of remote repository.
   215 :outgoing:  Exclude ``draft`` changeset of local repository.
   215 :outgoing:  Exclude ``draft`` changeset of local repository.
   216 :pull:      As :hg:`in`  + change state of local changeset according to remote side.
   216 :pull:      As :hg:`in`  + change state of local changeset according to remote side.
   217 :push:      As :hg:`out` + sync state of common changeset on both side
   217 :push:      As :hg:`out` + sync state of common changeset on both side
       
   218 :rollback:  rollback restore states heads as before the last transaction (see bookmark)
   218 
   219 
   219 Template
   220 Template
   220 ........
   221 ........
   221 
   222 
   222 A new template keyword ``{state}`` has been added.
   223 A new template keyword ``{state}`` has been added.
   232     - move the current ``<state>heads()`` directives to
   233     - move the current ``<state>heads()`` directives to
   233       _``<state>heads()``
   234       _``<state>heads()``
   234 
   235 
   235     - add ``<state>heads()`` directives to that return the currently in used heads
   236     - add ``<state>heads()`` directives to that return the currently in used heads
   236 
   237 
   237     - add ``<state>()`` directives that 
   238     - add ``<state>()`` directives that match all node in a state.
   238 
   239 
   239 implementation
   240 Implementation
   240 =========================
   241 ==============
   241 
   242 
   242 To be completed
   243 State definition
   243 
   244 ................
   244 Why to you store activate state outside ``.hg/hgrc``? :
   245 
   245 
   246 Conceptually:
   246     ``.hg/hgrc`` might be ignored for trust reason. We don't want the trust
   247 
   247     issue to interfer with enabled state information.
   248 The set of node in the states are defined by the set of the state heads. This allow
       
   249 easy storage, exchange and consistency.
       
   250 
       
   251 .. note: A cache of the complete set of node that belong to a states will
       
   252          probably be need for performance.
       
   253 
       
   254 Code wise:
       
   255 
       
   256 There is a ``state`` class that hold the state property and several useful
       
   257 logic (name, revset entry etc).
       
   258 
       
   259 All defined states are accessible thought the STATES tuple at the ROOT of the
       
   260 module. Or the STATESMAP dictionary that allow to fetch a state from it's
       
   261 name.
       
   262 
       
   263 You can get and edit the list head node that define a state with two methods on
       
   264 repo.
       
   265 
       
   266 :stateheads(<state>):        Returns the list of heads node that define a states
       
   267 :setstate(<state>, [nodes]): Move states boundary forward to include the given
       
   268                              nodes in the given states.
       
   269 
       
   270 Those methods handle ``node`` and not rev as it seems more resilient to me that
       
   271 rev in a mutable world. Maybe it' would make more sens to have ``node`` store
       
   272 on disk but revision in the code.
       
   273 
       
   274 Storage
       
   275 .......
       
   276 
       
   277 States related data are stored in the ``.hg/states/`` directory.
       
   278 
       
   279 The ``.hg/states/Enabled`` file list the states enabled in this
       
   280 repository. This data is *not* stored in the .hg/hgrc because the .hg/hgrc
       
   281 might be ignored for trust reason. As missing und with states can be pretty
       
   282 annoying. (publishing unfinalized changeset, pulling draft one etc) we don't
       
   283 want trust issue to interfer with enabled states information.
       
   284 
       
   285 ``.hg/states/<state>-heads`` file list the nodes that define a states.
       
   286 
       
   287 _NOSHARE filtering
       
   288 ..................
       
   289 
       
   290 Any changeset in a state with a _NOSHARE property will be exclude from pull,
       
   291 push, clone, incoming, outgoing and bundle. It is done through three mechanism:
       
   292 
       
   293 1. Wrapping the findcommonincoming and findcommonoutgoing code with (not very
       
   294    efficient) logic that recompute the exchanged heads.
       
   295 
       
   296 2. Altering ``heads`` wireprotocol command to return sharead heads.
       
   297 
       
   298 3. Disabling hardlink cloning when there is _NOSHARE changeset available.
       
   299 
       
   300 Internal plumbery
       
   301 -----------------
       
   302 
       
   303 sum up of what we do:
       
   304 
       
   305 * state are object
       
   306 
       
   307 * repo.__class__ is extended
       
   308 
       
   309 * discovery is wrapped up
       
   310 
       
   311 * wire protocol is patched
       
   312 
       
   313 * transaction and rollback mechanism are wrapped up.
       
   314 
       
   315 * XXX we write new version of the boundard whenever something happen. We need a
       
   316   smarter and faster way to do this.
   248 
   317 
   249 
   318 
   250 '''
   319 '''
   251 import os
   320 import os
   252 from functools import partial
   321 from functools import partial
   266 from mercurial import pushkey
   335 from mercurial import pushkey
   267 from mercurial import error
   336 from mercurial import error
   268 from mercurial.lock import release
   337 from mercurial.lock import release
   269 
   338 
   270 
   339 
       
   340 # states property constante
   271 _NOSHARE=2
   341 _NOSHARE=2
   272 _MUTABLE=1
   342 _MUTABLE=1
   273 
   343 
   274 class state(object):
   344 class state(object):
       
   345     """State of changeset
       
   346 
       
   347     An utility object that handle several behaviour and containts useful code
       
   348 
       
   349     A state is defined by:
       
   350         - It's name
       
   351         - It's property (defined right above)
       
   352 
       
   353         - It's next state.
       
   354 
       
   355     XXX maybe we could stick description of the state semantic here.
       
   356     """
   275 
   357 
   276     def __init__(self, name, properties=0, next=None):
   358     def __init__(self, name, properties=0, next=None):
   277         self.name = name
   359         self.name = name
   278         self.properties = properties
   360         self.properties = properties
   279         assert next is None or self < next
   361         assert next is None or self < next
   287 
   369 
   288     @util.propertycache
   370     @util.propertycache
   289     def trackheads(self):
   371     def trackheads(self):
   290         """Do we need to track heads of changeset in this state ?
   372         """Do we need to track heads of changeset in this state ?
   291 
   373 
   292         We don't need to track heads for the last state as this is repos heads"""
   374         We don't need to track heads for the last state as this is repo heads"""
   293         return self.next is not None
   375         return self.next is not None
   294 
   376 
   295     def __cmp__(self, other):
   377     def __cmp__(self, other):
       
   378         """Use property to compare states.
       
   379 
       
   380         This is a naiv approach that assume the  the next state are strictly
       
   381         more property than the one before
       
   382         # assert min(self, other).properties = self.properties & other.properties
       
   383         """
   296         return cmp(self.properties, other.properties)
   384         return cmp(self.properties, other.properties)
   297 
   385 
   298     @util.propertycache
   386     @util.propertycache
   299     def _revsetheads(self):
   387     def _revsetheads(self):
   300         """function to be used by revset to finds heads of this states"""
   388         """function to be used by revset to finds heads of this states"""
   317         if self.trackheads:
   405         if self.trackheads:
   318             return "%sheads" % self.name
   406             return "%sheads" % self.name
   319         else:
   407         else:
   320             return 'heads'
   408             return 'heads'
   321 
   409 
       
   410 # Actual state definition
       
   411 
   322 ST2 = state('draft', _NOSHARE | _MUTABLE)
   412 ST2 = state('draft', _NOSHARE | _MUTABLE)
   323 ST1 = state('ready', _MUTABLE, next=ST2)
   413 ST1 = state('ready', _MUTABLE, next=ST2)
   324 ST0 = state('published', next=ST1)
   414 ST0 = state('published', next=ST1)
   325 
   415 
       
   416 # all available state
   326 STATES = (ST0, ST1, ST2)
   417 STATES = (ST0, ST1, ST2)
       
   418 # all available state by name
   327 STATESMAP =dict([(st.name, st) for st in STATES])
   419 STATESMAP =dict([(st.name, st) for st in STATES])
   328 
   420 
   329 @util.cachefunc
   421 @util.cachefunc
   330 def laststatewithout(prop):
   422 def laststatewithout(prop):
       
   423     """Find the states with the most property but <prop>
       
   424 
       
   425     (This function is necessary because the whole state stuff are abstracted)"""
   331     for state in STATES:
   426     for state in STATES:
   332         if not state.properties & prop:
   427         if not state.properties & prop:
   333             candidate = state
   428             candidate = state
   334         else:
   429         else:
   335             return candidate
   430             return candidate
   336 
   431 
   337 # util function
   432 # util function
   338 #############################
   433 #############################
   339 def noderange(repo, revsets):
   434 def noderange(repo, revsets):
       
   435     """The same as revrange but return node"""
   340     return map(repo.changelog.node,
   436     return map(repo.changelog.node,
   341                scmutil.revrange(repo, revsets))
   437                scmutil.revrange(repo, revsets))
   342 
   438 
   343 # Patch changectx
   439 # Patch changectx
   344 #############################
   440 #############################
   345 
   441 
   346 def state(ctx):
   442 def state(ctx):
       
   443     """return the state objet associated to the context"""
   347     if ctx.node()is None:
   444     if ctx.node()is None:
   348         return STATES[-1]
   445         return STATES[-1]
   349     return ctx._repo.nodestate(ctx.node())
   446     return ctx._repo.nodestate(ctx.node())
   350 context.changectx.state = state
   447 context.changectx.state = state
   351 
   448 
   352 # improve template
   449 # improve template
   353 #############################
   450 #############################
   354 
   451 
   355 def showstate(ctx, **args):
   452 def showstate(ctx, **args):
       
   453     """Show the name of the state associated with the context"""
   356     return ctx.state()
   454     return ctx.state()
   357 
   455 
   358 
   456 
   359 # New commands
   457 # New commands
   360 #############################
   458 #############################
   389                 repo._enabledstates.add(st)
   487                 repo._enabledstates.add(st)
   390         repo._writeenabledstates()
   488         repo._writeenabledstates()
   391     return 0
   489     return 0
   392 
   490 
   393 cmdtable = {'states': (cmdstates, [ ('', 'off', False, _('desactivate the state') )], '<state>')}
   491 cmdtable = {'states': (cmdstates, [ ('', 'off', False, _('desactivate the state') )], '<state>')}
   394 #cmdtable = {'states': (cmdstates, [], '<state>')}
   492 
   395 
   493 # automatic generation of command that set state
   396 def makecmd(state):
   494 def makecmd(state):
   397     def cmdmoveheads(ui, repo, *changesets):
   495     def cmdmoveheads(ui, repo, *changesets):
   398         """set a revision in %s state""" % state
   496         """set revisions in %s state
       
   497 
       
   498         This command also alter state of ancestors if necessary.
       
   499         """ % state
   399         revs = scmutil.revrange(repo, changesets)
   500         revs = scmutil.revrange(repo, changesets)
   400         repo.setstate(state, [repo.changelog.node(rev) for rev in revs])
   501         repo.setstate(state, [repo.changelog.node(rev) for rev in revs])
   401         return 0
   502         return 0
   402     return cmdmoveheads
   503     return cmdmoveheads
   403 
   504 
   408 
   509 
   409 # Pushkey mechanism for mutable
   510 # Pushkey mechanism for mutable
   410 #########################################
   511 #########################################
   411 
   512 
   412 def pushstatesheads(repo, key, old, new):
   513 def pushstatesheads(repo, key, old, new):
       
   514     """receive a new state for a revision via pushkey
       
   515 
       
   516     It only move revision from a state to a <= one
       
   517 
       
   518     Return True if the <key> revision exist in the repository
       
   519     Return False otherwise. (and doesn't alter any state)"""
   413     st = STATESMAP[new]
   520     st = STATESMAP[new]
   414     w = repo.wlock()
   521     w = repo.wlock()
   415     try:
   522     try:
   416         newhead = node.bin(key)
   523         newhead = node.bin(key)
   417         try:
   524         try:
   422         return True
   529         return True
   423     finally:
   530     finally:
   424         w.release()
   531         w.release()
   425 
   532 
   426 def liststatesheads(repo):
   533 def liststatesheads(repo):
       
   534     """List the boundary of all states.
       
   535 
       
   536     {"node-hex" -> "comma separated list of state",}
       
   537     """
   427     keys = {}
   538     keys = {}
   428     for state in [st for st in STATES if st.trackheads]:
   539     for state in [st for st in STATES if st.trackheads]:
   429         for head in repo.stateheads(state):
   540         for head in repo.stateheads(state):
   430             head = node.hex(head)
   541             head = node.hex(head)
   431             if head in keys:
   542             if head in keys:
   435     return keys
   546     return keys
   436 
   547 
   437 pushkey.register('states-heads', pushstatesheads, liststatesheads)
   548 pushkey.register('states-heads', pushstatesheads, liststatesheads)
   438 
   549 
   439 
   550 
   440 
   551 # Wrap discovery
   441 
   552 ####################
       
   553 def filterprivateout(orig, repo, *args,**kwargs):
       
   554     """wrapper for findcommonoutgoing that remove _NOSHARE"""
       
   555     common, heads = orig(repo, *args, **kwargs)
       
   556     if getattr(repo, '_reducehead', None) is not None:
       
   557         return common, repo._reducehead(heads)
       
   558 def filterprivatein(orig, repo, remote, *args, **kwargs):
       
   559     """wrapper for findcommonincoming that remove _NOSHARE"""
       
   560     common, anyinc, heads = orig(repo, remote, *args, **kwargs)
       
   561     if getattr(remote, '_reducehead', None) is not None:
       
   562         heads = remote._reducehead(heads)
       
   563     return common, anyinc, heads
       
   564 
       
   565 # WireProtocols
       
   566 ####################
       
   567 def wireheads(repo, proto):
       
   568     """Altered head command that doesn't include _NOSHARE
       
   569 
       
   570     This is a write protocol command"""
       
   571     st = laststatewithout(_NOSHARE)
       
   572     h = repo.stateheads(st)
       
   573     return wireproto.encodelist(h) + "\n"
   442 
   574 
   443 def uisetup(ui):
   575 def uisetup(ui):
   444     def filterprivateout(orig, repo, *args,**kwargs):
   576     """
   445         common, heads = orig(repo, *args, **kwargs)
   577     * patch stuff for the _NOSHARE property
   446         return common, repo._reducehead(heads)
   578     * add template keyword
   447     def filterprivatein(orig, repo, remote, *args, **kwargs):
   579     """
   448         common, anyinc, heads = orig(repo, remote, *args, **kwargs)
   580     # patch discovery
   449         heads = remote._reducehead(heads)
       
   450         return common, anyinc, heads
       
   451 
       
   452     extensions.wrapfunction(discovery, 'findcommonoutgoing', filterprivateout)
   581     extensions.wrapfunction(discovery, 'findcommonoutgoing', filterprivateout)
   453     extensions.wrapfunction(discovery, 'findcommonincoming', filterprivatein)
   582     extensions.wrapfunction(discovery, 'findcommonincoming', filterprivatein)
   454 
   583 
   455     # Write protocols
   584     # patch wireprotocol
   456     ####################
   585     wireproto.commands['heads'] = (wireheads, '')
   457     def heads(repo, proto):
   586 
   458         st = laststatewithout(_NOSHARE)
   587     # add template keyword
   459         h = repo.stateheads(st)
       
   460         return wireproto.encodelist(h) + "\n"
       
   461 
       
   462     def _reducehead(wirerepo, heads):
       
   463         """heads filtering is done repo side"""
       
   464         return heads
       
   465 
       
   466     wireproto.wirerepository._reducehead = _reducehead
       
   467     wireproto.commands['heads'] = (heads, '')
       
   468 
       
   469     templatekw.keywords['state'] = showstate
   588     templatekw.keywords['state'] = showstate
   470 
   589 
   471 def extsetup(ui):
   590 def extsetup(ui):
       
   591     """Extension setup
       
   592 
       
   593     * add revset entry"""
   472     for state in STATES:
   594     for state in STATES:
   473         if state.trackheads:
   595         if state.trackheads:
   474             revset.symbols[state.headssymbol] = state._revsetheads
   596             revset.symbols[state.headssymbol] = state._revsetheads
   475 
   597 
   476 def reposetup(ui, repo):
   598 def reposetup(ui, repo):
       
   599     """Repository setup
       
   600 
       
   601     * extend repo class with states logic"""
   477 
   602 
   478     if not repo.local():
   603     if not repo.local():
   479         return
   604         return
   480 
   605 
   481     ocancopy =repo.cancopy
   606     ocancopy =repo.cancopy
   483     opush = repo.push
   608     opush = repo.push
   484     o_tag = repo._tag
   609     o_tag = repo._tag
   485     orollback = repo.rollback
   610     orollback = repo.rollback
   486     o_writejournal = repo._writejournal
   611     o_writejournal = repo._writejournal
   487     class statefulrepo(repo.__class__):
   612     class statefulrepo(repo.__class__):
       
   613         """An extension of repo class that handle state logic
       
   614 
       
   615         - nodestate
       
   616         - stateheads
       
   617         """
   488 
   618 
   489         def nodestate(self, node):
   619         def nodestate(self, node):
       
   620             """return the state object associated to the given node"""
   490             rev = self.changelog.rev(node)
   621             rev = self.changelog.rev(node)
   491 
   622 
   492             for state in STATES:
   623             for state in STATES:
   493                 # XXX avoid for untracked heads
   624                 # avoid for untracked heads
   494                 if state.next is not None:
   625                 if state.next is not None:
   495                     ancestors = map(self.changelog.rev, self.stateheads(state))
   626                     ancestors = map(self.changelog.rev, self.stateheads(state))
   496                     ancestors.extend(self.changelog.ancestors(*ancestors))
   627                     ancestors.extend(self.changelog.ancestors(*ancestors))
   497                     if rev in ancestors:
   628                     if rev in ancestors:
   498                         break
   629                         break
   499             return state
   630             return state
   500 
   631 
   501 
   632 
   502 
   633 
   503         def stateheads(self, state):
   634         def stateheads(self, state):
       
   635             """Return the set of head that define the state"""
   504             # look for a relevant state
   636             # look for a relevant state
   505             while state.trackheads and state.next not in self._enabledstates:
   637             while state.trackheads and state.next not in self._enabledstates:
   506                 state = state.next
   638                 state = state.next
   507             # last state have no cached head.
   639             # last state have no cached head.
   508             if state.trackheads:
   640             if state.trackheads:
   509                 return self._statesheads[state]
   641                 return self._statesheads[state]
   510             return self.heads()
   642             return self.heads()
   511 
   643 
   512         @util.propertycache
   644         @util.propertycache
   513         def _statesheads(self):
   645         def _statesheads(self):
       
   646             """{ state-object -> set(defining head)} mapping"""
   514             return self._readstatesheads()
   647             return self._readstatesheads()
   515 
   648 
   516 
   649 
   517         def _readheadsfile(self, filename):
   650         def _readheadsfile(self, filename):
       
   651             """read head from the given file
       
   652 
       
   653             XXX move me elsewhere"""
   518             heads = [nullid]
   654             heads = [nullid]
   519             try:
   655             try:
   520                 f = self.opener(filename)
   656                 f = self.opener(filename)
   521                 try:
   657                 try:
   522                     heads = sorted([node.bin(n) for n in f.read().split() if n])
   658                     heads = sorted([node.bin(n) for n in f.read().split() if n])
   525             except IOError:
   661             except IOError:
   526                 pass
   662                 pass
   527             return heads
   663             return heads
   528 
   664 
   529         def _readstatesheads(self, undo=False):
   665         def _readstatesheads(self, undo=False):
       
   666             """read all state heads
       
   667 
       
   668             XXX move me elsewhere"""
   530             statesheads = {}
   669             statesheads = {}
   531             for state in STATES:
   670             for state in STATES:
   532                 if state.trackheads:
   671                 if state.trackheads:
   533                     filemask = 'states/%s-heads'
   672                     filemask = 'states/%s-heads'
   534                     filename = filemask % state.name
   673                     filename = filemask % state.name
   535                     statesheads[state] = self._readheadsfile(filename)
   674                     statesheads[state] = self._readheadsfile(filename)
   536             return statesheads
   675             return statesheads
   537 
   676 
   538         def _writeheadsfile(self, filename, heads):
   677         def _writeheadsfile(self, filename, heads):
       
   678             """write given <heads> in the file with at <filename>
       
   679 
       
   680             XXX move me elsewhere"""
   539             f = self.opener(filename, 'w', atomictemp=True)
   681             f = self.opener(filename, 'w', atomictemp=True)
   540             try:
   682             try:
   541                 for h in heads:
   683                 for h in heads:
   542                     f.write(hex(h) + '\n')
   684                     f.write(hex(h) + '\n')
   543                 f.rename()
   685                 f.rename()
   544             finally:
   686             finally:
   545                 f.close()
   687                 f.close()
   546 
   688 
   547         def _writestateshead(self):
   689         def _writestateshead(self):
   548             # transaction!
   690             """write all heads
       
   691 
       
   692             XXX move me elsewhere"""
       
   693             # XXX transaction!
   549             for state in STATES:
   694             for state in STATES:
   550                 if state.trackheads:
   695                 if state.trackheads:
   551                     filename = 'states/%s-heads' % state.name
   696                     filename = 'states/%s-heads' % state.name
   552                     self._writeheadsfile(filename, self._statesheads[state])
   697                     self._writeheadsfile(filename, self._statesheads[state])
   553 
   698 
   568                 self._writestateshead()
   713                 self._writestateshead()
   569             if state.next is not None and state.next.trackheads:
   714             if state.next is not None and state.next.trackheads:
   570                 self.setstate(state.next, nodes) # cascading
   715                 self.setstate(state.next, nodes) # cascading
   571 
   716 
   572         def _reducehead(self, candidates):
   717         def _reducehead(self, candidates):
       
   718             """recompute a set of heads so it doesn't include _NOSHARE changeset
       
   719 
       
   720             This is basically a complicated method that compute
       
   721             heads(::candidates - _NOSHARE)
       
   722             """
   573             selected = set()
   723             selected = set()
   574             st = laststatewithout(_NOSHARE)
   724             st = laststatewithout(_NOSHARE)
   575             candidates = set(map(self.changelog.rev, candidates))
   725             candidates = set(map(self.changelog.rev, candidates))
   576             heads = set(map(self.changelog.rev, self.stateheads(st)))
   726             heads = set(map(self.changelog.rev, self.stateheads(st)))
   577             shareable = set(self.changelog.ancestors(*heads))
   727             shareable = set(self.changelog.ancestors(*heads))
   586 
   736 
   587         ### enable // disable logic
   737         ### enable // disable logic
   588 
   738 
   589         @util.propertycache
   739         @util.propertycache
   590         def _enabledstates(self):
   740         def _enabledstates(self):
       
   741             """The set of state enabled in this repository"""
   591             return self._readenabledstates()
   742             return self._readenabledstates()
   592 
   743 
   593         def _readenabledstates(self):
   744         def _readenabledstates(self):
       
   745             """read enabled state from disk"""
   594             states = set()
   746             states = set()
   595             states.add(ST0)
   747             states.add(ST0)
   596             mapping = dict([(st.name, st) for st in STATES])
   748             mapping = dict([(st.name, st) for st in STATES])
   597             try:
   749             try:
   598                 f = self.opener('states/Enabled')
   750                 f = self.opener('states/Enabled')
   602                         states.add(st)
   754                         states.add(st)
   603             finally:
   755             finally:
   604                 return states
   756                 return states
   605 
   757 
   606         def _writeenabledstates(self):
   758         def _writeenabledstates(self):
       
   759             """read enabled state to disk"""
   607             f = self.opener('states/Enabled', 'w', atomictemp=True)
   760             f = self.opener('states/Enabled', 'w', atomictemp=True)
   608             try:
   761             try:
   609                 for st in self._enabledstates:
   762                 for st in self._enabledstates:
   610                     f.write(st.name + '\n')
   763                     f.write(st.name + '\n')
   611                 f.rename()
   764                 f.rename()
   613                 f.close()
   766                 f.close()
   614 
   767 
   615         ### local clone support
   768         ### local clone support
   616 
   769 
   617         def cancopy(self):
   770         def cancopy(self):
       
   771             """deny copy if there is _NOSHARE changeset"""
   618             st = laststatewithout(_NOSHARE)
   772             st = laststatewithout(_NOSHARE)
   619             return ocancopy() and (self.stateheads(st) == self.heads())
   773             return ocancopy() and (self.stateheads(st) == self.heads())
   620 
   774 
   621         ### pull // push support
   775         ### pull // push support
   622 
   776 
   623         def pull(self, remote, *args, **kwargs):
   777         def pull(self, remote, *args, **kwargs):
       
   778             """altered pull that also update states heads on local repo"""
   624             result = opull(remote, *args, **kwargs)
   779             result = opull(remote, *args, **kwargs)
   625             remoteheads = self._pullstatesheads(remote)
   780             remoteheads = self._pullstatesheads(remote)
   626             #print [node.short(h) for h in remoteheads]
       
   627             for st, heads in remoteheads.iteritems():
   781             for st, heads in remoteheads.iteritems():
   628                 self.setstate(st, heads)
   782                 self.setstate(st, heads)
   629             return result
   783             return result
   630 
   784 
   631         def push(self, remote, *args, **opts):
   785         def push(self, remote, *args, **opts):
       
   786             """altered push that also update states heads on local and remote"""
   632             result = opush(remote, *args, **opts)
   787             result = opush(remote, *args, **opts)
   633             remoteheads = self._pullstatesheads(remote)
   788             remoteheads = self._pullstatesheads(remote)
   634             for st, heads in remoteheads.iteritems():
   789             for st, heads in remoteheads.iteritems():
   635                 self.setstate(st, heads)
   790                 self.setstate(st, heads)
   636                 if heads != self.stateheads(st):
   791                 if heads != self.stateheads(st):
   637                     self._pushstatesheads(remote, st,  heads)
   792                     self._pushstatesheads(remote, st,  heads)
   638             return result
   793             return result
   639 
   794 
   640         def _pushstatesheads(self, remote, state, remoteheads):
   795         def _pushstatesheads(self, remote, state, remoteheads):
       
   796             """push head of a given state for remote
       
   797 
       
   798             This handle pushing boundary that does exist on remote host
       
   799             This is done a very naive way"""
   641             local = set(self.stateheads(state))
   800             local = set(self.stateheads(state))
   642             missing = local - set(remoteheads)
   801             missing = local - set(remoteheads)
   643             while missing:
   802             while missing:
   644                 h = missing.pop()
   803                 h = missing.pop()
   645                 ok = remote.pushkey('states-heads', node.hex(h), '', state.name)
   804                 ok = remote.pushkey('states-heads', node.hex(h), '', state.name)
   646                 if not ok:
   805                 if not ok:
   647                     missing.update(p.node() for p in repo[h].parents())
   806                     missing.update(p.node() for p in repo[h].parents())
   648 
   807 
   649 
   808 
   650         def _pullstatesheads(self, remote):
   809         def _pullstatesheads(self, remote):
       
   810             """pull all remote states boundary locally
       
   811 
       
   812             This can only make the boundary move on a newer changeset"""
   651             remoteheads = {}
   813             remoteheads = {}
   652             self.ui.debug('checking for states-heads on remote server')
   814             self.ui.debug('checking for states-heads on remote server')
   653             if 'states-heads' not in remote.listkeys('namespaces'):
   815             if 'states-heads' not in remote.listkeys('namespaces'):
   654                 self.ui.debug('states-heads not enabled on the remote server, '
   816                 self.ui.debug('states-heads not enabled on the remote server, '
   655                               'marking everything as published')
   817                               'marking everything as published')
   662             return remoteheads
   824             return remoteheads
   663 
   825 
   664         ### Tag support
   826         ### Tag support
   665 
   827 
   666         def _tag(self, names, node, *args, **kwargs):
   828         def _tag(self, names, node, *args, **kwargs):
       
   829             """Altered version of _tag that make tag (and tagging) published"""
   667             tagnode = o_tag(names, node, *args, **kwargs)
   830             tagnode = o_tag(names, node, *args, **kwargs)
   668             if tagnode is not None: # do nothing for local one
   831             if tagnode is not None: # do nothing for local one
   669                 self.setstate(ST0, [node, tagnode])
   832                 self.setstate(ST0, [node, tagnode])
   670             return tagnode
   833             return tagnode
   671 
   834 
   672         ### rollback support
   835         ### rollback support
   673 
   836 
   674         def _writejournal(self, desc):
   837         def _writejournal(self, desc):
       
   838             """extended _writejournal that also save states"""
   675             entries = list(o_writejournal(desc))
   839             entries = list(o_writejournal(desc))
   676             for state in STATES:
   840             for state in STATES:
   677                 if state.trackheads:
   841                 if state.trackheads:
   678                     filename = 'states/%s-heads' % state.name
   842                     filename = 'states/%s-heads' % state.name
   679                     filepath = self.join(filename)
   843                     filepath = self.join(filename)
   683                         util.copyfile(filepath, journalpath)
   847                         util.copyfile(filepath, journalpath)
   684                         entries.append(journalpath)
   848                         entries.append(journalpath)
   685             return tuple(entries)
   849             return tuple(entries)
   686 
   850 
   687         def rollback(self, dryrun=False):
   851         def rollback(self, dryrun=False):
       
   852             """extended rollback that also restore states"""
   688             wlock = lock = None
   853             wlock = lock = None
   689             try:
   854             try:
   690                 wlock = self.wlock()
   855                 wlock = self.wlock()
   691                 lock = self.lock()
   856                 lock = self.lock()
   692                 ret = orollback(dryrun)
   857                 ret = orollback(dryrun)