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