Commits

Pierre-Yves David  committed 6989d8f

merge evolve and obsolete

  • Participants
  • Parent commits 8096833
  • Branches default

Comments (0)

Files changed (13)

File hgext/evolve.py

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

File hgext/obsolete.py

 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
-"""Introduce the Obsolete concept to mercurial
+"""Deprecated extension that formely introduces "Changeset Obsolescence".
 
-General concept
-===============
+This concept is now partially in Mercurial core (starting with mercurial 2.3). The remaining logic have been grouped with the evolve extension.
 
-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* 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 descendants.
-:extinct:      an obsolete changeset without unstable descendants.
-               (subject to garbage collection)
-
-Another name for unstable could be out of sync.
-
-
-Usage and Feature
-=================
-
-
-New commands
-------------
-
-Note that rebased changesets are now marked obsolete instead of being stripped.
-
+Some code cemains in this extensions to detect and convert prehistoric format of obsolete marker than early user may have create. Keep it enabled if you were such user.
 """
 
-
-
 from mercurial import util
 
 try:
     raise util.Abort('Obsolete extension requires Mercurial 2.3 (or later)')
 
 import sys
-from mercurial.i18n import _
+import json
+
 from mercurial import cmdutil
-from mercurial import commands
-from mercurial import context
-from mercurial import discovery
 from mercurial import error
-from mercurial import extensions
-from mercurial import localrepo
-from mercurial import merge
-from mercurial import phases
-from mercurial import revset
-from mercurial import scmutil
-from mercurial import templatekw
-from mercurial.node import bin, short, nullid
-
-# This extension contains the following code
-#
-# - Extension Helper code
-# - Obsolescence cache
-# - ...
-# - Older format compat
-
-
-
-#####################################################################
-### Extension helper                                              ###
-#####################################################################
-
-class exthelper(object):
-    """Helper for modular extension setup
-
-    A single helper should be instanciated for each extension. Helper
-    methods are then used as decorator for various purpose.
-
-    All decorators return the original function and may be chained.
-    """
-
-    def __init__(self):
-        self._uicallables = []
-        self._extcallables = []
-        self._repocallables = []
-        self._revsetsymbols = []
-        self._templatekws = []
-        self._commandwrappers = []
-        self._extcommandwrappers = []
-        self._functionwrappers = []
-        self._duckpunchers = []
-
-    def final_uisetup(self, ui):
-        """Method to be used as the extension uisetup
-
-        The following operations belong here:
-
-        - Changes to ui.__class__ . The ui object that will be used to run the
-          command has not yet been created. Changes made here will affect ui
-          objects created after this, and in particular the ui that will be
-          passed to runcommand
-        - Command wraps (extensions.wrapcommand)
-        - Changes that need to be visible to other extensions: because
-          initialization occurs in phases (all extensions run uisetup, then all
-          run extsetup), a change made here will be visible to other extensions
-          during extsetup
-        - Monkeypatch or wrap function (extensions.wrapfunction) of dispatch
-          module members
-        - Setup of pre-* and post-* hooks
-        - pushkey setup
-        """
-        for cont, funcname, func in self._duckpunchers:
-            setattr(cont, funcname, func)
-        for command, wrapper in self._commandwrappers:
-            extensions.wrapcommand(commands.table, command, wrapper)
-        for cont, funcname, wrapper in self._functionwrappers:
-            extensions.wrapfunction(cont, funcname, wrapper)
-        for c in self._uicallables:
-            c(ui)
-
-    def final_extsetup(self, ui):
-        """Method to be used as a the extension extsetup
-
-        The following operations belong here:
-
-        - Changes depending on the status of other extensions. (if
-          extensions.find('mq'))
-        - Add a global option to all commands
-        - Register revset functions
-        """
-        knownexts = {}
-        for name, symbol in self._revsetsymbols:
-            revset.symbols[name] = symbol
-        for name, kw in self._templatekws:
-            templatekw.keywords[name] = kw
-        for ext, command, wrapper in self._extcommandwrappers:
-            if ext not in knownexts:
-                e = extensions.find(ext)
-                if e is None:
-                    raise util.Abort('extension %s not found' % ext)
-                knownexts[ext] = e.cmdtable
-            extensions.wrapcommand(knownexts[ext], commands, wrapper)
-        for c in self._extcallables:
-            c(ui)
-
-    def final_reposetup(self, ui, repo):
-        """Method to be used as a the extension reposetup
-
-        The following operations belong here:
-
-        - All hooks but pre-* and post-*
-        - Modify configuration variables
-        - Changes to repo.__class__, repo.dirstate.__class__
-        """
-        for c in self._repocallables:
-            c(ui, repo)
-
-    def uisetup(self, call):
-        """Decorated function will be executed during uisetup
-
-        example::
-
-            @eh.uisetup
-            def setupbabar(ui):
-                print 'this is uisetup!'
-        """
-        self._uicallables.append(call)
-        return call
-
-    def extsetup(self, call):
-        """Decorated function will be executed during extsetup
-
-        example::
-
-            @eh.extsetup
-            def setupcelestine(ui):
-                print 'this is extsetup!'
-        """
-        self._uicallables.append(call)
-        return call
-
-    def reposetup(self, call):
-        """Decorated function will be executed during reposetup
-
-        example::
-
-            @eh.reposetup
-            def setupzephir(ui, repo):
-                print 'this is reposetup!'
-        """
-        self._repocallables.append(call)
-        return call
-
-    def revset(self, symbolname):
-        """Decorated function is a revset symbol
-
-        The name of the symbol must be given as the decorator argument.
-        The symbol is added during `extsetup`.
-
-        example::
-
-            @eh.revset('hidden')
-            def revsetbabar(repo, subset, x):
-                args = revset.getargs(x, 0, 0, 'babar accept no argument')
-                return [r for r in subset if 'babar' in repo[r].description()]
-        """
-        def dec(symbol):
-            self._revsetsymbols.append((symbolname, symbol))
-            return symbol
-        return dec
-
-
-    def templatekw(self, keywordname):
-        """Decorated function is a revset keyword
-
-        The name of the keyword must be given as the decorator argument.
-        The symbol is added during `extsetup`.
-
-        example::
-
-            @eh.templatekw('babar')
-            def kwbabar(ctx):
-                return 'babar'
-        """
-        def dec(keyword):
-            self._templatekws.append((keywordname, keyword))
-            return keyword
-        return dec
-
-    def wrapcommand(self, command, extension=None):
-        """Decorated function is a command wrapper
-
-        The name of the command must be given as the decorator argument.
-        The wrapping is installed during `uisetup`.
-
-        If the second option `extension` argument is provided, the wrapping
-        will be applied in the extension commandtable. This argument must be a
-        string that will be searched using `extension.find` if not found and
-        Abort error is raised. If the wrapping applies to an extension, it is
-        installed during `extsetup`
-
-        example::
-
-            @eh.wrapcommand('summary')
-            def wrapsummary(orig, ui, repo, *args, **kwargs):
-                ui.note('Barry!')
-                return orig(ui, repo, *args, **kwargs)
-
-        """
-        def dec(wrapper):
-            if extension is None:
-                self._commandwrappers.append((command, wrapper))
-            else:
-                self._extcommandwrappers.append((extension, command, wrapper))
-            return wrapper
-        return dec
-
-    def wrapfunction(self, container, funcname):
-        """Decorated function is a function wrapper
-
-        This function takes two arguments, the container and the name of the
-        function to wrap. The wrapping is performed during `uisetup`.
-        (there is no extension support)
-
-        example::
-
-            @eh.function(discovery, 'checkheads')
-            def wrapfunction(orig, *args, **kwargs):
-                ui.note('His head smashed in and his heart cut out')
-                return orig(*args, **kwargs)
-        """
-        def dec(wrapper):
-            self._functionwrappers.append((container, funcname, wrapper))
-            return wrapper
-        return dec
-
-    def addattr(self, container, funcname):
-        """Decorated function is to be added to the container
-
-        This function takes two arguments, the container and the name of the
-        function to wrap. The wrapping is performed during `uisetup`.
-
-        example::
-
-            @eh.function(context.changectx, 'babar')
-            def babar(ctx):
-                return 'babar' in ctx.description
-        """
-        def dec(func):
-            self._duckpunchers.append((container, funcname, func))
-            return func
-        return dec
-
-eh = exthelper()
-uisetup = eh.final_uisetup
-extsetup = eh.final_extsetup
-reposetup = eh.final_reposetup
-
-#####################################################################
-### Obsolescence Caching Logic                                    ###
-#####################################################################
-
-# Obsolescence related logic can be very slow if we don't have efficient cache.
-#
-# This section implements a cache mechanism that did not make it into core for
-# time reason. It store meaningful set of revision related to obsolescence
-# (obsolete, unstabletble ...
-#
-# Here is:
-#
-# - Computation of meaningful set,
-# - Cache access logic,
-# - Cache invalidation logic,
-# - revset and ctx using this cache.
-#
-
-
-### Computation of meaningful set
-#
-# Most set can be computed with "simple" revset.
-
-#: { set name -> function to compute this set } mapping
-#:   function take a single "repo" argument.
-#:
-#: Use the `cachefor` decorator to register new cache function
-cachefuncs = {}
-def cachefor(name):
-    """Decorator to register a function as computing the cache for a set"""
-    def decorator(func):
-        assert name not in cachefuncs
-        cachefuncs[name] = func
-        return func
-    return decorator
-
-@cachefor('obsolete')
-def _computeobsoleteset(repo):
-    """the set of obsolete revisions"""
-    obs = set()
-    nm = repo.changelog.nodemap
-    for prec in repo.obsstore.precursors:
-        rev = nm.get(prec)
-        if rev is not None:
-            obs.add(rev)
-    return set(repo.revs('%ld - public()', obs))
-
-@cachefor('unstable')
-def _computeunstableset(repo):
-    """the set of non obsolete revisions with obsolete parents"""
-    return set(repo.revs('(obsolete()::) - obsolete()'))
-
-@cachefor('suspended')
-def _computesuspendedset(repo):
-    """the set of obsolete parents with non obsolete descendants"""
-    return set(repo.revs('obsolete() and obsolete()::unstable()'))
-
-@cachefor('extinct')
-def _computeextinctset(repo):
-    """the set of obsolete parents without non obsolete descendants"""
-    return set(repo.revs('obsolete() - obsolete()::unstable()'))
-
-@eh.wrapfunction(obsolete.obsstore, '__init__')
-def _initobsstorecache(orig, obsstore, *args, **kwargs):
-    """add a cache attribute to obsstore"""
-    obsstore.caches = {}
-    return orig(obsstore, *args, **kwargs)
-
-### Cache access
-
-def getobscache(repo, name):
-    """Return the set of revision that belong to the <name> set
-
-    Such access may compute the set and cache it for future use"""
-    if not repo.obsstore:
-        return ()
-    if name not in repo.obsstore.caches:
-        repo.obsstore.caches[name] = cachefuncs[name](repo)
-    return repo.obsstore.caches[name]
-
-### Cache clean up
-#
-# To be simple we need to invalidate obsolescence cache when:
-#
-# - new changeset is added:
-# - public phase is changed
-# - obsolescence marker are added
-# - strip is used a repo
-
-
-def clearobscaches(repo):
-    """Remove all obsolescence related cache from a repo
-
-    This remove all cache in obsstore is the obsstore already exist on the
-    repo.
-
-    (We could be smarter here)"""
-    if 'obsstore' in repo._filecache:
-        repo.obsstore.caches.clear()
-
-@eh.wrapfunction(localrepo.localrepository, 'addchangegroup')  # new changeset
-@eh.wrapfunction(phases, 'retractboundary')  # phase movement
-@eh.wrapfunction(phases, 'advanceboundary')  # phase movement
-@eh.wrapfunction(localrepo.localrepository, 'destroyed')  # strip
-def wrapclearcache(orig, repo, *args, **kwargs):
-    try:
-        return orig(repo, *args, **kwargs)
-    finally:
-        # we are a bit wide here
-        # we could restrict to:
-        # advanceboundary + phase==public
-        # retractboundary + phase==draft
-        clearobscaches(repo)
-
-@eh.wrapfunction(obsolete.obsstore, 'add')  # new marker
-def clearonadd(orig, obsstore, *args, **kwargs):
-    try:
-        return orig(obsstore, *args, **kwargs)
-    finally:
-        obsstore.caches.clear()
-
-### Use the case
-# Function in core that could benefic from the cache are overwritten by cache using version
-
-# changectx method
-
-@eh.addattr(context.changectx, 'unstable')
-def unstable(ctx):
-    """is the changeset unstable (have obsolete ancestor)"""
-    if ctx.node() is None:
-        return False
-    return ctx.rev() in getobscache(ctx._repo, 'unstable')
-
-
-@eh.addattr(context.changectx, 'extinct')
-def extinct(ctx):
-    """is the changeset extinct by other"""
-    if ctx.node() is None:
-        return False
-    return ctx.rev() in getobscache(ctx._repo, 'extinct')
-
-# revset
-
-@eh.revset('obsolete')
-def revsetobsolete(repo, subset, x):
-    """``obsolete()``
-    Changeset is obsolete.
-    """
-    args = revset.getargs(x, 0, 0, 'obsolete takes no argument')
-    obsoletes = getobscache(repo, 'obsolete')
-    return [r for r in subset if r in obsoletes]
-
-@eh.revset('unstable')
-def revsetunstable(repo, subset, x):
-    """``unstable()``
-    Unstable changesets are non-obsolete with obsolete ancestors.
-    """
-    args = revset.getargs(x, 0, 0, 'unstable takes no arguments')
-    unstables = getobscache(repo, 'unstable')
-    return [r for r in subset if r in unstables]
-
-@eh.revset('extinct')
-def revsetextinct(repo, subset, x):
-    """``extinct()``
-    Obsolete changesets with obsolete descendants only.
-    """
-    args = revset.getargs(x, 0, 0, 'extinct takes no arguments')
-    extincts = getobscache(repo, 'extinct')
-    return [r for r in subset if r in extincts]
-
-#####################################################################
-### Complete troubles computation logic                           ###
-#####################################################################
-
-# there is two kind of trouble not handled by core right now:
-# - latecomer: (successors for public changeset)
-# - conflicting: (two changeset try to succeed to the same precursors)
-#
-# This section add support for those two addition trouble
-#
-# - Cache computation
-# - revset and ctx method
-# - push warning
-
-### Cache computation
-latediff = 1  # flag to prevent taking late comer fix into account
-
-@cachefor('latecomer')
-def _computelatecomerset(repo):
-    """the set of rev trying to obsolete public revision"""
-    candidates = _allsuccessors(repo, repo.revs('public()'),
-                                                haltonflags=latediff)
-    query = '%ld - obsolete() - public()'
-    return set(repo.revs(query, candidates))
-
-@cachefor('conflicting')
-def _computeconflictingset(repo):
-    """the set of rev trying to obsolete public revision"""
-    conflicting = set()
-    obsstore = repo.obsstore
-    newermap = {}
-    for ctx in repo.set('(not public()) - obsolete()'):
-        prec = obsstore.successors.get(ctx.node(), ())
-        toprocess = set(prec)
-        while toprocess:
-            prec = toprocess.pop()[0]
-            if prec not in newermap:
-                newermap[prec] = newerversion(repo, prec)
-            newer = [n for n in newermap[prec] if n] # filter kill
-            if len(newer) > 1:
-                conflicting.add(ctx.rev())
-                break
-        toprocess.update(obsstore.successors.get(prec, ()))
-    return conflicting
-
-### changectx method
-
-@eh.addattr(context.changectx, 'latecomer')
-def latecomer(ctx):
-    """is the changeset latecomer (Try to succeed to public change)"""
-    if ctx.node() is None:
-        return False
-    return ctx.rev() in getobscache(ctx._repo, 'latecomer')
-
-@eh.addattr(context.changectx, 'conflicting')
-def conflicting(ctx):
-    """is the changeset conflicting (Try to succeed to public change)"""
-    if ctx.node() is None:
-        return False
-    return ctx.rev() in getobscache(ctx._repo, 'conflicting')
-
-### revset symbol
-
-@eh.revset('latecomer')
-def revsetlatecomer(repo, subset, x):
-    """``latecomer()``
-    Changesets marked as successors of public changesets.
-    """
-    args = revset.getargs(x, 0, 0, 'latecomer takes no arguments')
-    lates = getobscache(repo, 'latecomer')
-    return [r for r in subset if r in lates]
-
-@eh.revset('conflicting')
-def revsetconflicting(repo, subset, x):
-    """``conflicting()``
-    Changesets marked as successors of a same changeset.
-    """
-    args = revset.getargs(x, 0, 0, 'conflicting takes no arguments')
-    conf = getobscache(repo, 'conflicting')
-    return [r for r in subset if r in conf]
-
-
-### Discovery wrapping
-
-@eh.wrapfunction(discovery, 'checkheads')
-def wrapcheckheads(orig, repo, remote, outgoing, *args, **kwargs):
-    """wrap mercurial.discovery.checkheads
-
-    * prevent latecomer and unstable to be pushed
-    """
-    # do not push instability
-    for h in outgoing.missingheads:
-        # Checking heads is enough, obsolete descendants are either
-        # obsolete or unstable.
-        ctx = repo[h]
-        if ctx.latecomer():
-            raise util.Abort(_("push includes a latecomer changeset: %s!")
-                             % ctx)
-        if ctx.conflicting():
-            raise util.Abort(_("push includes a conflicting changeset: %s!")
-                             % ctx)
-    return orig(repo, remote, outgoing, *args, **kwargs)
-
-#####################################################################
-### Filter extinct changeset from common operation                ###
-#####################################################################
-
-@eh.wrapfunction(merge, 'update')
-def wrapmergeupdate(orig, repo, node, *args, **kwargs):
-    """ensure we don't automatically update on hidden changeset"""
-    if node is None:
-        # tip of current branch
-        branch = repo[None].branch()
-        node = repo.revs('last((.:: and branch(%s)) - hidden())', branch)[0]
-    return orig(repo, node, *args, **kwargs)
-
-@eh.wrapfunction(localrepo.localrepository, 'branchtip')
-def obsbranchtip(orig, repo, branch):
-    """ensure "stable" reference does not end on a hidden changeset"""
-    result = ()
-    heads = repo.branchmap().get(branch, ())
-    if heads:
-        result = list(repo.set('last(heads(branch(%n) - hidden()))', heads[0]))
-    if not result:
-        raise error.RepoLookupError(_("unknown branch '%s'") % branch)
-    return result[0].node()
-
-
-#####################################################################
-### Additional Utilities                                          ###
-#####################################################################
-
-# This section contains a lot of small utility function and method
-
-# - Function to create markers
-# - useful alias pstatus and pdiff (should probably go in evolve)
-# - "troubles" method on changectx
-# - function to travel throught the obsolescence graph
-# - function to find useful changeset to stabilize
-
-### Marker Create
-
-def createmarkers(repo, relations, metadata=None, flag=0):
-    """Add obsolete markers between changeset in a repo
-
-    <relations> must be an iterable of (<old>, (<new>, ...)) tuple.
-    `old` and `news` are changectx.
-
-    Current user and date are used except if specified otherwise in the
-    metadata attribute.
-
-    /!\ assume the repo have been locked by the user /!\
-    """
-    # prepare metadata
-    if metadata is None:
-        metadata = {}
-    if 'date' not in metadata:
-        metadata['date'] = '%i %i' % util.makedate()
-    if 'user' not in metadata:
-        metadata['user'] = repo.ui.username()
-    # check future marker
-    tr = repo.transaction('add-obsolescence-marker')
-    try:
-        for prec, sucs in relations:
-            if not prec.mutable():
-                raise util.Abort("Cannot obsolete immutable changeset: %s" % prec)
-            nprec = prec.node()
-            nsucs = tuple(s.node() for s in sucs)
-            if nprec in nsucs:
-                raise util.Abort("Changeset %s cannot obsolete himself" % prec)
-            repo.obsstore.create(tr, nprec, nsucs, flag, metadata)
-            clearobscaches(repo)
-        tr.close()
-    finally:
-        tr.release()
-
-
-### Useful alias
-
-@eh.uisetup
-def _installalias(ui):
-    if ui.config('alias', 'pstatus', None) is None:
-        ui.setconfig('alias', 'pstatus', 'status --rev .^')
-    if ui.config('alias', 'pdiff', None) is None:
-        ui.setconfig('alias', 'pdiff', 'diff --rev .^')
-    if ui.config('alias', 'olog', None) is None:
-        ui.setconfig('alias', 'olog', "log -r 'precursors(.)' --hidden")
-
-# - "troubles" method on changectx
-
-@eh.addattr(context.changectx, 'troubles')
-def troubles(ctx):
-    """Return a tuple listing all the troubles that affect a changeset
-
-    Troubles may be "unstable", "latecomer" or "conflicting".
-    """
-    troubles = []
-    if ctx.unstable():
-        troubles.append('unstable')
-    if ctx.latecomer():
-        troubles.append('latecomer')
-    if ctx.conflicting():
-        troubles.append('conflicting')
-    return tuple(troubles)
-
-### Troubled revset symbol
-
-@eh.revset('troubled')
-def revsetlatecomer(repo, subset, x):
-    """``troubled()``
-    Changesets with troubles.
-    """
-    _ = revset.getargs(x, 0, 0, 'troubled takes no arguments')
-    return list(repo.revs('%ld and (unstable() + latecomer() + conflicting())',
-                          subset))
-
-
-### Obsolescence graph
-
-# XXX SOME MAJOR CLEAN UP TO DO HERE XXX
-
-def _precursors(repo, s):
-    """Precursor of a changeset"""
-    cs = set()
-    nm = repo.changelog.nodemap
-    markerbysubj = repo.obsstore.successors
-    for r in s:
-        for p in markerbysubj.get(repo[r].node(), ()):
-            pr = nm.get(p[0])
-            if pr is not None:
-                cs.add(pr)
-    return cs
-
-def _allprecursors(repo, s):  # XXX we need a better naming
-    """transitive precursors of a subset"""
-    toproceed = [repo[r].node() for r in s]
-    seen = set()
-    allsubjects = repo.obsstore.successors
-    while toproceed:
-        nc = toproceed.pop()
-        for mark in allsubjects.get(nc, ()):
-            np = mark[0]
-            if np not in seen:
-                seen.add(np)
-                toproceed.append(np)
-    nm = repo.changelog.nodemap
-    cs = set()
-    for p in seen:
-        pr = nm.get(p)
-        if pr is not None:
-            cs.add(pr)
-    return cs
-
-def _successors(repo, s):
-    """Successors of a changeset"""
-    cs = set()
-    nm = repo.changelog.nodemap
-    markerbyobj = repo.obsstore.precursors
-    for r in s:
-        for p in markerbyobj.get(repo[r].node(), ()):
-            for sub in p[1]:
-                sr = nm.get(sub)
-                if sr is not None:
-                    cs.add(sr)
-    return cs
-
-def _allsuccessors(repo, s, haltonflags=0):  # XXX we need a better naming
-    """transitive successors of a subset
-
-    haltonflags allows to provide flags which prevent the evaluation of a
-    marker.  """
-    toproceed = [repo[r].node() for r in s]
-    seen = set()
-    allobjects = repo.obsstore.precursors
-    while toproceed:
-        nc = toproceed.pop()
-        for mark in allobjects.get(nc, ()):
-            if mark[2] & haltonflags:
-                continue
-            for sub in mark[1]:
-                if sub == nullid:
-                    continue # should not be here!
-                if sub not in seen:
-                    seen.add(sub)
-                    toproceed.append(sub)
-    nm = repo.changelog.nodemap
-    cs = set()
-    for s in seen:
-        sr = nm.get(s)
-        if sr is not None:
-            cs.add(sr)
-    return cs
-
-
-
-def newerversion(repo, obs):
-    """Return the newer version of an obsolete changeset"""
-    toproceed = set([(obs,)])
-    # XXX known optimization available
-    newer = set()
-    objectrels = repo.obsstore.precursors
-    while toproceed:
-        current = toproceed.pop()
-        assert len(current) <= 1, 'splitting not handled yet. %r' % current
-        current = [n for n in current if n != nullid]
-        if current:
-            n, = current
-            if n in objectrels:
-                markers = objectrels[n]
-                for mark in markers:
-                    toproceed.add(tuple(mark[1]))
-            else:
-                newer.add(tuple(current))
-        else:
-            newer.add(())
-    return sorted(newer)
-
-
-#####################################################################
-### Extending revset and template                                 ###
-#####################################################################
-
-# this section add several useful revset symbol not yet in core.
-# they are subject to changes
-
-### hidden revset is not in core yet
-
-@eh.revset('hidden')
-def revsethidden(repo, subset, x):
-    """``hidden()``
-    Changeset is hidden.
-    """
-    args = revset.getargs(x, 0, 0, 'hidden takes no argument')
-    return [r for r in subset if r in repo.hiddenrevs]
-
-### XXX I'm not sure this revset is useful
-@eh.revset('suspended')
-def revsetsuspended(repo, subset, x):
-    """``suspended()``
-    Obsolete changesets with non-obsolete descendants.
-    """
-    args = revset.getargs(x, 0, 0, 'suspended takes no arguments')
-    suspended = getobscache(repo, 'suspended')
-    return [r for r in subset if r in suspended]
-
-
-@eh.revset('precursors')
-def revsetprecursors(repo, subset, x):
-    """``precursors(set)``
-    Immediate precursors of changesets in set.
-    """
-    s = revset.getset(repo, range(len(repo)), x)