hgext/states.py
changeset 110 6d461c2143a0
parent 109 a2e8057117d3
child 111 ab4cef4fbd03
equal deleted inserted replaced
109:a2e8057117d3 110:6d461c2143a0
     1 # states.py - introduce the state concept for mercurial changeset
       
     2 #
       
     3 # Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
       
     4 #                Logilab SA        <contact@logilab.fr>
       
     5 #                Augie Fackler     <durin42@gmail.com>
       
     6 #
       
     7 # This software may be used and distributed according to the terms of the
       
     8 # GNU General Public License version 2 or any later version.
       
     9 
       
    10 '''introduce the state concept for mercurial changeset
       
    11 
       
    12 (see http://mercurial.selenic.com/wiki/StatesPlan)
       
    13 
       
    14 General concept
       
    15 ===============
       
    16 
       
    17 This extension adds the state concept. A changeset are now in a specific state
       
    18 that control they mutability and they exchange.
       
    19 
       
    20 States properties
       
    21 .................
       
    22 
       
    23 The states extension currently alter two property for changeset
       
    24 
       
    25 :mutability:  history rewritten tool should refuse to work on immutable changeset
       
    26 :sharing:     shared changeset are exchanged during pull and push. other are not
       
    27 
       
    28 Here is a small summary of the current property of state existing state::
       
    29 
       
    30     ||           || mutable || shared ||
       
    31     || published ||         ||   x    ||
       
    32     || ready     ||    x    ||   x    ||
       
    33     || draft     ||    x    ||        ||
       
    34 
       
    35 States consistency and ordering
       
    36 ...............................
       
    37 
       
    38 States of changesets have to be consistent with each other. A changeset can only have ancestors of it's state (or a compatible states)
       
    39 
       
    40 Example:
       
    41 
       
    42     A ``published`` changeset can't have a ``draft`` parent.
       
    43 
       
    44 a state is compatible with itself and all "smaller" states. Order is as follow::
       
    45 
       
    46     published < ready < draft
       
    47 
       
    48 
       
    49 .. note:
       
    50 
       
    51     This section if probably far too conceptual for people. The result is just
       
    52     that: A ``published`` changeset can only have ``published`` ancestors. A
       
    53     ``ready`` changeset can only have ``published`` or ``ready`` ancestors.
       
    54 
       
    55     Moreover There is a need for a nice word to refer to "a state smaller than another"
       
    56 
       
    57 
       
    58 States details
       
    59 ==============
       
    60 
       
    61 
       
    62 published
       
    63     Changesets in the ``published`` state are the core of the history.  They are
       
    64     changesets that you published to the world. People can expect them to always
       
    65     exist. They are changesets as you know them. **By default all changesets
       
    66     are published**
       
    67 
       
    68     - They are exchanged with other repositories (included in pull//push).
       
    69 
       
    70     - They are not mutable, extensions rewriting history should refuse to
       
    71       rewrite them.
       
    72 
       
    73 ready
       
    74     Changesets in the ``ready`` state have not yet been accepted in the
       
    75     immutable history. You can share them with others for review, testing or
       
    76     improvement. Any ``ready`` changeset can either be included in the
       
    77     published history (and become immutable) or be rewritten and never make it
       
    78     to the published history.
       
    79 
       
    80     - They are exchanged with other repositories (included in pull//push).
       
    81 
       
    82     - They are mutable, extensions rewriting history accept to work on them.
       
    83 
       
    84 draft
       
    85 
       
    86     Changesets in the ``draft`` state are heavy work in progress you are not
       
    87     yet willing to share with others.
       
    88 
       
    89     - They are not exchanged with other repositories. pull//push do not see them.
       
    90     - They are mutable, extensions rewriting history accept to work on them.
       
    91 
       
    92 --
       
    93 
       
    94 .. note:
       
    95 
       
    96     The Dead states mentionned in on the wiki page are missing. There is two main reason for it:
       
    97 
       
    98     1. The ``dead`` state has a different behaviour that requires more work to be
       
    99        implemented.
       
   100 
       
   101     2. I believe that the use cases of ``dead changeset`` are better covered by
       
   102        the ``obsolete`` extension.
       
   103 
       
   104 --
       
   105 
       
   106 .. note:
       
   107 
       
   108     I'm tempted to add a state with the same property that ``ready`` for review
       
   109     workflow.::
       
   110 
       
   111         ||           || mutable || shared ||
       
   112         || published ||         ||   x    ||
       
   113         || ready     ||    x    ||   x    ||
       
   114         || inprogress||    x    ||   x    ||
       
   115         || draft     ||    x    ||        ||
       
   116 
       
   117     The ``ready`` state would be for changeset that wait review of someone that
       
   118     can "publish" them.
       
   119 
       
   120 
       
   121 
       
   122 Current Feature and usage
       
   123 =========================
       
   124 
       
   125 
       
   126 Enabling states
       
   127 ...............
       
   128 
       
   129 The extension adds a :hg:`hg states` command to display and choose which states
       
   130 are used by a repository, see :hg:`hg states` for details.
       
   131 
       
   132 By default all changesets in the repository are ``published``. Other states
       
   133 must be explicitly activated. Changeset in a remote repository that doesn't
       
   134 support states are all seen as ``published``.
       
   135 
       
   136 .. note:
       
   137 
       
   138     When a state is not activated, changesets in this state are handled as
       
   139     changesets of the previous state it (``draft`` are handled as ``ready``,
       
   140     ``ready`` are handled as ``published``).
       
   141 
       
   142 TODO:
       
   143 
       
   144 - have a configuration in hgrc::
       
   145 
       
   146     [states]
       
   147     ready=(off|on)(-inherit)?
       
   148     <state>=(off|on)(-inherit)?
       
   149 
       
   150  :off:     state disabled for new repo
       
   151  :on:      state enabled  for new repo
       
   152  :inherit: if present, inherit states of source on :hg:`clone`.
       
   153 
       
   154 - display the number of changesets that change state when activating a state.
       
   155 
       
   156 
       
   157 
       
   158 State transition
       
   159 ................
       
   160 
       
   161 Changeset you create locally will be in the ``draft`` state. (or any previous
       
   162 state if draft isn't enabled)
       
   163 
       
   164 There is some situation where the state of a changeset will change
       
   165 automatically. Automatic movement always go in the same direction.: ``draft ->
       
   166 ``ready`` -> ``published``
       
   167 
       
   168 1. When you pull or push boundary move. Common changeset that are ``published`` in
       
   169 one of the two repository are set to ``published``. Same goes for ``ready`` etc
       
   170 (states are evaluated from in increasing order XXX I bet no one understand this
       
   171 parenthesis. Pull operation alter the local repository. push alter both local
       
   172 and remote repository.
       
   173 
       
   174 .. note:
       
   175 
       
   176     As Repository without any specific state have all their changeset
       
   177     ``published``, Pushing to such repo will ``publish`` all common changeset.
       
   178 
       
   179 2. Tagged changeset get automatically Published. The tagging changeset is
       
   180 tagged too... This doesn't apply to local tag.
       
   181 
       
   182 
       
   183 You can also manually change changeset state with a dedicated command for each
       
   184 state. See :hg:`published`, :hg:`ready` and :hg:`draft` for details.
       
   185 
       
   186 XXX maybe we can details the general behaviour here
       
   187 
       
   188 :hg <state> revs:                 move boundary of state so it includes revs
       
   189                                   ( revs included in ::<state>heads())
       
   190 :hg --exact <state> revs:         move boundary so that revs are exactly in state
       
   191                                   <state> ( all([rev.state == <state> for rev in
       
   192                                   revs]))
       
   193 :hg --exact --force <state> revs: move boundary event if it create inconsistency
       
   194                                   (with tag for example)
       
   195 
       
   196 TODO:
       
   197 
       
   198 - implement consistency check
       
   199 
       
   200 - implement --force
       
   201 
       
   202 
       
   203 Existing command change
       
   204 .......................
       
   205 
       
   206 As said in the previous section:
       
   207 
       
   208 :commit:    Create draft changeset (or the first enabled previous changeset).
       
   209 :tag:       Move tagged and tagging changeset in the ``published`` state.
       
   210 :incoming:  Exclude ``draft`` changeset of remote repository.
       
   211 :outgoing:  Exclude ``draft`` changeset of local repository.
       
   212 :pull:      As :hg:`in`  + change state of local changeset according to remote side.
       
   213 :push:      As :hg:`out` + sync state of common changeset on both side
       
   214 :rollback:  rollback restore states heads as before the last transaction (see bookmark)
       
   215 
       
   216 State Transition control
       
   217 .........................
       
   218 
       
   219 There is currently no way to control who can alter boundary (The most notable
       
   220 usecase is about the published one).
       
   221 
       
   222 This is probably needed quickly
       
   223 
       
   224 XXX TODO: Proper behaviour when heads file are chmoded whould be a first step.
       
   225 
       
   226 XXX We are going to need hooks (pre and post) hook on state transition too.
       
   227 
       
   228 Template
       
   229 ........
       
   230 
       
   231 A new template keyword ``{state}`` has been added.
       
   232 
       
   233 Revset
       
   234 ......
       
   235 
       
   236 We add new ``readyheads()`` and ``publishedheads()`` revset directives. This
       
   237 returns the heads of each state **as if all of them were activated**.
       
   238 
       
   239 XXX TODO - I would like to
       
   240 
       
   241 - move the current ``<state>heads()`` directives to
       
   242   _``<state>heads()``
       
   243 
       
   244 - add ``<state>heads()`` directives to that return the currently in used heads
       
   245 
       
   246 - add ``<state>()`` directives that match all node in a state.
       
   247 
       
   248 Context
       
   249 .......
       
   250 
       
   251 The ``context`` class gain a new method  ``states()`` that return a ``state`` object. The
       
   252 most notable property of this states object are ```name`` and ``mutable``.
       
   253 
       
   254 Other extensions
       
   255 ................
       
   256 
       
   257 :rebase:     can't rebase immutable changeset.
       
   258 :mq:         can't qimport immutable changeset.
       
   259 
       
   260 TODO: publishing a changeset should qfinish mq patches.
       
   261 
       
   262 
       
   263 
       
   264 Implementation
       
   265 ==============
       
   266 
       
   267 State definition
       
   268 ................
       
   269 
       
   270 Conceptually:
       
   271 
       
   272 The set of node in the states are defined by the set of the state heads. This allow
       
   273 easy storage, exchange and consistency.
       
   274 
       
   275 .. note: A cache of the complete set of node that belong to a states will
       
   276          probably be need for performance.
       
   277 
       
   278 Code wise:
       
   279 
       
   280 There is a ``state`` class that hold the state property and several useful
       
   281 logic (name, revset entry etc).
       
   282 
       
   283 All defined states are accessible thought the STATES tuple at the ROOT of the
       
   284 module. Or the STATESMAP dictionary that allow to fetch a state from it's
       
   285 name.
       
   286 
       
   287 You can get and edit the list head node that define a state with two methods on
       
   288 repo.
       
   289 
       
   290 :stateheads(<state>):        Returns the list of heads node that define a states
       
   291 :setstate(<state>, [nodes]): Move states boundary forward to include the given
       
   292                              nodes in the given states.
       
   293 
       
   294 Those methods handle ``node`` and not rev as it seems more resilient to me that
       
   295 rev in a mutable world. Maybe it' would make more sens to have ``node`` store
       
   296 on disk but revision in the code.
       
   297 
       
   298 Storage
       
   299 .......
       
   300 
       
   301 States related data are stored in the ``.hg/states/`` directory.
       
   302 
       
   303 The ``.hg/states/Enabled`` file list the states enabled in this
       
   304 repository. This data is *not* stored in the .hg/hgrc because the .hg/hgrc
       
   305 might be ignored for trust reason. As missing und with states can be pretty
       
   306 annoying. (publishing unfinalized changeset, pulling draft one etc) we don't
       
   307 want trust issue to interfer with enabled states information.
       
   308 
       
   309 ``.hg/states/<state>-heads`` file list the nodes that define a states.
       
   310 
       
   311 _NOSHARE filtering
       
   312 ..................
       
   313 
       
   314 Any changeset in a state with a _NOSHARE property will be exclude from pull,
       
   315 push, clone, incoming, outgoing and bundle. It is done through three mechanism:
       
   316 
       
   317 1. Wrapping the findcommonincoming and findcommonoutgoing code with (not very
       
   318    efficient) logic that recompute the exchanged heads.
       
   319 
       
   320 2. Altering ``heads`` wireprotocol command to return sharead heads.
       
   321 
       
   322 3. Disabling hardlink cloning when there is _NOSHARE changeset available.
       
   323 
       
   324 Internal plumbery
       
   325 -----------------
       
   326 
       
   327 sum up of what we do:
       
   328 
       
   329 * state are object
       
   330 
       
   331 * repo.__class__ is extended
       
   332 
       
   333 * discovery is wrapped up
       
   334 
       
   335 * wire protocol is patched
       
   336 
       
   337 * transaction and rollback mechanism are wrapped up.
       
   338 
       
   339 * XXX we write new version of the boundard whenever something happen. We need a
       
   340   smarter and faster way to do this.
       
   341 
       
   342 
       
   343 '''
       
   344 import os
       
   345 from functools import partial
       
   346 from operator import or_
       
   347 
       
   348 from mercurial.i18n import _
       
   349 from mercurial import cmdutil
       
   350 from mercurial import scmutil
       
   351 from mercurial import context
       
   352 from mercurial import revset
       
   353 from mercurial import templatekw
       
   354 from mercurial import util
       
   355 from mercurial import node
       
   356 from mercurial.node import nullid, hex, short
       
   357 from mercurial import discovery
       
   358 from mercurial import extensions
       
   359 from mercurial import wireproto
       
   360 from mercurial import pushkey
       
   361 from mercurial import error
       
   362 from mercurial import repair
       
   363 from mercurial.lock import release
       
   364 
       
   365 
       
   366 
       
   367 # states property constante
       
   368 _NOSHARE=2
       
   369 _MUTABLE=1
       
   370 
       
   371 class state(object):
       
   372     """State of changeset
       
   373 
       
   374     An utility object that handle several behaviour and containts useful code
       
   375 
       
   376     A state is defined by:
       
   377         - It's name
       
   378         - It's property (defined right above)
       
   379 
       
   380         - It's next state.
       
   381 
       
   382     XXX maybe we could stick description of the state semantic here.
       
   383     """
       
   384 
       
   385     # plumbery utily
       
   386     def __init__(self, name, properties=0, next=None):
       
   387         self.name = name
       
   388         self.properties = properties
       
   389         assert next is None or self < next
       
   390         self.next = next
       
   391     @util.propertycache
       
   392     def trackheads(self):
       
   393         """Do we need to track heads of changeset in this state ?
       
   394 
       
   395         We don't need to track heads for the last state as this is repo heads"""
       
   396         return self.next is not None
       
   397 
       
   398     # public utility
       
   399     def __cmp__(self, other):
       
   400         """Use property to compare states.
       
   401 
       
   402         This is a naiv approach that assume the  the next state are strictly
       
   403         more property than the one before
       
   404         # assert min(self, other).properties = self.properties & other.properties
       
   405         """
       
   406         return cmp(self.properties, other.properties)
       
   407 
       
   408     @property
       
   409     def mutable(self):
       
   410         return bool(self.properties & _MUTABLE)
       
   411 
       
   412     # display code
       
   413     def __repr__(self):
       
   414         return 'state(%s)' % self.name
       
   415 
       
   416     def __str__(self):
       
   417         return self.name
       
   418 
       
   419 
       
   420     # revset utility
       
   421     @util.propertycache
       
   422     def _revsetheads(self):
       
   423         """function to be used by revset to finds heads of this states"""
       
   424         assert self.trackheads
       
   425         def revsetheads(repo, subset, x):
       
   426             args = revset.getargs(x, 0, 0, 'publicheads takes no arguments')
       
   427             heads = []
       
   428             for h in repo._statesheads[self]:
       
   429                 try:
       
   430                     heads.append(repo.changelog.rev(h))
       
   431                 except error.LookupError:
       
   432                     pass
       
   433             heads.sort()
       
   434             return heads
       
   435         return revsetheads
       
   436 
       
   437     @util.propertycache
       
   438     def headssymbol(self):
       
   439         """name of the revset symbols"""
       
   440         if self.trackheads:
       
   441             return "%sheads" % self.name
       
   442         else:
       
   443             return 'heads'
       
   444 
       
   445 # Actual state definition
       
   446 
       
   447 ST2 = state('draft', _NOSHARE | _MUTABLE)
       
   448 ST1 = state('ready', _MUTABLE, next=ST2)
       
   449 ST0 = state('published', next=ST1)
       
   450 
       
   451 # all available state
       
   452 STATES = (ST0, ST1, ST2)
       
   453 # all available state by name
       
   454 STATESMAP =dict([(st.name, st) for st in STATES])
       
   455 
       
   456 @util.cachefunc
       
   457 def laststatewithout(prop):
       
   458     """Find the states with the most property but <prop>
       
   459 
       
   460     (This function is necessary because the whole state stuff are abstracted)"""
       
   461     for state in STATES:
       
   462         if not state.properties & prop:
       
   463             candidate = state
       
   464         else:
       
   465             return candidate
       
   466 
       
   467 # util function
       
   468 #############################
       
   469 def noderange(repo, revsets):
       
   470     """The same as revrange but return node"""
       
   471     return map(repo.changelog.node,
       
   472                scmutil.revrange(repo, revsets))
       
   473 
       
   474 # Patch changectx
       
   475 #############################
       
   476 
       
   477 def state(ctx):
       
   478     """return the state objet associated to the context"""
       
   479     if ctx.node()is None:
       
   480         return STATES[-1]
       
   481     return ctx._repo.nodestate(ctx.node())
       
   482 context.changectx.state = state
       
   483 
       
   484 # improve template
       
   485 #############################
       
   486 
       
   487 def showstate(ctx, **args):
       
   488     """Show the name of the state associated with the context"""
       
   489     return ctx.state()
       
   490 
       
   491 
       
   492 # New commands
       
   493 #############################
       
   494 
       
   495 
       
   496 def cmdstates(ui, repo, *states, **opt):
       
   497     """view and modify activated states.
       
   498 
       
   499     With no argument, list activated state.
       
   500 
       
   501     With argument, activate the state in argument.
       
   502 
       
   503     With argument plus the --off switch, deactivate the state in argument.
       
   504 
       
   505     note: published state are alway activated."""
       
   506 
       
   507     if not states:
       
   508         for st in sorted(repo._enabledstates):
       
   509             ui.write('%s\n' % st)
       
   510     else:
       
   511         off = opt.get('off', False)
       
   512         for state_name in states:
       
   513             for st in STATES:
       
   514                 if st.name == state_name:
       
   515                     break
       
   516             else:
       
   517                 ui.write_err(_('no state named %s\n') % state_name)
       
   518                 return 1
       
   519             if off:
       
   520                 if st in repo._enabledstates:
       
   521                     repo.disablestate(st)
       
   522                 else:
       
   523                     ui.write_err(_('state %s already deactivated\n') %
       
   524                                  state_name)
       
   525 
       
   526             else:
       
   527                 repo.enablestate(st, not opt.get('clever'))
       
   528         repo._writeenabledstates()
       
   529     return 0
       
   530 
       
   531 cmdtable = {'states': (cmdstates, [
       
   532     ('', 'off', False, _('desactivate the state') ),
       
   533     ('', 'clever', False, _('do not fix lower when activating the state') )],
       
   534     '<state>')}
       
   535 
       
   536 # automatic generation of command that set state
       
   537 def makecmd(state):
       
   538     def cmdmoveheads(ui, repo, *changesets, **opts):
       
   539         """set revisions in %s state
       
   540 
       
   541         This command also alter state of ancestors if necessary.
       
   542         """ % state
       
   543         if not state in repo._enabledstates:
       
   544             raise error.Abort(
       
   545                     _('state %s is not activated' % state),
       
   546                     hint=_('try ``hg states %s`` before' % state))
       
   547         if opts.get('exact'):
       
   548             repo.setstate_unsafe(state, changesets)
       
   549             return 0
       
   550         revs = scmutil.revrange(repo, changesets)
       
   551         repo.setstate(state, [repo.changelog.node(rev) for rev in revs])
       
   552         return 0
       
   553     return cmdmoveheads
       
   554 
       
   555 for state in STATES:
       
   556     cmdmoveheads = makecmd(state)
       
   557     cmdtable[state.name] = (cmdmoveheads, [
       
   558         ('e', 'exact', False, _('move boundary so that revs are exactly in '
       
   559                                'state <state> ( all([rev.state == <state> for '
       
   560                                 'rev in revs]))'))
       
   561         ], '<revset>')
       
   562 
       
   563 # Pushkey mechanism for mutable
       
   564 #########################################
       
   565 
       
   566 def pushstatesheads(repo, key, old, new):
       
   567     """receive a new state for a revision via pushkey
       
   568 
       
   569     It only move revision from a state to a <= one
       
   570 
       
   571     Return True if the <key> revision exist in the repository
       
   572     Return False otherwise. (and doesn't alter any state)"""
       
   573     st = STATESMAP[new]
       
   574     w = repo.wlock()
       
   575     try:
       
   576         newhead = node.bin(key)
       
   577         try:
       
   578             repo[newhead]
       
   579         except error.RepoLookupError:
       
   580             return False
       
   581         repo.setstate(st, [newhead])
       
   582         return True
       
   583     finally:
       
   584         w.release()
       
   585 
       
   586 def liststatesheads(repo):
       
   587     """List the boundary of all states.
       
   588 
       
   589     {"node-hex" -> "comma separated list of state",}
       
   590     """
       
   591     keys = {}
       
   592     for state in [st for st in STATES if st.trackheads]:
       
   593         for head in repo.stateheads(state):
       
   594             head = node.hex(head)
       
   595             if head in keys:
       
   596                 keys[head] += ',' + state.name
       
   597             else:
       
   598                 keys[head] = state.name
       
   599     return keys
       
   600 
       
   601 pushkey.register('states-heads', pushstatesheads, liststatesheads)
       
   602 
       
   603 
       
   604 # Wrap discovery
       
   605 ####################
       
   606 def filterprivateout(orig, repo, *args,**kwargs):
       
   607     """wrapper for findcommonoutgoing that remove _NOSHARE"""
       
   608     common, heads = orig(repo, *args, **kwargs)
       
   609     if getattr(repo, '_reducehead', None) is not None:
       
   610         return common, repo._reducehead(heads)
       
   611 def filterprivatein(orig, repo, remote, *args, **kwargs):
       
   612     """wrapper for findcommonincoming that remove _NOSHARE"""
       
   613     common, anyinc, heads = orig(repo, remote, *args, **kwargs)
       
   614     if getattr(remote, '_reducehead', None) is not None:
       
   615         heads = remote._reducehead(heads)
       
   616     return common, anyinc, heads
       
   617 
       
   618 # states boundary IO
       
   619 #####################
       
   620 
       
   621 def _readheadsfile(repo, filename):
       
   622     """read head from the given file
       
   623 
       
   624     XXX move me elsewhere"""
       
   625     heads = [nullid]
       
   626     try:
       
   627         f = repo.opener(filename)
       
   628         try:
       
   629             heads = sorted([node.bin(n) for n in f.read().split() if n])
       
   630         finally:
       
   631             f.close()
       
   632     except IOError:
       
   633         pass
       
   634     return heads
       
   635 
       
   636 def _readstatesheads(repo, undo=False):
       
   637     """read all state heads
       
   638 
       
   639     XXX move me elsewhere"""
       
   640     statesheads = {}
       
   641     for state in STATES:
       
   642         if state.trackheads:
       
   643             filemask = 'states/%s-heads'
       
   644             filename = filemask % state.name
       
   645             statesheads[state] = _readheadsfile(repo, filename)
       
   646     return statesheads
       
   647 
       
   648 def _writeheadsfile(repo, filename, heads):
       
   649     """write given <heads> in the file with at <filename>
       
   650 
       
   651     XXX move me elsewhere"""
       
   652     f = repo.opener(filename, 'w', atomictemp=True)
       
   653     try:
       
   654         for h in heads:
       
   655             f.write(hex(h) + '\n')
       
   656         try:
       
   657             f.rename()
       
   658         except AttributeError: # old version
       
   659             f.close()
       
   660     finally:
       
   661         f.close()
       
   662 
       
   663 def _writestateshead(repo):
       
   664     """write all heads
       
   665 
       
   666     XXX move me elsewhere"""
       
   667     # XXX transaction!
       
   668     for state in STATES:
       
   669         if state.trackheads:
       
   670             filename = 'states/%s-heads' % state.name
       
   671             _writeheadsfile(repo, filename, repo._statesheads[state])
       
   672 
       
   673 # WireProtocols
       
   674 ####################
       
   675 def wireheads(repo, proto):
       
   676     """Altered head command that doesn't include _NOSHARE
       
   677 
       
   678     This is a write protocol command"""
       
   679     st = laststatewithout(_NOSHARE)
       
   680     h = repo.stateheads(st)
       
   681     return wireproto.encodelist(h) + "\n"
       
   682 
       
   683 # Other extension support
       
   684 #########################
       
   685 
       
   686 def wraprebasebuildstate(orig, repo, *args, **kwargs):
       
   687     """Wrapped rebuild state that check for immutable changeset
       
   688 
       
   689     buildstate are the best place i found to hook :-/"""
       
   690     result = orig(repo, *args, **kwargs)
       
   691     if result is not None:
       
   692         # rebase.nullmerge is issued in the detach case
       
   693         rebase = extensions.find('rebase')
       
   694         rebased = [rev for rev, rbst in result[2].items() if rbst != rebase.nullmerge]
       
   695         base = repo.changelog.node(min(rebased))
       
   696         state = repo.nodestate(base)
       
   697         if not state.mutable:
       
   698             raise util.Abort(_('can not rebase published changeset %s')
       
   699                              % node.short(base),
       
   700                              hint=_('see `hg help --extension states` for details'))
       
   701     return result
       
   702 
       
   703 def wrapmqqimport(orig, queue, repo, *args, **kwargs):
       
   704     """Wrapper for rebuild state that deny importing immutable changeset
       
   705     """
       
   706     if 'rev' in kwargs:
       
   707        # we can take the min as non linear import will break
       
   708        # anyway
       
   709        revs = scmutil.revrange(repo, kwargs['rev'])
       
   710        if revs:
       
   711            base = min(revs)
       
   712            basenode = repo.changelog.node(base)
       
   713            state = repo.nodestate(basenode)
       
   714            if not state.mutable:
       
   715                raise util.Abort(_('can not qimport published changeset %s')
       
   716                     % node.short(basenode),
       
   717                     hint=_('see `hg help --extension states` for details'))
       
   718     return orig(queue, repo, *args, **kwargs)
       
   719 
       
   720 def strip(orig, ui, repo, node, backup="all"):
       
   721     cl = repo.changelog
       
   722     striprev = cl.rev(node)
       
   723     revstostrip = set(cl.descendants(striprev))
       
   724     revstostrip.add(striprev)
       
   725     tostrip = set(map(cl.node, revstostrip))
       
   726     # compute the potentially new created states bondaries which are any
       
   727     # parent of the stripped node that are not stripped (may not be heads)
       
   728     newbondaries = set(par for nod in tostrip for par in cl.parents(nod)
       
   729                        if par not in tostrip)
       
   730     # save the current states of newbondaries in a chache as repo.nodestate
       
   731     # must work along the loop. We will use the next loop to add them.
       
   732     statesheads={}
       
   733     for nd in newbondaries:
       
   734         state = repo.nodestate(nd)
       
   735         if state.trackheads:
       
   736             statesheads.setdefault(state, set([])).add(nd)
       
   737 
       
   738     for state, heads in repo._statesheads.iteritems():
       
   739         if not state.trackheads:
       
   740             continue
       
   741         heads = set(heads) - tostrip | statesheads.get(state, set([]))
       
   742         # reduce heads (make them really heads)
       
   743         revs = set(map(cl.rev, heads))
       
   744         minrev = min(revs)
       
   745         for rev in cl.ancestors(*revs):
       
   746             if rev >= minrev:
       
   747                 revs.discard(rev)
       
   748         repo._statesheads[state] = map(cl.node, revs)
       
   749     _writestateshead(repo)
       
   750 
       
   751     return orig(ui, repo, node, backup)
       
   752 
       
   753 
       
   754 def uisetup(ui):
       
   755     """
       
   756     * patch stuff for the _NOSHARE property
       
   757     * add template keyword
       
   758     """
       
   759     # patch discovery
       
   760     extensions.wrapfunction(discovery, 'findcommonoutgoing', filterprivateout)
       
   761     extensions.wrapfunction(discovery, 'findcommonincoming', filterprivatein)
       
   762     extensions.wrapfunction(repair, 'strip', strip)
       
   763 
       
   764     # patch wireprotocol
       
   765     wireproto.commands['heads'] = (wireheads, '')
       
   766 
       
   767     # add template keyword
       
   768     templatekw.keywords['state'] = showstate
       
   769 
       
   770 def extsetup(ui):
       
   771     """Extension setup
       
   772 
       
   773     * add revset entry"""
       
   774     for state in STATES:
       
   775         if state.trackheads:
       
   776             revset.symbols[state.headssymbol] = state._revsetheads
       
   777     # wrap rebase
       
   778     try:
       
   779         rebase = extensions.find('rebase')
       
   780         if rebase:
       
   781             extensions.wrapfunction(rebase, 'buildstate', wraprebasebuildstate)
       
   782     except KeyError:
       
   783         pass # rebase not found
       
   784     # wrap mq
       
   785     try:
       
   786         mq = extensions.find('mq')
       
   787         if mq:
       
   788             extensions.wrapfunction(mq.queue, 'qimport', wrapmqqimport)
       
   789     except KeyError:
       
   790         pass # mq not found
       
   791 
       
   792 
       
   793 
       
   794 def reposetup(ui, repo):
       
   795     """Repository setup
       
   796 
       
   797     * extend repo class with states logic"""
       
   798 
       
   799     if not repo.local():
       
   800         return
       
   801 
       
   802     ocancopy =repo.cancopy
       
   803     opull = repo.pull
       
   804     opush = repo.push
       
   805     o_tag = repo._tag
       
   806     orollback = repo.rollback
       
   807     o_writejournal = repo._writejournal
       
   808     class statefulrepo(repo.__class__):
       
   809         """An extension of repo class that handle state logic
       
   810 
       
   811         - nodestate
       
   812         - stateheads
       
   813         """
       
   814 
       
   815         def nodestate(self, node):
       
   816             """return the state object associated to the given node"""
       
   817             rev = self.changelog.rev(node)
       
   818             for state in STATES:
       
   819                 # avoid for untracked heads
       
   820                 if state.next is not None:
       
   821                     ancestors = map(self.changelog.rev, self.stateheads(state))
       
   822                     ancestors.extend(self.changelog.ancestors(*ancestors))
       
   823                     if rev in ancestors:
       
   824                         break
       
   825             return state
       
   826 
       
   827         def enablestate(self, state, fix_lower=True):
       
   828             if fix_lower:
       
   829                 # at least published which is always activated
       
   830                 lower = max(st for st in self._enabledstates if st < state)
       
   831                 self.setstate(lower, self.stateheads(state))
       
   832             self._enabledstates.add(state)
       
   833 
       
   834         def disablestate(self, state):
       
   835             """Disable empty state.
       
   836             Raise error.Abort if the state is not empty.
       
   837             """
       
   838             # the lowest is mandatory
       
   839             if state == ST0:
       
   840                 raise error.Abort(_('could not disable %s' % state.name))
       
   841             enabled =  self._enabledstates
       
   842             # look up for lower state that is enabled (at least published)
       
   843             lower = max(st for st in self._enabledstates if st < state)
       
   844             if repo.stateheads(state) != repo.stateheads(lower):
       
   845                 raise error.Abort(
       
   846                     _('could not disable non empty state %s' % state.name),
       
   847                     hint=_("You may want to use `hg %s '%sheads()'`"
       
   848                             % (lower.name, state.name))
       
   849                     )
       
   850             else:
       
   851                 enabled.remove(state)
       
   852 
       
   853         def stateheads(self, state):
       
   854             """Return the set of head that define the state"""
       
   855             # look for a relevant state
       
   856             while state.trackheads and state.next not in self._enabledstates:
       
   857                 state = state.next
       
   858             # last state have no cached head.
       
   859             if state.trackheads:
       
   860                 return self._statesheads[state]
       
   861             return self.heads()
       
   862 
       
   863         @util.propertycache
       
   864         def _statesheads(self):
       
   865             """{ state-object -> set(defining head)} mapping"""
       
   866             return _readstatesheads(self)
       
   867 
       
   868         def setstate_unsafe(self, state, changesets):
       
   869             """Change state of targets changesets and it's ancestors.
       
   870 
       
   871             Simplify the list of heads.
       
   872 
       
   873             Unlike ``setstate``, the "lower" states are also changed
       
   874             """
       
   875             #modify "lower" states
       
   876             req_nodes_rst = '|'.join('((%s)::)' % rst for rst in changesets)
       
   877             for st in STATES:
       
   878                 if st >= state: # only modify lower state heads for now
       
   879                     continue
       
   880                 try:
       
   881                     heads = self._statesheads[st]
       
   882                 except KeyError: # forget non-activated states
       
   883                     continue
       
   884                 olds = heads[:]
       
   885                 rst = "heads((::%s()) - (%s))" % (st.headssymbol, req_nodes_rst)
       
   886                 heads[:] = noderange(repo, [rst])
       
   887                 if olds != heads:
       
   888                     _writestateshead(self)
       
   889             #modify the state
       
   890             if state in self._statesheads:
       
   891                 revs = scmutil.revrange(repo, changesets)
       
   892                 repo.setstate(state, [repo.changelog.node(rev) for rev in revs])
       
   893 
       
   894         def setstate(self, state, nodes):
       
   895             """change state of targets changeset and it's ancestors.
       
   896 
       
   897             Simplify the list of head."""
       
   898             assert not isinstance(nodes, basestring), repr(nodes)
       
   899             if not state.trackheads:
       
   900                 return
       
   901             heads = self._statesheads[state]
       
   902             olds = heads[:]
       
   903             heads.extend(nodes)
       
   904             heads[:] = set(heads)
       
   905             heads.sort()
       
   906             if olds != heads:
       
   907                 heads[:] = noderange(repo, ["heads(::%s())" % state.headssymbol])
       
   908                 heads.sort()
       
   909             if olds != heads:
       
   910                 _writestateshead(self)
       
   911             if state.next is not None and state.next.trackheads:
       
   912                 self.setstate(state.next, nodes) # cascading
       
   913 
       
   914         def _reducehead(self, candidates):
       
   915             """recompute a set of heads so it doesn't include _NOSHARE changeset
       
   916 
       
   917             This is basically a complicated method that compute
       
   918             heads(::candidates - _NOSHARE)
       
   919             """
       
   920             selected = set()
       
   921             st = laststatewithout(_NOSHARE)
       
   922             candidates = set(map(self.changelog.rev, candidates))
       
   923             heads = set(map(self.changelog.rev, self.stateheads(st)))
       
   924             shareable = set(self.changelog.ancestors(*heads))
       
   925             shareable.update(heads)
       
   926             selected = candidates & shareable
       
   927             unselected = candidates - shareable
       
   928             for rev in unselected:
       
   929                 for revh in heads:
       
   930                     if self.changelog.descendant(revh, rev):
       
   931                         selected.add(revh)
       
   932             return sorted(map(self.changelog.node, selected))
       
   933 
       
   934         ### enable // disable logic
       
   935 
       
   936         @util.propertycache
       
   937         def _enabledstates(self):
       
   938             """The set of state enabled in this repository"""
       
   939             return self._readenabledstates()
       
   940 
       
   941         def _readenabledstates(self):
       
   942             """read enabled state from disk"""
       
   943             states = set()
       
   944             states.add(ST0)
       
   945             mapping = dict([(st.name, st) for st in STATES])
       
   946             try:
       
   947                 f = self.opener('states/Enabled')
       
   948                 for line in f:
       
   949                     st =  mapping.get(line.strip())
       
   950                     if st is not None:
       
   951                         states.add(st)
       
   952             finally:
       
   953                 return states
       
   954 
       
   955         def _writeenabledstates(self):
       
   956             """read enabled state to disk"""
       
   957             f = self.opener('states/Enabled', 'w', atomictemp=True)
       
   958             try:
       
   959                 for st in self._enabledstates:
       
   960                     f.write(st.name + '\n')
       
   961                 try:
       
   962                     f.rename()
       
   963                 except AttributeError: # old version
       
   964                     f.close()
       
   965             finally:
       
   966                 f.close()
       
   967 
       
   968         ### local clone support
       
   969 
       
   970         def cancopy(self):
       
   971             """deny copy if there is _NOSHARE changeset"""
       
   972             st = laststatewithout(_NOSHARE)
       
   973             return ocancopy() and (self.stateheads(st) == self.heads())
       
   974 
       
   975         ### pull // push support
       
   976 
       
   977         def pull(self, remote, *args, **kwargs):
       
   978             """altered pull that also update states heads on local repo"""
       
   979             result = opull(remote, *args, **kwargs)
       
   980             remoteheads = self._pullstatesheads(remote)
       
   981             for st, heads in remoteheads.iteritems():
       
   982                 self.setstate(st, heads)
       
   983             return result
       
   984 
       
   985         def push(self, remote, *args, **opts):
       
   986             """altered push that also update states heads on local and remote"""
       
   987             result = opush(remote, *args, **opts)
       
   988             if not self.ui.configbool('states', 'bypass', False):
       
   989                 remoteheads = self._pullstatesheads(remote)
       
   990                 for st, heads in remoteheads.iteritems():
       
   991                     self.setstate(st, heads)
       
   992                     if heads != self.stateheads(st):
       
   993                         self._pushstatesheads(remote, st,  heads)
       
   994             return result
       
   995 
       
   996         def _pushstatesheads(self, remote, state, remoteheads):
       
   997             """push head of a given state for remote
       
   998 
       
   999             This handle pushing boundary that does exist on remote host
       
  1000             This is done a very naive way"""
       
  1001             local = set(self.stateheads(state))
       
  1002             missing = local - set(remoteheads)
       
  1003             while missing:
       
  1004                 h = missing.pop()
       
  1005                 ok = remote.pushkey('states-heads', node.hex(h), '', state.name)
       
  1006                 if not ok:
       
  1007                     missing.update(p.node() for p in repo[h].parents())
       
  1008 
       
  1009 
       
  1010         def _pullstatesheads(self, remote):
       
  1011             """pull all remote states boundary locally
       
  1012 
       
  1013             This can only make the boundary move on a newer changeset"""
       
  1014             remoteheads = {}
       
  1015             self.ui.debug('checking for states-heads on remote server')
       
  1016             if 'states-heads' not in remote.listkeys('namespaces'):
       
  1017                 self.ui.debug('states-heads not enabled on the remote server, '
       
  1018                               'marking everything as published\n')
       
  1019                 remoteheads[ST0] = remote.heads()
       
  1020             else:
       
  1021                 self.ui.debug('server has states-heads enabled, merging lists')
       
  1022                 for hex, statenames in  remote.listkeys('states-heads').iteritems():
       
  1023                     for stn in statenames.split(','):
       
  1024                         remoteheads.setdefault(STATESMAP[stn], []).append(node.bin(hex))
       
  1025             return remoteheads
       
  1026 
       
  1027         ### Tag support
       
  1028 
       
  1029         def _tag(self, names, node, *args, **kwargs):
       
  1030             """Altered version of _tag that make tag (and tagging) published"""
       
  1031             tagnode = o_tag(names, node, *args, **kwargs)
       
  1032             if tagnode is not None: # do nothing for local one
       
  1033                 self.setstate(ST0, [node, tagnode])
       
  1034             return tagnode
       
  1035 
       
  1036         ### rollback support
       
  1037 
       
  1038         def _writejournal(self, desc):
       
  1039             """extended _writejournal that also save states"""
       
  1040             entries = list(o_writejournal(desc))
       
  1041             for state in STATES:
       
  1042                 if state.trackheads:
       
  1043                     filename = 'states/%s-heads' % state.name
       
  1044                     filepath = self.join(filename)
       
  1045                     if  os.path.exists(filepath):
       
  1046                         journalname = 'states/journal.%s-heads' % state.name
       
  1047                         journalpath = self.join(journalname)
       
  1048                         util.copyfile(filepath, journalpath)
       
  1049                         entries.append(journalpath)
       
  1050             return tuple(entries)
       
  1051 
       
  1052         def rollback(self, dryrun=False):
       
  1053             """extended rollback that also restore states"""
       
  1054             wlock = lock = None
       
  1055             try:
       
  1056                 wlock = self.wlock()
       
  1057                 lock = self.lock()
       
  1058                 ret = orollback(dryrun)
       
  1059                 if not (ret or dryrun): #rollback did not failed
       
  1060                     for state in STATES:
       
  1061                         if state.trackheads:
       
  1062                             src  = self.join('states/undo.%s-heads') % state.name
       
  1063                             dest = self.join('states/%s-heads') % state.name
       
  1064                             if os.path.exists(src):
       
  1065                                 util.rename(src, dest)
       
  1066                             elif os.path.exists(dest): #unlink in any case
       
  1067                                 os.unlink(dest)
       
  1068                     self.__dict__.pop('_statesheads', None)
       
  1069                 return ret
       
  1070             finally:
       
  1071                 release(lock, wlock)
       
  1072 
       
  1073     repo.__class__ = statefulrepo
       
  1074