merge
authorPierre-Yves David <pierre-yves.david@ens-lyon.org>
Tue, 21 Aug 2012 02:37:53 +0200
changeset 461 cd43fd9f923f
parent 459 3477326461e3 (diff)
parent 460 19bba3c91246 (current diff)
child 462 d38e02332c2c
merge
hgext/obsolete.py
--- a/.hgignore	Tue Aug 21 02:36:33 2012 +0200
+++ b/.hgignore	Tue Aug 21 02:37:53 2012 +0200
@@ -5,6 +5,8 @@
 ^html/
 \.pyc$
 ~$
+\.swp$
 \.orig$
 \.rej$
 \.err$
+^tests/easy_run.sh$
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/nopushpublish.py	Tue Aug 21 02:37:53 2012 +0200
@@ -0,0 +1,36 @@
+# 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)
--- a/docs/evolve-faq.rst	Tue Aug 21 02:36:33 2012 +0200
+++ b/docs/evolve-faq.rst	Tue Aug 21 02:37:53 2012 +0200
@@ -96,9 +96,9 @@
 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
@@ -112,8 +112,7 @@
 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::
@@ -209,7 +208,7 @@
 
 You can also use a debug command
 
-    $ hg debugsuccessors
+    $ hg debugobsolete
       5eb72dbe0cb4 e8db4aa611f6
       c4cbebac3751 4f1c269eab68
 
@@ -223,8 +222,8 @@
 
 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.
 
 
 
--- a/docs/from-mq.rst	Tue Aug 21 02:36:33 2012 +0200
+++ b/docs/from-mq.rst	Tue Aug 21 02:37:53 2012 +0200
@@ -84,7 +84,7 @@
 hg qref -X
 ````````````
 
-To remove change from you current commit use::
+To remove changes from you current commit use::
 
   $ hg uncommit not-ready.txt
 
--- a/docs/tutorials/tutorial.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/docs/tutorials/tutorial.t	Tue Aug 21 02:37:53 2012 +0200
@@ -223,7 +223,7 @@
   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
 
@@ -585,7 +585,7 @@
   |
   | @  ffa278c50818 (draft): bathroom stuff
   | |
-  o |  8a79ae8b029e (draft): bathroom stuff
+  x |  8a79ae8b029e (draft): bathroom stuff
   |/
   o  a2fccc2e7b08 (public): SPAM SPAM
   |
@@ -610,7 +610,7 @@
   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]
  
 
@@ -723,7 +723,7 @@
   $ hg log -G
   o  ae45c0c3092a (draft): SPAM SPAM SPAM
   |
-  o  437efbcaf700 (draft): animals
+  x  437efbcaf700 (draft): animals
   |
   @  ffa278c50818 (draft): bathroom stuff
   |
--- a/enable.sh	Tue Aug 21 02:36:33 2012 +0200
+++ b/enable.sh	Tue Aug 21 02:37:53 2012 +0200
@@ -3,8 +3,8 @@
 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
+if !( hg --version -q | grep -qe 'version 2\.[3-9]' ); then
+    echo 'You need mercurial 2.3 or later' >&2
     exit 2
 fi
 
@@ -35,12 +35,6 @@
 ### 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
--- a/hgext/evolve.py	Tue Aug 21 02:36:33 2012 +0200
+++ b/hgext/evolve.py	Tue Aug 21 02:37:53 2012 +0200
@@ -17,12 +17,11 @@
 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.i18n import _
-from mercurial.commands import walkopts, commitopts, commitopts2, logopts
+from mercurial.commands import walkopts, commitopts, commitopts2
 from mercurial import hg
 
 ### util function
@@ -118,8 +117,10 @@
         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,)))
+            obsolete = extensions.find('obsolete')
+            obsolete.createmarkers(repo, markers)
         else:
             # newid is an existing revision. It could make sense to
             # replace revisions with existing ones but probably not by
@@ -157,13 +158,14 @@
             repo.ui.write_err(_('/!\\ use "hg up -C . ; hg stabilize --dry-run" /!\\\n'))
             raise
         oldbookmarks = repo.nodebookmarks(nodesrc)
+        obsolete = extensions.find('obsolete')
         if nodenew is not None:
             phases.retractboundary(repo, destphase, [nodenew])
-            repo.addobsolete(nodenew, nodesrc)
+            obsolete.createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))])
             for book in oldbookmarks:
                 repo._bookmarks[book] = nodenew
         else:
-            repo.addobsolete(node.nullid, nodesrc)
+            obsolete.createmarkers(repo, [(repo[nodesrc], ())])
             # Behave like rebase, move bookmarks to dest
             for book in oldbookmarks:
                 repo._bookmarks[book] = dest.node()
@@ -189,7 +191,7 @@
     # 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]
@@ -353,25 +355,30 @@
     """
     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))
+            obsolete = extensions.find('obsolete')
+            obsolete.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()
 
@@ -463,7 +470,8 @@
                     # the intermediate revision if any. No need to update
                     # phases or parents.
                     if tempid is not None:
-                        repo.addobsolete(node.nullid, tempid)
+                        obsolete = extensions.find('obsolete')
+                        obsolete.createmarkers(repo, [(repo[tempid], ())])
                     # XXX: need another message in collapse case.
                     tr.close()
                     raise error.Abort(_('no updates found'))
@@ -599,7 +607,8 @@
             if newid is None:
                 raise util.Abort(_('nothing to uncommit'))
             # Move local changes on filtered changeset
-            repo.addobsolete(newid, old.node())
+            obsolete = extensions.find('obsolete')
+            obsolete.createmarkers(repo, [(old, (repo[newid],))])
             phases.retractboundary(repo, oldphase, [newid])
             repo.dirstate.setparents(newid, node.nullid)
             _uncommitdirstate(repo, old, match)
@@ -622,9 +631,13 @@
         if not result: # commit successed
             new = repo['-1']
             oldbookmarks = []
+            obsolete = extensions.find('obsolete')
+            markers = []
             for old in obsoleted:
                 oldbookmarks.extend(repo.nodebookmarks(old.node()))
-                repo.addobsolete(new.node(), old.node())
+                markers.append((old, (new,)))
+            if markers:
+                obsolete.createmarkers(repo, markers)
             for book in oldbookmarks:
                 repo._bookmarks[book] = new.node()
             if oldbookmarks:
@@ -634,7 +647,11 @@
         lock.release()
 
 def graftwrapper(orig, ui, repo, *revs, **kwargs):
+    kwargs = dict(kwargs)
+    revs = list(revs) + kwargs.get('rev', [])
+    kwargs['rev'] = []
     obsoleted = kwargs.setdefault('obsolete', [])
+
     lock = repo.lock()
     try:
         if kwargs.get('old_obsolete'):
--- a/hgext/obsolete.py	Tue Aug 21 02:36:33 2012 +0200
+++ b/hgext/obsolete.py	Tue Aug 21 02:37:53 2012 +0200
@@ -10,19 +10,18 @@
 General concept
 ===============
 
-This extension introduces the *obsolete* concept. It adds a new *obsolete*
-relation between two changesets. A relation ``<changeset B> obsolete <changeset
-A>`` is set to denote that ``<changeset B>`` is new version of ``<changeset
-A>``.
+This extension introduces the *obsolete* concept. The relation
+``<changeset B> obsoletes <changeset A>`` denotes that ``<changeset B>``
+is a new version of ``<changeset A>``.
 
-The *obsolete* relation act as a **perpendicular history** to the standard
-changeset history. Standard changeset history versions files. The *obsolete*
-relation versions changesets.
+The *obsolete* relations act as an history **orthogonal** to the regular
+changesets history. Regular changesets history versions files. *Obsolete*
+relations version changesets.
 
 :obsolete:     a changeset that has been replaced by another one.
 :unstable:     a changeset that is not obsolete but has an obsolete ancestor.
-:suspended:    an obsolete changeset with unstable descendant.
-:extinct:      an obsolete changeset without unstable descendant.
+:suspended:    an obsolete changeset with unstable descendants.
+:extinct:      an obsolete changeset without unstable descendants.
                (subject to garbage collection)
 
 Another name for unstable could be out of sync.
@@ -31,176 +30,628 @@
 Usage and Feature
 =================
 
-Display and Exchange
---------------------
-
-obsolete changesets are hidden. (except if they have non obsolete changeset)
-
-obsolete changesets are not exchanged. This will probably change later but it
-was the simpler solution for now.
 
 New commands
 ------------
 
 Note that rebased changesets are now marked obsolete instead of being stripped.
 
-Context object
---------------
-
-Context gains a ``obsolete`` method that will return True if a changeset is
-obsolete False otherwise.
-
-revset
-------
-
-Add an ``obsolete()`` entry.
-
-repo extension
---------------
-
-To Do
-~~~~~
-
-- refuse to obsolete published changesets
-
-- handle split
-
-- handle conflict
-
-- handle unstable // out of sync
-
 """
 
-import os
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from StringIO import StringIO
 
-from mercurial.i18n import _
-
-import base64
-import json
-
-import struct
-from mercurial import util, base85
-
-_pack = struct.pack
-_unpack = struct.unpack
 
 from mercurial import util
+
+try:
+    from mercurial import obsolete
+    obsolete._enabled = True
+except ImportError:
+    raise util.Abort('Obsolete extension requires Mercurial 2.3 (or later)')
+
+import sys
+from mercurial.i18n import _
+from mercurial import cmdutil
+from mercurial import commands
 from mercurial import context
-from mercurial import revset
-from mercurial import scmutil
-from mercurial import extensions
-from mercurial import pushkey
 from mercurial import discovery
 from mercurial import error
-from mercurial import commands
-from mercurial import changelog
+from mercurial import extensions
+from mercurial import localrepo
 from mercurial import phases
-from mercurial.node import hex, bin, short, nullid
-from mercurial.lock import release
-from mercurial import localrepo
-from mercurial import cmdutil
+from mercurial import revset
+from mercurial import scmutil
 from mercurial import templatekw
+from mercurial.node import bin, short, nullid
 
-try:
-    from mercurial.localrepo import storecache
-    storecache('babar') # to trigger import
-except (TypeError, ImportError):
-    def storecache(*args):
-        return scmutil.filecache(*args, instore=True)
+# 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('rebase')
+                if e is None:
+                    raise util.Abort('extension %s not found' %e)
+                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
 
 
-### Patch changectx
-#############################
+    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.
+#
+
 
-def obsolete(ctx):
-    """is the changeset obsolete by other"""
-    if ctx.node()is None:
-        return False
-    return bool(ctx._repo.obsoletedby(ctx.node())) and ctx.phase()
+### 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
 
-context.changectx.obsolete = obsolete
+    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 ctx._repo._unstableset
+    return ctx.rev() in getobscache(ctx._repo, 'unstable')
 
-context.changectx.unstable = 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 ctx._repo._extinctset
+    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                           ###
+#####################################################################
 
-context.changectx.extinct = extinct
+# 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
+
+@cachefor('latecomer')
+def _computelatecomerset(repo):
+    """the set of rev trying to obsolete public revision"""
+    query = 'allsuccessors(public()) - obsolete() - public()'
+    return set(repo.revs(query))
 
+@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 ctx._repo._latecomerset
+    return ctx.rev() in getobscache(ctx._repo, 'latecomer')
 
-context.changectx.latecomer = 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 ctx._repo._conflictingset
+    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]
 
-context.changectx.conflicting = conflicting
+
+### 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)
 
 
-### revset
-#############################
+#####################################################################
+### Additional Utilities                                          ###
+#####################################################################
 
-def revsethidden(repo, subset, x):
-    """hidden changesets"""
-    args = revset.getargs(x, 0, 0, 'hidden takes no argument')
-    return [r for r in subset if r in repo.changelog.hiddenrevs]
+# This section contains a lot of small utility function and method
 
-def revsetobsolete(repo, subset, x):
-    """obsolete changesets"""
-    args = revset.getargs(x, 0, 0, 'obsolete takes no argument')
-    return [r for r in subset if r in repo._obsoleteset and repo._phasecache.phase(repo, r) > 0]
+# - 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
 
-# XXX Backward compatibility, to be removed once stabilized
-if '_phasecache' not in vars(localrepo.localrepository): # new api
-    def revsetobsolete(repo, subset, x):
-        """obsolete changesets"""
-        args = revset.getargs(x, 0, 0, 'obsolete takes no argument')
-        return [r for r in subset if r in repo._obsoleteset and repo._phaserev[r] > 0]
+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.
 
-def revsetunstable(repo, subset, x):
-    """non obsolete changesets descendant of obsolete one"""
-    args = revset.getargs(x, 0, 0, 'unstable takes no arguments')
-    return [r for r in subset if r in repo._unstableset]
+    /!\ 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()
 
-def revsetsuspended(repo, subset, x):
-    """obsolete changesets with non obsolete descendants"""
-    args = revset.getargs(x, 0, 0, 'suspended takes no arguments')
-    return [r for r in subset if r in repo._suspendedset]
+
+### Useful alias
 
-def revsetextinct(repo, subset, x):
-    """obsolete changesets without obsolete descendants"""
-    args = revset.getargs(x, 0, 0, 'extinct takes no arguments')
-    return [r for r in subset if r in repo._extinctset]
+@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 .^')
+
+# - "troubles" method on changectx
+
+@eh.addattr(context.changectx, 'troubles')
+def troubles(ctx):
+    """Return a tuple listing all the troubles that affect a changeset
 
-def revsetlatecomer(repo, subset, x):
-    """latecomer, Try to succeed to public change"""
-    args = revset.getargs(x, 0, 0, 'latecomer takes no arguments')
-    return [r for r in subset if r in repo._latecomerset]
+    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)
 
-def revsetconflicting(repo, subset, x):
-    """conflicting, Try to succeed to public change"""
-    args = revset.getargs(x, 0, 0, 'conflicting takes no arguments')
-    return [r for r in subset if r in repo._conflictingset]
+
+### Obsolescence graph
+
+# XXX SOME MAJOR CLEAN UP TO DO HERE XXX
 
 def _precursors(repo, s):
     """Precursor of a changeset"""
@@ -214,12 +665,6 @@
                 cs.add(pr)
     return cs
 
-def revsetprecursors(repo, subset, x):
-    """precursors of a subset"""
-    s = revset.getset(repo, range(len(repo)), x)
-    cs = _precursors(repo, s)
-    return [r for r in subset if r in cs]
-
 def _allprecursors(repo, s):  # XXX we need a better naming
     """transitive precursors of a subset"""
     toproceed = [repo[r].node() for r in s]
@@ -240,12 +685,6 @@
             cs.add(pr)
     return cs
 
-def revsetallprecursors(repo, subset, x):
-    """obsolete parents"""
-    s = revset.getset(repo, range(len(repo)), x)
-    cs = _allprecursors(repo, s)
-    return [r for r in subset if r in cs]
-
 def _successors(repo, s):
     """Successors of a changeset"""
     cs = set()
@@ -259,12 +698,6 @@
                     cs.add(sr)
     return cs
 
-def revsetsuccessors(repo, subset, x):
-    """successors of a subset"""
-    s = revset.getset(repo, range(len(repo)), x)
-    cs = _successors(repo, s)
-    return [r for r in subset if r in cs]
-
 def _allsuccessors(repo, s):  # XXX we need a better naming
     """transitive successors of a subset"""
     toproceed = [repo[r].node() for r in s]
@@ -287,32 +720,219 @@
             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):
-    """obsolete parents"""
+    """``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 :-/
 
-### template keywords
-#####################
-
+@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 rev in repo._extinctset:
-        return 'extinct'
-    if rev in repo._suspendedset:
-        return 'suspended'
-    if rev in repo._unstableset:
+    if ctx.obsolete():
+        if ctx.extinct():
+            return 'extinct'
+        else:
+            return 'suspended'
+    elif ctx.unstable():
         return 'unstable'
     return 'stable'
 
-### Other Extension compat
-############################
+#####################################################################
+### 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()'))
+    try:
+        return orig(ui, repo, *args, **kwargs)
+    finally:
+        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 unstables changesets\n') % newunstables)
+        if newlatecomers > 0:
+            ui.warn(_('%i new latecomers changesets\n') % newlatecomers)
+        if newconflictings > 0:
+            ui.warn(_('%i new conflictings changesets\n') % newconflictings)
+
+@eh.reposetup
+def _repostabilizesetup(ui, repo):
+    """Add a hint for "hg stabilize" when troubles make push fails
+    """
+    if not repo.local():
+        return
+
+    opush = repo.push
 
+    class stabilizerrepo(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 stabilize' 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__ = stabilizerrepo
+
+#####################################################################
+### 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"""
@@ -354,67 +974,50 @@
     repo._rebasestate = {}
     repo._rebasetarget = None
     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.
-                targetrev = repo[repo._rebasetarget].rev()
-                newrevs = set([targetrev])
-                replacements = {}
-                for rev, newrev in sorted(repo._rebasestate.items()):
-                    oldnode = repo[rev].node()
-                    if newrev not in newrevs:
-                        newnode = repo[newrev].node()
-                        newrevs.add(newrev)
+        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:
-                        newnode = nullid
-                    replacements[oldnode] = newnode
-
-                if kwargs.get('collapse'):
-                    newnodes = set(n for n in replacements.values() if n != nullid)
-                    if newnodes:
-                        # Collapsing into more than one revision?
-                        assert len(newnodes) == 1, newnodes
-                        newnode = newnodes.pop()
-                    else:
-                        newnode = nullid
-                    repo.addcollapsedobsolete(replacements, newnode)
-                else:
-                    for oldnode, newnode in replacements.iteritems():
-                        repo.addobsolete(newnode, oldnode)
-        return res
+                        # 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')
 
-
-def extsetup(ui):
-
-    revset.symbols["hidden"] = revsethidden
-    revset.symbols["obsolete"] = revsetobsolete
-    revset.symbols["unstable"] = revsetunstable
-    revset.symbols["suspended"] = revsetsuspended
-    revset.symbols["extinct"] = revsetextinct
-    revset.symbols["latecomer"] = revsetlatecomer
-    revset.symbols["conflicting"] = revsetconflicting
-    revset.symbols["obsparents"] = revsetprecursors  # DEPR
-    revset.symbols["precursors"] = revsetprecursors
-    revset.symbols["obsancestors"] = revsetallprecursors  # DEPR
-    revset.symbols["allprecursors"] = revsetallprecursors  # bad name
-    revset.symbols["successors"] = revsetsuccessors
-    revset.symbols["allsuccessors"] = revsetallsuccessors  # bad name
-
-    templatekw.keywords['obsolete'] = obsoletekw
-
+@eh.extsetup
+def _rebasewrapping(ui):
     # warning about more obsolete
-    for cmd in ['commit', 'push', 'pull', 'graft', 'phase', 'unbundle']:
-        entry = extensions.wrapcommand(commands.table, cmd, warnobserrors)
     try:
         rebase = extensions.find('rebase')
         if rebase:
@@ -426,356 +1029,39 @@
     except KeyError:
         pass  # rebase not found
 
-# Pushkey mechanism for mutable
-#########################################
 
-def listmarkers(repo):
-    """List markers over pushkey"""
-    if not repo.obsstore:
-        return {}
-    data = repo.obsstore._writemarkers()
-    encdata = base85.b85encode(data)
-    return {'dump0': encdata,
-            'dump': encdata} # legacy compat
-
-def pushmarker(repo, key, old, new):
-    """Push markers over pushkey"""
-    if not key.startswith('dump'):
-        repo.ui.warn(_('unknown key: %r') % key)
-        return 0
-    if old:
-        repo.ui.warn(_('unexpected old value') % key)
-        return 0
-    data = base85.b85decode(new)
-    lock = repo.lock()
-    try:
-        try:
-            repo.obsstore.mergemarkers(data)
-            return 1
-        except util.Abort:
-            return 0
-    finally:
-        lock.release()
-
-pushkey.register('obsolete', pushmarker, listmarkers)
-
-### Discovery wrapping
-#############################
-
-class blist(list, object):
-    """silly class to have non False but empty list"""
-
-    def __nonzero__(self):
-        return bool(len(self.orig))
-
-def wrapfindcommonoutgoing(orig, repo, *args, **kwargs):
-    """wrap mercurial.discovery.findcommonoutgoing to remove extinct changeset
-
-    Such excluded changeset are removed from excluded  and will *not* appear
-    are excluded secret changeset.
-    """
-    outgoing = orig(repo, *args, **kwargs)
-    orig = outgoing.excluded
-    outgoing.excluded = blist(n for n in orig if not repo[n].extinct())
-    # when no revision is specified (push everything) a shortcut is taken when
-    # nothign was exclude. taking this code path when extinct changeset have
-    # been excluded leads to repository corruption.
-    outgoing.excluded.orig = orig
-    return outgoing
-
-def wrapcheckheads(orig, repo, remote, outgoing, *args, **kwargs):
-    """wrap mercurial.discovery.checkheads
+#####################################################################
+### Older format management                                       ###
+#####################################################################
 
-    * prevent unstability to be pushed
-    * patch remote to ignore obsolete heads on remote
-    """
-    # do not push instability
-    for h in outgoing.missingheads:
-        # checking heads only is enought because any thing base on obsolete
-        # changeset is either obsolete or unstable.
-        ctx =  repo[h]
-        if ctx.unstable():
-            raise util.Abort(_("push includes an unstable changeset: %s!")
-                             % ctx)
-        if ctx.obsolete():
-            raise util.Abort(_("push includes an  obsolete changeset: %s!")
-                             % ctx)
-        if ctx.latecomer():
-            raise util.Abort(_("push includes an latecomer changeset: %s!")
-                             % ctx)
-        if ctx.conflicting():
-            raise util.Abort(_("push includes conflicting changeset: %s!")
-                             % ctx)
-    ### patch remote branch map
-    # do not read it this burn eyes
-    try:
-        if 'oldbranchmap' not in vars(remote):
-            remote.oldbranchmap = remote.branchmap
-            def branchmap():
-                newbm = {}
-                oldbm = None
-                if (util.safehasattr(phases, 'visiblebranchmap')
-                    and not util.safehasattr(remote, 'ignorevisiblebranchmap')
-                   ):
-                    remote.ignorevisiblebranchmap = False
-                    remote.branchmap = remote.oldbranchmap
-                    oldbm = phases.visiblebranchmap(remote)
-                    remote.branchmap = remote.newbranchmap
-                    remote.ignorevisiblebranchmap = True
-                if oldbm is None:
-                    oldbm = remote.oldbranchmap()
-                for branch, nodes in oldbm.iteritems():
-                    nodes = list(nodes)
-                    new = set()
-                    while nodes:
-                        n = nodes.pop()
-                        if n in repo.obsstore.precursors:
-                            markers = repo.obsstore.precursors[n]
-                            for mark in markers:
-                                for newernode in mark[1]:
-                                    if newernode is not None:
-                                        nodes.append(newernode)
-                        else:
-                            new.add(n)
-                    if new:
-                        newbm[branch] = list(new)
-                return newbm
-            remote.ignorevisiblebranchmap = True
-            remote.branchmap = branchmap
-            remote.newbranchmap = branchmap
-        return orig(repo, remote, outgoing, *args, **kwargs)
-    finally:
-        remote.__dict__.pop('branchmap', None) # restore class one
-        remote.__dict__.pop('oldbranchmap', None)
-        remote.__dict__.pop('newbranchmap', None)
-        remote.__dict__.pop('ignorevisiblebranchmap', None)
+# Code related to detection and management of older legacy format never
+# handled by core
 
-# eye are still burning
-def wrapvisiblebranchmap(orig, repo):
-    ignore = getattr(repo, 'ignorevisiblebranchmap', None)
-    if ignore is None:
-        return orig(repo)
-    elif ignore:
-        return repo.branchmap()
-    else:
-        return None # break recursion
-
-def wrapclearcache(orig, repo, *args, **kwargs):
-    try:
-        return orig(repo, *args, **kwargs)
-    finally:
-        repo._clearobsoletecache()
-
-
-### New commands
-#############################
-
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-
-@command('debugobsolete', [], _('SUBJECT OBJECT'))
-def cmddebugobsolete(ui, repo, subject, object):
-    """add an obsolete relation between two nodes
-
-    The subject is expected to be a newer version of the object.
-    """
-    lock = repo.lock()
-    try:
-        sub = repo[subject]
-        obj = repo[object]
-        repo.addobsolete(sub.node(), obj.node())
-    finally:
-        lock.release()
-    return 0
+import json
 
-@command('debugconvertobsolete', [], '')
-def cmddebugconvertobsolete(ui, repo):
-    """import markers from an .hg/obsolete-relations file"""
-    cnt = 0
-    err = 0
-    l = repo.lock()
-    some = False
-    try:
-        repo._importoldobsolete = True
-        store = repo.obsstore
-        ### very first format
-        try:
-            f = repo.opener('obsolete-relations')
-            try:
-                some = True
-                for line in f:
-                    subhex, objhex = line.split()
-                    suc = bin(subhex)
-                    prec = bin(objhex)
-                    sucs = (suc==nullid) and [] or [suc]
-                    meta = {
-                        'date':  '%i %i' % util.makedate(),
-                        'user': ui.username(),
-                        }
-                    try:
-                        store.create(prec, sucs, 0, meta)
-                        cnt += 1
-                    except ValueError:
-                        repo.ui.write_err("invalid old marker line: %s"
-                                          % (line))
-                        err += 1
-            finally:
-                f.close()
-            util.unlink(repo.join('obsolete-relations'))
-        except IOError:
-            pass
-        ### second (json) format
-        data = repo.sopener.tryread('obsoletemarkers')
-        if data:
-            some = True
-            for oldmark in json.loads(data):
-                del oldmark['id']  # dropped for now
-                del oldmark['reason']  # unused until then
-                oldobject = str(oldmark.pop('object'))
-                oldsubjects = [str(s) for s in oldmark.pop('subjects', [])]
-                LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError)
-                if len(oldobject) != 40:
-                    try:
-                        oldobject = repo[oldobject].node()
-                    except LOOKUP_ERRORS:
-                        pass
-                if any(len(s) != 40 for s in oldsubjects):
-                    try:
-                        oldsubjects = [repo[s].node() for s in oldsubjects]
-                    except LOOKUP_ERRORS:
-                        pass
-
-                oldmark['date'] = '%i %i' % tuple(oldmark['date'])
-                meta = dict((k.encode('utf-8'), v.encode('utf-8'))
-                             for k, v in oldmark.iteritems())
-                try:
-                    succs = [bin(n) for n in oldsubjects]
-                    succs = [n for n in succs if n != nullid]
-                    store.create(bin(oldobject), succs,
-                                 0, meta)
-                    cnt += 1
-                except ValueError:
-                    repo.ui.write_err("invalid marker %s -> %s\n"
-                                 % (oldobject, oldsubjects))
-                    err += 1
-            util.unlink(repo.sjoin('obsoletemarkers'))
-    finally:
-        del repo._importoldobsolete
-        l.release()
-    if not some:
-            ui.warn('nothing to do\n')
-    ui.status('%i obsolete marker converted\n' % cnt)
-    if err:
-        ui.write_err('%i conversion failed. check you graph!\n' % err)
-
-@command('debugsuccessors', [], '')
-def cmddebugsuccessors(ui, repo):
-    """dump obsolete changesets and their successors
-
-    Each line matches an existing marker, the first identifier is the
-    obsolete changeset identifier, followed by it successors.
+@eh.reposetup
+def _checkoldobsolete(ui, repo):
+    """Detect that a repo still contains some old obsolete format
     """
-    lock = repo.lock()
-    try:
-        allsuccessors = repo.obsstore.precursors
-        for old in sorted(allsuccessors):
-            successors = [sorted(m[1]) for m in allsuccessors[old]]
-            for i, group in enumerate(sorted(successors)):
-                ui.write('%s' % short(old))
-                for new in group:
-                    ui.write(' %s' % short(new))
-                ui.write('\n')
-    finally:
-        lock.release()
-
-### Altering existing command
-#############################
-
-def wrapmayobsoletewc(origfn, ui, repo, *args, **opts):
-    res = origfn(ui, repo, *args, **opts)
-    if repo['.'].obsolete():
-        ui.warn(_('Working directory parent is obsolete\n'))
-    return res
-
-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()'))
-    #print orig, priorunstables
-    #print len(repo.revs('secret() - obsolete()'))
-    try:
-        return orig(ui, repo, *args, **kwargs)
-    finally:
-        newunstables = len(repo.revs('unstable()')) - priorunstables
-        newlatecomers = len(repo.revs('latecomer()')) - priorlatecomers
-        newconflictings = len(repo.revs('conflicting()')) - priorconflictings
-        #print orig, newunstables
-        #print len(repo.revs('secret() - obsolete()'))
-        if newunstables > 0:
-            ui.warn(_('%i new unstables changesets\n') % newunstables)
-        if newlatecomers > 0:
-            ui.warn(_('%i new latecomers changesets\n') % newlatecomers)
-        if newconflictings > 0:
-            ui.warn(_('%i new conflictings changesets\n') % newconflictings)
-
-def noextinctsvisibleheads(orig, repo):
-    repo._turn_extinct_secret()
-    return orig(repo)
-
-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:
-            meta = {
-                'subjects':  [new],
-                'object': oldnode,
-                'date':  util.makedate(),
-                'user': ui.username(),
-                'reason': 'commit --amend',
-                }
-            repo.obsstore.create(oldnode, [new], 0, meta)
-            repo._clearobsoletecache()
-            repo._turn_extinct_secret()
-        finally:
-            lock.release()
-    return new
-
-def uisetup(ui):
-    extensions.wrapcommand(commands.table, "update", wrapmayobsoletewc)
-    extensions.wrapcommand(commands.table, "pull", wrapmayobsoletewc)
-    if util.safehasattr(cmdutil, 'amend'):
-        extensions.wrapfunction(cmdutil, 'amend', wrapcmdutilamend)
-    extensions.wrapfunction(discovery, 'findcommonoutgoing', wrapfindcommonoutgoing)
-    extensions.wrapfunction(discovery, 'checkheads', wrapcheckheads)
-    extensions.wrapfunction(phases, 'visibleheads', noextinctsvisibleheads)
-    extensions.wrapfunction(phases, 'advanceboundary', wrapclearcache)
-    if util.safehasattr(phases, 'visiblebranchmap'):
-        extensions.wrapfunction(phases, 'visiblebranchmap', wrapvisiblebranchmap)
-
-### serialisation
-#############################
-
-def _obsserialise(obssubrels, flike):
-    """serialise an obsolete relation mapping in a plain text one
-
-    this is for subject -> [objects] mapping
-
-    format is::
-
-        <subject-full-hex> <object-full-hex>\n"""
-    for sub, objs in obssubrels.iteritems():
-        for obj in objs:
-            if sub is None:
-                sub = nullid
-            flike.write('%s %s\n' % (hex(sub), hex(obj)))
+    if not repo.local():
+        return
+    for arg in sys.argv:
+        if 'debugc' in arg:
+            break
+    else:
+        data = repo.opener.tryread('obsolete-relations')
+        if not data:
+            data = repo.sopener.tryread('obsoletemarkers')
+        if data:
+            raise util.Abort('old format of obsolete marker detected!\n'
+                             'run `hg debugconvertobsolete` once.')
 
 def _obsdeserialise(flike):
     """read a file like object serialised with _obsserialise
 
-    this desierialize into a {subject -> objects} mapping"""
+    this desierialize into a {subject -> objects} mapping
+
+    this was the very first format ever."""
     rels = {}
     for line in flike:
         subhex, objhex = line.split()
@@ -785,527 +1071,92 @@
         rels.setdefault( subnode, set()).add(bin(objhex))
     return rels
 
-### diagnostique tools
-#############################
-
-def unstables(repo):
-    """Return all unstable changeset"""
-    return scmutil.revrange(repo, ['obsolete():: and (not obsolete())'])
-
-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)
-
-### obsolete relation storage
-#############################
-def add2set(d, key, mark):
-    """add <mark> to a `set` in <d>[<key>]"""
-    d.setdefault(key, []).append(mark)
-
-def markerid(marker):
-    KEYS = ['subjects', "object", "date", "user", "reason"]
-    for key in KEYS:
-        assert key in marker
-    keys = sorted(marker.keys())
-    a = util.sha1()
-    for key in keys:
-        if key == 'subjects':
-            for sub in sorted(marker[key]):
-                a.update(sub)
-        elif key == 'id':
-            pass
-        else:
-            a.update(str(marker[key]))
-    a.update('\0')
-    return a.digest()
-
-# mercurial backport
-
-def encodemeta(meta):
-    """Return encoded metadata string to string mapping.
-
-    Assume no ':' in key and no '\0' in both key and value."""
-    for key, value in meta.iteritems():
-        if ':' in key or '\0' in key:
-            raise ValueError("':' and '\0' are forbidden in metadata key'")
-        if '\0' in value:
-            raise ValueError("':' are forbidden in metadata value'")
-    return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
-
-def decodemeta(data):
-    """Return string to string dictionary from encoded version."""
-    d = {}
-    for l in data.split('\0'):
-        if l:
-            key, value = l.split(':')
-            d[key] = value
-    return d
-
-# data used for parsing and writing
-_fmversion = 0
-_fmfixed   = '>BIB20s'
-_fmnode = '20s'
-_fmfsize = struct.calcsize(_fmfixed)
-_fnodesize = struct.calcsize(_fmnode)
-
-def _readmarkers(data):
-    """Read and enumerate markers from raw data"""
-    off = 0
-    diskversion = _unpack('>B', data[off:off + 1])[0]
-    off += 1
-    if diskversion != _fmversion:
-        raise util.Abort(_('parsing obsolete marker: unknown version %r')
-                         % diskversion)
-
-    # Loop on markers
-    l = len(data)
-    while off + _fmfsize <= l:
-        # read fixed part
-        cur = data[off:off + _fmfsize]
-        off += _fmfsize
-        nbsuc, mdsize, flags, pre = _unpack(_fmfixed, cur)
-        # read replacement
-        sucs = ()
-        if nbsuc:
-            s = (_fnodesize * nbsuc)
-            cur = data[off:off + s]
-            sucs = _unpack(_fmnode * nbsuc, cur)
-            off += s
-        # read metadata
-        # (metadata will be decoded on demand)
-        metadata = data[off:off + mdsize]
-        if len(metadata) != mdsize:
-            raise util.Abort(_('parsing obsolete marker: metadata is too '
-                               'short, %d bytes expected, got %d')
-                             % (len(metadata), mdsize))
-        off += mdsize
-        yield (pre, sucs, flags, metadata)
-
-class obsstore(object):
-    """Store obsolete markers
-
-    Markers can be accessed with two mappings:
-    - precursors: old -> set(new)
-    - successors: new -> set(old)
-    """
-
-    def __init__(self):
-        self._all = []
-        # new markers to serialize
-        self._new = []
-        self.precursors = {}
-        self.successors = {}
-
-    def __iter__(self):
-        return iter(self._all)
-
-    def __nonzero__(self):
-        return bool(self._all)
-
-    def create(self, prec, succs=(), flag=0, metadata=None):
-        """obsolete: add a new obsolete marker
-
-        * ensuring it is hashable
-        * check mandatory metadata
-        * encode metadata
-        """
-        if metadata is None:
-            metadata = {}
-        if len(prec) != 20:
-            raise ValueError(repr(prec))
-        for succ in succs:
-            if len(succ) != 20:
-                raise ValueError((succs))
-        marker = (str(prec), tuple(succs), int(flag), encodemeta(metadata))
-        self.add(marker)
-
-    def add(self, marker):
-        """Add a new marker to the store
-
-        This marker still needs to be written to disk"""
-        self._new.append(marker)
-        self._load(marker)
-
-    def loadmarkers(self, data):
-        """Load all markers in data, mark them as known."""
-        for marker in _readmarkers(data):
-            self._load(marker)
-
-    def mergemarkers(self, data):
-        other = set(_readmarkers(data))
-        local = set(self._all)
-        new = other - local
-        for marker in new:
-            self.add(marker)
-
-    def flushmarkers(self, stream):
-        """Write all markers to a stream
-
-        After this operation, "new" markers are considered "known"."""
-        self._writemarkers(stream)
-        self._new[:] = []
-
-    def _load(self, marker):
-        self._all.append(marker)
-        pre, sucs = marker[:2]
-        self.precursors.setdefault(pre, set()).add(marker)
-        for suc in sucs:
-            self.successors.setdefault(suc, set()).add(marker)
-
-    def _writemarkers(self, stream=None):
-        # Kept separate from flushmarkers(), it will be reused for
-        # markers exchange.
-        if stream is None:
-            final = []
-            w = final.append
-        else:
-            w = stream.write
-        w(_pack('>B', _fmversion))
-        for marker in self._all:
-            pre, sucs, flags, metadata = marker
-            nbsuc = len(sucs)
-            format = _fmfixed + (_fmnode * nbsuc)
-            data = [nbsuc, len(metadata), flags, pre]
-            data.extend(sucs)
-            w(_pack(format, *data))
-            w(metadata)
-        if stream is None:
-            return ''.join(final)
-
-
-### repo subclassing
-#############################
-
-def reposetup(ui, repo):
-    if not repo.local():
-        return
-
-    if not util.safehasattr(repo.opener, 'tryread'):
-        raise util.Abort('Obsolete extension requires Mercurial 2.2 (or later)')
-    opull = repo.pull
-    opush = repo.push
-    olock = repo.lock
-    o_rollback = repo._rollback
-    o_updatebranchcache = repo.updatebranchcache
-
-    # /!\ api change in  Hg 2.2 (97efd26eb9576f39590812ea9) /!\
-    if util.safehasattr(repo, '_journalfiles'): # Hg 2.2
-        o_journalfiles = repo._journalfiles
-    o_writejournal = repo._writejournal
-    o_hook = repo.hook
-
-
-    class obsoletingrepo(repo.__class__):
-
-        # workaround
-        def hook(self, name, throw=False, **args):
-            if 'pushkey' in name:
-                args.pop('new')
-                args.pop('old')
-            return o_hook(name, throw=False, **args)
-
-        ### Public method
-        def obsoletedby(self, node):
-            """return the set of node that make <node> obsolete (obj)"""
-            others = set()
-            for marker in self.obsstore.precursors.get(node, []):
-                others.update(marker[1])
-            return others
-
-        def obsolete(self, node):
-            """return the set of node that <node> make obsolete (sub)"""
-            return set(marker[0] for marker in self.obsstore.successors.get(node, []))
-
-        @storecache('obsstore')
-        def obsstore(self):
-            if not getattr(self, '_importoldobsolete', False):
-                data = repo.opener.tryread('obsolete-relations')
-                if not data:
-                    data = repo.sopener.tryread('obsoletemarkers')
-                if data:
-                    raise util.Abort('old format of obsolete marker detected!\n'
-                                     'run `hg debugconvertobsolete` once.')
-            store = obsstore()
-            data = self.sopener.tryread('obsstore')
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+@command('debugconvertobsolete', [], '')
+def cmddebugconvertobsolete(ui, repo):
+    """import markers from an .hg/obsolete-relations file"""
+    cnt = 0
+    err = 0
+    l = repo.lock()
+    some = False
+    try:
+        unlink = []
+        tr = repo.transaction('convert-obsolete')
+        try:
+            repo._importoldobsolete = True
+            store = repo.obsstore
+            ### very first format
+            try:
+                f = repo.opener('obsolete-relations')
+                try:
+                    some = True
+                    for line in f:
+                        subhex, objhex = line.split()
+                        suc = bin(subhex)
+                        prec = bin(objhex)
+                        sucs = (suc==nullid) and [] or [suc]
+                        meta = {
+                            'date':  '%i %i' % util.makedate(),
+                            'user': ui.username(),
+                            }
+                        try:
+                            store.create(tr, prec, sucs, 0, meta)
+                            cnt += 1
+                        except ValueError:
+                            repo.ui.write_err("invalid old marker line: %s"
+                                              % (line))
+                            err += 1
+                finally:
+                    f.close()
+                unlink.append(repo.join('obsolete-relations'))
+            except IOError:
+                pass
+            ### second (json) format
+            data = repo.sopener.tryread('obsoletemarkers')
             if data:
-                store.loadmarkers(data)
-            return store
-
-        @util.propertycache
-        def _obsoleteset(self):
-            """the set of obsolete revision"""
-            obs = set()
-            nm = self.changelog.nodemap
-            for obj in self.obsstore.precursors:
-                try: # /!\api change in Hg 2.2 (e8d37b78acfb22ae2c1fb126c2)/!\
-                    rev = nm.get(obj)
-                except TypeError:  #XXX to remove while breaking Hg 2.1 support
-                    rev = nm.get(obj, None)
-                if rev is not None:
-                    obs.add(rev)
-            return obs
-
-        @util.propertycache
-        def _unstableset(self):
-            """the set of non obsolete revision with obsolete parent"""
-            return set(self.revs('(obsolete()::) - obsolete()'))
-
-        @util.propertycache
-        def _suspendedset(self):
-            """the set of obsolete parent with non obsolete descendant"""
-            return set(self.revs('obsolete() and obsolete()::unstable()'))
-
-        @util.propertycache
-        def _extinctset(self):
-            """the set of obsolete parent without non obsolete descendant"""
-            return set(self.revs('obsolete() - obsolete()::unstable()'))
-
-        @util.propertycache
-        def _latecomerset(self):
-            """the set of rev trying to obsolete public revision"""
-            query = 'allsuccessors(public()) - obsolete() - public()'
-            return set(self.revs(query))
-
-        @util.propertycache
-        def _conflictingset(self):
-            """the set of rev trying to obsolete public revision"""
-            conflicting = set()
-            obsstore = self.obsstore
-            newermap = {}
-            for ctx in self.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(self, 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
-
-        def _clearobsoletecache(self):
-            if '_obsoleteset' in vars(self):
-                del self._obsoleteset
-            self._clearunstablecache()
-
-        def updatebranchcache(self):
-            o_updatebranchcache()
-            self._clearunstablecache()
-
-        def _clearunstablecache(self):
-            if '_unstableset' in vars(self):
-                del self._unstableset
-            if '_suspendedset' in vars(self):
-                del self._suspendedset
-            if '_extinctset' in vars(self):
-                del self._extinctset
-            if '_latecomerset' in vars(self):
-                del self._latecomerset
-            if '_conflictingset' in vars(self):
-                del self._conflictingset
-
-        def addobsolete(self, sub, obj):
-            """Add a relation marking that node <sub> is a new version of <obj>"""
-            assert sub != obj
-            if not repo[obj].phase():
-                if sub is None:
-                    self.ui.warn(
-                        _("trying to kill immutable changeset %(obj)s\n")
-                        % {'obj': short(obj)})
-                if sub is not None:
-                    self.ui.warn(
-                        _("%(sub)s try to obsolete immutable changeset %(obj)s\n")
-                        % {'sub': short(sub), 'obj': short(obj)})
-            lock = self.lock()
-            try:
-                meta = {
-                    'date':  util.makedate(),
-                    'user': ui.username(),
-                    'reason': 'unknown',
-                    }
-                subs = (sub == nullid) and [] or [sub]
-                mid = self.obsstore.create(obj, subs, 0, meta)
-                self._clearobsoletecache()
-                self._turn_extinct_secret()
-                return mid
-            finally:
-                lock.release()
-
-        def addcollapsedobsolete(self, oldnodes, newnode):
-            """Mark oldnodes as collapsed into newnode."""
-            # Assume oldnodes are all descendants of a single rev
-            rootrevs = self.revs('roots(%ln)', oldnodes)
-            assert len(rootrevs) == 1, rootrevs
-            rootnode = self[rootrevs[0]].node()
-            for n in oldnodes:
-                self.addobsolete(newnode, n)
-
-        def _turn_extinct_secret(self):
-            """ensure all extinct changeset are secret"""
-            self._clearobsoletecache()
-            # this is mainly for safety purpose
-            # both pull and push
-            query = '(obsolete() - obsolete()::(unstable() - secret())) - secret()'
-            expobs = [c.node() for c in repo.set(query)]
-            phases.retractboundary(repo, 2, expobs)
-
-        ### Disk IO
+                some = True
+                for oldmark in json.loads(data):
+                    del oldmark['id']  # dropped for now
+                    del oldmark['reason']  # unused until then
+                    oldobject = str(oldmark.pop('object'))
+                    oldsubjects = [str(s) for s in oldmark.pop('subjects', [])]
+                    LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError)
+                    if len(oldobject) != 40:
+                        try:
+                            oldobject = repo[oldobject].node()
+                        except LOOKUP_ERRORS:
+                            pass
+                    if any(len(s) != 40 for s in oldsubjects):
+                        try:
+                            oldsubjects = [repo[s].node() for s in oldsubjects]
+                        except LOOKUP_ERRORS:
+                            pass
 
-        def lock(self, *args, **kwargs):
-            l = olock(*args, **kwargs)
-            if not getattr(l.releasefn, 'obspatched', False):
-                oreleasefn = l.releasefn
-                def releasefn(*args, **kwargs):
-                    if 'obsstore' in vars(self) and self.obsstore._new:
-                        f = self.sopener('obsstore', 'wb', atomictemp=True)
-                        try:
-                            self.obsstore.flushmarkers(f)
-                            f.close()
-                        except: # re-raises
-                            f.discard()
-                            raise
-                    oreleasefn(*args, **kwargs)
-                releasefn.obspatched = True
-                l.releasefn = releasefn
-            return l
-
-
-        ### pull // push support
-
-        def pull(self, remote, *args, **kwargs):
-            """wrapper around push that push obsolete relation"""
-            l = repo.lock()
-            try:
-                result = opull(remote, *args, **kwargs)
-                remoteobs = remote.listkeys('obsolete')
-                if 'dump' in remoteobs:
-                    remoteobs['dump0'] = remoteobs.pop('dump')
-                if 'dump0' in remoteobs:
-                    for key, values in remoteobs.iteritems():
-                        if key.startswith('dump'):
-                            data = base85.b85decode(remoteobs['dump0'])
-                            self.obsstore.mergemarkers(data)
-                    self._clearobsoletecache()
-                    self._turn_extinct_secret()
-                    return result
-            finally:
-                l.release()
-
-        def push(self, remote, *args, **opts):
-            """wrapper around pull that pull obsolete relation"""
-            self._turn_extinct_secret()
-            try:
-                result = opush(remote, *args, **opts)
-            except util.Abort, ex:
-                hint = _("use 'hg stabilize' to get a stable history (or --force to proceed)")
-                if (len(ex.args) >= 1
-                    and ex.args[0].startswith('push includes ')
-                    and ex.hint is None):
-                    ex.hint = hint
-                raise
-            if 'obsolete' in remote.listkeys('namespaces') and self.obsstore:
-                data = self.obsstore._writemarkers()
-                r = remote.pushkey('obsolete', 'dump0', '',
-                                   base85.b85encode(data))
-                if not r:
-                    self.ui.warn(_('failed to push obsolete markers!\n'))
-            self._turn_extinct_secret()
-
-            return result
-
-
-        ### rollback support
-
-        # /!\ api change in  Hg 2.2 (97efd26eb9576f39590812ea9) /!\
-        if util.safehasattr(repo, '_journalfiles'): # Hg 2.2
-            def _journalfiles(self):
-                return o_journalfiles() + (self.sjoin('journal.obsstore'),) 
-
-            def _writejournal(self, desc):
-                """wrapped version of _writejournal that save obsolete data"""
-                o_writejournal(desc)
-                filename = 'obsstore'
-                filepath = self.sjoin(filename)
-                if os.path.exists(filepath):
-                    journalname = 'journal.' + filename
-                    journalpath = self.sjoin(journalname)
-                    util.copyfile(filepath, journalpath)
-
-        else: # XXX removing this bloc will break Hg 2.1 support
-            def _writejournal(self, desc):
-                """wrapped version of _writejournal that save obsolete data"""
-                entries = list(o_writejournal(desc))
-                filename = 'obsstore'
-                filepath = self.sjoin(filename)
-                if  os.path.exists(filepath):
-                    journalname = 'journal.' + filename
-                    journalpath = self.sjoin(journalname)
-                    util.copyfile(filepath, journalpath)
-                    entries.append(journalpath)
-                return tuple(entries)
-
-        def _rollback(self, dryrun, force):
-            """wrapped version of _rollback that restore obsolete data"""
-            ret = o_rollback(dryrun, force)
-            if not (ret or dryrun): #rollback did not failed
-                src = self.sjoin('undo.obsstore')
-                dst = self.sjoin('obsstore')
-                if os.path.exists(src):
-                    util.rename(src, dst)
-                elif os.path.exists(dst):
-                    # If no state was saved because the file did not existed before.
-                    os.unlink(dst)
-                # invalidate cache
-                self.__dict__.pop('obsstore', None)
-            return ret
-
-        @storecache('00changelog.i')
-        def changelog(self):
-            # << copy pasted from mercurial source
-            c = changelog.changelog(self.sopener)
-            if 'HG_PENDING' in os.environ:
-                p = os.environ['HG_PENDING']
-                if p.startswith(self.root):
-                    c.readpending('00changelog.i.a')
-            # >> end of the copy paste
-            old = c.__dict__.pop('hiddenrevs', ())
-            if old:
-                ui.warn("old wasn't empty ? %r" % old)
-            def _sethidden(c, value):
-                assert not value
-
-
-            class hchangelog(c.__class__):
-                @util.propertycache
-                def hiddenrevs(c):
-                    shown = ['not obsolete()', '.', 'bookmark()', 'tagged()',
-                             'public()']
-                    basicquery = 'obsolete() - (::(%s))' % (' or '.join(shown))
-                    # !!! self is repo not changelog
-                    result = set(scmutil.revrange(self, [basicquery]))
-                    return result
-            c.__class__ = hchangelog
-            return c
-
-    repo.__class__ = obsoletingrepo
+                    oldmark['date'] = '%i %i' % tuple(oldmark['date'])
+                    meta = dict((k.encode('utf-8'), v.encode('utf-8'))
+                                 for k, v in oldmark.iteritems())
+                    try:
+                        succs = [bin(n) for n in oldsubjects]
+                        succs = [n for n in succs if n != nullid]
+                        store.create(tr, bin(oldobject), succs,
+                                     0, meta)
+                        cnt += 1
+                    except ValueError:
+                        repo.ui.write_err("invalid marker %s -> %s\n"
+                                     % (oldobject, oldsubjects))
+                        err += 1
+                unlink.append(repo.sjoin('obsoletemarkers'))
+            tr.close()
+            for path in unlink:
+                util.unlink(path)
+        finally:
+            tr.release()
+    finally:
+        del repo._importoldobsolete
+        l.release()
+    if not some:
+            ui.warn('nothing to do\n')
+    ui.status('%i obsolete marker converted\n' % cnt)
+    if err:
+        ui.write_err('%i conversion failed. check you graph!\n' % err)
--- a/tests/test-amend.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/tests/test-amend.t	Tue Aug 21 02:37:53 2012 +0200
@@ -24,9 +24,9 @@
   marked working directory as branch foo
   (branches are permanent and global, did you want a bookmark?)
   $ hg amend
-  $ hg debugsuccessors
-  07f494440405 a34b93d251e4
-  bd19cbe78fbf a34b93d251e4
+  $ hg debugobsolete
+  bd19cbe78fbfbd87eb33420c63986fe5f3154f2c a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob)
+  07f4944404050f47db2e5c5071e0e84e7a27bba9 a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob)
   $ hg branch
   foo
   $ hg branches
@@ -58,13 +58,24 @@
   $ echo a >> a
   $ hg ci -m changea
   $ echo a > a
+  $ hg status
+  M a
+  $ hg pstatus
+  $ hg diff
+  diff -r 2f97fe38810f a
+  --- a/a	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a	* +0000 (glob)
+  @@ -1,2 +1,1 @@
+   a
+  -a
+  $ hg pdiff
   $ hg ci -m reseta
   $ hg amend --change 2
   abort: no updates found
   [255]
-  $ hg debugsuccessors
-  07f494440405 a34b93d251e4
-  bd19cbe78fbf a34b93d251e4
+  $ hg debugobsolete
+  bd19cbe78fbfbd87eb33420c63986fe5f3154f2c a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob)
+  07f4944404050f47db2e5c5071e0e84e7a27bba9 a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob)
   $ hg phase 2
   2: draft
   $ glog
@@ -88,12 +99,12 @@
   $ hg amend --change 2
   abort: no updates found
   [255]
-  $ hg debugsuccessors
-  07f494440405 a34b93d251e4
-  7384bbcba36f 000000000000
-  bd19cbe78fbf a34b93d251e4
+  $ hg debugobsolete
+  bd19cbe78fbfbd87eb33420c63986fe5f3154f2c a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob)
+  07f4944404050f47db2e5c5071e0e84e7a27bba9 a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob)
+  7384bbcba36fde1a789cd00f9cd6f9b919ab5910 0 {'date': '* *', 'user': 'test'} (glob)
   $ glog
-  @  6@foo(secret) amends a34b93d251e49c93d5685ebacad785c73a7e8605
+  @  6@foo(draft) amends a34b93d251e49c93d5685ebacad785c73a7e8605
   |
   o  5@default(draft) resetbranch
   |
--- a/tests/test-evolve.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/tests/test-evolve.t	Tue Aug 21 02:37:53 2012 +0200
@@ -59,7 +59,8 @@
   $ hg log -r 1 --template '{rev} {phase} {obsolete}\n'
   1 public stable
   $ hg kill 1
-  cannot kill immutable changeset 7c3bad9141dc
+  abort: Cannot obsolete immutable changeset: 7c3bad9141dc
+  [255]
   $ hg log -r 1 --template '{rev} {phase} {obsolete}\n'
   1 public stable
 
@@ -240,23 +241,23 @@
   $ glog --hidden
   o  6:23409eba69a0@default(draft) a nifty feature
   |
-  | o  5:e416e48b2742@default(secret) french looks better
+  | x  5:e416e48b2742@default(draft) french looks better
   | |
   | | o  4:f8111a076f09@default(draft) another feature
   | |/
-  | | o  3:524e478d4811@default(secret) fix spelling of Zwei
+  | | x  3:524e478d4811@default(draft) fix spelling of Zwei
   | | |
-  | | o  2:7b36850622b2@default(secret) another feature
+  | | x  2:7b36850622b2@default(draft) another feature
   | |/
-  | o  1:568a468b60fc@default(draft) a nifty feature
+  | x  1:568a468b60fc@default(draft) a nifty feature
   |/
   @  0:e55e0562ee93@default(draft) base
   
-  $ hg debugsuccessors
-  524e478d4811 f8111a076f09
-  568a468b60fc 23409eba69a0
-  7b36850622b2 f8111a076f09
-  e416e48b2742 23409eba69a0
+  $ hg debugobsolete
+  524e478d4811d405c8771e4c441de4483bdf8b33 f8111a076f0975cbecb336e2bd3411be22b673fb 0 {'date': '* *', 'user': 'test'} (glob)
+  7b36850622b2fd159fa30a4fb2a1edd2043b4a14 f8111a076f0975cbecb336e2bd3411be22b673fb 0 {'date': '* *', 'user': 'test'} (glob)
+  e416e48b27428695d00c2a2cc4a0b9619482e63f 23409eba69a0986e90cd42252852c1e6da97af5b 0 {'date': '* *', 'user': 'test'} (glob)
+  568a468b60fc99a42d5d4ddbe181caff1eef308d 23409eba69a0986e90cd42252852c1e6da97af5b 0 {'date': '* *', 'user': 'test'} (glob)
   $ hg stabilize
   move:[4] another feature
   atop:[6] a nifty feature
@@ -390,9 +391,9 @@
   |/
   o  0:8685c6d34325@default(draft) add 0
   
-  $ hg graft 3 -O
+  $ hg graft -r3 -O
   grafting revision 3
-  $ hg graft 1 -o 2
+  $ hg graft -r1 -o 2
   grafting revision 1
   $ glog --hidden
   @  6:acb28cd497b7@default(draft) add 1
@@ -401,17 +402,17 @@
   |
   o  4:ce341209337f@default(draft) add 4
   |
-  | o  3:0e84df4912da@default(secret) add 3
+  | x  3:0e84df4912da@default(draft) add 3
   | |
-  | o  2:db038628b9e5@default(secret) add 2
+  | x  2:db038628b9e5@default(draft) add 2
   | |
   | o  1:73d38bb17fd7@default(draft) add 1
   |/
   o  0:8685c6d34325@default(draft) add 0
   
-  $ hg debugsuccessors
-  0e84df4912da 0b9e50c35132
-  db038628b9e5 acb28cd497b7
+  $ hg debugobsolete
+  0e84df4912da4c7cad22a3b4fcfd58ddfb7c8ae9 0b9e50c35132ff548ec0065caea6a87e1ebcef32 0 {'date': '* *', 'user': 'test'} (glob)
+  db038628b9e56f51a454c0da0c508df247b41748 acb28cd497b7f8767e01ef70f68697a959573c2d 0 {'date': '* *', 'user': 'test'} (glob)
 
 Test graft --continue
 
@@ -437,7 +438,7 @@
   $ glog --hidden
   @  8:920e58bb443b@default(draft) conflict
   |
-  | o  7:a5bfd90a2f29@default(secret) conflict
+  | x  7:a5bfd90a2f29@default(draft) conflict
   | |
   o |  6:acb28cd497b7@default(draft) add 1
   | |
@@ -445,18 +446,18 @@
   | |
   o |  4:ce341209337f@default(draft) add 4
   |/
-  | o  3:0e84df4912da@default(secret) add 3
+  | x  3:0e84df4912da@default(draft) add 3
   | |
-  | o  2:db038628b9e5@default(secret) add 2
+  | x  2:db038628b9e5@default(draft) add 2
   | |
   | o  1:73d38bb17fd7@default(draft) add 1
   |/
   o  0:8685c6d34325@default(draft) add 0
   
-  $ hg debugsuccessors
-  0e84df4912da 0b9e50c35132
-  a5bfd90a2f29 920e58bb443b
-  db038628b9e5 acb28cd497b7
+  $ hg debugobsolete
+  0e84df4912da4c7cad22a3b4fcfd58ddfb7c8ae9 0b9e50c35132ff548ec0065caea6a87e1ebcef32 0 {'date': '* *', 'user': 'test'} (glob)
+  db038628b9e56f51a454c0da0c508df247b41748 acb28cd497b7f8767e01ef70f68697a959573c2d 0 {'date': '* *', 'user': 'test'} (glob)
+  a5bfd90a2f29c7ccb8f917ff4e5013a9053d0a04 920e58bb443b73eea9d6d65570b4241051ea3229 0 {'date': '* *', 'user': 'test'} (glob)
 
   $ cd ..
 
--- a/tests/test-obsolete-push.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/tests/test-obsolete-push.t	Tue Aug 21 02:37:53 2012 +0200
@@ -32,9 +32,9 @@
   $ glog --hidden
   @  2:244232c2222a@default(unstable/secret) C
   |
-  | o  1:6c81ed0049f8@default(extinct/secret) B
+  | x  1:6c81ed0049f8@default(extinct/draft) B
   |/
-  o  0:1994f17a630e@default(suspended/secret) A
+  x  0:1994f17a630e@default(suspended/draft) A
   
   $ hg init ../clone
   $ cat >  ../clone/.hg/hgrc <<EOF
@@ -44,5 +44,4 @@
   $ hg outgoing ../clone --template "$template"
   comparing with ../clone
   searching for changes
-  no changes found (ignored 2 secret changesets)
-  [1]
+  0:1994f17a630e@default(suspended/draft) A
--- a/tests/test-obsolete-rebase.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/tests/test-obsolete-rebase.t	Tue Aug 21 02:37:53 2012 +0200
@@ -53,7 +53,7 @@
   |/
   o  0:07f494440405@default(draft) adda
   
-  $ hg debugsuccessors
+  $ hg debugobsolete
   $ hg --config extensions.hgext.mq= strip tip
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
   saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9c5494949763-backup.hg
@@ -70,7 +70,7 @@
   $ glog --hidden
   @  4:9c5494949763@default(draft) adde
   |
-  | o  3:98e4a024635e@default(secret) adde
+  | x  3:98e4a024635e@default(draft) adde
   | |
   | o  2:102a90ea7b4a@default(draft) addb
   | |
@@ -78,8 +78,8 @@
   |/
   o  0:07f494440405@default(draft) adda
   
-  $ hg debugsuccessors
-  98e4a024635e 9c5494949763
+  $ hg debugobsolete
+  98e4a024635e8c50928144c9277a4388d26bd786 9c54949497631abfb5a255d96746bbd3a42ed2ba 0 {'date': '* *', 'user': 'test'} (glob)
 
 Test rebase with deleted empty revision
 
@@ -92,11 +92,11 @@
   $ hg ci -m changea
   $ hg rebase -d 1
   $ glog --hidden
-  o  5:4e322f7ce8e3@foo(secret) changea
+  x  5:4e322f7ce8e3@foo(draft) changea
   |
   | o  4:9c5494949763@default(draft) adde
   | |
-  | | o  3:98e4a024635e@default(secret) adde
+  | | x  3:98e4a024635e@default(draft) adde
   | | |
   +---o  2:102a90ea7b4a@default(draft) addb
   | |
@@ -104,9 +104,9 @@
   |/
   o  0:07f494440405@default(draft) adda
   
-  $ hg debugsuccessors
-  4e322f7ce8e3 000000000000
-  98e4a024635e 9c5494949763
+  $ hg debugobsolete
+  98e4a024635e8c50928144c9277a4388d26bd786 9c54949497631abfb5a255d96746bbd3a42ed2ba 0 {'date': '* *', 'user': 'test'} (glob)
+  4e322f7ce8e3e4203950eac9ece27bf7e45ffa6c 540395c442253af3b991be882b539e7e198b5808 0 {'date': '* *', 'user': 'test'} (glob)
 
 Test rebase --collapse
 
@@ -123,15 +123,15 @@
   $ glog --hidden
   @  8:a7773ffa7edc@default(draft) Collapsed revision
   |
-  | o  7:03f31481307a@default(secret) changec
+  | x  7:03f31481307a@default(draft) changec
   | |
-  | o  6:076e9b2ffbe1@default(secret) addc
+  | x  6:076e9b2ffbe1@default(draft) addc
   | |
-  | | o  5:4e322f7ce8e3@foo(secret) changea
+  | | x  5:4e322f7ce8e3@foo(draft) changea
   | |/
   +---o  4:9c5494949763@default(draft) adde
   | |
-  | | o  3:98e4a024635e@default(secret) adde
+  | | x  3:98e4a024635e@default(draft) adde
   | | |
   | | o  2:102a90ea7b4a@default(draft) addb
   | |/
@@ -139,15 +139,15 @@
   |/
   o  0:07f494440405@default(draft) adda
   
-  $ hg debugsuccessors
-  03f31481307a a7773ffa7edc
-  076e9b2ffbe1 a7773ffa7edc
-  4e322f7ce8e3 000000000000
-  98e4a024635e 9c5494949763
+  $ hg debugobsolete
+  98e4a024635e8c50928144c9277a4388d26bd786 9c54949497631abfb5a255d96746bbd3a42ed2ba 0 {'date': '* *', 'user': 'test'} (glob)
+  4e322f7ce8e3e4203950eac9ece27bf7e45ffa6c 540395c442253af3b991be882b539e7e198b5808 0 {'date': '* *', 'user': 'test'} (glob)
+  076e9b2ffbe11c7bcb9ee97f5c0c8b88a1a10b93 a7773ffa7edcfac27b5dcdb2d5c1036e15a49861 0 {'date': '* *', 'user': 'test'} (glob)
+  03f31481307aaf5275d07ec28c1c59931759ccd2 a7773ffa7edcfac27b5dcdb2d5c1036e15a49861 0 {'date': '* *', 'user': 'test'} (glob)
 
 Test rebase --abort
 
-  $ hg debugsuccessors > ../successors.old
+  $ hg debugobsolete > ../successors.old
   $ hg up 0
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
   $ echo d > d
@@ -164,7 +164,7 @@
   $ hg rebase --abort
   saved backup bundle to $TESTTMP/repo/.hg/strip-backup/03f165c84ea8-backup.hg
   rebase aborted
-  $ hg debugsuccessors > ../successors.new
+  $ hg debugobsolete > ../successors.new
   $ diff -u ../successors.old ../successors.new
 
 Test rebase --continue
@@ -182,21 +182,21 @@
   |
   o  11:03f165c84ea8@default(draft) addd
   |
-  | o  10:4b9d80f48523@default(secret) appendab
+  | x  10:4b9d80f48523@default(draft) appendab
   | |
-  | o  9:a31943eabc43@default(secret) addd
+  | x  9:a31943eabc43@default(draft) addd
   | |
   +---o  8:a7773ffa7edc@default(draft) Collapsed revision
   | |
-  | | o  7:03f31481307a@default(secret) changec
+  | | x  7:03f31481307a@default(draft) changec
   | | |
-  | | o  6:076e9b2ffbe1@default(secret) addc
+  | | x  6:076e9b2ffbe1@default(draft) addc
   | |/
-  | | o  5:4e322f7ce8e3@foo(secret) changea
+  | | x  5:4e322f7ce8e3@foo(draft) changea
   | |/
   +---o  4:9c5494949763@default(draft) adde
   | |
-  | | o  3:98e4a024635e@default(secret) adde
+  | | x  3:98e4a024635e@default(draft) adde
   | | |
   | | o  2:102a90ea7b4a@default(draft) addb
   | |/
@@ -204,15 +204,14 @@
   |/
   o  0:07f494440405@default(draft) adda
   
-  $ hg debugsuccessors > ../successors.new
+  $ hg debugobsolete > ../successors.new
   $ diff -u ../successors.old ../successors.new
   --- ../successors.old* (glob)
   +++ ../successors.new* (glob)
-  @@ -1,4 +1,6 @@
-   03f31481307a a7773ffa7edc
-   076e9b2ffbe1 a7773ffa7edc
-  +4b9d80f48523 1951ead97108
-   4e322f7ce8e3 000000000000
-   98e4a024635e 9c5494949763
-  +a31943eabc43 03f165c84ea8
+  @@ -2,3 +2,5 @@
+   4e322f7ce8e3e4203950eac9ece27bf7e45ffa6c 540395c442253af3b991be882b539e7e198b5808 0 {'date': '* *', 'user': 'test'} (glob)
+   076e9b2ffbe11c7bcb9ee97f5c0c8b88a1a10b93 a7773ffa7edcfac27b5dcdb2d5c1036e15a49861 0 {'date': '* *', 'user': 'test'} (glob)
+   03f31481307aaf5275d07ec28c1c59931759ccd2 a7773ffa7edcfac27b5dcdb2d5c1036e15a49861 0 {'date': '* *', 'user': 'test'} (glob)
+  +a31943eabc4327df16f9eca71bf7779c32f815f7 03f165c84ea8889fc35a64a392caa7a0084dd212 0 {'date': '* *', 'user': 'test'} (glob)
+  +4b9d80f48523e296f4402cc8e37236b768dfb981 1951ead9710803dbf117e95901954d5ed717f80b 0 {'date': '* *', 'user': 'test'} (glob)
   [1]
--- a/tests/test-obsolete.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/tests/test-obsolete.t	Tue Aug 21 02:37:53 2012 +0200
@@ -15,6 +15,9 @@
   >    hg add "$1"
   >    hg ci -m "add $1"
   > }
+  $ getid() {
+  >    hg id --debug -ir "$1"
+  > }
 
   $ alias qlog="hg log --template='{rev}\n- {node|short}\n'"
   $ hg init local
@@ -27,9 +30,21 @@
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
   $ mkcommit obsol_c # 3
   created new head
-  $ hg debugobsolete 3 2
+  $ getid 2
+  4538525df7e2b9f09423636c61ef63a4cb872a2d
+  $ getid 3
+  0d3f46688ccc6e756c7e96cf64c391c411309597
+  $ hg debugobsolete 4538525df7e2b9f09423636c61ef63a4cb872a2d 0d3f46688ccc6e756c7e96cf64c391c411309597
+  $ hg debugobsolete
+  4538525df7e2b9f09423636c61ef63a4cb872a2d 0d3f46688ccc6e756c7e96cf64c391c411309597 0 {'date': '', 'user': 'test'} (glob)
 
 
+Test hidden() revset
+
+  $ qlog -r 'hidden()' --hidden
+  2
+  - 4538525df7e2
+
 Test that obsolete changeset are hidden
 
   $ qlog
@@ -82,7 +97,7 @@
   $ hg up 1 -q
   $ mkcommit "obsol_c'" # 4 (on 1)
   created new head
-  $ hg debugobsolete 4 3
+  $ hg debugobsolete `getid 3` `getid 4`
   $ qlog
   4
   - 725c380fe99b
@@ -127,13 +142,13 @@
 
   $ hg glog --template '{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' \
   >   --hidden
-  @  5:a7a6f2b5d8a5@default(unstable/secret) add d
+  @  5:a7a6f2b5d8a5@default(unstable/draft) add d
   |
   | o  4:725c380fe99b@default(stable/draft) add obsol_c'
   | |
-  o |  3:0d3f46688ccc@default(suspended/secret) add obsol_c
+  x |  3:0d3f46688ccc@default(suspended/draft) add obsol_c
   |/
-  | o  2:4538525df7e2@default(extinct/secret) add c
+  | x  2:4538525df7e2@default(extinct/draft) add c
   |/
   o  1:7c3bad9141dc@default(stable/draft) add b
   |
@@ -144,11 +159,13 @@
 
   $ hg init ../other-new
   $ hg phase --draft 'secret() - extinct()' # until we fix exclusion
+  abort: empty revision set
+  [255]
   $ hg push ../other-new
   pushing to ../other-new
   searching for changes
   abort: push includes an unstable changeset: a7a6f2b5d8a5!
-  (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 push -f ../other-new
   pushing to ../other-new
@@ -182,7 +199,7 @@
   $ mkcommit obsol_d # 6
   created new head
   1 new unstables changesets
-  $ hg debugobsolete 6 5
+  $ hg debugobsolete `getid 5` `getid 6`
   $ qlog
   6
   - 95de7fc6918d
@@ -201,7 +218,7 @@
   pushing to ../other-new
   searching for changes
   abort: push includes an unstable changeset: 95de7fc6918d!
-  (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 push ../other-new -f # use f because there is unstability
   pushing to ../other-new
@@ -230,7 +247,7 @@
   $ hg push ../other-new
   pushing to ../other-new
   searching for changes
-  no changes found (ignored 0 secret changesets)
+  no changes found
   [1]
 
   $ hg up -q .^ # 3
@@ -238,7 +255,7 @@
   $ mkcommit "obsol_d'" # 7
   created new head
   1 new unstables changesets
-  $ hg debugobsolete 7 6
+  $ hg debugobsolete `getid 6` `getid 7`
   $ hg pull -R ../other-new .
   pulling from .
   searching for changes
@@ -261,55 +278,58 @@
 
 pushing to stuff that doesn't support obsolete
 
-  $ hg init ../other-old
-  > # XXX I don't like this but changeset get published otherwise
-  > # remove it when we will get a --keep-state flag for push
-  $ echo '[extensions]'  > ../other-old/.hg/hgrc
-  $ echo "obsolete=!$(echo $(dirname $TESTDIR))/obsolete.py" >> ../other-old/.hg/hgrc
-  $ hg push ../other-old
-  pushing to ../other-old
-  searching for changes
-  abort: push includes an unstable changeset: 909a0fb57e5d!
-  (use 'hg stabilize' to get a stable history (or --force to proceed))
-  [255]
-  $ hg push -f ../other-old
-  pushing to ../other-old
-  searching for changes
-  adding changesets
-  adding manifests
-  adding file changes
-  added 5 changesets with 5 changes to 5 files (+1 heads)
-  $ qlog -R ../other-old
-  4
-  - 909a0fb57e5d
-  3
-  - 725c380fe99b
-  2
-  - 0d3f46688ccc
-  1
-  - 7c3bad9141dc
-  0
-  - 1f0dee641bb7
+DISABLED. the _enable switch it global :-/
+
+..  $ hg init ../other-old
+..  > # XXX I don't like this but changeset get published otherwise
+..  > # remove it when we will get a --keep-state flag for push
+..  $ echo '[extensions]'  > ../other-old/.hg/hgrc
+..  $ echo "obsolete=!$(echo $(dirname $TESTDIR))/obsolete.py" >> ../other-old/.hg/hgrc
+..  $ hg push ../other-old
+..  pushing to ../other-old
+..  searching for changes
+..  abort: push includes an unstable changeset: 909a0fb57e5d!
+..  (use 'hg stabilize' to get a stable history or --force to ignore warnings)
+..  [255]
+..  $ hg push -f ../other-old
+..  pushing to ../other-old
+..  searching for changes
+..  adding changesets
+..  adding manifests
+..  adding file changes
+..  added 5 changesets with 5 changes to 5 files (+1 heads)
+..  $ qlog -R ../other-ol
+..  4
+..  - 909a0fb57e5d
+..  3
+..  - 725c380fe99b
+..  2
+..  - 0d3f46688ccc
+..  1
+..  - 7c3bad9141dc
+..  0
+..  - 1f0dee641bb7
 
 clone support
 
   $ hg clone . ../cloned
   > # The warning should go away once we have default value to set ready before we pull
-  requesting all changes
-  adding changesets
-  adding manifests
-  adding file changes
-  added 5 changesets with 5 changes to 5 files (+1 heads)
   updating to branch default
   4 files updated, 0 files merged, 0 files removed, 0 files unresolved
 
-  $ qlog -R ../cloned
+  $ qlog -R ../cloned --hidden
+  7
+  - 909a0fb57e5d
+  6
+  - 95de7fc6918d
+  5
+  - a7a6f2b5d8a5
   4
-  - 909a0fb57e5d
+  - 725c380fe99b
   3
-  - 725c380fe99b
+  - 0d3f46688ccc
   2
-  - 0d3f46688ccc
+  - 4538525df7e2
   1
   - 7c3bad9141dc
   0
@@ -322,7 +342,7 @@
   $ mkcommit "obsol_d''"
   created new head
   1 new unstables changesets
-  $ hg debugobsolete 8 7
+  $ hg debugobsolete `getid 7` `getid 8`
   $ cd ../other-new
   $ hg up -q 3
   $ hg pull ../local/
@@ -379,8 +399,10 @@
   created new head
   $ hg id -n
   9
-  $ hg debugobsolete 9 0
-  83b5778897ad try to obsolete immutable changeset 1f0dee641bb7
+  $ hg debugobsolete `getid 0` `getid 9`
+83b5778897ad try to obsolete immutable changeset 1f0dee641bb7
+# at core level the warning is not issued
+# this is now a big issue now that we have latecomer warning
   $ qlog -r 'obsolete()'
   3
   - 0d3f46688ccc
@@ -400,7 +422,7 @@
   0
   - 1f0dee641bb7
 
-  $ hg debugobsolete null 9 #kill
+  $ hg debugobsolete `getid 9` #kill
   $ hg up null -q # to be not based on 9 anymore
   $ qlog
   8
@@ -421,29 +443,29 @@
   |
   | o  4 - 725c380fe99b
   | |
-  o |  3 - 0d3f46688ccc
+  x |  3 - 0d3f46688ccc
   |/
   o  1 - 7c3bad9141dc
   |
   o  0 - 1f0dee641bb7
   
 
-  $ hg glog  --template='{rev} - {node|short}\n' `(hg --version | grep -q 'version 2.1') ||  echo '--hidden'`
-  o  9 - 83b5778897ad
+  $ hg glog  --template='{rev} - {node|short}\n' --hidden
+  x  9 - 83b5778897ad
   
   o  8 - 159dfc9fa5d3
   |
-  | o  7 - 909a0fb57e5d
+  | x  7 - 909a0fb57e5d
   |/
-  | o  6 - 95de7fc6918d
+  | x  6 - 95de7fc6918d
   |/
-  | o  5 - a7a6f2b5d8a5
+  | x  5 - a7a6f2b5d8a5
   |/
   | o  4 - 725c380fe99b
   | |
-  o |  3 - 0d3f46688ccc
+  x |  3 - 0d3f46688ccc
   |/
-  | o  2 - 4538525df7e2
+  | x  2 - 4538525df7e2
   |/
   o  1 - 7c3bad9141dc
   |
@@ -477,7 +499,7 @@
   $ hg up -q 10
   $ mkcommit "obsol_d'''"
   created new head
-  $ hg debugobsolete 12 11
+  $ hg debugobsolete `getid 11` `getid 12`
   $ hg push ../other-new --traceback
   pushing to ../other-new
   searching for changes
@@ -517,8 +539,8 @@
   $ hg push ../other-new/
   pushing to ../other-new/
   searching for changes
-  abort: push includes an latecomer changeset: 6db5e282cb91!
-  (use 'hg stabilize' to get a stable history (or --force to proceed))
+  abort: push includes a latecomer changeset: 6db5e282cb91!
+  (use 'hg stabilize' to get a stable history or --force to ignore warnings)
   [255]
 
 Check hg commit --amend compat
@@ -571,18 +593,18 @@
      date:        Thu Jan 01 00:00:00 1970 +0000
      summary:     add a
   
-  $ hg debugsuccessors
-  0b1b6dd009c0 3734a65252e6
-  0d3f46688ccc 2033b4e49474
-  0d3f46688ccc 725c380fe99b
-  159dfc9fa5d3 9468a5f5d8b2
-  1f0dee641bb7 83b5778897ad
-  4538525df7e2 0d3f46688ccc
-  83b5778897ad 000000000000
-  909a0fb57e5d 159dfc9fa5d3
-  9468a5f5d8b2 6db5e282cb91
-  95de7fc6918d 909a0fb57e5d
-  a7a6f2b5d8a5 95de7fc6918d
+  $ hg debugobsolete
+  4538525df7e2b9f09423636c61ef63a4cb872a2d 0d3f46688ccc6e756c7e96cf64c391c411309597 0 {'date': '', 'user': 'test'} (glob)
+  0d3f46688ccc6e756c7e96cf64c391c411309597 725c380fe99b5e76613493f0903e8d11ddc70d54 0 {'date': '', 'user': 'test'} (glob)
+  a7a6f2b5d8a54b81bc7aa2fba2934ad6d700a79e 95de7fc6918dea4c9c8d5382f50649794b474c4a 0 {'date': '', 'user': 'test'} (glob)
+  95de7fc6918dea4c9c8d5382f50649794b474c4a 909a0fb57e5d909f353d89e394ffd7e0890fec88 0 {'date': '', 'user': 'test'} (glob)
+  909a0fb57e5d909f353d89e394ffd7e0890fec88 159dfc9fa5d334d7e03a0aecfc7f7ab4c3431fea 0 {'date': '', 'user': 'test'} (glob)
+  1f0dee641bb7258c56bd60e93edfa2405381c41e 83b5778897adafb967ef2f75be3aaa4fce49a4cc 0 {'date': '', 'user': 'test'} (glob)
+  83b5778897adafb967ef2f75be3aaa4fce49a4cc 0 {'date': '', 'user': 'test'} (glob)
+  0d3f46688ccc6e756c7e96cf64c391c411309597 2033b4e494742365851fac84d276640cbf52833e 0 {'date': '* *', 'user': 'test'} (glob)
+  159dfc9fa5d334d7e03a0aecfc7f7ab4c3431fea 9468a5f5d8b2c5d91e17474e95ae4791e9718fdf 0 {'date': '* *', 'user': 'test'} (glob)
+  9468a5f5d8b2c5d91e17474e95ae4791e9718fdf 6db5e282cb91df5c43ff1f1287c119ff83230d42 0 {'date': '', 'user': 'test'} (glob)
+  0b1b6dd009c037985363e2290a0b579819f659db 3734a65252e69ddcced85901647a4f335d40de1e 0 {'date': '* *', 'user': 'test'} (glob)
 
 Check conflict detection
 
@@ -595,7 +617,7 @@
   branch: default
   commit: (clean)
   update: 9 new changesets, 9 branch heads (merge)
-  $ hg debugobsolete 50f11e5e3a63 a7a6f2b5d8a5
+  $ hg debugobsolete `getid a7a6f2b5d8a5` `getid 50f11e5e3a63`
   $ hg log -r 'conflicting()'
   changeset:   14:50f11e5e3a63
   tag:         tip
--- a/tests/test-oldconvert.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/tests/test-oldconvert.t	Tue Aug 21 02:37:53 2012 +0200
@@ -53,8 +53,8 @@
      date:        Thu Jan 01 00:00:00 1970 +0000
      summary:     add a
   
-  $ hg debugsuccessors
-  7c3bad9141dc d67cd0334eee
+  $ hg debugobsolete
+  7c3bad9141dcb46ff89abf5f61856facd56e476c d67cd0334eeecfded222fed9009f0db4beb57585 0 {'date': '* *', 'user': 'test'} (glob)
   $ hg debugconvertobsolete
   nothing to do
   0 obsolete marker converted
@@ -107,8 +107,8 @@
   [255]
   $ hg debugconvertobsolete --traceback
   3 obsolete marker converted
-  $ hg debugsuccessors
-  2c3784e102bb
-  3e03d82708d4 3218406b50ed
-  5c722672795c
-  7c3bad9141dc d67cd0334eee
+  $ hg debugobsolete
+  7c3bad9141dcb46ff89abf5f61856facd56e476c d67cd0334eeecfded222fed9009f0db4beb57585 0 {'date': '* *', 'user': 'test'} (glob)
+  3e03d82708d4da97a92158558dd13386d8f09ad5 3218406b50ed13480765e7c260669620f37fba6e 0 {'date': '* *', 'user': 'Pierre-Yves David <pierre-yves.david@ens-lyon.org>'} (glob)
+  5c722672795c3a2cb94d0cc9a821c394c1475f87 0 {'date': '* *', 'user': 'Pierre-Yves David <pierre-yves.david@logilab.fr>'} (glob)
+  2c3784e102bb34ccc93862af5bd6d609ee30c577 0 {'date': '* *', 'user': 'Pierre-Yves David <pierre-yves.david@logilab.fr>'} (glob)
--- a/tests/test-qsync.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/tests/test-qsync.t	Tue Aug 21 02:37:53 2012 +0200
@@ -182,7 +182,6 @@
   pulling from ../local2
   searching for changes
   no changes found
-  (run 'hg update' to get a working copy)
   $ hg pull --mq ../local2/.hg/patches
   pulling from ../local2/.hg/patches
   searching for changes
--- a/tests/test-stabilize-order.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/tests/test-stabilize-order.t	Tue Aug 21 02:37:53 2012 +0200
@@ -54,9 +54,9 @@
   | |
   | | o  3:7a7552255fb5@default(draft) addc
   | | |
-  | | o  2:ef23d6ef94d6@default(draft) addb
+  | | x  2:ef23d6ef94d6@default(draft) addb
   | |/
-  | o  1:93418d2c0979@default(draft) adda
+  | x  1:93418d2c0979@default(draft) adda
   |/
   o  0:c471ef929e6a@default(draft) addroot
   
@@ -77,9 +77,9 @@
   |
   | o  3:7a7552255fb5@default(draft) addc
   | |
-  | o  2:ef23d6ef94d6@default(draft) addb
+  | x  2:ef23d6ef94d6@default(draft) addb
   | |
-  | o  1:93418d2c0979@default(draft) adda
+  | x  1:93418d2c0979@default(draft) adda
   |/
   o  0:c471ef929e6a@default(draft) addroot
   
@@ -88,7 +88,7 @@
 
   $ hg up 7
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
-  $ hg debugsuccessors > successors.old
+  $ hg debugobsolete > successors.old
   $ hg stabilize -v
   move:[3] addc
   atop:[8] addb
@@ -98,17 +98,15 @@
   resolving manifests
   getting c
   c
-  $ hg debugsuccessors > successors.new
+  $ hg debugobsolete > successors.new
   $ diff -u successors.old successors.new
   --- successors.old* (glob)
   +++ successors.new* (glob)
-  @@ -1,5 +1,6 @@
-   3a4a591493f8 f5ff10856e5a
-   3ca0ded0dc50 ab8cbb6d87ff
-  +7a7552255fb5 5e819fbb0d27
-   93418d2c0979 f5ff10856e5a
-   ab8cbb6d87ff 6bf44048e43f
-   ef23d6ef94d6 ab8cbb6d87ff
+  @@ -3,3 +3,4 @@
+   3a4a591493f80708e46f2bf6d3b4debfad8ff91e f5ff10856e5ab3c8dc420b9c11460e6832a3b78c 0 {'date': '* *', 'user': 'test'} (glob)
+   93418d2c0979643ad446f621195e78720edb05b4 f5ff10856e5ab3c8dc420b9c11460e6832a3b78c 0 {'date': '* *', 'user': 'test'} (glob)
+   ab8cbb6d87ff3ab5526735a051cba6b63f3d6775 6bf44048e43f830accbf7d2bd7bc252ad7a3b99c 0 {'date': '* *', 'user': 'test'} (glob)
+  +7a7552255fb5f8bd745e46fba6f0ca633a4dd716 5e819fbb0d278117c0a83b7f6f6486689732cfb2 0 {'date': '* *', 'user': 'test'} (glob)
   [1]
   $ glog
   @  9:5e819fbb0d27@default(draft) addc
@@ -135,7 +133,7 @@
   |
   | o  9:5e819fbb0d27@default(draft) addc
   | |
-  | o  8:6bf44048e43f@default(draft) addb
+  | x  8:6bf44048e43f@default(draft) addb
   |/
   o  7:f5ff10856e5a@default(draft) adda
   |
--- a/tests/test-stabilize-result.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/tests/test-stabilize-result.t	Tue Aug 21 02:37:53 2012 +0200
@@ -37,15 +37,15 @@
   $ glog --hidden
   @  4:1447e1c4828d@default(draft) bk:[changea] changea
   |
-  | o  3:41ad4fe8c795@default(secret) bk:[] amends 102a90ea7b4a3361e4082ed620918c261189a36a
+  | x  3:41ad4fe8c795@default(draft) bk:[] amends 102a90ea7b4a3361e4082ed620918c261189a36a
   | |
-  | | o  2:cce2c55b8965@default(secret) bk:[] changea
+  | | x  2:cce2c55b8965@default(draft) bk:[] changea
   | |/
-  | o  1:102a90ea7b4a@default(secret) bk:[] addb
+  | x  1:102a90ea7b4a@default(draft) bk:[] addb
   |/
   o  0:07f494440405@default(draft) bk:[] adda
   
-  $ hg debugsuccessors
-  102a90ea7b4a 1447e1c4828d
-  41ad4fe8c795 1447e1c4828d
-  cce2c55b8965 000000000000
+  $ hg debugobsolete
+  41ad4fe8c79565a06c89f032ef0937b3cbd68a04 1447e1c4828d2347df8f858aa041305fa4cf7db1 0 {'date': '* *', 'user': 'test'} (glob)
+  102a90ea7b4a3361e4082ed620918c261189a36a 1447e1c4828d2347df8f858aa041305fa4cf7db1 0 {'date': '* *', 'user': 'test'} (glob)
+  cce2c55b896511e0b6e04173c9450ba822ebc740 0 {'date': '* *', 'user': 'test'} (glob)
--- a/tests/test-tutorial.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/tests/test-tutorial.t	Tue Aug 21 02:37:53 2012 +0200
@@ -1,1 +1,775 @@
-../docs/tutorials/tutorial.t
\ No newline at end of file
+
+Initial setup
+-------------
+
+This Mercurial configuration example is used for testing.
+.. Various setup
+
+  $ cat >> $HGRCPATH << EOF
+  > [ui]
+  > logtemplate ="{node|short} ({phase}): {desc}\n"
+  > [diff]
+  > git = 1
+  > [alias]
+  > # "-d '0 0'" means that the new commit will be at January 1st 1970.
+  > # This is used for stable hash during test
+  > amend = amend -d '0 0'
+  > [extensions]
+  > hgext.graphlog=
+  > EOF
+
+  $ hg init local
+  $ cat >> local/.hg/hgrc << EOF
+  > [paths]
+  > remote = ../remote
+  > other = ../other
+  > [ui]
+  > user = Babar the King
+  > EOF
+
+  $ hg init remote
+  $ cat >> remote/.hg/hgrc << EOF
+  > [paths]
+  > local = ../local
+  > [ui]
+  > user = Celestine the Queen
+  > EOF
+
+  $ hg init other
+  $ cat >> other/.hg/hgrc << EOF
+  > [ui]
+  > user = Princess Flore
+  > EOF
+
+
+This tutorial use the following configuration for Mercurial:
+
+A compact log template with phase data:
+
+  $ hg showconfig ui
+  ui.slash=True
+  ui.logtemplate="{node|short} ({phase}): {desc}\n"
+
+Improved git format diff:
+
+  $ hg showconfig diff
+  diff.git=1
+
+And the graphlog extension
+  $ hg showconfig extensions
+  extensions.hgext.graphlog=
+
+And of course, we anabled the experimental extensions for mutable history:
+
+  $ $(dirname $TESTDIR)/enable.sh >> $HGRCPATH 2> /dev/null
+
+
+-----------------------
+Single Developer Usage
+-----------------------
+
+This tutorial shows how to use evolution to rewrite history locally.
+
+
+Fixing mistake with `hg amend`
+--------------------------------
+
+We are versionning a shopping list
+
+  $ cd local
+  $ cat  >> shopping << EOF
+  > Spam
+  > Whizzo butter
+  > Albatross
+  > Rat (rather a lot)
+  > Jugged fish
+  > Blancmange
+  > Salmon mousse
+  > EOF
+  $ hg commit -A -m "Monthy Python Shopping list"
+  adding shopping
+
+Its first version is shared with the outside.
+
+  $ hg push remote
+  pushing to $TESTTMP/remote
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+
+Later I add additional item to my list
+
+  $ cat >> shopping << EOF
+  > Egg
+  > Suggar
+  > Vinegar
+  > Oil
+  > EOF
+  $ hg commit -m "adding condiment"
+  $ cat >> shopping << EOF
+  > Bananos
+  > Pear
+  > Apple
+  > EOF
+  $ hg commit -m "adding fruit"
+
+This history is very linear
+
+  $ hg glog
+  @  d85de4546133 (draft): adding fruit
+  |
+  o  4d5dc8187023 (draft): adding condiment
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+But a typo was made in Babanas!
+
+  $ hg export tip
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  # Node ID d85de4546133030c82d257bbcdd9b1b416d0c31c
+  # Parent  4d5dc81870237d492284826e21840b2ca00e26d1
+  adding fruit
+  
+  diff --git a/shopping b/shopping
+  --- a/shopping
+  +++ b/shopping
+  @@ -9,3 +9,6 @@
+   Suggar
+   Vinegar
+   Oil
+  +Bananos
+  +Pear
+  +Apple
+
+The faulty changeset is in the "draft" phase because he was not exchanged with
+the outside. The first one have been exchanged and is an immutable public
+changeset.
+
+  $ hg glog
+  @  d85de4546133 (draft): adding fruit
+  |
+  o  4d5dc8187023 (draft): adding condiment
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+hopefully. I can use hg amend to rewrite my faulty changeset!
+
+  $ sed -i'' -e s/Bananos/Banana/ shopping
+  $ hg diff
+  diff --git a/shopping b/shopping
+  --- a/shopping
+  +++ b/shopping
+  @@ -9,6 +9,6 @@
+   Suggar
+   Vinegar
+   Oil
+  -Bananos
+  +Banana
+   Pear
+   Apple
+  $ hg amend
+
+A new changeset with the right diff replace the wrong one.
+
+  $ hg glog
+  @  0cacb48f4482 (draft): adding fruit
+  |
+  o  4d5dc8187023 (draft): adding condiment
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+  $ hg export tip
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  # Node ID 0cacb48f44828d2fd31c4e45e18fde32a5b2f07b
+  # Parent  4d5dc81870237d492284826e21840b2ca00e26d1
+  adding fruit
+  
+  diff --git a/shopping b/shopping
+  --- a/shopping
+  +++ b/shopping
+  @@ -9,3 +9,6 @@
+   Suggar
+   Vinegar
+   Oil
+  +Banana
+  +Pear
+  +Apple
+
+Getting Ride of branchy history
+----------------------------------
+
+While I was working on my list. someone help made a change remotly.
+
+  $ cd ../remote
+  $ hg up -q
+  $ sed -i'' -e 's/Spam/Spam Spam Spam/' shopping
+  $ hg ci -m 'SPAM'
+  $ cd ../local
+
+I'll get this remote changeset when pulling
+
+  $ hg pull remote
+  pulling from $TESTTMP/remote
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  (run 'hg heads .' to see heads, 'hg merge' to merge)
+
+I now have a new heads. Note that this remote head is immutable
+
+  $ hg log -G
+  o  9ca060c80d74 (public): SPAM
+  |
+  | @  0cacb48f4482 (draft): adding fruit
+  | |
+  | o  4d5dc8187023 (draft): adding condiment
+  |/
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+instead of merging my head with the new one. I'm going to rebase my work
+
+  $ hg diff
+  $ hg rebase -d 9ca060c80d74 -s 4d5dc8187023
+  merging shopping
+  merging shopping
+
+
+My local work is now rebased on the remote one.
+
+  $ hg log -G
+  @  387187ad9bd9 (draft): adding fruit
+  |
+  o  dfd3a2d7691e (draft): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+Removing changeset
+------------------------
+
+I add new item to my list
+
+  $ cat >> shopping << EOF
+  > car
+  > bus
+  > plane
+  > boat
+  > EOF
+  $ hg ci -m 'transport'
+  $ hg log -G
+  @  d58c77aa15d7 (draft): transport
+  |
+  o  387187ad9bd9 (draft): adding fruit
+  |
+  o  dfd3a2d7691e (draft): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+I have a new commit but I realize that don't want it. (transport shop list does
+not fit well in my standard shopping list)
+
+  $ hg prune . # . is for working directory parent
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  working directory now at 387187ad9bd9
+
+The silly changeset is gone.
+
+  $ hg log -G
+  @  387187ad9bd9 (draft): adding fruit
+  |
+  o  dfd3a2d7691e (draft): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+Reordering changeset
+------------------------
+
+
+We create two changesets.
+
+
+  $ cat >> shopping << EOF
+  > Shampoo
+  > Toothbrush
+  > ... More bathroom stuff to come
+  > Towel
+  > Soap
+  > EOF
+  $ hg ci -m 'bathroom stuff' -q # XXX remove the -q
+
+  $ sed -i'' -e 's/Spam/Spam Spam Spam/g' shopping
+  $ hg ci -m 'SPAM SPAM'
+  $ hg log -G
+  @  c48f32fb1787 (draft): SPAM SPAM
+  |
+  o  8d39a843582d (draft): bathroom stuff
+  |
+  o  387187ad9bd9 (draft): adding fruit
+  |
+  o  dfd3a2d7691e (draft): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+.. note: don't amend changeset 7e82d3f3c2cb or 9ca060c80d74 as they are immutable.
+
+I now want to push to remote all my change but the bathroom one that i'm not
+totally happy with yet. To be able to push "SPAM SPAM" I need a version of "SPAM SPAM" not children of
+"bathroom stuff"
+
+You can use 'rebase -r' or 'graft -O' for that:
+
+  $ hg up 'p1(8d39a843582d)' # going on "bathroom stuff" parent
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg graft -O c48f32fb1787 # moving "SPAM SPAM" to the working directory parent
+  grafting revision 10
+  merging shopping
+  $ hg log -G
+  @  a2fccc2e7b08 (draft): SPAM SPAM
+  |
+  | o  8d39a843582d (draft): bathroom stuff
+  |/
+  o  387187ad9bd9 (draft): adding fruit
+  |
+  o  dfd3a2d7691e (draft): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+We have a new SPAM SPAM version without the bathroom stuff
+
+  $ grep Spam shopping  # enouth spam
+  Spam Spam Spam Spam Spam Spam Spam Spam Spam
+  $ grep Toothbrush shopping # no Toothbrush
+  [1]
+  $ hg export .
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  # Node ID a2fccc2e7b08bbce6af7255b989453f7089e4cf0
+  # Parent  387187ad9bd9d8f9a00a9fa804a26231db547429
+  SPAM SPAM
+  
+  diff --git a/shopping b/shopping
+  --- a/shopping
+  +++ b/shopping
+  @@ -1,4 +1,4 @@
+  -Spam Spam Spam
+  +Spam Spam Spam Spam Spam Spam Spam Spam Spam
+   Whizzo butter
+   Albatross
+   Rat (rather a lot)
+
+To make sure I do not push unready changeset by mistake I set the "bathroom
+stuff" changeset in the secret phase.
+
+  $ hg phase --force --secret 8d39a843582d
+
+we can now push our change:
+
+  $ hg push remote
+  pushing to $TESTTMP/remote
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 1 files
+
+for simplicity shake we get the bathroom change in line again
+
+  $ hg rebase -Dr 8d39a843582d -d a2fccc2e7b08
+  merging shopping
+  $ hg phase --draft .
+  $ hg log -G
+  @  8a79ae8b029e (draft): bathroom stuff
+  |
+  o  a2fccc2e7b08 (public): SPAM SPAM
+  |
+  o  387187ad9bd9 (public): adding fruit
+  |
+  o  dfd3a2d7691e (public): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+
+
+Splitting change
+------------------
+
+To be done (currently achieve with "two commit + debugobsolete")
+
+Collapsing change
+------------------
+
+To be done (currently achieve with "revert + debugobsolete" or "rebase --collapse")
+
+
+
+
+
+
+-----------------------
+Collaboration
+-----------------------
+
+
+sharing mutable changeset
+----------------------------
+
+To share mutable changeset with other just check that the repo you interact
+with is "not publishing". Otherwise you will get the previously observe
+behavior where exchanged changeset are automatically published.
+
+  $ cd ../remote
+  $ hg -R ../local/ showconfig phases
+
+the localrepo does not have any specific configuration for `phases.publish`. It
+is ``true`` by default.
+
+  $ hg pull local
+  pulling from $TESTTMP/local
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg log -G
+  o  8a79ae8b029e (public): bathroom stuff
+  |
+  o  a2fccc2e7b08 (public): SPAM SPAM
+  |
+  o  387187ad9bd9 (public): adding fruit
+  |
+  o  dfd3a2d7691e (public): adding condiment
+  |
+  @  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+
+
+We do not want to publish the "bathroom changeset". Let's rollback the last transaction
+
+  $ hg rollback
+  repository tip rolled back to revision 4 (undo pull)
+  $ hg log -G
+  o  a2fccc2e7b08 (public): SPAM SPAM
+  |
+  o  387187ad9bd9 (public): adding fruit
+  |
+  o  dfd3a2d7691e (public): adding condiment
+  |
+  @  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+Let's make the local repo "non publishing"
+
+  $ echo '[phases]' >> ../local/.hg/hgrc
+  $ echo 'publish=false' >> ../local/.hg/hgrc
+  $ echo '[phases]' >> .hg/hgrc
+  $ echo 'publish=false' >> .hg/hgrc
+  $ hg showconfig phases
+  phases.publish=false
+  $ hg -R ../local/ showconfig phases
+  phases.publish=false
+
+
+I can now exchange mutable changeset between "remote" and "local" repository.
+
+  $ hg pull local
+  pulling from $TESTTMP/local
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg log -G
+  o  8a79ae8b029e (draft): bathroom stuff
+  |
+  o  a2fccc2e7b08 (public): SPAM SPAM
+  |
+  o  387187ad9bd9 (public): adding fruit
+  |
+  o  dfd3a2d7691e (public): adding condiment
+  |
+  @  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+Rebasing unstable change after pull
+----------------------------------------------
+
+Remotely someone add a new changeset on top of the mutable "bathroom" on.
+
+  $ hg up 8a79ae8b029e -q
+  $ cat >> shopping << EOF
+  > Giraffe
+  > Rhino
+  > Lion
+  > Bear
+  > EOF
+  $ hg ci -m 'animals'
+
+But at the same time, locally, this same "bathroom changeset" was updated.
+
+  $ cd ../local
+  $ hg up 8a79ae8b029e -q
+  $ sed -i'' -e 's/... More bathroom stuff to come/Bath Robe/' shopping
+  $ hg amend
+  $ hg log -G
+  @  ffa278c50818 (draft): bathroom stuff
+  |
+  o  a2fccc2e7b08 (public): SPAM SPAM
+  |
+  o  387187ad9bd9 (public): adding fruit
+  |
+  o  dfd3a2d7691e (public): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+
+When we pull from remote again we get an unstable state!
+
+
+  $ hg pull remote
+  pulling from $TESTTMP/remote
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  (run 'hg heads .' to see heads, 'hg merge' to merge)
+  1 new unstables changesets
+
+
+The new changeset "animal" is based one an old changeset of "bathroom". You can
+see both version showing up in the log.
+
+  $ hg log -G
+  o  9ac5d0e790a2 (draft): animals
+  |
+  | @  ffa278c50818 (draft): bathroom stuff
+  | |
+  x |  8a79ae8b029e (draft): bathroom stuff
+  |/
+  o  a2fccc2e7b08 (public): SPAM SPAM
+  |
+  o  387187ad9bd9 (public): adding fruit
+  |
+  o  dfd3a2d7691e (public): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+The older version 8a79ae8b029e never ceased to exist in the local repo. It was
+jsut hidden and excluded from pull and push.
+
+.. note:: In hgview there is a nice doted relation highlighting ffa278c50818 as a new version of 8a79ae8b029e. this is not yet ported to graphlog.
+
+Their is **unstable** changeset in this history now. Mercurial will refuse to
+share it with the outside:
+
+  $ hg push other
+  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 ignore warnings)
+  [255]
+ 
+
+
+
+To resolve this unstable state, you need to rebase 9ac5d0e790a2 onto
+ffa278c50818 the "hg stabilize" command will make this for you.
+
+It has a --dry-run option to only suggest the next move.
+
+  $ hg stabilize --dry-run
+  move:[15] animals
+  atop:[14] bathroom stuff
+  hg rebase -Dr 9ac5d0e790a2 -d ffa278c50818
+
+Let's do it
+
+  $ hg rebase -Dr 9ac5d0e790a2 -d ffa278c50818
+  merging shopping
+
+The old version of bathroom is hidden again.
+
+  $ hg log -G
+  @  437efbcaf700 (draft): animals
+  |
+  o  ffa278c50818 (draft): bathroom stuff
+  |
+  o  a2fccc2e7b08 (public): SPAM SPAM
+  |
+  o  387187ad9bd9 (public): adding fruit
+  |
+  o  dfd3a2d7691e (public): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+
+We can push this evolution to remote
+
+  $ hg push remote
+  pushing to $TESTTMP/remote
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 1 files (+1 heads)
+
+remote get a warning that current working directory is based on an obsolete changeset
+
+  $ cd ../remote
+  $ hg pull local # we up again to trigger the warning. it was displayed during the push
+  pulling from $TESTTMP/local
+  searching for changes
+  no changes found
+  Working directory parent is obsolete
+
+  $ hg up 437efbcaf700
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+Relocating unstable change after prune
+----------------------------------------------
+
+The remote guy keep working
+
+  $ sed -i'' -e 's/Spam/Spam Spam Spam Spam/g' shopping
+  $ hg commit -m "SPAM SPAM SPAM"
+
+I'm pulling its work locally.
+
+  $ cd ../local
+  $ hg pull remote
+  pulling from $TESTTMP/remote
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg log -G
+  o  ae45c0c3092a (draft): SPAM SPAM SPAM
+  |
+  @  437efbcaf700 (draft): animals
+  |
+  o  ffa278c50818 (draft): bathroom stuff
+  |
+  o  a2fccc2e7b08 (public): SPAM SPAM
+  |
+  o  387187ad9bd9 (public): adding fruit
+  |
+  o  dfd3a2d7691e (public): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+In the mean time I noticed you can't buy animals in a super market and I prune the animal changeset:
+
+  $ hg prune 437efbcaf700
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  working directory now at ffa278c50818
+  1 new unstables changesets
+
+
+The animals changeset is still displayed because the "SPAM SPAM SPAM" changeset
+is neither dead or obsolete.  My repository is in an unstable state again.
+
+  $ hg log -G
+  o  ae45c0c3092a (draft): SPAM SPAM SPAM
+  |
+  x  437efbcaf700 (draft): animals
+  |
+  @  ffa278c50818 (draft): bathroom stuff
+  |
+  o  a2fccc2e7b08 (public): SPAM SPAM
+  |
+  o  387187ad9bd9 (public): adding fruit
+  |
+  o  dfd3a2d7691e (public): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+  $ hg log -r 'unstable()'
+  ae45c0c3092a (draft): SPAM SPAM SPAM
+
+# XXX make prune stabilization works
+#  $ hg stabilize --any
+#  merging shopping
+
+  $ hg graft -O ae45c0c3092a
+  grafting revision 17
+  merging shopping
+
+  $ hg log -G
+  @  20de1fb1cec5 (draft): SPAM SPAM SPAM
+  |
+  o  ffa278c50818 (draft): bathroom stuff
+  |
+  o  a2fccc2e7b08 (public): SPAM SPAM
+  |
+  o  387187ad9bd9 (public): adding fruit
+  |
+  o  dfd3a2d7691e (public): adding condiment
+  |
+  o  9ca060c80d74 (public): SPAM
+  |
+  o  7e82d3f3c2cb (public): Monthy Python Shopping list
+  
+
+
+Handling Conflicting amend
+----------------------------------------------
+
+We can detect that multiple diverging//conflicting amend have been made. There
+will be a "evol-merge" command to merge conflicting amend
+
+This command is not ready yet.
--- a/tests/test-uncommit.t	Tue Aug 21 02:36:33 2012 +0200
+++ b/tests/test-uncommit.t	Tue Aug 21 02:37:53 2012 +0200
@@ -220,7 +220,7 @@
   $ glog --hidden
   @  4:e8db4aa611f6@bar(stable/draft) touncommit
   |
-  | o  3:5eb72dbe0cb4@bar(extinct/secret) touncommit
+  | x  3:5eb72dbe0cb4@bar(extinct/draft) touncommit
   |/
   o    2:f63b90038565@default(stable/draft) merge
   |\
@@ -232,8 +232,8 @@
    * touncommit-bm             4:e8db4aa611f6
      touncommit-bm-inactive    4:e8db4aa611f6
      unrelated                 2:f63b90038565
-  $ hg debugsuccessors
-  5eb72dbe0cb4 e8db4aa611f6
+  $ hg debugobsolete
+  5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob)
 
 Test phase is preserved, no local changes
 
@@ -263,11 +263,11 @@
   R m
   R n
   $ glog --hidden
-  @  5:c706fe2c12f8@bar(stable/secret) touncommit
+  @  5:c706fe2c12f8@bar(stable/draft) touncommit
   |
   | o  4:e8db4aa611f6@bar(stable/draft) touncommit
   |/
-  | o  3:5eb72dbe0cb4@bar(extinct/secret) touncommit
+  | x  3:5eb72dbe0cb4@bar(extinct/draft) touncommit
   |/
   o    2:f63b90038565@default(stable/draft) merge
   |\
@@ -275,9 +275,9 @@
   |
   o  0:07f494440405@default(stable/draft) adda
   
-  $ hg debugsuccessors
-  5eb72dbe0cb4 c706fe2c12f8
-  5eb72dbe0cb4 e8db4aa611f6
+  $ hg debugobsolete
+  5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob)
+  5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c706fe2c12f83ba5010cb60ea6af3bd1f0c2d6d3 0 {'date': '* *', 'user': 'test'} (glob)
 
 Test --all
 
@@ -308,21 +308,21 @@
   $ hg st --copies --change .
   A e
 
-  $ hg debugsuccessors
-  5eb72dbe0cb4 c4cbebac3751
-  5eb72dbe0cb4 c706fe2c12f8
-  5eb72dbe0cb4 e8db4aa611f6
+  $ hg debugobsolete
+  5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob)
+  5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c706fe2c12f83ba5010cb60ea6af3bd1f0c2d6d3 0 {'date': '* *', 'user': 'test'} (glob)
+  5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c4cbebac3751269bdf12d1466deabcc78521d272 0 {'date': '* *', 'user': 'test'} (glob)
 
 Display a warning if nothing left
 
   $ hg uncommit e
   new changeset is empty
   (use "hg kill ." to remove it)
-  $ hg debugsuccessors
-  5eb72dbe0cb4 c4cbebac3751
-  5eb72dbe0cb4 c706fe2c12f8
-  5eb72dbe0cb4 e8db4aa611f6
-  c4cbebac3751 4f1c269eab68
+  $ hg debugobsolete
+  5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob)
+  5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c706fe2c12f83ba5010cb60ea6af3bd1f0c2d6d3 0 {'date': '* *', 'user': 'test'} (glob)
+  5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c4cbebac3751269bdf12d1466deabcc78521d272 0 {'date': '* *', 'user': 'test'} (glob)
+  c4cbebac3751269bdf12d1466deabcc78521d272 4f1c269eab68720f54e88ce3c1dc02b2858b6b89 0 {'date': '* *', 'user': 'test'} (glob)
 
 Test instability warning