Commits

Pierre-Yves David  committed a9c27df Merge

Prepare 0.1.0 by merging default into stable

stable is now compatible with 2.3 only.

  • Participants
  • Parent commits f17a0f8, 53d7e34

Comments (0)

Files changed (28)

 ^html/
 \.pyc$
 ~$
+\.swp$
 \.orig$
 \.rej$
 \.err$
+^tests/easy_run.sh$
+^build/
 Changelog
 ==================
 
+1.0
+
+- Align with Mercurial version 2.3 (drop 2.2 support).
+- stabilize handle killed parent
+- stabilize handle late comer
+- stabilize handle conflicting
+- stabilize get a --continue switch
+- merge and update ignore extinct changeset in most case.
+- new "troubled()" revset
+- summary now reports troubles changesets
+- new touch command
+- new fold command
+- new basic olog alias
+
+- rebase refuse to work on public changeset again
+- rebase explicitly state that there is nothing to rebase because everything is
+  extinct() when that happen.
+- amend now cleanly abort when --change switch is misused
+
+
 0.7 -- 2012-08-06
 
 - hook: work around insanely huge value in obsolete pushkey call

File contrib/nopushpublish.py

+# Extension which prevent changeset to be turn public by push operation
+#
+# Copyright 2011 Logilab SA        <contact@logilab.fr>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+
+from mercurial import extensions, util
+from mercurial import discovery
+
+def checkpublish(orig, repo, remote, outgoing, *args):
+
+    # is remote publishing?
+    publish = True
+    if 'phases' in remote.listkeys('namespaces'):
+        remotephases = remote.listkeys('phases')
+        publish = remotephases.get('publishing', False)
+
+    npublish = 0
+    if publish:
+        for rev in outgoing.missing:
+            if repo[rev].phase():
+                npublish += 1
+    if npublish:
+        repo.ui.warn("Push would publish %s changesets" % npublish)
+
+    ret = orig(repo, remote, outgoing, *args)
+    if npublish:
+        raise util.Abort("Publishing push forbiden",
+                         hint="Use `hg phase -p <rev>` to manually publish them")
+
+    return ret
+
+def uisetup(ui):
+    extensions.wrapfunction(discovery, 'checkheads', checkpublish)

File docs/evolve-collaboration.rst

 Being now done with grammar and typo fixes, Alice decides it is time to
 stabilize again the tree. She does::
 
-    $ hg stabilize
+    $ hg evolve
 
 two times, one for each unstable descendant. The last time, hgview
 shows her a straight line again. Wow! that feels a bit like a

File docs/evolve-faq.rst

 Getting changes out of a commit
 ------------------------------------------------------------
 
-the ``hg uncommit`` commands allow you to rewrite the current commit to not
-include change for some file. The content of target files are not altered on
-disk and back as "modified"::
+The ``hg uncommit`` command lets you rewrite the parent commit without
+selected changed files. Target files content is not altered and
+appears again as "modified"::
 
   $ hg st
   M babar
 Split a changeset
 -----------------------
 
-I you just want to split whole file, you can just use the ``uncommit`` command.
-
+To split on file boundaries, just use ``uncommit`` command.
 
 If you need fine-grained split, there is no official command for that yet.
 However, it is easily achieved by manual operation::
 .. warning:: Beware that rebasing obsolete changesets will result in
              conflicting versions of the changesets.
 
-Stabilize history: ``stabilize``
+Stabilize history: ``evolve``
 ------------------------------------------------------------
 
 When you rewrite (amend) a changeset with children without rewriting
 newest version. This is not done automatically to avoid the
 proliferation of useless hidden changesets.
 
-.. warning:: ``hg stabilize`` have no --continue to use after conflict
-             resolution
-
-.. warning:: stabilization does not handle deletion yet.
-
-.. warning:: obsolete currently relies on changesets in secret phase
-              to avoid exchanging obsolete and unstable changesets.
-
-             XXX details issue here
-
 
 Fix my history afterward: ``prune -n``
 ------------------------------------------------------------
 
 You can also use a debug command
 
-    $ hg debugsuccessors
+    $ hg debugobsolete
       5eb72dbe0cb4 e8db4aa611f6
       c4cbebac3751 4f1c269eab68
 
 
 Extinct changesets are hidden using the *hidden* feature of mercurial.
 
-Only ``hg log`` and ``hgview`` support it. ``hg glog`` Only support that since
-2.2. Other visual viewer don't.
+Only ``hg log``, ``hg glog`` and ``hgview`` support it, other
+graphical viewer do not.
 
 
 

File docs/from-mq.rst

 qnew                            ``commit``
 qrefresh                        ``amend``
 qpop                            ``update`` or ``qdown``
-qpush                           ``update`` or ``gup`` sometimes ``stabilize``
+qpush                           ``update`` or ``gup`` sometimes ``evolve``
 qrm                             ``prune``
 qfold                           ``amend -c`` (for now, ``collapse`` soon)
 qdiff                           ``odiff``
 hg qref -X
 ````````````
 
-To remove change from you current commit use::
+To remove changes from you current commit use::
 
   $ hg uncommit not-ready.txt
 
 The evolution extension adds a command to rewrite the "out of sync"
 changesets:::
 
-  $ hg stabilize
+  $ hg evolve
 
 You can also decide to do it manually using::
 

File docs/index.rst

 
 The effort is split in two parts:
 
- * The **obsolete marker** concept aims to provide an alternative to ``strip``
-   to get rid of changesets.
+ * The **obsolescence marker** concept aims to provide an alternative to ``strip``
+   to get rid of changesets. This concept have been partially implemented in
+   Mercurial 2.3.
 
  * The **evolve** mercurial extension rewrites history using obsolete
    *marker* under the hood.
 
-The first and most important step is by far the **obsolete marker**. However
+The first and most important step is by far the **obsolescence marker**. However
 most users will never be directly exposed to the concept. For this reason
 this manual starts with changeset evolution.
 
 
             Production ready version should hide such details to normal user.
 
+The evolve extension require mercurial 2.3
+
 To enable the evolve extension use::
 
     $ hg clone https://bitbucket.org/marmoute/mutable-history -u stable
-    $ mutable-history/enable.sh >> ~/.hgrc
+    $ echo '[extensions]\nevolve=$PWD/mutable-history/hgext/evolve.py' >> ~/.hgrc
 
-You will probably want to use the associated version of hgview (qt viewer
-recommended). ::
+You will probably want to use hgview_ to visualize obsolescence. Version 1.6.2
+or later is required.
 
-    $ hg clone http://hg-lab.logilab.org/wip/hgview/ -u obsolete
-    $ cd hgview
-    $ python setup.py install --user
+.. _hgview: http://www.logilab.org/project/hgview/
 
-Works with mercurial 2.2
+(The old version 0.7 of evolve works with mercurial 2.2 but have far less feature)
+
 
  ---
 
 Here is a list of know issue that will be fixed later:
 
 
-* ``hg stabilize`` does not handle merge conflict.
-
-    You must fallback to graft or rebase when that happen.
-
-* rewriting conflict are not detected yet``hg stabilize`` does not
-  handle them.
-
-* ``hg update`` can move an obsolete parent
-
 * you need to provide to `graft --continue -O` if you started you
   graft using `-O`.
 
-* ``hg merge`` considers an extinct head to be a valid target, hence requiring
   you to manually specify target all the time.
 
 * trying to exchange obsolete marker with a static http repo will crash.
 
-* trying to exchange a lot of obsolete markers through http crash.
-
-* Extinct changesets are turned secret by various commands.
-
 * Extinct changesets are hidden using the *hidden* feature of mercurial only
   supported by a few commands.
 

File docs/instability.rst

     $ hg pull
     added 3 changeset
     +2 unstable changeset
-    (do you want "hg stabilize" ?)
+    (do you want "hg evolve" ?)
     working directory parent is obsolete!
     $ hg push
     outgoing unstable changesets
-    (use "hg stabilize" or force the push)
+    (use "hg evolve" or force the push)
 
 And should not not encourage people to create instability
 

File docs/obs-terms.rst

 |                     |                          |                             |
 |                     +--------------------------+-----------------------------+
 |                     |                          |                             |
-|                     | **troublesome**          | **unstable**                |
+|                     | **troubled**             | **unstable**                |
 |                     |                          |                             |
-|                     | *troublesome* has        | *unstable* is a changeset   |
+|                     | *troubled*    has        | *unstable* is a changeset   |
 |                     | unresolved issue caused  | with obsolete ancestors.    |
 |                     | by *obsolete* relations. |                             |
 |                     |                          |                             |
 |                     | Possible issues are      | It must be rebased on a     |
-|                     | listed in the next       | non *troublesome* base to   |
+|                     | listed in the next       | non *troubled*    base to   |
 |                     | column. It is possible   | solve the problem.          |
-|                     | for *troublesome*        |                             |
+|                     | for *troubled*           |                             |
 |                     | changeset to combine     | (possible alternative name: |
 |                     | multiple issue at once.  | precarious)                 |
 |                     | (a.k.a. conflicting and  |                             |
 |                     |                          |                             |
 |                     | (possible alternative    | **latecomer**               |
 |                     | names: unsettled,        |                             |
-|                     | troubled)                | *latecomer* is a changeset  |
+|                     | troublesome              | *latecomer* is a changeset  |
 |                     |                          | that tries to be successor  |
 |                     |                          | of  public changesets.      |
 |                     |                          |                             |
 |                     |                          | properly not detected as a  |
 |                     |                          | conflict)                   |
 |                     |                          |                             |
+|                     |                          | (possible alternative names:|
+|                     |                          | clashing, rival)            |
+|                     |                          |                             |
 |                     +--------------------------+-----------------------------+
 |                     |                                                        |
 |                     | Mutable changesets which are neither *obsolete* or     |
-|                     | *troublesome* are *"ok"*.                              |
+|                     | *troubled*    are *"ok"*.                              |
 |                     |                                                        |
 |                     | Do we really need a name for it ? *"ok"* is a pretty   |
 |                     | crappy name :-/ other possibilities are:               |
 
           - "ok" changeset
           - latecomer
-          - troublesome
+          - conflicting
 
           Any better idea are welcome.
 
 - kill: shall has funny effects when you forget "hg" in front of ``hg kill``.
 - obsolete: too vague, too long and too generic.
 
-Stabilize
+evolve
 ```````````````
 
 Automatically resolve *troublesome* changesets
 This is an important name as hg pull/push will suggest it the same way it
 suggest merging when you add heads.
 
-I do not like stabilize much.
-
 alternative names:
 
 - solve (too generic ?)
-- evolve (too vague)
+- stabilize

File docs/tutorials/tutorial.t

   adding manifests
   adding file changes
   added 1 changesets with 1 changes to 1 files (+1 heads)
-  (run 'hg update' to get a working copy)
+  (run 'hg heads .' to see heads, 'hg merge' to merge)
 
 I now have a new heads. Note that this remote head is immutable
 
   |
   | @  ffa278c50818 (draft): bathroom stuff
   | |
-  o |  8a79ae8b029e (draft): bathroom stuff
+  x |  8a79ae8b029e (draft): bathroom stuff
   |/
   o  a2fccc2e7b08 (public): SPAM SPAM
   |
   pushing to $TESTTMP/other
   searching for changes
   abort: push includes an unstable changeset: 9ac5d0e790a2!
-  (use 'hg stabilize' to get a stable history (or --force to proceed))
+  (use 'hg stabilize' to get a stable history or --force to ignore warnings)
   [255]
  
 
   $ hg log -G
   o  ae45c0c3092a (draft): SPAM SPAM SPAM
   |
-  o  437efbcaf700 (draft): animals
+  x  437efbcaf700 (draft): animals
   |
   @  ffa278c50818 (draft): bathroom stuff
   |

File enable.sh

-#!/bin/sh
-
-here=`python -c "import os; print os.path.realpath('$0')"`
-repo_root=`dirname "$here"`
-
-if !( hg --version -q | grep -qe 'version 2\.[2-9]' ); then
-    echo 'You need mercurial 2.2 or later' >&2
-    exit 2
-fi
-
-
-
-cat << EOF >&2
-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-XXX Add lines below to the [extensions] section of you hgrc XXX
-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-
-
-EOF
-
-cat << EOF | sed -e "s#XXXREPOPATHXXX#${repo_root}#"
-[extensions]
-### experimental extensions for history rewriting
-
-# obsolete relation support (will move in core)
-obsolete=XXXREPOPATHXXX/hgext/obsolete.py
-
-# history rewriting UI
-# needed by evolve
-hgext.rebase=
-evolve=XXXREPOPATHXXX/hgext/evolve.py
-
-
-[alias]
-### useful alias to check future amend result
-# equivalent to the qdiff command for mq
-
-# diff
-pdiff=diff --rev .^
-
-# status
-pstatus=status --rev .^
-
-# diff with the previous amend
-odiff=diff --rev 'limit(precursors(.),1)' --rev .
-EOF
-
-cat << EOF >&2
-
-
-### check qsync-enable.sh if your need mq export too.
-EOF

File hgext/__init__.py

+#f00

File hgext/evolve.py

 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-'''a set of commands to handle changeset mutation'''
+'''Extends Mercurial feature related to Changeset Evolution
 
+This extension Provide several command tommutate history and deal with issue it may raise.
+
+It also:
+
+    - enable the "Changeset Obsolescence" feature of mercurial,
+    - alter core command and extension that rewrite history to use this feature,
+    - improve some aspect of the early implementation in 2.3
+'''
+
+import random
+
+from mercurial import util
+
+try:
+    from mercurial import obsolete
+    if not obsolete._enabled:
+        obsolete._enabled = True
+except ImportError:
+    raise util.Abort('Obsolete extension requires Mercurial 2.3 (or later)')
+
+from mercurial import bookmarks
 from mercurial import cmdutil
-from mercurial import scmutil
-from mercurial import node
-from mercurial import error
-from mercurial import extensions
-from mercurial import commands
-from mercurial import bookmarks
-from mercurial import phases
 from mercurial import commands
 from mercurial import context
 from mercurial import copies
-from mercurial import util
+from mercurial import discovery
+from mercurial import error
+from mercurial import extensions
+from mercurial import hg
+from mercurial import localrepo
+from mercurial import merge
+from mercurial import node
+from mercurial import phases
+from mercurial import revset
+from mercurial import scmutil
+from mercurial import templatekw
 from mercurial.i18n import _
-from mercurial.commands import walkopts, commitopts, commitopts2, logopts
-from mercurial import hg
+from mercurial.commands import walkopts, commitopts, commitopts2
+from mercurial.node import nullid
+
+
+
+# This extension contains the following code
+#
+# - Extension Helper code
+# - Obsolescence cache
+# - ...
+# - Older format compat
+
+
+
+#####################################################################
+### Extension helper                                              ###
+#####################################################################
+
+class exthelper(object):
+    """Helper for modular extension setup
+
+    A single helper should be instanciated for each extension. Helper
+    methods are then used as decorator for various purpose.
+
+    All decorators return the original function and may be chained.
+    """
+
+    def __init__(self):
+        self._uicallables = []
+        self._extcallables = []
+        self._repocallables = []
+        self._revsetsymbols = []
+        self._templatekws = []
+        self._commandwrappers = []
+        self._extcommandwrappers = []
+        self._functionwrappers = []
+        self._duckpunchers = []
+
+    def final_uisetup(self, ui):
+        """Method to be used as the extension uisetup
+
+        The following operations belong here:
+
+        - Changes to ui.__class__ . The ui object that will be used to run the
+          command has not yet been created. Changes made here will affect ui
+          objects created after this, and in particular the ui that will be
+          passed to runcommand
+        - Command wraps (extensions.wrapcommand)
+        - Changes that need to be visible to other extensions: because
+          initialization occurs in phases (all extensions run uisetup, then all
+          run extsetup), a change made here will be visible to other extensions
+          during extsetup
+        - Monkeypatch or wrap function (extensions.wrapfunction) of dispatch
+          module members
+        - Setup of pre-* and post-* hooks
+        - pushkey setup
+        """
+        for cont, funcname, func in self._duckpunchers:
+            setattr(cont, funcname, func)
+        for command, wrapper in self._commandwrappers:
+            extensions.wrapcommand(commands.table, command, wrapper)
+        for cont, funcname, wrapper in self._functionwrappers:
+            extensions.wrapfunction(cont, funcname, wrapper)
+        for c in self._uicallables:
+            c(ui)
+
+    def final_extsetup(self, ui):
+        """Method to be used as a the extension extsetup
+
+        The following operations belong here:
+
+        - Changes depending on the status of other extensions. (if
+          extensions.find('mq'))
+        - Add a global option to all commands
+        - Register revset functions
+        """
+        knownexts = {}
+        for name, symbol in self._revsetsymbols:
+            revset.symbols[name] = symbol
+        for name, kw in self._templatekws:
+            templatekw.keywords[name] = kw
+        for ext, command, wrapper in self._extcommandwrappers:
+            if ext not in knownexts:
+                e = extensions.find(ext)
+                if e is None:
+                    raise util.Abort('extension %s not found' % ext)
+                knownexts[ext] = e.cmdtable
+            extensions.wrapcommand(knownexts[ext], commands, wrapper)
+        for c in self._extcallables:
+            c(ui)
+
+    def final_reposetup(self, ui, repo):
+        """Method to be used as a the extension reposetup
+
+        The following operations belong here:
+
+        - All hooks but pre-* and post-*
+        - Modify configuration variables
+        - Changes to repo.__class__, repo.dirstate.__class__
+        """
+        for c in self._repocallables:
+            c(ui, repo)
+
+    def uisetup(self, call):
+        """Decorated function will be executed during uisetup
+
+        example::
+
+            @eh.uisetup
+            def setupbabar(ui):
+                print 'this is uisetup!'
+        """
+        self._uicallables.append(call)
+        return call
+
+    def extsetup(self, call):
+        """Decorated function will be executed during extsetup
+
+        example::
+
+            @eh.extsetup
+            def setupcelestine(ui):
+                print 'this is extsetup!'
+        """
+        self._uicallables.append(call)
+        return call
+
+    def reposetup(self, call):
+        """Decorated function will be executed during reposetup
+
+        example::
+
+            @eh.reposetup
+            def setupzephir(ui, repo):
+                print 'this is reposetup!'
+        """
+        self._repocallables.append(call)
+        return call
+
+    def revset(self, symbolname):
+        """Decorated function is a revset symbol
+
+        The name of the symbol must be given as the decorator argument.
+        The symbol is added during `extsetup`.
+
+        example::
+
+            @eh.revset('hidden')
+            def revsetbabar(repo, subset, x):
+                args = revset.getargs(x, 0, 0, 'babar accept no argument')
+                return [r for r in subset if 'babar' in repo[r].description()]
+        """
+        def dec(symbol):
+            self._revsetsymbols.append((symbolname, symbol))
+            return symbol
+        return dec
+
+
+    def templatekw(self, keywordname):
+        """Decorated function is a revset keyword
+
+        The name of the keyword must be given as the decorator argument.
+        The symbol is added during `extsetup`.
+
+        example::
+
+            @eh.templatekw('babar')
+            def kwbabar(ctx):
+                return 'babar'
+        """
+        def dec(keyword):
+            self._templatekws.append((keywordname, keyword))
+            return keyword
+        return dec
+
+    def wrapcommand(self, command, extension=None):
+        """Decorated function is a command wrapper
+
+        The name of the command must be given as the decorator argument.
+        The wrapping is installed during `uisetup`.
+
+        If the second option `extension` argument is provided, the wrapping
+        will be applied in the extension commandtable. This argument must be a
+        string that will be searched using `extension.find` if not found and
+        Abort error is raised. If the wrapping applies to an extension, it is
+        installed during `extsetup`
+
+        example::
+
+            @eh.wrapcommand('summary')
+            def wrapsummary(orig, ui, repo, *args, **kwargs):
+                ui.note('Barry!')
+                return orig(ui, repo, *args, **kwargs)
+
+        """
+        def dec(wrapper):
+            if extension is None:
+                self._commandwrappers.append((command, wrapper))
+            else:
+                self._extcommandwrappers.append((extension, command, wrapper))
+            return wrapper
+        return dec
+
+    def wrapfunction(self, container, funcname):
+        """Decorated function is a function wrapper
+
+        This function takes two arguments, the container and the name of the
+        function to wrap. The wrapping is performed during `uisetup`.
+        (there is no extension support)
+
+        example::
+
+            @eh.function(discovery, 'checkheads')
+            def wrapfunction(orig, *args, **kwargs):
+                ui.note('His head smashed in and his heart cut out')
+                return orig(*args, **kwargs)
+        """
+        def dec(wrapper):
+            self._functionwrappers.append((container, funcname, wrapper))
+            return wrapper
+        return dec
+
+    def addattr(self, container, funcname):
+        """Decorated function is to be added to the container
+
+        This function takes two arguments, the container and the name of the
+        function to wrap. The wrapping is performed during `uisetup`.
+
+        example::
+
+            @eh.function(context.changectx, 'babar')
+            def babar(ctx):
+                return 'babar' in ctx.description
+        """
+        def dec(func):
+            self._duckpunchers.append((container, funcname, func))
+            return func
+        return dec
+
+eh = exthelper()
+uisetup = eh.final_uisetup
+extsetup = eh.final_extsetup
+reposetup = eh.final_reposetup
+
+#####################################################################
+### Obsolescence Caching Logic                                    ###
+#####################################################################
+
+# Obsolescence related logic can be very slow if we don't have efficient cache.
+#
+# This section implements a cache mechanism that did not make it into core for
+# time reason. It store meaningful set of revision related to obsolescence
+# (obsolete, unstabletble ...
+#
+# Here is:
+#
+# - Computation of meaningful set,
+# - Cache access logic,
+# - Cache invalidation logic,
+# - revset and ctx using this cache.
+#
+
+
+### Computation of meaningful set
+#
+# Most set can be computed with "simple" revset.
+
+#: { set name -> function to compute this set } mapping
+#:   function take a single "repo" argument.
+#:
+#: Use the `cachefor` decorator to register new cache function
+cachefuncs = {}
+def cachefor(name):
+    """Decorator to register a function as computing the cache for a set"""
+    def decorator(func):
+        assert name not in cachefuncs
+        cachefuncs[name] = func
+        return func
+    return decorator
+
+@cachefor('obsolete')
+def _computeobsoleteset(repo):
+    """the set of obsolete revisions"""
+    obs = set()
+    nm = repo.changelog.nodemap
+    for prec in repo.obsstore.precursors:
+        rev = nm.get(prec)
+        if rev is not None:
+            obs.add(rev)
+    return set(repo.revs('%ld - public()', obs))
+
+@cachefor('unstable')
+def _computeunstableset(repo):
+    """the set of non obsolete revisions with obsolete parents"""
+    return set(repo.revs('(obsolete()::) - obsolete()'))
+
+@cachefor('suspended')
+def _computesuspendedset(repo):
+    """the set of obsolete parents with non obsolete descendants"""
+    return set(repo.revs('obsolete() and obsolete()::unstable()'))
+
+@cachefor('extinct')
+def _computeextinctset(repo):
+    """the set of obsolete parents without non obsolete descendants"""
+    return set(repo.revs('obsolete() - obsolete()::unstable()'))
+
+@eh.wrapfunction(obsolete.obsstore, '__init__')
+def _initobsstorecache(orig, obsstore, *args, **kwargs):
+    """add a cache attribute to obsstore"""
+    obsstore.caches = {}
+    return orig(obsstore, *args, **kwargs)
+
+### Cache access
+
+def getobscache(repo, name):
+    """Return the set of revision that belong to the <name> set
+
+    Such access may compute the set and cache it for future use"""
+    if not repo.obsstore:
+        return ()
+    if name not in repo.obsstore.caches:
+        repo.obsstore.caches[name] = cachefuncs[name](repo)
+    return repo.obsstore.caches[name]
+
+### Cache clean up
+#
+# To be simple we need to invalidate obsolescence cache when:
+#
+# - new changeset is added:
+# - public phase is changed
+# - obsolescence marker are added
+# - strip is used a repo
+
+
+def clearobscaches(repo):
+    """Remove all obsolescence related cache from a repo
+
+    This remove all cache in obsstore is the obsstore already exist on the
+    repo.
+
+    (We could be smarter here)"""
+    if 'obsstore' in repo._filecache:
+        repo.obsstore.caches.clear()
+
+@eh.wrapfunction(localrepo.localrepository, 'addchangegroup')  # new changeset
+@eh.wrapfunction(phases, 'retractboundary')  # phase movement
+@eh.wrapfunction(phases, 'advanceboundary')  # phase movement
+@eh.wrapfunction(localrepo.localrepository, 'destroyed')  # strip
+def wrapclearcache(orig, repo, *args, **kwargs):
+    try:
+        return orig(repo, *args, **kwargs)
+    finally:
+        # we are a bit wide here
+        # we could restrict to:
+        # advanceboundary + phase==public
+        # retractboundary + phase==draft
+        clearobscaches(repo)
+
+@eh.wrapfunction(obsolete.obsstore, 'add')  # new marker
+def clearonadd(orig, obsstore, *args, **kwargs):
+    try:
+        return orig(obsstore, *args, **kwargs)
+    finally:
+        obsstore.caches.clear()
+
+### Use the case
+# Function in core that could benefic from the cache are overwritten by cache using version
+
+# changectx method
+
+@eh.addattr(context.changectx, 'unstable')
+def unstable(ctx):
+    """is the changeset unstable (have obsolete ancestor)"""
+    if ctx.node() is None:
+        return False
+    return ctx.rev() in getobscache(ctx._repo, 'unstable')
+
+
+@eh.addattr(context.changectx, 'extinct')
+def extinct(ctx):
+    """is the changeset extinct by other"""
+    if ctx.node() is None:
+        return False
+    return ctx.rev() in getobscache(ctx._repo, 'extinct')
+
+# revset
+
+@eh.revset('obsolete')
+def revsetobsolete(repo, subset, x):
+    """``obsolete()``
+    Changeset is obsolete.
+    """
+    args = revset.getargs(x, 0, 0, 'obsolete takes no argument')
+    obsoletes = getobscache(repo, 'obsolete')
+    return [r for r in subset if r in obsoletes]
+
+@eh.revset('unstable')
+def revsetunstable(repo, subset, x):
+    """``unstable()``
+    Unstable changesets are non-obsolete with obsolete ancestors.
+    """
+    args = revset.getargs(x, 0, 0, 'unstable takes no arguments')
+    unstables = getobscache(repo, 'unstable')
+    return [r for r in subset if r in unstables]
+
+@eh.revset('extinct')
+def revsetextinct(repo, subset, x):
+    """``extinct()``
+    Obsolete changesets with obsolete descendants only.
+    """
+    args = revset.getargs(x, 0, 0, 'extinct takes no arguments')
+    extincts = getobscache(repo, 'extinct')
+    return [r for r in subset if r in extincts]
+
+#####################################################################
+### Complete troubles computation logic                           ###
+#####################################################################
+
+# there is two kind of trouble not handled by core right now:
+# - latecomer: (successors for public changeset)
+# - conflicting: (two changeset try to succeed to the same precursors)
+#
+# This section add support for those two addition trouble
+#
+# - Cache computation
+# - revset and ctx method
+# - push warning
+
+### Cache computation
+latediff = 1  # flag to prevent taking late comer fix into account
+
+@cachefor('latecomer')
+def _computelatecomerset(repo):
+    """the set of rev trying to obsolete public revision"""
+    candidates = _allsuccessors(repo, repo.revs('public()'),
+                                                haltonflags=latediff)
+    query = '%ld - obsolete() - public()'
+    return set(repo.revs(query, candidates))
+
+@cachefor('conflicting')
+def _computeconflictingset(repo):
+    """the set of rev trying to obsolete public revision"""
+    conflicting = set()
+    obsstore = repo.obsstore
+    newermap = {}
+    for ctx in repo.set('(not public()) - obsolete()'):
+        prec = obsstore.successors.get(ctx.node(), ())
+        toprocess = set(prec)
+        while toprocess:
+            prec = toprocess.pop()[0]
+            if prec not in newermap:
+                newermap[prec] = newerversion(repo, prec)
+            newer = [n for n in newermap[prec] if n] # filter kill
+            if len(newer) > 1:
+                conflicting.add(ctx.rev())
+                break
+        toprocess.update(obsstore.successors.get(prec, ()))
+    return conflicting
+
+### changectx method
+
+@eh.addattr(context.changectx, 'latecomer')
+def latecomer(ctx):
+    """is the changeset latecomer (Try to succeed to public change)"""
+    if ctx.node() is None:
+        return False
+    return ctx.rev() in getobscache(ctx._repo, 'latecomer')
+
+@eh.addattr(context.changectx, 'conflicting')
+def conflicting(ctx):
+    """is the changeset conflicting (Try to succeed to public change)"""
+    if ctx.node() is None:
+        return False
+    return ctx.rev() in getobscache(ctx._repo, 'conflicting')
+
+### revset symbol
+
+@eh.revset('latecomer')
+def revsetlatecomer(repo, subset, x):
+    """``latecomer()``
+    Changesets marked as successors of public changesets.
+    """
+    args = revset.getargs(x, 0, 0, 'latecomer takes no arguments')
+    lates = getobscache(repo, 'latecomer')
+    return [r for r in subset if r in lates]
+
+@eh.revset('conflicting')
+def revsetconflicting(repo, subset, x):
+    """``conflicting()``
+    Changesets marked as successors of a same changeset.
+    """
+    args = revset.getargs(x, 0, 0, 'conflicting takes no arguments')
+    conf = getobscache(repo, 'conflicting')
+    return [r for r in subset if r in conf]
+
+
+### Discovery wrapping
+
+@eh.wrapfunction(discovery, 'checkheads')
+def wrapcheckheads(orig, repo, remote, outgoing, *args, **kwargs):
+    """wrap mercurial.discovery.checkheads
+
+    * prevent latecomer and unstable to be pushed
+    """
+    # do not push instability
+    for h in outgoing.missingheads:
+        # Checking heads is enough, obsolete descendants are either
+        # obsolete or unstable.
+        ctx = repo[h]
+        if ctx.latecomer():
+            raise util.Abort(_("push includes a latecomer changeset: %s!")
+                             % ctx)
+        if ctx.conflicting():
+            raise util.Abort(_("push includes a conflicting changeset: %s!")
+                             % ctx)
+    return orig(repo, remote, outgoing, *args, **kwargs)
+
+#####################################################################
+### Filter extinct changeset from common operation                ###
+#####################################################################
+
+@eh.wrapfunction(merge, 'update')
+def wrapmergeupdate(orig, repo, node, *args, **kwargs):
+    """ensure we don't automatically update on hidden changeset"""
+    if node is None:
+        # tip of current branch
+        branch = repo[None].branch()
+        node = repo.revs('last((.:: and branch(%s)) - hidden())', branch)[0]
+    return orig(repo, node, *args, **kwargs)
+
+@eh.wrapfunction(localrepo.localrepository, 'branchtip')
+def obsbranchtip(orig, repo, branch):
+    """ensure "stable" reference does not end on a hidden changeset"""
+    result = ()
+    heads = repo.branchmap().get(branch, ())
+    if heads:
+        result = list(repo.set('last(heads(branch(%n) - hidden()))', heads[0]))
+    if not result:
+        raise error.RepoLookupError(_("unknown branch '%s'") % branch)
+    return result[0].node()
+
+
+#####################################################################
+### Additional Utilities                                          ###
+#####################################################################
+
+# This section contains a lot of small utility function and method
+
+# - Function to create markers
+# - useful alias pstatus and pdiff (should probably go in evolve)
+# - "troubles" method on changectx
+# - function to travel throught the obsolescence graph
+# - function to find useful changeset to stabilize
+
+### Marker Create
+
+def createmarkers(repo, relations, metadata=None, flag=0):
+    """Add obsolete markers between changeset in a repo
+
+    <relations> must be an iterable of (<old>, (<new>, ...)) tuple.
+    `old` and `news` are changectx.
+
+    Current user and date are used except if specified otherwise in the
+    metadata attribute.
+
+    /!\ assume the repo have been locked by the user /!\
+    """
+    # prepare metadata
+    if metadata is None:
+        metadata = {}
+    if 'date' not in metadata:
+        metadata['date'] = '%i %i' % util.makedate()
+    if 'user' not in metadata:
+        metadata['user'] = repo.ui.username()
+    # check future marker
+    tr = repo.transaction('add-obsolescence-marker')
+    try:
+        for prec, sucs in relations:
+            if not prec.mutable():
+                raise util.Abort("Cannot obsolete immutable changeset: %s" % prec)
+            nprec = prec.node()
+            nsucs = tuple(s.node() for s in sucs)
+            if nprec in nsucs:
+                raise util.Abort("Changeset %s cannot obsolete himself" % prec)
+            repo.obsstore.create(tr, nprec, nsucs, flag, metadata)
+            clearobscaches(repo)
+        tr.close()
+    finally:
+        tr.release()
+
+
+### Useful alias
+
+@eh.uisetup
+def _installalias(ui):
+    if ui.config('alias', 'pstatus', None) is None:
+        ui.setconfig('alias', 'pstatus', 'status --rev .^')
+    if ui.config('alias', 'pdiff', None) is None:
+        ui.setconfig('alias', 'pdiff', 'diff --rev .^')
+    if ui.config('alias', 'olog', None) is None:
+        ui.setconfig('alias', 'olog', "log -r 'precursors(.)' --hidden")
+    if ui.config('alias', 'odiff', None) is None:
+        ui.setconfig('alias', 'odiff', "diff --rev 'limit(precursors(.),1)' --rev .")
+
+# - "troubles" method on changectx
+
+@eh.addattr(context.changectx, 'troubles')
+def troubles(ctx):
+    """Return a tuple listing all the troubles that affect a changeset
+
+    Troubles may be "unstable", "latecomer" or "conflicting".
+    """
+    troubles = []
+    if ctx.unstable():
+        troubles.append('unstable')
+    if ctx.latecomer():
+        troubles.append('latecomer')
+    if ctx.conflicting():
+        troubles.append('conflicting')
+    return tuple(troubles)
+
+### Troubled revset symbol
+
+@eh.revset('troubled')
+def revsetlatecomer(repo, subset, x):
+    """``troubled()``
+    Changesets with troubles.
+    """
+    _ = revset.getargs(x, 0, 0, 'troubled takes no arguments')
+    return list(repo.revs('%ld and (unstable() + latecomer() + conflicting())',
+                          subset))
+
+
+### Obsolescence graph
+
+# XXX SOME MAJOR CLEAN UP TO DO HERE XXX
+
+def _precursors(repo, s):
+    """Precursor of a changeset"""
+    cs = set()
+    nm = repo.changelog.nodemap
+    markerbysubj = repo.obsstore.successors
+    for r in s:
+        for p in markerbysubj.get(repo[r].node(), ()):
+            pr = nm.get(p[0])
+            if pr is not None:
+                cs.add(pr)
+    return cs
+
+def _allprecursors(repo, s):  # XXX we need a better naming
+    """transitive precursors of a subset"""
+    toproceed = [repo[r].node() for r in s]
+    seen = set()
+    allsubjects = repo.obsstore.successors
+    while toproceed:
+        nc = toproceed.pop()
+        for mark in allsubjects.get(nc, ()):
+            np = mark[0]
+            if np not in seen:
+                seen.add(np)
+                toproceed.append(np)
+    nm = repo.changelog.nodemap
+    cs = set()
+    for p in seen:
+        pr = nm.get(p)
+        if pr is not None:
+            cs.add(pr)
+    return cs
+
+def _successors(repo, s):
+    """Successors of a changeset"""
+    cs = set()
+    nm = repo.changelog.nodemap
+    markerbyobj = repo.obsstore.precursors
+    for r in s:
+        for p in markerbyobj.get(repo[r].node(), ()):
+            for sub in p[1]:
+                sr = nm.get(sub)
+                if sr is not None:
+                    cs.add(sr)
+    return cs
+
+def _allsuccessors(repo, s, haltonflags=0):  # XXX we need a better naming
+    """transitive successors of a subset
+
+    haltonflags allows to provide flags which prevent the evaluation of a
+    marker.  """
+    toproceed = [repo[r].node() for r in s]
+    seen = set()
+    allobjects = repo.obsstore.precursors
+    while toproceed:
+        nc = toproceed.pop()
+        for mark in allobjects.get(nc, ()):
+            if mark[2] & haltonflags:
+                continue
+            for sub in mark[1]:
+                if sub == nullid:
+                    continue # should not be here!
+                if sub not in seen:
+                    seen.add(sub)
+                    toproceed.append(sub)
+    nm = repo.changelog.nodemap
+    cs = set()
+    for s in seen:
+        sr = nm.get(s)
+        if sr is not None:
+            cs.add(sr)
+    return cs
+
+
+
+def newerversion(repo, obs):
+    """Return the newer version of an obsolete changeset"""
+    toproceed = set([(obs,)])
+    # XXX known optimization available
+    newer = set()
+    objectrels = repo.obsstore.precursors
+    while toproceed:
+        current = toproceed.pop()
+        assert len(current) <= 1, 'splitting not handled yet. %r' % current
+        current = [n for n in current if n != nullid]
+        if current:
+            n, = current
+            if n in objectrels:
+                markers = objectrels[n]
+                for mark in markers:
+                    toproceed.add(tuple(mark[1]))
+            else:
+                newer.add(tuple(current))
+        else:
+            newer.add(())
+    return sorted(newer)
+
+
+#####################################################################
+### Extending revset and template                                 ###
+#####################################################################
+
+# this section add several useful revset symbol not yet in core.
+# they are subject to changes
+
+### hidden revset is not in core yet
+
+@eh.revset('hidden')
+def revsethidden(repo, subset, x):
+    """``hidden()``
+    Changeset is hidden.
+    """
+    args = revset.getargs(x, 0, 0, 'hidden takes no argument')
+    return [r for r in subset if r in repo.hiddenrevs]
+
+### XXX I'm not sure this revset is useful
+@eh.revset('suspended')
+def revsetsuspended(repo, subset, x):
+    """``suspended()``
+    Obsolete changesets with non-obsolete descendants.
+    """
+    args = revset.getargs(x, 0, 0, 'suspended takes no arguments')
+    suspended = getobscache(repo, 'suspended')
+    return [r for r in subset if r in suspended]
+
+
+@eh.revset('precursors')
+def revsetprecursors(repo, subset, x):
+    """``precursors(set)``
+    Immediate precursors of changesets in set.
+    """
+    s = revset.getset(repo, range(len(repo)), x)
+    cs = _precursors(repo, s)
+    return [r for r in subset if r in cs]
+
+
+@eh.revset('allprecursors')
+def revsetallprecursors(repo, subset, x):
+    """``allprecursors(set)``
+    Transitive precursors of changesets in set.
+    """
+    s = revset.getset(repo, range(len(repo)), x)
+    cs = _allprecursors(repo, s)
+    return [r for r in subset if r in cs]
+
+
+@eh.revset('successors')
+def revsetsuccessors(repo, subset, x):
+    """``successors(set)``
+    Immediate successors of changesets in set.
+    """
+    s = revset.getset(repo, range(len(repo)), x)
+    cs = _successors(repo, s)
+    return [r for r in subset if r in cs]
+
+@eh.revset('allsuccessors')
+def revsetallsuccessors(repo, subset, x):
+    """``allsuccessors(set)``
+    Transitive successors of changesets in set.
+    """
+    s = revset.getset(repo, range(len(repo)), x)
+    cs = _allsuccessors(repo, s)
+    return [r for r in subset if r in cs]
+
+### template keywords
+# XXX it does not handle troubles well :-/
+
+@eh.templatekw('obsolete')
+def obsoletekw(repo, ctx, templ, **args):
+    """:obsolete: String. The obsolescence level of the node, could be
+    ``stable``, ``unstable``, ``suspended`` or ``extinct``.
+    """
+    rev = ctx.rev()
+    if ctx.obsolete():
+        if ctx.extinct():
+            return 'extinct'
+        else:
+            return 'suspended'
+    elif ctx.unstable():
+        return 'unstable'
+    return 'stable'
+
+#####################################################################
+### Various trouble warning                                       ###
+#####################################################################
+
+# This section take care of issue warning to the user when troubles appear
+
+@eh.wrapcommand("update")
+@eh.wrapcommand("pull")
+def wrapmayobsoletewc(origfn, ui, repo, *args, **opts):
+    """Warn that the working directory parent is an obsolete changeset"""
+    res = origfn(ui, repo, *args, **opts)
+    if repo['.'].obsolete():
+        ui.warn(_('Working directory parent is obsolete\n'))
+    return res
+
+# XXX this could wrap transaction code
+# XXX (but this is a bit a layer violation)
+@eh.wrapcommand("commit")
+@eh.wrapcommand("push")
+@eh.wrapcommand("pull")
+@eh.wrapcommand("graft")
+@eh.wrapcommand("phase")
+@eh.wrapcommand("unbundle")
+def warnobserrors(orig, ui, repo, *args, **kwargs):
+    """display warning is the command resulted in more instable changeset"""
+    priorunstables = len(repo.revs('unstable()'))
+    priorlatecomers = len(repo.revs('latecomer()'))
+    priorconflictings = len(repo.revs('conflicting()'))
+    ret = orig(ui, repo, *args, **kwargs)
+    newunstables = len(repo.revs('unstable()')) - priorunstables
+    newlatecomers = len(repo.revs('latecomer()')) - priorlatecomers
+    newconflictings = len(repo.revs('conflicting()')) - priorconflictings
+    if newunstables > 0:
+        ui.warn(_('%i new unstable changesets\n') % newunstables)
+    if newlatecomers > 0:
+        ui.warn(_('%i new latecomer changesets\n') % newlatecomers)
+    if newconflictings > 0:
+        ui.warn(_('%i new conflicting changesets\n') % newconflictings)
+    return ret
+
+@eh.reposetup
+def _repostabilizesetup(ui, repo):
+    """Add a hint for "hg evolve" when troubles make push fails
+    """
+    if not repo.local():
+        return
+
+    opush = repo.push
+
+    class evolvingrepo(repo.__class__):
+        def push(self, remote, *args, **opts):
+            """wrapper around pull that pull obsolete relation"""
+            try:
+                result = opush(remote, *args, **opts)
+            except util.Abort, ex:
+                hint = _("use 'hg evolve' to get a stable history "
+                         "or --force to ignore warnings")
+                if (len(ex.args) >= 1
+                    and ex.args[0].startswith('push includes ')
+                    and ex.hint is None):
+                    ex.hint = hint
+                raise
+            return result
+    repo.__class__ = evolvingrepo
+
+@eh.wrapcommand("summary")
+def obssummary(orig, ui, repo, *args, **kwargs):
+    ret = orig(ui, repo, *args, **kwargs)
+    nbunstable = len(getobscache(repo, 'unstable'))
+    nblatecomer = len(getobscache(repo, 'latecomer'))
+    nbconflicting = len(getobscache(repo, 'unstable'))
+    if nbunstable:
+        ui.write('unstable: %i changesets\n' % nbunstable)
+    else:
+        ui.note('unstable: 0 changesets\n')
+    if nblatecomer:
+        ui.write('latecomer: %i changesets\n' % nblatecomer)
+    else:
+        ui.note('latecomer: 0 changesets\n')
+    if nbconflicting:
+        ui.write('conflicting: %i changesets\n' % nbconflicting)
+    else:
+        ui.note('conflicting: 0 changesets\n')
+    return ret
+
+
+#####################################################################
+### Core Other extension compat                                   ###
+#####################################################################
+
+# This section make official history rewritter create obsolete marker
+
+
+### commit --amend
+# make commit --amend create obsolete marker
+#
+# The precursor is still strip from the repository.
+
+@eh.wrapfunction(cmdutil, 'amend')
+def wrapcmdutilamend(orig, ui, repo, commitfunc, old, *args, **kwargs):
+    oldnode = old.node()
+    new = orig(ui, repo, commitfunc, old, *args, **kwargs)
+    if new != oldnode:
+        lock = repo.lock()
+        try:
+            tr = repo.transaction('post-amend-obst')
+            try:
+                meta = {
+                    'date':  '%i %i' % util.makedate(),
+                    'user': ui.username(),
+                    }
+                repo.obsstore.create(tr, oldnode, [new], 0, meta)
+                tr.close()
+                clearobscaches(repo)
+            finally:
+                tr.release()
+        finally:
+            lock.release()
+    return new
+
+### rebase
+#
+# - ignore obsolete changeset
+# - create obsolete marker *instead of* striping
+
+def buildstate(orig, repo, dest, rebaseset, *ags, **kws):
+    """wrapper for rebase 's buildstate that exclude obsolete changeset"""
+
+    rebaseset = repo.revs('%ld - extinct()', rebaseset)
+    if not rebaseset:
+        repo.ui.warn(_('whole rebase set is extinct and ignored.\n'))
+        return {}
+    root = min(rebaseset)
+    if not repo._rebasekeep and not repo[root].mutable():
+        raise util.Abort(_("can't rebase immutable changeset %s") % repo[root],
+                         hint=_('see hg help phases for details'))
+    return orig(repo, dest, rebaseset, *ags, **kws)
+
+def defineparents(orig, repo, rev, target, state, *args, **kwargs):
+    rebasestate = getattr(repo, '_rebasestate', None)
+    if rebasestate is not None:
+        repo._rebasestate = dict(state)
+        repo._rebasetarget = target
+    return orig(repo, rev, target, state, *args, **kwargs)
+
+def concludenode(orig, repo, rev, p1, *args, **kwargs):
+    """wrapper for rebase 's concludenode that set obsolete relation"""
+    newrev = orig(repo, rev, p1, *args, **kwargs)
+    rebasestate = getattr(repo, '_rebasestate', None)
+    if rebasestate is not None:
+        if newrev is not None:
+            nrev = repo[newrev].rev()
+        else:
+            nrev = p1
+        repo._rebasestate[rev] = nrev
+    return newrev
+
+def cmdrebase(orig, ui, repo, *args, **kwargs):
+
+    reallykeep = kwargs.get('keep', False)
+    kwargs = dict(kwargs)
+    kwargs['keep'] = True
+    repo._rebasekeep = reallykeep
+
+    # We want to mark rebased revision as obsolete and set their
+    # replacements if any. Doing it in concludenode() prevents
+    # aborting the rebase, and is not called with all relevant
+    # revisions in --collapse case. Instead, we try to track the
+    # rebase state structure by sampling/updating it in
+    # defineparents() and concludenode(). The obsolete markers are
+    # added from this state after a successful call.
+    repo._rebasestate = {}
+    repo._rebasetarget = None
+    try:
+        l = repo.lock()
+        try:
+            res = orig(ui, repo, *args, **kwargs)
+            if not reallykeep:
+                # Filter nullmerge or unrebased entries
+                repo._rebasestate = dict(p for p in repo._rebasestate.iteritems()
+                                         if p[1] >= 0)
+                if not res and not kwargs.get('abort') and repo._rebasestate:
+                    # Rebased revisions are assumed to be descendants of
+                    # targetrev. If a source revision is mapped to targetrev
+                    # or to another rebased revision, it must have been
+                    # removed.
+                    markers = []
+                    if kwargs.get('collapse'):
+                        # collapse assume revision disapear because they are all
+                        # in the created revision
+                        newrevs = set(repo._rebasestate.values())
+                        newrevs.remove(repo._rebasetarget)
+                        if newrevs:
+                            # we create new revision.
+                            # A single one by --collapse design
+                            assert len(newrevs) == 1
+                            new = tuple(repo[n] for n in newrevs)
+                        else:
+                            # every body died. no new changeset created
+                            new = (repo[repo._rebasetarget],)
+                        for rev, newrev in sorted(repo._rebasestate.items()):
+                            markers.append((repo[rev], new))
+                    else:
+                        # no collapse assume revision disapear because they are
+                        # contained in parent
+                        for rev, newrev in sorted(repo._rebasestate.items()):
+                            markers.append((repo[rev], (repo[newrev],)))
+                    createmarkers(repo, markers)
+            return res
+        finally:
+            l.release()
+    finally:
+        delattr(repo, '_rebasestate')
+        delattr(repo, '_rebasetarget')
+
+@eh.extsetup
+def _rebasewrapping(ui):
+    # warning about more obsolete
+    try:
+        rebase = extensions.find('rebase')
+        if rebase:
+            entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors)
+            extensions.wrapfunction(rebase, 'buildstate', buildstate)
+            extensions.wrapfunction(rebase, 'defineparents', defineparents)
+            extensions.wrapfunction(rebase, 'concludenode', concludenode)
+            extensions.wrapcommand(rebase.cmdtable, "rebase", cmdrebase)
+    except KeyError:
+        pass  # rebase not found
+
+
+#####################################################################
+### Old Evolve extension content                                  ###
+#####################################################################
+
+# XXX need clean up and proper sorting in other section
 
 ### util function
 #############################
         if created:
             updatebookmarks(newid)
             # add evolution metadata
-            collapsed = set([u.node() for u in updates] + [old.node()])
-            repo.addcollapsedobsolete(collapsed, new.node())
+            markers = [(u, (new,)) for u in updates]
+            markers.append((old, (new,)))
+            createmarkers(repo, markers)
         else:
             # newid is an existing revision. It could make sense to
             # replace revisions with existing ones but probably not by
 
     return newid, created
 
+class MergeFailure(util.Abort):
+    pass
+
 def relocate(repo, orig, dest):
     """rewrite <rev> on dest"""
     try:
         try:
             nodenew = rebase.concludenode(repo, orig.node(), dest.node(),
                                           node.nullid)
-        except util.Abort:
-            repo.ui.write_err(_('/!\\ stabilize failed                          /!\\\n'))
-            repo.ui.write_err(_('/!\\ Their is no "hg stabilize --continue"     /!\\\n'))
-            repo.ui.write_err(_('/!\\ use "hg up -C . ; hg stabilize --dry-run" /!\\\n'))
+        except util.Abort, exc:
+            class LocalMergeFailure(MergeFailure, exc.__class__):
+                pass
+            exc.__class__ = LocalMergeFailure
             raise
         oldbookmarks = repo.nodebookmarks(nodesrc)
         if nodenew is not None:
             phases.retractboundary(repo, destphase, [nodenew])
-            repo.addobsolete(nodenew, nodesrc)
+            createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))])
             for book in oldbookmarks:
                 repo._bookmarks[book] = nodenew
         else:
-            repo.addobsolete(node.nullid, nodesrc)
+            createmarkers(repo, [(repo[nodesrc], ())])
             # Behave like rebase, move bookmarks to dest
             for book in oldbookmarks:
                 repo._bookmarks[book] = dest.node()
             repo._bookmarks[book] = dest.node()
         if oldbookmarks or destbookmarks:
             bookmarks.write(repo)
+        return nodenew
     except util.Abort:
         # Invalidate the previous setparents
         repo.dirstate.invalidate()
         raise
 
-def stabilizableunstable(repo, pctx):
+def _stabilizableunstable(repo, pctx):
     """Return a changectx for an unstable changeset which can be
     stabilized on top of pctx or one of its descendants. None if none
     can be found.
     # Look for an unstable which can be stabilized as a child of
     # node. The unstable must be a child of one of node predecessors.
     for ctx in selfanddescendants(repo, pctx):
-        unstables = list(repo.set('unstable() and children(obsancestors(%d))',
+        unstables = list(repo.set('unstable() and children(allprecursors(%d))',
                                   ctx.rev()))
         if unstables:
             return unstables[0]
 cmdtable = {}
 command = cmdutil.command(cmdtable)
 
-@command('^stabilize|evolve|solve',
+@command('^evolve|stabilize|evolve|solve',
     [('n', 'dry-run', False, 'do not perform actions, print what to be done'),
-    ('A', 'any', False, 'stabilize any unstable changeset'),],
+    ('A', 'any', False, 'evolve any troubled changeset'),
+    ('c', 'continue', False, 'continue an interrupted evolution'), ],
     _('[OPTIONS]...'))
-def stabilize(ui, repo, **opts):
-    """rebase an unstable changeset to make it stable again
+def evolve(ui, repo, **opts):
+    """Solve trouble in your repository
 
-    By default, take the first unstable changeset which could be
-    rebased as child of the working directory parent revision or one
-    of its descendants and rebase it.
+    - rebase unstable changeset to make it stable again,
+    - create proper diff from latecomer changeset,
+    - merge conflicting changeset.
 
-    With --any, stabilize any unstable changeset.
+    By default, take the first troubles changeset that looks relevant.
 
-    The working directory is updated to the rebased revision.
+    (The logic is still a bit fuzzy)
+
+    - For unstable, that mean the first which could be rebased as child of the
+      working directory parent revision or one of its descendants and rebase
+      it.
+
+    - For conflicting this mean "." if applicable.
+
+    With --any, evolve pick any troubled changeset to solve
+
+    The working directory is updated to the newly created revision.
     """
 
-    obsolete = extensions.find('obsolete')
+    contopt = opts['continue']
+    anyopt = opts['any']
 
-    node = None
-    if not opts['any']:
-        node = stabilizableunstable(repo, repo['.'])
-    if node is None:
-        unstables = list(repo.set('unstable()'))
-        if unstables and not opts['any']:
-            ui.write_err(_('nothing to stabilize here\n'))
-            ui.status(_('(%i unstable changesets, do you want --any ?)\n')
-                      % len(unstables))
+    if contopt:
+        if anyopt:
+            raise util.Abort('can not specify both "--any" and "--continue"')
+        graftcmd = commands.table['graft'][0]
+        return graftcmd(ui, repo, old_obsolete=True, **{'continue': True})
+
+    troubled = list(repo.revs('troubled()'))
+    tr = _picknexttroubled(ui, repo, anyopt)
+    if tr is None:
+        if troubled:
+            ui.write_err(_('nothing to evolve here\n'))
+            ui.status(_('(%i troubled changesets, do you want --any ?)\n')
+                      % len(troubled))
             return 2
-        elif not unstables:
-            ui.write_err(_('no unstable changeset\n'))
+        else:
+            ui.write_err(_('no troubled changeset\n'))
             return 1
-        node = unstables[0]
+    cmdutil.bailifchanged(repo)
+    troubles = tr.troubles()
+    if 'unstable' in troubles:
+        return _solveunstable(ui, repo, tr, opts['dry_run'])
+    elif 'latecomer' in troubles:
+        return _solvelatecomer(ui, repo, tr, opts['dry_run'])
+    elif 'conflicting' in troubles:
+        return _solveconflicting(ui, repo, tr, opts['dry_run'])
+    else:
+        assert False  # WHAT? unknown troubles
 
-    obs = node.parents()[0]
+def _picknexttroubled(ui, repo, any=False):
+    """Pick a the next trouble changeset to solve"""
+    tr = _stabilizableunstable(repo, repo['.'])
+    if tr is None:
+        wdp = repo['.']
+        if 'conflicting' in wdp.troubles():
+            tr = wdp
+    if tr is None and any:
+        troubled = list(repo.set('unstable()'))
+        if not troubled:
+            troubled = list(repo.set('latecomer()'))
+        if not troubled:
+            troubled = list(repo.set('conflicting()'))
+        if troubled:
+            tr = troubled[0]
+
+    return tr
+
+
+def _solveunstable(ui, repo, orig, dryrun=False):
+    """Stabilize a unstable changeset"""
+    obs = orig.parents()[0]
     if not obs.obsolete():
-        obs = node.parents()[1]
+        obs = orig.parents()[1]
     assert obs.obsolete()
-    newer = obsolete.newerversion(repo, obs.node())
+    newer = newerversion(repo, obs.node())
+    # search of a parent which is not killed
+    while newer == [()]:
+        ui.debug("stabilize target %s is plain dead,"
+                 " trying to stabilize on it's parent")
+        obs = obs.parents()[0]
+        newer = newerversion(repo, obs.node())
     if len(newer) > 1:
         ui.write_err(_("conflict rewriting. can't choose destination\n"))
         return 2
     targets = newer[0]
-    if not targets:
-        ui.write_err(_("does not handle kill parent yet\n"))
-        return 2
+    assert targets
     if len(targets) > 1:
         ui.write_err(_("does not handle splitted parent yet\n"))
         return 2
     target = repo[target]
     repo.ui.status(_('move:'))
     if not ui.quiet:
-        displayer.show(node)
+        displayer.show(orig)
     repo.ui.status(_('atop:'))
     if not ui.quiet:
         displayer.show(target)
-    todo= 'hg rebase -Dr %s -d %s\n' % (node, target)
-    if opts['dry_run']:
+    todo = 'hg rebase -Dr %s -d %s\n' % (orig, target)
+    if dryrun:
         repo.ui.write(todo)
     else:
         repo.ui.note(todo)
         lock = repo.lock()
         try:
-            relocate(repo, node, target)
+            relocate(repo, orig, target)
+        except MergeFailure:
+            repo.opener.write('graftstate', orig.hex() + '\n')
+            repo.ui.write_err(_('evolve failed!\n'))
+            repo.ui.write_err(_('fix conflict and run "hg evolve --continue"\n'))
+            raise
         finally:
             lock.release()
 
+def _solvelatecomer(ui, repo, latecomer, dryrun=False):
+    """Stabilize a latecomer changeset"""
+    # For now we deny latecomer merge
+    if len(latecomer.parents()) > 1:
+        raise util.Abort('late comer stabilization is confused by latecomer'
+                         ' %s being a merge' % latecomer)
+    prec = repo.set('last(allprecursors(%d) and public())', latecomer).next()
+    # For now we deny target merge
+    if len(prec.parents()) > 1:
+        raise util.Abort('late comer evolution is confused by precursors'
+                         ' %s being a merge' % prec)
+
+    displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
+    repo.ui.status(_('recreate:'))
+    if not ui.quiet:
+        displayer.show(latecomer)
+    repo.ui.status(_('atop:'))
+    if not ui.quiet:
+        displayer.show(prec)
+    if dryrun:
+        todo = 'hg rebase --rev %s --detach %s;\n' % (latecomer, prec.p1())
+        repo.ui.write(todo)
+        repo.ui.write('hg update %s;\n' % prec)
+        repo.ui.write('hg revert --all --rev %s;\n' % latecomer)
+        repo.ui.write('hg commit --msg "latecomer update to %s"')
+        return 0
+    wlock = repo.wlock()
+    try:
+        newid = tmpctx = None
+        tmpctx = latecomer
+        lock = repo.lock()
+        try:
+            bmupdate = _bookmarksupdater(repo, latecomer.node())
+            # Basic check for common parent. Far too complicated and fragile
+            tr = repo.transaction('latecomer-stabilize')
+            try:
+                if not list(repo.set('parents(%d) and parents(%d)', latecomer, prec)):
+                    # Need to rebase the changeset at the right place
+                    repo.ui.status(_('rebasing to destination parent: %s\n') % prec.p1())
+                    try:
+                        tmpid = relocate(repo, latecomer, prec.p1())
+                        if tmpid is not None:
+                            tmpctx = repo[tmpid]
+                            createmarkers(repo, [(latecomer, (tmpctx,))])
+                    except MergeFailure:
+                        repo.opener.write('graftstate', latecomer.hex() + '\n')
+                        repo.ui.write_err(_('evolution failed!\n'))
+                        repo.ui.write_err(_('fix conflict and run "hg evolve --continue"\n'))
+                        raise
+                # Create the new commit context
+                repo.ui.status(_('computing new diff\n'))
+                files = set()
+                copied = copies.pathcopies(prec, latecomer)
+                precmanifest = prec.manifest()
+                for key, val in latecomer.manifest().iteritems():
+                    if precmanifest.pop(key, None) != val:
+                        files.add(key)
+                files.update(precmanifest)  # add missing files
+                # commit it
+                if files: # something to commit!
+                    def filectxfn(repo, ctx, path):
+                        if path in latecomer:
+                            fctx = latecomer[path]
+                            flags = fctx.flags()
+                            mctx = context.memfilectx(fctx.path(), fctx.data(),
+                                                      islink='l' in flags,
+                                                      isexec='x' in flags,
+                                                      copied=copied.get(path))
+                            return mctx
+                        raise IOError()
+                    text = 'latecomer update to %s:\n\n' % prec
+                    text += latecomer.description()
+
+                    new = context.memctx(repo,
+                                         parents=[prec.node(), node.nullid],
+                                         text=text,
+                                         files=files,
+                                         filectxfn=filectxfn,
+                                         user=latecomer.user(),
+                                         date=latecomer.date(),
+                                         extra=latecomer.extra())
+
+                    newid = repo.commitctx(new)
+                if newid is None:
+                    createmarkers(repo, [(tmpctx, ())])
+                    newid = prec.node()
+                else:
+                    phases.retractboundary(repo, latecomer.phase(), [newid])
+                    createmarkers(repo, [(tmpctx, (repo[newid],))],
+                                           flag=latediff)
+                bmupdate(newid)
+                tr.close()
+                repo.ui.status(_('commited as %s\n') % node.short(newid))
+            finally:
+                tr.release()
+        finally:
+            lock.release()
+        # reroute the working copy parent to the new changeset
+        repo.dirstate.setparents(newid, node.nullid)
+    finally:
+        wlock.release()
+
+def _solveconflicting(ui, repo, conflicting, dryrun=False):
+    base, others = conflictingdata(conflicting)
+    if len(others) > 1:
+        raise util.Abort("We do not handle split yet")
+    other = others[0]
+    if conflicting.phase() <= phases.public:
+        raise util.Abort("We can't resolve this conflict from the public side")
+    if len(other.parents()) > 1:
+        raise util.Abort("conflicting changeset can't be a merge (yet)")
+    if other.p1() not in conflicting.parents():
+        raise util.Abort("parent are not common (not handled yet)")
+
+    displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
+    ui.status(_('merge:'))
+    if not ui.quiet:
+        displayer.show(conflicting)
+    ui.status(_('with: '))
+    if not ui.quiet:
+        displayer.show(other)
+    ui.status(_('base: '))
+    if not ui.quiet:
+        displayer.show(base)
+    if dryrun:
+        ui.write('hg update -c %s &&\n' % conflicting)
+        ui.write('hg merge %s && \n' % other)
+        ui.write('hg commit -m "auto merge resolving conflict between %s and %s"&&\n'
+                  % (conflicting, other))
+        ui.write('hg up -C %s &&\n' % base)
+        ui.write('hg revert --all --rev tip &&\n')
+        ui.write('hg commit -m "`hg log -r %s --template={desc}`";\n' % conflicting)
+        return
+    #oldphase = max(conflicting.phase(), other.phase())
+    wlock = repo.wlock()
+    try:
+        lock = repo.lock()
+        try:
+            if conflicting not in repo[None].parents():
+                repo.ui.status(_('updating to "local" conflict\n'))
+                hg.update(repo, conflicting.rev())
+            repo.ui.note(_('merging conflicting changeset\n'))
+            stats = merge.update(repo,
+                                 other.node(),
+                                 branchmerge=True,
+                                 force=False,
+                                 partial=None,
+                                 ancestor=base.node(),
+                                 mergeancestor=True)
+            hg._showstats(repo, stats)
+            if stats[3]:
+                repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
+                                 "or 'hg update -C .' to abandon\n"))
+            #repo.dirstate.write()
+            if stats[3] > 0:
+                raise util.Abort('GASP! Merge Conflict! You are on you own chap!',
+                                 hint='/!\\ hg evolve --continue will NOT work /!\\')
+            tr = repo.transaction('stabilize-conflicting')
+            try:
+                repo.dirstate.setparents(conflicting.node(), node.nullid)
+                oldlen = len(repo)
+                amend(ui, repo)
+                if oldlen == len(repo):
+                    new = conflicting
+                    # no changes
+                else:
+                    new = repo['.']
+                createmarkers(repo, [(other, (new,))])
+                phases.retractboundary(repo, other.phase(), [new.node()])
+                tr.close()
+            finally:
+                tr.release()
+        finally:
+            lock.release()
+    finally:
+        wlock.release()
+
+
+def conflictingdata(ctx):
+    """return base, other part of a conflict
+
+    This only return the first one.
+
+    XXX this woobly function won't survive XXX
+    """
+    for base in ctx._repo.set('reverse(precursors(%d))', ctx):
+        newer = newerversion(ctx._repo, base.node())
+        # drop filter and solution including the original ctx
+        newer = [n for n in newer if n and ctx.node() not in n]
+        if newer:
+            return base, tuple(ctx._repo[o] for o in newer[0])
+    raise KeyError('Base seem unknown. This case is not handled yet.')
+
+
+
 shorttemplate = '[{rev}] {desc|firstline}\n'
 
 @command('^gdown',
     """
     wlock = repo.wlock()
     try:
-        new = set(noderange(repo, opts['new']))
-        targetnodes = set(noderange(repo, revs))
-        if not new:
-            new = [node.nullid]
-        for n in targetnodes:
-            if not repo[n].mutable():
-                ui.warn(_("cannot kill immutable changeset %s\n") % repo[n])
+        lock = repo.lock()
+        try:
+            new = set(noderange(repo, opts['new']))
+            targetnodes = set(noderange(repo, revs))
+            if new:
+                sucs = tuple(repo[n] for n in new)
             else:
-                for ne in new:
-                    repo.addobsolete(ne, n)
-        # update to an unkilled parent
-        wdp = repo['.']
-        newnode = wdp
-        while newnode.obsolete():
-            newnode = newnode.parents()[0]
-        if newnode.node() != wdp.node():
-            commands.update(ui, repo, newnode.rev())
-            ui.status(_('working directory now at %s\n') % newnode)
+                sucs = ()
+            markers = []
+            for n in targetnodes:
+                markers.append((repo[n], sucs))
+            createmarkers(repo, markers)
 
+            # update to an unkilled parent
+            wdp = repo['.']
+            newnode = wdp
+            while newnode.obsolete():
+                newnode = newnode.parents()[0]
+            if newnode.node() != wdp.node():
+                commands.update(ui, repo, newnode.rev())
+                ui.status(_('working directory now at %s\n') % newnode)
+        finally:
+            lock.release()
     finally:
         wlock.release()
 
                     # the intermediate revision if any. No need to update
                     # phases or parents.
                     if tempid is not None:
-                        repo.addobsolete(node.nullid, tempid)
+                        createmarkers(repo, [(repo[tempid], ())])
                     # XXX: need another message in collapse case.
                     tr.close()
                     raise error.Abort(_('no updates found'))
             if newid is None:
                 raise util.Abort(_('nothing to uncommit'))
             # Move local changes on filtered changeset
-            repo.addobsolete(newid, old.node())
+            createmarkers(repo, [(old, (repo[newid],))])
             phases.retractboundary(repo, oldphase, [newid])
             repo.dirstate.setparents(newid, node.nullid)
             _uncommitdirstate(repo, old, match)
     finally:
         lock.release()
 
+@eh.wrapcommand('commit')
 def commitwrapper(orig, ui, repo, *arg, **kwargs):
-    lock = repo.lock()
+    if kwargs.get('amend', False):
+        lock = None
+    else:
+        lock = repo.lock()
     try:
         obsoleted = kwargs.get('obsolete', [])
         if obsoleted:
         if not result: # commit successed
             new = repo['-1']
             oldbookmarks = []
+            markers = []
             for old in obsoleted:
                 oldbookmarks.extend(repo.nodebookmarks(old.node()))
-                repo.addobsolete(new.node(), old.node())
+                markers.append((old, (new,)))
+            if markers:
+                createmarkers(repo, markers)
             for book in oldbookmarks:
                 repo._bookmarks[book] = new.node()
             if oldbookmarks:
                 bookmarks.write(repo)
         return result
     finally:
-        lock.release()
+        if lock is not None:
+            lock.release()
 
+@command('^touch',
+    [('r', 'rev', [], 'revision to update'),],
+    # allow to choose the seed ?
+    _('[-r] revs'))
+def touch(ui, repo, *revs, **opts):
+    """Create successors with exact same property but hash
+    
+    This is used to "resurect" changeset"""
+    revs = list(revs)
+    revs.extend(opts['rev'])
+    if not revs:
+        revs = ['.']
+    revs = scmutil.revrange(repo, revs)
+    if not revs:
+        ui.write_err('no revision to touch\n')
+        return 1
+    if repo.revs('public() and %ld', revs):
+        raise util.Abort("can't touch public revision")
+    wlock = repo.wlock()
+    try:
+        lock = repo.lock()
+        try:
+            tr = repo.transaction('touch')
+            try:
+                for r in revs:
+                    ctx = repo[r]
+                    extra = ctx.extra().copy()
+                    extra['__touch-noise__'] = random.randint(0, 0xffffffff)
+                    new, _ = rewrite(repo, ctx, [], ctx,
+                                     [ctx.p1().node(), ctx.p2().node()],
+                                     commitopts={'extra': extra})
+                    createmarkers(repo, [(ctx, (repo[new],))])
+                    phases.retractboundary(repo, ctx.phase(), [new])
+                    if ctx in repo[None].parents():
+                        repo.dirstate.setparents(new, node.nullid)
+                tr.close()
+            finally:
+                tr.release()
+        finally:
+            lock.release()