obsolete: introduce an extension helper and use it
authorPierre-Yves David <pierre-yves.david@logilab.fr>
Tue, 07 Aug 2012 14:08:31 +0200
changeset 429 079b231b8ea4
parent 428 1c82147e9395
child 430 07db1d511faf
obsolete: introduce an extension helper and use it this extension helper allow most of setup operation to be declared in place using decorator. This allow clear separation between independant part of the code. This is the first HUGE wave of changes related to this introduction. The goal is to have a clear distinction and documention of every part of this extension.
hgext/obsolete.py
--- a/hgext/obsolete.py	Tue Aug 07 11:35:45 2012 +0200
+++ b/hgext/obsolete.py	Tue Aug 07 14:08:31 2012 +0200
@@ -76,46 +76,253 @@
 obsolete._enabled = True
 
 
+### Extension helper
+#############################
+
+### setup code
+
+class exthelper(object):
+    """Helper for modular extension setup
+
+    A single helper should be intanciated for each extension. Helper method are
+    then used as decorator for various purpose.
+
+    All decorator returns the original function and may be chained.
+    """
+
+    def __init__(self):
+        self._uicallables = []
+        self._extcallables = []
+        self._repocallables = []
+        self._revsetsymbols = []
+        self._commandwrappers = []
+        self._extcommandwrappers = []
+        self._functionwrappers = []
+        self._duckpunchers = []
+
+    def final_uisetup(self, ui):
+        """Method to be used as a 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 by other extensions: because
+          initialization occurs in phases (all extensions run uisetup, then all
+          run extsetup), a change made here will be visible by other extensions
+          during extsetup
+        - Monkeypatches or function wraps (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
+        - Extend revsets 
+        """
+        knownexts = {}
+        for name, symbol in self._revsetsymbols:
+            revset.symbols[name] = symbol
+        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
+
+    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 apply 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 take two argument, the container and the name of the
+        function to wrap. The wrapping is performed during `uisetup`.
+        (there is don't support extension)
+
+        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 take two argument, 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
+
 
 ### Patch changectx
 #############################
 
+@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
 
-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
 
-context.changectx.extinct = extinct
-
+@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
 
-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
 
-context.changectx.conflicting = conflicting
-
 
 ### revset
 #############################
 
+@eh.revset('hidden')
 def revsethidden(repo, subset, x):
     """``hidden()``
     Changeset is hidden.
@@ -123,6 +330,7 @@
     args = revset.getargs(x, 0, 0, 'hidden takes no argument')
     return [r for r in subset if r in repo.hiddenrevs]
 
+@eh.revset('obsolete')
 def revsetobsolete(repo, subset, x):
     """``obsolete()``
     Changeset is obsolete.
@@ -130,6 +338,7 @@
     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]
 
+@eh.revset('unstable')
 def revsetunstable(repo, subset, x):
     """``unstable()``
     Unstable changesets are non-obsolete with obsolete ancestors.
@@ -137,6 +346,7 @@
     args = revset.getargs(x, 0, 0, 'unstable takes no arguments')
     return [r for r in subset if r in repo._unstableset]
 
+@eh.revset('suspended')
 def revsetsuspended(repo, subset, x):
     """``suspended()``
     Obsolete changesets with non-obsolete descendants.
@@ -144,6 +354,7 @@
     args = revset.getargs(x, 0, 0, 'suspended takes no arguments')
     return [r for r in subset if r in repo._suspendedset]
 
+@eh.revset('extinct')
 def revsetextinct(repo, subset, x):
     """``extinct()``
     Obsolete changesets with obsolete descendants only.
@@ -151,6 +362,7 @@
     args = revset.getargs(x, 0, 0, 'extinct takes no arguments')
     return [r for r in subset if r in repo._extinctset]
 
+@eh.revset('latecomer')
 def revsetlatecomer(repo, subset, x):
     """``latecomer()``
     Changesets marked as successors of public changesets.
@@ -158,6 +370,7 @@
     args = revset.getargs(x, 0, 0, 'latecomer takes no arguments')
     return [r for r in subset if r in repo._latecomerset]
 
+@eh.revset('conflicting')
 def revsetconflicting(repo, subset, x):
     """``conflicting()``
     Changesets marked as successors of a same changeset.
@@ -177,6 +390,8 @@
                 cs.add(pr)
     return cs
 
+@eh.revset('obsparents')
+@eh.revset('precursors')
 def revsetprecursors(repo, subset, x):
     """``precursors(set)``
     Immediate precursors of changesets in set.
@@ -205,6 +420,8 @@
             cs.add(pr)
     return cs
 
+@eh.revset('obsancestors')
+@eh.revset('allprecursors')
 def revsetallprecursors(repo, subset, x):
     """``allprecursors(set)``
     Transitive precursors of changesets in set.
@@ -226,6 +443,8 @@
                     cs.add(sr)
     return cs
 
+@eh.revset('obschildrend')
+@eh.revset('successors')
 def revsetsuccessors(repo, subset, x):
     """``successors(set)``
     Immediate successors of changesets in set.
@@ -254,6 +473,8 @@
             cs.add(sr)
     return cs
 
+@eh.revset('obsdescendants')
+@eh.revset('allsuccessors')
 def revsetallsuccessors(repo, subset, x):
     """``allsuccessors(set)``
     Transitive successors of changesets in set.
@@ -279,6 +500,10 @@
         return 'unstable'
     return 'stable'
 
+@eh.extsetup
+def addkeyword(ui):
+    templatekw.keywords['obsolete'] = obsoletekw
+
 ### Other Extension compat
 ############################
 
@@ -362,28 +587,9 @@
         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:
@@ -398,7 +604,7 @@
 ### Discovery wrapping
 #############################
 
-
+@eh.wrapfunction(discovery, 'checkheads')
 def wrapcheckheads(orig, repo, remote, outgoing, *args, **kwargs):
     """wrap mercurial.discovery.checkheads
 
@@ -418,6 +624,7 @@
                              % ctx)
     return orig(repo, remote, outgoing, *args, **kwargs)
 
+@eh.wrapfunction(phases, 'advanceboundary')
 def wrapclearcache(orig, repo, *args, **kwargs):
     try:
         return orig(repo, *args, **kwargs)
@@ -432,6 +639,331 @@
 command = cmdutil.command(cmdtable)
 
 
+
+@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.
+    """
+    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
+#############################
+
+@eh.wrapcommand("update")
+@eh.wrapcommand("pull")
+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)
+
+@eh.extsetup
+def _coreobserrorwrapping(ui):
+    # warning about more obsolete
+    for cmd in ['commit', 'push', 'pull', 'graft', 'phase', 'unbundle']:
+        entry = extensions.wrapcommand(commands.table, cmd, warnobserrors)
+
+@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()
+                repo._clearobsoletecache()
+            finally:
+                tr.release()
+        finally:
+            lock.release()
+    return new
+
+
+### 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)
+
+### repo subclassing
+#############################
+
+@eh.reposetup
+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)')
+    opush = repo.push
+    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
+        # XXX Kill me
+        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
+
+        # XXX Kill me
+        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, []))
+
+        # XXX move me on obssotre
+        @util.propertycache
+        def _obsoleteset(self):
+            """the set of obsolete revision"""
+            obs = set()
+            nm = self.changelog.nodemap
+            for prec in self.obsstore.precursors:
+                rev = nm.get(prec)
+                if rev is not None:
+                    obs.add(rev)
+            return obs
+
+        # XXX move me on obssotre
+        @util.propertycache
+        def _unstableset(self):
+            """the set of non obsolete revision with obsolete parent"""
+            return set(self.revs('(obsolete()::) - obsolete()'))
+
+        # XXX move me on obssotre
+        @util.propertycache
+        def _suspendedset(self):
+            """the set of obsolete parent with non obsolete descendant"""
+            return set(self.revs('obsolete() and obsolete()::unstable()'))
+
+        # XXX move me on obssotre
+        @util.propertycache
+        def _extinctset(self):
+            """the set of obsolete parent without non obsolete descendant"""
+            return set(self.revs('obsolete() - obsolete()::unstable()'))
+
+        # XXX move me on obssotre
+        @util.propertycache
+        def _latecomerset(self):
+            """the set of rev trying to obsolete public revision"""
+            query = 'allsuccessors(public()) - obsolete() - public()'
+            return set(self.revs(query))
+
+        # XXX move me on obssotre
+        @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
+
+        # XXX kill me
+        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:
+                tr = self.transaction('add-obsolete')
+                try:
+                    meta = {
+                        'date':  '%i %i' % util.makedate(),
+                        'user': ui.username(),
+                        }
+                    subs = (sub == nullid) and [] or [sub]
+                    mid = self.obsstore.create(tr, obj, subs, 0, meta)
+                    tr.close()
+                    self._clearobsoletecache()
+                    return mid
+                finally:
+                    tr.release()
+            finally:
+                lock.release()
+
+        # XXX kill me
+        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)
+
+        ### pull // push support
+
+        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__ = obsoletingrepo
+
+@eh.reposetup
+def _checkoldobsolete(ui, repo):
+    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.')
+
+### serialisation
+#############################
+
+def _obsdeserialise(flike):
+    """read a file like object serialised with _obsserialise
+
+    this desierialize into a {subject -> objects} mapping"""
+    rels = {}
+    for line in flike:
+        subhex, objhex = line.split()
+        subnode = bin(subhex)
+        if subnode == nullid:
+            subnode = None
+        rels.setdefault( subnode, set()).add(bin(objhex))
+    return rels
+
 @command('debugconvertobsolete', [], '')
 def cmddebugconvertobsolete(ui, repo):
     """import markers from an .hg/obsolete-relations file"""
@@ -519,322 +1051,3 @@
     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.
-    """
-    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 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()
-                repo._clearobsoletecache()
-            finally:
-                tr.release()
-        finally:
-            lock.release()
-    return new
-
-def uisetup(ui):
-    extensions.wrapcommand(commands.table, "update", wrapmayobsoletewc)
-    extensions.wrapcommand(commands.table, "pull", wrapmayobsoletewc)
-    extensions.wrapfunction(cmdutil, 'amend', wrapcmdutilamend)
-    extensions.wrapfunction(discovery, 'checkheads', wrapcheckheads)
-    extensions.wrapfunction(phases, 'advanceboundary', wrapclearcache)
-
-### 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)))
-
-def _obsdeserialise(flike):
-    """read a file like object serialised with _obsserialise
-
-    this desierialize into a {subject -> objects} mapping"""
-    rels = {}
-    for line in flike:
-        subhex, objhex = line.split()
-        subnode = bin(subhex)
-        if subnode == nullid:
-            subnode = None
-        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)
-
-### 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)')
-    opush = repo.push
-    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, []))
-
-        @util.propertycache
-        def _obsoleteset(self):
-            """the set of obsolete revision"""
-            obs = set()
-            nm = self.changelog.nodemap
-            for prec in self.obsstore.precursors:
-                rev = nm.get(prec)
-                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:
-                tr = self.transaction('add-obsolete')
-                try:
-                    meta = {
-                        'date':  '%i %i' % util.makedate(),
-                        'user': ui.username(),
-                        }
-                    subs = (sub == nullid) and [] or [sub]
-                    mid = self.obsstore.create(tr, obj, subs, 0, meta)
-                    tr.close()
-                    self._clearobsoletecache()
-                    return mid
-                finally:
-                    tr.release()
-            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)
-
-        ### pull // push support
-
-        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__ = obsoletingrepo
-    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.')