comparison hgext/states.py @ 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
comparison
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)