--- a/hgext/states.py Mon Dec 19 12:19:00 2011 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1074 +0,0 @@
-# states.py - introduce the state concept for mercurial changeset
-#
-# Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
-# Logilab SA <contact@logilab.fr>
-# Augie Fackler <durin42@gmail.com>
-#
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-
-'''introduce the state concept for mercurial changeset
-
-(see http://mercurial.selenic.com/wiki/StatesPlan)
-
-General concept
-===============
-
-This extension adds the state concept. A changeset are now in a specific state
-that control they mutability and they exchange.
-
-States properties
-.................
-
-The states extension currently alter two property for changeset
-
-:mutability: history rewritten tool should refuse to work on immutable changeset
-:sharing: shared changeset are exchanged during pull and push. other are not
-
-Here is a small summary of the current property of state existing state::
-
- || || mutable || shared ||
- || published || || x ||
- || ready || x || x ||
- || draft || x || ||
-
-States consistency and ordering
-...............................
-
-States of changesets have to be consistent with each other. A changeset can only have ancestors of it's state (or a compatible states)
-
-Example:
-
- A ``published`` changeset can't have a ``draft`` parent.
-
-a state is compatible with itself and all "smaller" states. Order is as follow::
-
- published < ready < draft
-
-
-.. note:
-
- This section if probably far too conceptual for people. The result is just
- that: A ``published`` changeset can only have ``published`` ancestors. A
- ``ready`` changeset can only have ``published`` or ``ready`` ancestors.
-
- Moreover There is a need for a nice word to refer to "a state smaller than another"
-
-
-States details
-==============
-
-
-published
- Changesets in the ``published`` state are the core of the history. They are
- changesets that you published to the world. People can expect them to always
- exist. They are changesets as you know them. **By default all changesets
- are published**
-
- - They are exchanged with other repositories (included in pull//push).
-
- - They are not mutable, extensions rewriting history should refuse to
- rewrite them.
-
-ready
- Changesets in the ``ready`` state have not yet been accepted in the
- immutable history. You can share them with others for review, testing or
- improvement. Any ``ready`` changeset can either be included in the
- published history (and become immutable) or be rewritten and never make it
- to the published history.
-
- - They are exchanged with other repositories (included in pull//push).
-
- - They are mutable, extensions rewriting history accept to work on them.
-
-draft
-
- Changesets in the ``draft`` state are heavy work in progress you are not
- yet willing to share with others.
-
- - They are not exchanged with other repositories. pull//push do not see them.
- - They are mutable, extensions rewriting history accept to work on them.
-
---
-
-.. note:
-
- The Dead states mentionned in on the wiki page are missing. There is two main reason for it:
-
- 1. The ``dead`` state has a different behaviour that requires more work to be
- implemented.
-
- 2. I believe that the use cases of ``dead changeset`` are better covered by
- the ``obsolete`` extension.
-
---
-
-.. note:
-
- I'm tempted to add a state with the same property that ``ready`` for review
- workflow.::
-
- || || mutable || shared ||
- || published || || x ||
- || ready || x || x ||
- || inprogress|| x || x ||
- || draft || x || ||
-
- The ``ready`` state would be for changeset that wait review of someone that
- can "publish" them.
-
-
-
-Current Feature and usage
-=========================
-
-
-Enabling states
-...............
-
-The extension adds a :hg:`hg states` command to display and choose which states
-are used by a repository, see :hg:`hg states` for details.
-
-By default all changesets in the repository are ``published``. Other states
-must be explicitly activated. Changeset in a remote repository that doesn't
-support states are all seen as ``published``.
-
-.. note:
-
- When a state is not activated, changesets in this state are handled as
- changesets of the previous state it (``draft`` are handled as ``ready``,
- ``ready`` are handled as ``published``).
-
-TODO:
-
-- have a configuration in hgrc::
-
- [states]
- ready=(off|on)(-inherit)?
- <state>=(off|on)(-inherit)?
-
- :off: state disabled for new repo
- :on: state enabled for new repo
- :inherit: if present, inherit states of source on :hg:`clone`.
-
-- display the number of changesets that change state when activating a state.
-
-
-
-State transition
-................
-
-Changeset you create locally will be in the ``draft`` state. (or any previous
-state if draft isn't enabled)
-
-There is some situation where the state of a changeset will change
-automatically. Automatic movement always go in the same direction.: ``draft ->
-``ready`` -> ``published``
-
-1. When you pull or push boundary move. Common changeset that are ``published`` in
-one of the two repository are set to ``published``. Same goes for ``ready`` etc
-(states are evaluated from in increasing order XXX I bet no one understand this
-parenthesis. Pull operation alter the local repository. push alter both local
-and remote repository.
-
-.. note:
-
- As Repository without any specific state have all their changeset
- ``published``, Pushing to such repo will ``publish`` all common changeset.
-
-2. Tagged changeset get automatically Published. The tagging changeset is
-tagged too... This doesn't apply to local tag.
-
-
-You can also manually change changeset state with a dedicated command for each
-state. See :hg:`published`, :hg:`ready` and :hg:`draft` for details.
-
-XXX maybe we can details the general behaviour here
-
-:hg <state> revs: move boundary of state so it includes revs
- ( revs included in ::<state>heads())
-:hg --exact <state> revs: move boundary so that revs are exactly in state
- <state> ( all([rev.state == <state> for rev in
- revs]))
-:hg --exact --force <state> revs: move boundary event if it create inconsistency
- (with tag for example)
-
-TODO:
-
-- implement consistency check
-
-- implement --force
-
-
-Existing command change
-.......................
-
-As said in the previous section:
-
-:commit: Create draft changeset (or the first enabled previous changeset).
-:tag: Move tagged and tagging changeset in the ``published`` state.
-:incoming: Exclude ``draft`` changeset of remote repository.
-:outgoing: Exclude ``draft`` changeset of local repository.
-:pull: As :hg:`in` + change state of local changeset according to remote side.
-:push: As :hg:`out` + sync state of common changeset on both side
-:rollback: rollback restore states heads as before the last transaction (see bookmark)
-
-State Transition control
-.........................
-
-There is currently no way to control who can alter boundary (The most notable
-usecase is about the published one).
-
-This is probably needed quickly
-
-XXX TODO: Proper behaviour when heads file are chmoded whould be a first step.
-
-XXX We are going to need hooks (pre and post) hook on state transition too.
-
-Template
-........
-
-A new template keyword ``{state}`` has been added.
-
-Revset
-......
-
-We add new ``readyheads()`` and ``publishedheads()`` revset directives. This
-returns the heads of each state **as if all of them were activated**.
-
-XXX TODO - I would like to
-
-- move the current ``<state>heads()`` directives to
- _``<state>heads()``
-
-- add ``<state>heads()`` directives to that return the currently in used heads
-
-- add ``<state>()`` directives that match all node in a state.
-
-Context
-.......
-
-The ``context`` class gain a new method ``states()`` that return a ``state`` object. The
-most notable property of this states object are ```name`` and ``mutable``.
-
-Other extensions
-................
-
-:rebase: can't rebase immutable changeset.
-:mq: can't qimport immutable changeset.
-
-TODO: publishing a changeset should qfinish mq patches.
-
-
-
-Implementation
-==============
-
-State definition
-................
-
-Conceptually:
-
-The set of node in the states are defined by the set of the state heads. This allow
-easy storage, exchange and consistency.
-
-.. note: A cache of the complete set of node that belong to a states will
- probably be need for performance.
-
-Code wise:
-
-There is a ``state`` class that hold the state property and several useful
-logic (name, revset entry etc).
-
-All defined states are accessible thought the STATES tuple at the ROOT of the
-module. Or the STATESMAP dictionary that allow to fetch a state from it's
-name.
-
-You can get and edit the list head node that define a state with two methods on
-repo.
-
-:stateheads(<state>): Returns the list of heads node that define a states
-:setstate(<state>, [nodes]): Move states boundary forward to include the given
- nodes in the given states.
-
-Those methods handle ``node`` and not rev as it seems more resilient to me that
-rev in a mutable world. Maybe it' would make more sens to have ``node`` store
-on disk but revision in the code.
-
-Storage
-.......
-
-States related data are stored in the ``.hg/states/`` directory.
-
-The ``.hg/states/Enabled`` file list the states enabled in this
-repository. This data is *not* stored in the .hg/hgrc because the .hg/hgrc
-might be ignored for trust reason. As missing und with states can be pretty
-annoying. (publishing unfinalized changeset, pulling draft one etc) we don't
-want trust issue to interfer with enabled states information.
-
-``.hg/states/<state>-heads`` file list the nodes that define a states.
-
-_NOSHARE filtering
-..................
-
-Any changeset in a state with a _NOSHARE property will be exclude from pull,
-push, clone, incoming, outgoing and bundle. It is done through three mechanism:
-
-1. Wrapping the findcommonincoming and findcommonoutgoing code with (not very
- efficient) logic that recompute the exchanged heads.
-
-2. Altering ``heads`` wireprotocol command to return sharead heads.
-
-3. Disabling hardlink cloning when there is _NOSHARE changeset available.
-
-Internal plumbery
------------------
-
-sum up of what we do:
-
-* state are object
-
-* repo.__class__ is extended
-
-* discovery is wrapped up
-
-* wire protocol is patched
-
-* transaction and rollback mechanism are wrapped up.
-
-* XXX we write new version of the boundard whenever something happen. We need a
- smarter and faster way to do this.
-
-
-'''
-import os
-from functools import partial
-from operator import or_
-
-from mercurial.i18n import _
-from mercurial import cmdutil
-from mercurial import scmutil
-from mercurial import context
-from mercurial import revset
-from mercurial import templatekw
-from mercurial import util
-from mercurial import node
-from mercurial.node import nullid, hex, short
-from mercurial import discovery
-from mercurial import extensions
-from mercurial import wireproto
-from mercurial import pushkey
-from mercurial import error
-from mercurial import repair
-from mercurial.lock import release
-
-
-
-# states property constante
-_NOSHARE=2
-_MUTABLE=1
-
-class state(object):
- """State of changeset
-
- An utility object that handle several behaviour and containts useful code
-
- A state is defined by:
- - It's name
- - It's property (defined right above)
-
- - It's next state.
-
- XXX maybe we could stick description of the state semantic here.
- """
-
- # plumbery utily
- def __init__(self, name, properties=0, next=None):
- self.name = name
- self.properties = properties
- assert next is None or self < next
- self.next = next
- @util.propertycache
- def trackheads(self):
- """Do we need to track heads of changeset in this state ?
-
- We don't need to track heads for the last state as this is repo heads"""
- return self.next is not None
-
- # public utility
- def __cmp__(self, other):
- """Use property to compare states.
-
- This is a naiv approach that assume the the next state are strictly
- more property than the one before
- # assert min(self, other).properties = self.properties & other.properties
- """
- return cmp(self.properties, other.properties)
-
- @property
- def mutable(self):
- return bool(self.properties & _MUTABLE)
-
- # display code
- def __repr__(self):
- return 'state(%s)' % self.name
-
- def __str__(self):
- return self.name
-
-
- # revset utility
- @util.propertycache
- def _revsetheads(self):
- """function to be used by revset to finds heads of this states"""
- assert self.trackheads
- def revsetheads(repo, subset, x):
- args = revset.getargs(x, 0, 0, 'publicheads takes no arguments')
- heads = []
- for h in repo._statesheads[self]:
- try:
- heads.append(repo.changelog.rev(h))
- except error.LookupError:
- pass
- heads.sort()
- return heads
- return revsetheads
-
- @util.propertycache
- def headssymbol(self):
- """name of the revset symbols"""
- if self.trackheads:
- return "%sheads" % self.name
- else:
- return 'heads'
-
-# Actual state definition
-
-ST2 = state('draft', _NOSHARE | _MUTABLE)
-ST1 = state('ready', _MUTABLE, next=ST2)
-ST0 = state('published', next=ST1)
-
-# all available state
-STATES = (ST0, ST1, ST2)
-# all available state by name
-STATESMAP =dict([(st.name, st) for st in STATES])
-
-@util.cachefunc
-def laststatewithout(prop):
- """Find the states with the most property but <prop>
-
- (This function is necessary because the whole state stuff are abstracted)"""
- for state in STATES:
- if not state.properties & prop:
- candidate = state
- else:
- return candidate
-
-# util function
-#############################
-def noderange(repo, revsets):
- """The same as revrange but return node"""
- return map(repo.changelog.node,
- scmutil.revrange(repo, revsets))
-
-# Patch changectx
-#############################
-
-def state(ctx):
- """return the state objet associated to the context"""
- if ctx.node()is None:
- return STATES[-1]
- return ctx._repo.nodestate(ctx.node())
-context.changectx.state = state
-
-# improve template
-#############################
-
-def showstate(ctx, **args):
- """Show the name of the state associated with the context"""
- return ctx.state()
-
-
-# New commands
-#############################
-
-
-def cmdstates(ui, repo, *states, **opt):
- """view and modify activated states.
-
- With no argument, list activated state.
-
- With argument, activate the state in argument.
-
- With argument plus the --off switch, deactivate the state in argument.
-
- note: published state are alway activated."""
-
- if not states:
- for st in sorted(repo._enabledstates):
- ui.write('%s\n' % st)
- else:
- off = opt.get('off', False)
- for state_name in states:
- for st in STATES:
- if st.name == state_name:
- break
- else:
- ui.write_err(_('no state named %s\n') % state_name)
- return 1
- if off:
- if st in repo._enabledstates:
- repo.disablestate(st)
- else:
- ui.write_err(_('state %s already deactivated\n') %
- state_name)
-
- else:
- repo.enablestate(st, not opt.get('clever'))
- repo._writeenabledstates()
- return 0
-
-cmdtable = {'states': (cmdstates, [
- ('', 'off', False, _('desactivate the state') ),
- ('', 'clever', False, _('do not fix lower when activating the state') )],
- '<state>')}
-
-# automatic generation of command that set state
-def makecmd(state):
- def cmdmoveheads(ui, repo, *changesets, **opts):
- """set revisions in %s state
-
- This command also alter state of ancestors if necessary.
- """ % state
- if not state in repo._enabledstates:
- raise error.Abort(
- _('state %s is not activated' % state),
- hint=_('try ``hg states %s`` before' % state))
- if opts.get('exact'):
- repo.setstate_unsafe(state, changesets)
- return 0
- revs = scmutil.revrange(repo, changesets)
- repo.setstate(state, [repo.changelog.node(rev) for rev in revs])
- return 0
- return cmdmoveheads
-
-for state in STATES:
- cmdmoveheads = makecmd(state)
- cmdtable[state.name] = (cmdmoveheads, [
- ('e', 'exact', False, _('move boundary so that revs are exactly in '
- 'state <state> ( all([rev.state == <state> for '
- 'rev in revs]))'))
- ], '<revset>')
-
-# Pushkey mechanism for mutable
-#########################################
-
-def pushstatesheads(repo, key, old, new):
- """receive a new state for a revision via pushkey
-
- It only move revision from a state to a <= one
-
- Return True if the <key> revision exist in the repository
- Return False otherwise. (and doesn't alter any state)"""
- st = STATESMAP[new]
- w = repo.wlock()
- try:
- newhead = node.bin(key)
- try:
- repo[newhead]
- except error.RepoLookupError:
- return False
- repo.setstate(st, [newhead])
- return True
- finally:
- w.release()
-
-def liststatesheads(repo):
- """List the boundary of all states.
-
- {"node-hex" -> "comma separated list of state",}
- """
- keys = {}
- for state in [st for st in STATES if st.trackheads]:
- for head in repo.stateheads(state):
- head = node.hex(head)
- if head in keys:
- keys[head] += ',' + state.name
- else:
- keys[head] = state.name
- return keys
-
-pushkey.register('states-heads', pushstatesheads, liststatesheads)
-
-
-# Wrap discovery
-####################
-def filterprivateout(orig, repo, *args,**kwargs):
- """wrapper for findcommonoutgoing that remove _NOSHARE"""
- common, heads = orig(repo, *args, **kwargs)
- if getattr(repo, '_reducehead', None) is not None:
- return common, repo._reducehead(heads)
-def filterprivatein(orig, repo, remote, *args, **kwargs):
- """wrapper for findcommonincoming that remove _NOSHARE"""
- common, anyinc, heads = orig(repo, remote, *args, **kwargs)
- if getattr(remote, '_reducehead', None) is not None:
- heads = remote._reducehead(heads)
- return common, anyinc, heads
-
-# states boundary IO
-#####################
-
-def _readheadsfile(repo, filename):
- """read head from the given file
-
- XXX move me elsewhere"""
- heads = [nullid]
- try:
- f = repo.opener(filename)
- try:
- heads = sorted([node.bin(n) for n in f.read().split() if n])
- finally:
- f.close()
- except IOError:
- pass
- return heads
-
-def _readstatesheads(repo, undo=False):
- """read all state heads
-
- XXX move me elsewhere"""
- statesheads = {}
- for state in STATES:
- if state.trackheads:
- filemask = 'states/%s-heads'
- filename = filemask % state.name
- statesheads[state] = _readheadsfile(repo, filename)
- return statesheads
-
-def _writeheadsfile(repo, filename, heads):
- """write given <heads> in the file with at <filename>
-
- XXX move me elsewhere"""
- f = repo.opener(filename, 'w', atomictemp=True)
- try:
- for h in heads:
- f.write(hex(h) + '\n')
- try:
- f.rename()
- except AttributeError: # old version
- f.close()
- finally:
- f.close()
-
-def _writestateshead(repo):
- """write all heads
-
- XXX move me elsewhere"""
- # XXX transaction!
- for state in STATES:
- if state.trackheads:
- filename = 'states/%s-heads' % state.name
- _writeheadsfile(repo, filename, repo._statesheads[state])
-
-# WireProtocols
-####################
-def wireheads(repo, proto):
- """Altered head command that doesn't include _NOSHARE
-
- This is a write protocol command"""
- st = laststatewithout(_NOSHARE)
- h = repo.stateheads(st)
- return wireproto.encodelist(h) + "\n"
-
-# Other extension support
-#########################
-
-def wraprebasebuildstate(orig, repo, *args, **kwargs):
- """Wrapped rebuild state that check for immutable changeset
-
- buildstate are the best place i found to hook :-/"""
- result = orig(repo, *args, **kwargs)
- if result is not None:
- # rebase.nullmerge is issued in the detach case
- rebase = extensions.find('rebase')
- rebased = [rev for rev, rbst in result[2].items() if rbst != rebase.nullmerge]
- base = repo.changelog.node(min(rebased))
- state = repo.nodestate(base)
- if not state.mutable:
- raise util.Abort(_('can not rebase published changeset %s')
- % node.short(base),
- hint=_('see `hg help --extension states` for details'))
- return result
-
-def wrapmqqimport(orig, queue, repo, *args, **kwargs):
- """Wrapper for rebuild state that deny importing immutable changeset
- """
- if 'rev' in kwargs:
- # we can take the min as non linear import will break
- # anyway
- revs = scmutil.revrange(repo, kwargs['rev'])
- if revs:
- base = min(revs)
- basenode = repo.changelog.node(base)
- state = repo.nodestate(basenode)
- if not state.mutable:
- raise util.Abort(_('can not qimport published changeset %s')
- % node.short(basenode),
- hint=_('see `hg help --extension states` for details'))
- return orig(queue, repo, *args, **kwargs)
-
-def strip(orig, ui, repo, node, backup="all"):
- cl = repo.changelog
- striprev = cl.rev(node)
- revstostrip = set(cl.descendants(striprev))
- revstostrip.add(striprev)
- tostrip = set(map(cl.node, revstostrip))
- # compute the potentially new created states bondaries which are any
- # parent of the stripped node that are not stripped (may not be heads)
- newbondaries = set(par for nod in tostrip for par in cl.parents(nod)
- if par not in tostrip)
- # save the current states of newbondaries in a chache as repo.nodestate
- # must work along the loop. We will use the next loop to add them.
- statesheads={}
- for nd in newbondaries:
- state = repo.nodestate(nd)
- if state.trackheads:
- statesheads.setdefault(state, set([])).add(nd)
-
- for state, heads in repo._statesheads.iteritems():
- if not state.trackheads:
- continue
- heads = set(heads) - tostrip | statesheads.get(state, set([]))
- # reduce heads (make them really heads)
- revs = set(map(cl.rev, heads))
- minrev = min(revs)
- for rev in cl.ancestors(*revs):
- if rev >= minrev:
- revs.discard(rev)
- repo._statesheads[state] = map(cl.node, revs)
- _writestateshead(repo)
-
- return orig(ui, repo, node, backup)
-
-
-def uisetup(ui):
- """
- * patch stuff for the _NOSHARE property
- * add template keyword
- """
- # patch discovery
- extensions.wrapfunction(discovery, 'findcommonoutgoing', filterprivateout)
- extensions.wrapfunction(discovery, 'findcommonincoming', filterprivatein)
- extensions.wrapfunction(repair, 'strip', strip)
-
- # patch wireprotocol
- wireproto.commands['heads'] = (wireheads, '')
-
- # add template keyword
- templatekw.keywords['state'] = showstate
-
-def extsetup(ui):
- """Extension setup
-
- * add revset entry"""
- for state in STATES:
- if state.trackheads:
- revset.symbols[state.headssymbol] = state._revsetheads
- # wrap rebase
- try:
- rebase = extensions.find('rebase')
- if rebase:
- extensions.wrapfunction(rebase, 'buildstate', wraprebasebuildstate)
- except KeyError:
- pass # rebase not found
- # wrap mq
- try:
- mq = extensions.find('mq')
- if mq:
- extensions.wrapfunction(mq.queue, 'qimport', wrapmqqimport)
- except KeyError:
- pass # mq not found
-
-
-
-def reposetup(ui, repo):
- """Repository setup
-
- * extend repo class with states logic"""
-
- if not repo.local():
- return
-
- ocancopy =repo.cancopy
- opull = repo.pull
- opush = repo.push
- o_tag = repo._tag
- orollback = repo.rollback
- o_writejournal = repo._writejournal
- class statefulrepo(repo.__class__):
- """An extension of repo class that handle state logic
-
- - nodestate
- - stateheads
- """
-
- def nodestate(self, node):
- """return the state object associated to the given node"""
- rev = self.changelog.rev(node)
- for state in STATES:
- # avoid for untracked heads
- if state.next is not None:
- ancestors = map(self.changelog.rev, self.stateheads(state))
- ancestors.extend(self.changelog.ancestors(*ancestors))
- if rev in ancestors:
- break
- return state
-
- def enablestate(self, state, fix_lower=True):
- if fix_lower:
- # at least published which is always activated
- lower = max(st for st in self._enabledstates if st < state)
- self.setstate(lower, self.stateheads(state))
- self._enabledstates.add(state)
-
- def disablestate(self, state):
- """Disable empty state.
- Raise error.Abort if the state is not empty.
- """
- # the lowest is mandatory
- if state == ST0:
- raise error.Abort(_('could not disable %s' % state.name))
- enabled = self._enabledstates
- # look up for lower state that is enabled (at least published)
- lower = max(st for st in self._enabledstates if st < state)
- if repo.stateheads(state) != repo.stateheads(lower):
- raise error.Abort(
- _('could not disable non empty state %s' % state.name),
- hint=_("You may want to use `hg %s '%sheads()'`"
- % (lower.name, state.name))
- )
- else:
- enabled.remove(state)
-
- def stateheads(self, state):
- """Return the set of head that define the state"""
- # look for a relevant state
- while state.trackheads and state.next not in self._enabledstates:
- state = state.next
- # last state have no cached head.
- if state.trackheads:
- return self._statesheads[state]
- return self.heads()
-
- @util.propertycache
- def _statesheads(self):
- """{ state-object -> set(defining head)} mapping"""
- return _readstatesheads(self)
-
- def setstate_unsafe(self, state, changesets):
- """Change state of targets changesets and it's ancestors.
-
- Simplify the list of heads.
-
- Unlike ``setstate``, the "lower" states are also changed
- """
- #modify "lower" states
- req_nodes_rst = '|'.join('((%s)::)' % rst for rst in changesets)
- for st in STATES:
- if st >= state: # only modify lower state heads for now
- continue
- try:
- heads = self._statesheads[st]
- except KeyError: # forget non-activated states
- continue
- olds = heads[:]
- rst = "heads((::%s()) - (%s))" % (st.headssymbol, req_nodes_rst)
- heads[:] = noderange(repo, [rst])
- if olds != heads:
- _writestateshead(self)
- #modify the state
- if state in self._statesheads:
- revs = scmutil.revrange(repo, changesets)
- repo.setstate(state, [repo.changelog.node(rev) for rev in revs])
-
- def setstate(self, state, nodes):
- """change state of targets changeset and it's ancestors.
-
- Simplify the list of head."""
- assert not isinstance(nodes, basestring), repr(nodes)
- if not state.trackheads:
- return
- heads = self._statesheads[state]
- olds = heads[:]
- heads.extend(nodes)
- heads[:] = set(heads)
- heads.sort()
- if olds != heads:
- heads[:] = noderange(repo, ["heads(::%s())" % state.headssymbol])
- heads.sort()
- if olds != heads:
- _writestateshead(self)
- if state.next is not None and state.next.trackheads:
- self.setstate(state.next, nodes) # cascading
-
- def _reducehead(self, candidates):
- """recompute a set of heads so it doesn't include _NOSHARE changeset
-
- This is basically a complicated method that compute
- heads(::candidates - _NOSHARE)
- """
- selected = set()
- st = laststatewithout(_NOSHARE)
- candidates = set(map(self.changelog.rev, candidates))
- heads = set(map(self.changelog.rev, self.stateheads(st)))
- shareable = set(self.changelog.ancestors(*heads))
- shareable.update(heads)
- selected = candidates & shareable
- unselected = candidates - shareable
- for rev in unselected:
- for revh in heads:
- if self.changelog.descendant(revh, rev):
- selected.add(revh)
- return sorted(map(self.changelog.node, selected))
-
- ### enable // disable logic
-
- @util.propertycache
- def _enabledstates(self):
- """The set of state enabled in this repository"""
- return self._readenabledstates()
-
- def _readenabledstates(self):
- """read enabled state from disk"""
- states = set()
- states.add(ST0)
- mapping = dict([(st.name, st) for st in STATES])
- try:
- f = self.opener('states/Enabled')
- for line in f:
- st = mapping.get(line.strip())
- if st is not None:
- states.add(st)
- finally:
- return states
-
- def _writeenabledstates(self):
- """read enabled state to disk"""
- f = self.opener('states/Enabled', 'w', atomictemp=True)
- try:
- for st in self._enabledstates:
- f.write(st.name + '\n')
- try:
- f.rename()
- except AttributeError: # old version
- f.close()
- finally:
- f.close()
-
- ### local clone support
-
- def cancopy(self):
- """deny copy if there is _NOSHARE changeset"""
- st = laststatewithout(_NOSHARE)
- return ocancopy() and (self.stateheads(st) == self.heads())
-
- ### pull // push support
-
- def pull(self, remote, *args, **kwargs):
- """altered pull that also update states heads on local repo"""
- result = opull(remote, *args, **kwargs)
- remoteheads = self._pullstatesheads(remote)
- for st, heads in remoteheads.iteritems():
- self.setstate(st, heads)
- return result
-
- def push(self, remote, *args, **opts):
- """altered push that also update states heads on local and remote"""
- result = opush(remote, *args, **opts)
- if not self.ui.configbool('states', 'bypass', False):
- remoteheads = self._pullstatesheads(remote)
- for st, heads in remoteheads.iteritems():
- self.setstate(st, heads)
- if heads != self.stateheads(st):
- self._pushstatesheads(remote, st, heads)
- return result
-
- def _pushstatesheads(self, remote, state, remoteheads):
- """push head of a given state for remote
-
- This handle pushing boundary that does exist on remote host
- This is done a very naive way"""
- local = set(self.stateheads(state))
- missing = local - set(remoteheads)
- while missing:
- h = missing.pop()
- ok = remote.pushkey('states-heads', node.hex(h), '', state.name)
- if not ok:
- missing.update(p.node() for p in repo[h].parents())
-
-
- def _pullstatesheads(self, remote):
- """pull all remote states boundary locally
-
- This can only make the boundary move on a newer changeset"""
- remoteheads = {}
- self.ui.debug('checking for states-heads on remote server')
- if 'states-heads' not in remote.listkeys('namespaces'):
- self.ui.debug('states-heads not enabled on the remote server, '
- 'marking everything as published\n')
- remoteheads[ST0] = remote.heads()
- else:
- self.ui.debug('server has states-heads enabled, merging lists')
- for hex, statenames in remote.listkeys('states-heads').iteritems():
- for stn in statenames.split(','):
- remoteheads.setdefault(STATESMAP[stn], []).append(node.bin(hex))
- return remoteheads
-
- ### Tag support
-
- def _tag(self, names, node, *args, **kwargs):
- """Altered version of _tag that make tag (and tagging) published"""
- tagnode = o_tag(names, node, *args, **kwargs)
- if tagnode is not None: # do nothing for local one
- self.setstate(ST0, [node, tagnode])
- return tagnode
-
- ### rollback support
-
- def _writejournal(self, desc):
- """extended _writejournal that also save states"""
- entries = list(o_writejournal(desc))
- for state in STATES:
- if state.trackheads:
- filename = 'states/%s-heads' % state.name
- filepath = self.join(filename)
- if os.path.exists(filepath):
- journalname = 'states/journal.%s-heads' % state.name
- journalpath = self.join(journalname)
- util.copyfile(filepath, journalpath)
- entries.append(journalpath)
- return tuple(entries)
-
- def rollback(self, dryrun=False):
- """extended rollback that also restore states"""
- wlock = lock = None
- try:
- wlock = self.wlock()
- lock = self.lock()
- ret = orollback(dryrun)
- if not (ret or dryrun): #rollback did not failed
- for state in STATES:
- if state.trackheads:
- src = self.join('states/undo.%s-heads') % state.name
- dest = self.join('states/%s-heads') % state.name
- if os.path.exists(src):
- util.rename(src, dest)
- elif os.path.exists(dest): #unlink in any case
- os.unlink(dest)
- self.__dict__.pop('_statesheads', None)
- return ret
- finally:
- release(lock, wlock)
-
- repo.__class__ = statefulrepo
-
--- a/tests/test-state-strip.t Mon Dec 19 12:19:00 2011 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,288 +0,0 @@
-
-
- $ cat >> $HGRCPATH <<EOF
- > [web]
- > push_ssl = false
- > allow_push = *
- > [extensions]
- > hgext.mq=
- > hgext.graphlog=
- > EOF
- $ echo "states=$(echo $(dirname $TESTDIR))/hgext/states.py" >> $HGRCPATH
-
- $ mkcommit() {
- > echo "$1" > "$1"
- > hg add "$1"
- > hg ci -m "$1"
- > }
- $ alias hglog='hg glog --template "{desc} {state} {node}\n"'
-
- $ hg init alpha
- $ cd alpha
- $ hg states draft ready
- $ mkcommit 0
- $ mkcommit 1
- $ hg up 0
- 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
- $ mkcommit 2
- created new head
- $ mkcommit 3
- $ mkcommit 4
- $ hg up 3
- 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
- $ mkcommit 5
- created new head
- $ hg up 1
- 1 files updated, 0 files merged, 3 files removed, 0 files unresolved
- $ mkcommit 6
- $ hg published 6
- $ hg ready 4
- $ hglog
- @ 6 published 2a653cad66937648173a936140f09a0e780afd76
- |
- | o 5 draft ffe7eb8acef3efeceaa566b85a1ac419b0ecb856
- | |
- | | o 4 ready 138777f75ddeb6ee0b527cfdb0eebbd1e0037bf6
- | |/
- | o 3 ready 0915e256b0ca7f81dace67bc6fd512bfd1bcab85
- | |
- | o 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- | |
- o | 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
-
-We strip a published heads, so published heads 6 -> 1
- $ hg strip -n 6
- 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
- $ hglog
- o 5 draft ffe7eb8acef3efeceaa566b85a1ac419b0ecb856
- |
- | o 4 ready 138777f75ddeb6ee0b527cfdb0eebbd1e0037bf6
- |/
- o 3 ready 0915e256b0ca7f81dace67bc6fd512bfd1bcab85
- |
- o 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- |
- | @ 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
-
-Back to the previous configuration.
-Then strip accros branches and remove draft changesets completly, and cut in
-the middle of ready changesets
- $ hg up 1
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
- $ mkcommit 6
- $ hg strip -n 3:6
- 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
- $ hglog
- o 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- |
- | @ 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
-Now merge 1 & 2 then strip merging changeset
- $ hg up 1
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
- $ hg merge 2
- 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
- (branch merge, don't forget to commit)
- $ hg commit -m Merge
- $ hg strip -n 3
- 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
- $ hglog
- o 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- |
- | @ 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
-Do the same but with merge changeset as ready
- $ hg up 1
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
- $ hg merge 2
- 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
- (branch merge, don't forget to commit)
- $ hg commit -m Merge
- $ hg ready 3
- $ hg strip -n 3
- 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
- $ hglog
- o 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- |
- | @ 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
- $ hg log --template "{rev} {node}\n" -r 'readyheads()'
- 2 a00ba83de58390cbbdae1fc580df0bb0db2e8e88
-
-More complecated case: a merging changeset inheritate a ready state of one of
-its child and have another child in draft. And We strip the ready child
- $ hg up 1
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
- $ hg merge 2
- 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
- (branch merge, don't forget to commit)
- $ hg ci -m 3
- $ mkcommit 4
- $ hg up 3
- 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
- $ mkcommit 5
- created new head
- $ mkcommit 6
- $ hg ready 6
- $ hglog
- @ 6 ready bfd1096b3dd69e57c184e9f43646a9b7e0dd5927
- |
- o 5 ready 8195da2a3c382a4acd7ce796b4bc74092f1875eb
- |
- | o 4 draft 6aaadc67da669af964adabe681c0a78f46b7ce58
- |/
- o 3 ready e7cd12398be70c568cefab9b4ad86a8a2a728a09
- |\
- | o 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- | |
- o | 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
- $ hg strip -n 3
- 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
- $ hglog
- o 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- |
- | @ 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
-Quitelly the same as before but we strip before the merging node, from another
-branch than the ready changeset, so this changeset is not a parent of the
-the explicitly stripped node.
- $ hg up 1
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
- $ mkcommit 3
- $ hg up 2
- 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
- $ hg merge 3
- 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
- (branch merge, don't forget to commit)
- $ hg ci -m '4'
- $ mkcommit 5
- $ hg published 3
- $ hg ready 4
- $ hglog
- @ 5 draft 0777a3135ec5396c57db4402c71ab8cba2a0ef7e
- |
- o 4 ready 667667458ecc8cf7763dee1ae172a5a9ebf115f3
- |\
- | o 3 published 03fc50a1c0074093104ff6c5357c486781742b64
- | |
- o | 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- | |
- | o 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
- $ hg strip -n 3
- 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
- $ hglog
- o 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- |
- | @ 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
-
-pathologic case
- $ hg up 1
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
- $ mkcommit 3
- $ hg up 2
- 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
- $ hg merge 3
- 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
- (branch merge, don't forget to commit)
- $ hg ci -m '4'
- $ mkcommit 5
- $ hg up 3
- 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
- $ mkcommit 6
- created new head
- $ hg published 3
- $ hg ready 4
- $ hglog
- @ 6 draft aeb74c71311d9305498bbf371746d095b80ff51f
- |
- | o 5 draft 0777a3135ec5396c57db4402c71ab8cba2a0ef7e
- | |
- | o 4 ready 667667458ecc8cf7763dee1ae172a5a9ebf115f3
- |/|
- o | 3 published 03fc50a1c0074093104ff6c5357c486781742b64
- | |
- | o 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- | |
- o | 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
- $ hg strip -n 3
- 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
- $ hglog
- o 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- |
- | @ 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
-pathologic case
- $ hg up 1
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
- $ hg merge 2
- 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
- (branch merge, don't forget to commit)
- $ hg ci -m 3
- $ hg up 2
- 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
- $ mkcommit 4
- created new head
- $ hg merge 3
- 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
- (branch merge, don't forget to commit)
- $ hg ci -m 5
- $ hg up 4
- 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
- $ mkcommit 6
- created new head
- $ hg ready 4
- $ hglog
- @ 6 draft 036d507f2b771a3b7cc88580c93d5037bf4bf1bf
- |
- | o 5 draft 19788060dab104e9385a14c4be2fc5678b9433f0
- |/|
- o | 4 ready 0fc8455e844047eab375a1f51816f697551e34cf
- | |
- | o 3 draft e7cd12398be70c568cefab9b4ad86a8a2a728a09
- |/|
- o | 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- | |
- | o 1 published e8342c9a2ed18fb27f9fdfc48a3105d164a06e77
- |/
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
- $ hg strip -n 1
- $ hglog
- @ 6 draft 036d507f2b771a3b7cc88580c93d5037bf4bf1bf
- |
- o 4 ready 0fc8455e844047eab375a1f51816f697551e34cf
- |
- o 2 ready a00ba83de58390cbbdae1fc580df0bb0db2e8e88
- |
- o 0 published 06254b90631198c9b9e426be9886af92fedc9a2d
-
- $ hg log --template '{desc} {state} {node}\n' -r 'readyheads()'
- 4 ready 0fc8455e844047eab375a1f51816f697551e34cf