hgext/obsolete.py
changeset 429 079b231b8ea4
parent 428 1c82147e9395
child 430 07db1d511faf
--- 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.')