Mercurial > evolve
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) |