changeset 1313:31e96036acd3

merge with stable bunch of cleanup on stable
author Pierre-Yves David <pierre-yves.david@fb.com>
date Mon, 04 May 2015 10:56:06 -0700
parents 1218ad94a351 (current diff) 508f9911b042 (diff)
children d13d97363521
files hgext/evolve.py
diffstat 17 files changed, 1209 insertions(+), 147 deletions(-) [+]
line wrap: on
line diff
--- a/hgext/evolve.py	Mon May 04 10:58:14 2015 -0700
+++ b/hgext/evolve.py	Mon May 04 10:56:06 2015 -0700
@@ -45,6 +45,8 @@
 except (ImportError, AttributeError):
     gboptslist = gboptsmap = None
 
+# Flags for enabling optional parts of evolve
+commandopt = 'allnewcommands'
 
 from mercurial import bookmarks
 from mercurial import cmdutil
@@ -144,8 +146,11 @@
         """
         for cont, funcname, func in self._duckpunchers:
             setattr(cont, funcname, func)
-        for command, wrapper in self._commandwrappers:
-            extensions.wrapcommand(commands.table, command, wrapper)
+        for command, wrapper, opts in self._commandwrappers:
+            entry = extensions.wrapcommand(commands.table, command, wrapper)
+            if opts:
+                for short, long, val, msg in opts:
+                    entry[1].append((short, long, val, msg))
         for cont, funcname, wrapper in self._functionwrappers:
             extensions.wrapfunction(cont, funcname, wrapper)
         for c in self._uicallables:
@@ -166,13 +171,20 @@
             revset.symbols[name] = symbol
         for name, kw in self._templatekws:
             templatekw.keywords[name] = kw
-        for ext, command, wrapper in self._extcommandwrappers:
+        for ext, command, wrapper, opts in self._extcommandwrappers:
             if ext not in knownexts:
-                e = extensions.find(ext)
-                if e is None:
-                    raise util.Abort('extension %s not found' % ext)
+                try:
+                    e = extensions.find(ext)
+                except KeyError:
+                    # Extension isn't enabled, so don't bother trying to wrap
+                    # it.
+                    continue
                 knownexts[ext] = e.cmdtable
-            extensions.wrapcommand(knownexts[ext], commands, wrapper)
+            entry = extensions.wrapcommand(knownexts[ext], command, wrapper)
+            if opts:
+                for short, long, val, msg in opts:
+                    entry[1].append((short, long, val, msg))
+
         for c in self._extcallables:
             c(ui)
 
@@ -260,7 +272,7 @@
             return keyword
         return dec
 
-    def wrapcommand(self, command, extension=None):
+    def wrapcommand(self, command, extension=None, opts=[]):
         """Decorated function is a command wrapper
 
         The name of the command must be given as the decorator argument.
@@ -279,12 +291,16 @@
                 ui.note('Barry!')
                 return orig(ui, repo, *args, **kwargs)
 
+        The `opts` argument allows specifying additional arguments for the
+        command.
+
         """
         def dec(wrapper):
             if extension is None:
-                self._commandwrappers.append((command, wrapper))
+                self._commandwrappers.append((command, wrapper, opts))
             else:
-                self._extcommandwrappers.append((extension, command, wrapper))
+                self._extcommandwrappers.append((extension, command, wrapper,
+                    opts))
             return wrapper
         return dec
 
@@ -330,6 +346,31 @@
 reposetup = eh.final_reposetup
 
 #####################################################################
+### Option configuration                                          ###
+#####################################################################
+
+@eh.reposetup # must be the first of its kin.
+def _configureoptions(ui, repo):
+    # If no capabilities are specified, enable everything.
+    # This is so existing evolve users don't need to change their config.
+    evolveopts = ui.configlist('experimental', 'evolution')
+    if not evolveopts:
+        evolveopts = ['all']
+        ui.setconfig('experimental', 'evolution', evolveopts)
+
+@eh.uisetup
+def _configurecmdoptions(ui):
+    # Unregister evolve commands if the command capability is not specified.
+    #
+    # This must be in the same function as the option configuration above to
+    # guarantee it happens after the above configuration, but before the
+    # extsetup functions.
+    evolveopts = ui.configlist('experimental', 'evolution')
+    if evolveopts and (commandopt not in evolveopts and
+                       'all' not in evolveopts):
+        cmdtable.clear()
+
+#####################################################################
 ### experimental behavior                                         ###
 #####################################################################
 
@@ -573,9 +614,16 @@
 @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'))
+    def warnobsolete():
+        if repo['.'].obsolete():
+            ui.warn(_('working directory parent is obsolete!\n'))
+    wlock = None
+    try:
+        wlock = repo.wlock()
+        repo._afterlock(warnobsolete)
+        res = origfn(ui, repo, *args, **opts)
+    finally:
+        lockmod.release(wlock)
     return res
 
 # XXX this could wrap transaction code
@@ -932,7 +980,11 @@
 
     This function is loosely based on the extensions.wrapcommand function.
     '''
-    aliases, entry = cmdutil.findcmd(newalias, cmdtable)
+    try:
+        aliases, entry = cmdutil.findcmd(newalias, cmdtable)
+    except error.UnknownCommand:
+        # Commands may be disabled
+        return
     for alias, e in cmdtable.iteritems():
         if e is entry:
             break
@@ -1420,6 +1472,9 @@
         files = set()
         copied = copies.pathcopies(prec, bumped)
         precmanifest = prec.manifest()
+        # 3.3.2 needs a list.
+        # future 3.4 don't detect the size change during iteration
+        # this is fishy
         for key, val in list(bumped.manifest().iteritems()):
             precvalue = precmanifest.get(key, None)
             if precvalue is not None:
@@ -1708,6 +1763,7 @@
     [('n', 'new', [], _("successor changeset (DEPRECATED)")),
      ('s', 'succ', [], _("successor changeset")),
      ('r', 'rev', [], _("revisions to prune")),
+     ('k', 'keep', None, _("does not modify working copy during prune")),
      ('', 'biject', False, _("do a 1-1 map between rev and successor ranges")),
      ('B', 'bookmark', '', _("remove revs only reachable from given"
                              " bookmark"))] + metadataopts,
@@ -1747,10 +1803,11 @@
     if not revs:
         raise util.Abort(_('nothing to prune'))
 
-    wlock = lock = None
+    wlock = lock = tr = None
     try:
         wlock = repo.wlock()
         lock = repo.lock()
+        tr = repo.transaction('prune')
         # defines pruned changesets
         precs = []
         revs.sort()
@@ -1781,12 +1838,6 @@
         if biject:
             relations = [(p, (s,)) for p, s in zip(precs, sucs)]
 
-        # create markers
-        obsolete.createmarkers(repo, relations, metadata=metadata)
-
-        # informs that changeset have been pruned
-        ui.status(_('%i changesets pruned\n') % len(precs))
-
         wdp = repo['.']
 
         if len(sucs) == 1 and len(precs) == 1 and wdp in precs:
@@ -1796,15 +1847,43 @@
             # update to an unkilled parent
             newnode = wdp
 
-            while newnode.obsolete():
+            while newnode in precs or newnode.obsolete():
                 newnode = newnode.parents()[0]
 
+
         if newnode.node() != wdp.node():
-            commands.update(ui, repo, newnode.rev())
-            ui.status(_('working directory now at %s\n') % newnode)
+            if opts.get('keep', False):
+                # This is largely the same as the implementation in
+                # strip.stripcmd(). We might want to refactor this somewhere
+                # common at some point.
+
+                # only reset the dirstate for files that would actually change
+                # between the working context and uctx
+                descendantrevs = repo.revs("%d::." % newnode.rev())
+                changedfiles = []
+                for rev in descendantrevs:
+                    # blindly reset the files, regardless of what actually changed
+                    changedfiles.extend(repo[rev].files())
+
+                # reset files that only changed in the dirstate too
+                dirstate = repo.dirstate
+                dirchanges = [f for f in dirstate if dirstate[f] != 'n']
+                changedfiles.extend(dirchanges)
+                repo.dirstate.rebuild(newnode.node(), newnode.manifest(), changedfiles)
+                repo.dirstate.write()
+            else:
+                commands.update(ui, repo, newnode.rev())
+                ui.status(_('working directory now at %s\n') % newnode)
         # update bookmarks
         if bookmark:
             _deletebookmark(ui, marks, bookmark)
+
+        # create markers
+        obsolete.createmarkers(repo, relations, metadata=metadata)
+        
+        # informs that changeset have been pruned
+        ui.status(_('%i changesets pruned\n') % len(precs))
+
         for ctx in repo.unfiltered().set('bookmark() and %ld', precs):
             # used to be:
             #
@@ -1819,8 +1898,10 @@
                     updatebookmarks = _bookmarksupdater(repo, ctx.node())
                     updatebookmarks(dest.node())
                     break
+
+        tr.close()
     finally:
-        lockmod.release(lock, wlock)
+        lockmod.release(tr, lock, wlock)
 
 @command('amend|refresh',
     [('A', 'addremove', None,
@@ -2063,6 +2144,31 @@
     finally:
         lockmod.release(lock, wlock)
 
+@eh.wrapcommand('strip', extension='strip', opts=[
+    ('', 'bundle', None, _("delete the commit entirely and move it to a "
+        "backup bundle")),
+    ])
+def stripwrapper(orig, ui, repo, *revs, **kwargs):
+    if (not ui.configbool('experimental', 'prunestrip') or
+        kwargs.get('bundle', False)):
+        return orig(ui, repo, *revs, **kwargs)
+
+    if kwargs.get('force'):
+        ui.warn(_("warning: --force has no effect during strip with evolve "
+                  "enabled\n"))
+    if kwargs.get('no_backup', False):
+        ui.warn(_("warning: --no-backup has no effect during strips with "
+                  "evolve enabled\n"))
+
+    revs = list(revs) + kwargs.pop('rev', [])
+    revs = set(scmutil.revrange(repo, revs))
+    revs = repo.revs("(%ld)::", revs)
+    kwargs['rev'] = []
+    kwargs['new'] = []
+    kwargs['succ'] = []
+    kwargs['biject'] = False
+    return cmdprune(ui, repo, *revs, **kwargs)
+
 @command('^touch',
     [('r', 'rev', [], 'revision to update'),
      ('D', 'duplicate', False,
@@ -2255,8 +2361,12 @@
 @eh.extsetup
 def oldevolveextsetup(ui):
     for cmd in ['kill', 'uncommit', 'touch', 'fold']:
-        entry = extensions.wrapcommand(cmdtable, cmd,
-                                       warnobserrors)
+        try:
+            entry = extensions.wrapcommand(cmdtable, cmd,
+                                           warnobserrors)
+        except error.UnknownCommand:
+            # Commands may be disabled
+            continue
 
     entry = cmdutil.findcmd('commit', commands.table)[1]
     entry[1].append(('o', 'obsolete', [],
@@ -2285,38 +2395,19 @@
         topic = 'OBSEXC'
     ui.progress(topic, *args, **kwargs)
 
-if getattr(exchange, '_pushdiscoveryobsmarkers', None) is not None:
-    @eh.wrapfunction(exchange, '_pushdiscoveryobsmarkers')
-    def _pushdiscoveryobsmarkers(orig, pushop):
-        if (obsolete._enabled
-            and pushop.repo.obsstore
-            and 'obsolete' in pushop.remote.listkeys('namespaces')):
-            repo = pushop.repo
-            obsexcmsg(repo.ui, "computing relevant nodes\n")
-            revs = list(repo.revs('::%ln', pushop.futureheads))
-            unfi = repo.unfiltered()
-            cl = unfi.changelog
-            if not pushop.remote.capable('_evoext_obshash_0'):
-                # do not trust core yet
-                # return orig(pushop)
-                nodes = [cl.node(r) for r in revs]
-                if nodes:
-                    obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n"
-                                       % len(nodes))
-                    pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes)
-                else:
-                    obsexcmsg(repo.ui, "markers already in sync\n")
-                    pushop.outobsmarkers = []
-                    pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes)
-                return
-
-            common = []
-            obsexcmsg(repo.ui, "looking for common markers in %i nodes\n"
-                               % len(revs))
-            commonrevs = list(unfi.revs('::%ln', pushop.outgoing.commonheads))
-            common = findcommonobsmarkers(pushop.ui, unfi, pushop.remote, commonrevs)
-
-            revs = list(unfi.revs('%ld - (::%ln)', revs, common))
+@eh.wrapfunction(exchange, '_pushdiscoveryobsmarkers')
+def _pushdiscoveryobsmarkers(orig, pushop):
+    if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
+        and pushop.repo.obsstore
+        and 'obsolete' in pushop.remote.listkeys('namespaces')):
+        repo = pushop.repo
+        obsexcmsg(repo.ui, "computing relevant nodes\n")
+        revs = list(repo.revs('::%ln', pushop.futureheads))
+        unfi = repo.unfiltered()
+        cl = unfi.changelog
+        if not pushop.remote.capable('_evoext_obshash_0'):
+            # do not trust core yet
+            # return orig(pushop)
             nodes = [cl.node(r) for r in revs]
             if nodes:
                 obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n"
@@ -2325,12 +2416,30 @@
             else:
                 obsexcmsg(repo.ui, "markers already in sync\n")
                 pushop.outobsmarkers = []
+                pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes)
+            return
+
+        common = []
+        obsexcmsg(repo.ui, "looking for common markers in %i nodes\n"
+                           % len(revs))
+        commonrevs = list(unfi.revs('::%ln', pushop.outgoing.commonheads))
+        common = findcommonobsmarkers(pushop.ui, unfi, pushop.remote, commonrevs)
+
+        revs = list(unfi.revs('%ld - (::%ln)', revs, common))
+        nodes = [cl.node(r) for r in revs]
+        if nodes:
+            obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n"
+                               % len(nodes))
+            pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes)
+        else:
+            obsexcmsg(repo.ui, "markers already in sync\n")
+            pushop.outobsmarkers = []
 
 @eh.wrapfunction(wireproto, 'capabilities')
 def discocapabilities(orig, repo, proto):
     """wrapper to advertise new capability"""
     caps = orig(repo, proto)
-    if obsolete._enabled:
+    if obsolete.isenabled(repo, obsolete.exchangeopt):
         caps += ' _evoext_obshash_0'
     return caps
 
@@ -2490,7 +2599,7 @@
     pushop.ui.debug('try to push obsolete markers to remote\n')
     repo = pushop.repo
     remote = pushop.remote
-    if (obsolete._enabled  and repo.obsstore and
+    if (obsolete.isenabled(repo, obsolete.exchangeopt) and repo.obsstore and
         'obsolete' in remote.listkeys('namespaces')):
         markers = pushop.outobsmarkers
         if not markers:
@@ -2630,34 +2739,32 @@
             kwargs['evo_obscommon'] = common
     return ret
 
-if getattr(exchange, '_getbundleobsmarkerpart', None) is not None:
-    @eh.wrapfunction(exchange, '_getbundleobsmarkerpart')
-    def _getbundleobsmarkerpart(orig, bundler, repo, source, **kwargs):
-        if 'evo_obscommon' not in kwargs:
-            return orig(bundler, repo, source, **kwargs)
-
-        heads = kwargs.get('heads')
-        if kwargs.get('obsmarkers', False):
-            if heads is None:
-                heads = repo.heads()
-            obscommon = kwargs.get('evo_obscommon', ())
-            assert obscommon
-            obsset = repo.unfiltered().set('::%ln - ::%ln', heads, obscommon)
-            subset = [c.node() for c in obsset]
-            markers = repo.obsstore.relevantmarkers(subset)
-            exchange.buildobsmarkerspart(bundler, markers)
-
-    @eh.uisetup
-    def installgetbundlepartgen(ui):
-        origfunc = exchange.getbundle2partsmapping['obsmarkers']
-        def newfunc(*args, **kwargs):
-            return _getbundleobsmarkerpart(origfunc, *args, **kwargs)
-        exchange.getbundle2partsmapping['obsmarkers'] = newfunc
-
+@eh.wrapfunction(exchange, '_getbundleobsmarkerpart')
+def _getbundleobsmarkerpart(orig, bundler, repo, source, **kwargs):
+    if 'evo_obscommon' not in kwargs:
+        return orig(bundler, repo, source, **kwargs)
+
+    heads = kwargs.get('heads')
+    if kwargs.get('obsmarkers', False):
+        if heads is None:
+            heads = repo.heads()
+        obscommon = kwargs.get('evo_obscommon', ())
+        assert obscommon
+        obsset = repo.unfiltered().set('::%ln - ::%ln', heads, obscommon)
+        subset = [c.node() for c in obsset]
+        markers = repo.obsstore.relevantmarkers(subset)
+        exchange.buildobsmarkerspart(bundler, markers)
+
+@eh.uisetup
+def installgetbundlepartgen(ui):
+    origfunc = exchange.getbundle2partsmapping['obsmarkers']
+    def newfunc(*args, **kwargs):
+        return _getbundleobsmarkerpart(origfunc, *args, **kwargs)
+    exchange.getbundle2partsmapping['obsmarkers'] = newfunc
 
 @eh.wrapfunction(exchange, '_pullobsolete')
 def _pullobsolete(orig, pullop):
-    if not obsolete._enabled:
+    if not obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
         return None
     if 'obsmarkers' not in getattr(pullop, 'todosteps', ['obsmarkers']):
         return None
@@ -2827,21 +2934,20 @@
 _bestformat = max(obsolete.formats.keys())
 
 
-if getattr(obsolete, '_checkinvalidmarkers', None) is not None:
-    @eh.wrapfunction(obsolete, '_checkinvalidmarkers')
-    def _checkinvalidmarkers(orig, markers):
-        """search for marker with invalid data and raise error if needed
-
-        Exist as a separated function to allow the evolve extension for a more
-        subtle handling.
-        """
-        if 'debugobsconvert' in sys.argv:
-            return
-        for mark in markers:
-            if node.nullid in mark[1]:
-                raise util.Abort(_('bad obsolescence marker detected: '
-                                   'invalid successors nullid'),
-                                 hint=_('You should run `hg debugobsconvert`'))
+@eh.wrapfunction(obsolete, '_checkinvalidmarkers')
+def _checkinvalidmarkers(orig, markers):
+    """search for marker with invalid data and raise error if needed
+
+    Exist as a separated function to allow the evolve extension for a more
+    subtle handling.
+    """
+    if 'debugobsconvert' in sys.argv:
+        return
+    for mark in markers:
+        if node.nullid in mark[1]:
+            raise util.Abort(_('bad obsolescence marker detected: '
+                               'invalid successors nullid'),
+                             hint=_('You should run `hg debugobsconvert`'))
 
 @command(
     'debugobsconvert',
@@ -2876,7 +2982,7 @@
 def capabilities(orig, repo, proto):
     """wrapper to advertise new capability"""
     caps = orig(repo, proto)
-    if obsolete._enabled:
+    if obsolete.isenabled(repo, obsolete.exchangeopt):
         caps += ' _evoext_pushobsmarkers_0'
         caps += ' _evoext_pullobsmarkers_0'
         caps += ' _evoext_obshash_0'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/inhibit.py	Mon May 04 10:56:06 2015 -0700
@@ -0,0 +1,280 @@
+"""Reduce the changesets evolution feature scope for early and noob friendly UI
+
+The full scale changeset evolution have some massive bleeding edge and it is
+very easy for people not very intimate with the concept to end up in intricate
+situation. In order to get some of the benefit sooner, this extension is
+disabling some of the less polished aspect of evolution. It should gradually
+get thinner and thinner as changeset evolution will get more polished. This
+extension is only recommended for large scale organisations. Individual user
+should probably stick on using Evolution in its current state, understand its
+concept and provide feedback
+
+The first feature provided by this extension is the ability to "inhibit"
+obsolescence markers. Obsolete revision can be cheaply brought back to life
+that way. However as the inhibitor are not fitting in an append only model,
+this is incompatible with sharing mutable history.
+
+The second feature is called direct access. It is the ability to refer and
+access hidden sha in commands provided that you know their value.
+For example hg log -r XXX where XXX is a commit has should work whether XXX is
+hidden or not as we assume that the user knows what he is doing when referring
+to XXX.
+"""
+from mercurial import localrepo
+from mercurial import obsolete
+from mercurial import extensions
+from mercurial import cmdutil
+from mercurial import scmutil
+from mercurial import repoview
+from mercurial import revset
+from mercurial import error
+from mercurial import commands
+from mercurial import lock as lockmod
+from mercurial import bookmarks
+from mercurial import lock as lockmod
+from mercurial.i18n import _
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+
+def reposetup(ui, repo):
+
+    class obsinhibitedrepo(repo.__class__):
+
+        @localrepo.storecache('obsinhibit')
+        def _obsinhibit(self):
+            # XXX we should make sure it is invalidated by transaction failure
+            obsinhibit = set()
+            raw = self.sopener.tryread('obsinhibit')
+            for i in xrange(0, len(raw), 20):
+                obsinhibit.add(raw[i:i+20])
+            return obsinhibit
+
+        def commit(self, *args, **kwargs):
+            newnode = super(obsinhibitedrepo, self).commit(*args, **kwargs)
+            _inhibitmarkers(repo, [newnode])
+            return newnode
+
+    # Wrapping this to inhibit obsolete revs resulting from a transaction
+    extensions.wrapfunction(localrepo.localrepository,
+                            'transaction', transactioncallback)
+
+    repo.__class__ = obsinhibitedrepo
+    repo._explicitaccess = set()
+
+
+def _update(orig, ui, repo, *args, **kwargs):
+    """
+    When moving to a commit we want to inhibit any obsolete commit affecting
+    the changeset we are updating to. In other words we don't want any visible
+    commit to be obsolete.
+    """
+    wlock = None
+    try:
+        # Evolve is running a hook on lock release to display a warning message 
+        # if the workind dir's parent is obsolete.
+        # We take the lock here to make sure that we inhibit the parent before
+        # that hook get a chance to run.
+        wlock = repo.wlock()
+        res = orig(ui, repo, *args, **kwargs)
+        newhead = repo['.'].node()
+        _inhibitmarkers(repo, [newhead])
+        return res
+    finally:
+        lockmod.release(wlock)
+
+def _bookmarkchanged(orig, bkmstoreinst, *args, **kwargs):
+    """ Add inhibition markers to every obsolete bookmarks """
+    repo = bkmstoreinst._repo
+    bkmstorenodes = [repo[v].node() for v in bkmstoreinst.values()]
+    _inhibitmarkers(repo, bkmstorenodes)
+    return orig(bkmstoreinst, *args, **kwargs)
+
+def _bookmark(orig, ui, repo, *bookmarks, **opts):
+    """ Add a -D option to the bookmark command, map it to prune -B """
+    haspruneopt = opts.get('prune', False)
+    if not haspruneopt:
+        return orig(ui, repo, *bookmarks, **opts)
+
+    # Call prune -B
+    evolve = extensions.find('evolve')
+    optsdict = {
+        'new': [],
+        'succ': [],
+        'rev': [],
+        'bookmark': bookmarks[0],
+        'keep': None,
+        'biject': False,
+    }
+    evolve.cmdprune(ui, repo, **optsdict)
+
+# obsolescence inhibitor
+########################
+
+def _schedulewrite(tr, obsinhibit):
+    """Make sure on disk content will be updated on transaction commit"""
+    def writer(fp):
+        """Serialize the inhibited list to disk.
+        """
+        raw = ''.join(obsinhibit)
+        fp.write(raw)
+    tr.addfilegenerator('obsinhibit', ('obsinhibit',), writer)
+    tr.hookargs['obs_inbihited'] = '1'
+
+def _filterpublic(repo, nodes):
+    """filter out inhibitor on public changeset
+
+    Public changesets are already immune to obsolescence"""
+    getrev = repo.changelog.nodemap.get
+    getphase = repo._phasecache.phase
+    return (n for n in repo._obsinhibit if getphase(repo, getrev(n)))
+
+def _inhibitmarkers(repo, nodes):
+    """add marker inhibitor for all obsolete revision under <nodes>
+
+    Content of <nodes> and all mutable ancestors are considered. Marker for
+    obsolete revision only are created.
+    """
+    newinhibit = repo.set('::%ln and obsolete()', nodes)
+    if newinhibit:
+        lock = tr = None
+        try:
+            lock = repo.lock()
+            tr = repo.transaction('obsinhibit')
+            repo._obsinhibit.update(c.node() for c in newinhibit)
+            _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit))
+            repo.invalidatevolatilesets()
+            tr.close()
+        finally:
+            lockmod.release(tr, lock)
+
+def _deinhibitmarkers(repo, nodes):
+    """lift obsolescence inhibition on a set of nodes
+
+    This will be triggered when inhibited nodes received new obsolescence
+    markers. Otherwise the new obsolescence markers would also be inhibited.
+    """
+    deinhibited = repo._obsinhibit & set(nodes)
+    if deinhibited:
+        tr = repo.transaction('obsinhibit')
+        try:
+            repo._obsinhibit -= deinhibited
+            _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit))
+            repo.invalidatevolatilesets()
+            tr.close()
+        finally:
+            tr.release()
+
+def _createmarkers(orig, repo, relations, flag=0, date=None, metadata=None):
+    """wrap markers create to make sure we de-inhibit target nodes"""
+    # wrapping transactio to unify the one in each function
+    tr = repo.transaction('add-obsolescence-marker')
+    try:
+        orig(repo, relations, flag, date, metadata)
+        precs = (r[0].node() for r in relations)
+        _deinhibitmarkers(repo, precs)
+        tr.close()
+    finally:
+        tr.release()
+
+
+def transactioncallback(orig, repo, *args, **kwargs):
+    """ Wrap localrepo.transaction to inhibit new obsolete changes """
+    def inhibitposttransaction(transaction):
+        # At the end of the transaction we catch all the new visible and
+        # obsolete commit to inhibit them
+        visibleobsolete = repo.revs('(not hidden()) and obsolete()')
+        if visibleobsolete:
+            _inhibitmarkers(repo, [repo[r].node() for r in visibleobsolete])
+    transaction = orig(repo, *args, **kwargs)
+    transaction.addpostclose('inhibitposttransaction', inhibitposttransaction)
+    return transaction
+
+def extsetup(ui):
+    # lets wrap the computation of the obsolete set
+    # We apply inhibition there
+    obsfunc = obsolete.cachefuncs['obsolete']
+    def _computeobsoleteset(repo):
+        """remove any inhibited nodes from the obsolete set
+
+        This will trickle down to other part of mercurial (hidden, log, etc)"""
+        obs = obsfunc(repo)
+        getrev = repo.changelog.nodemap.get
+        for n in repo._obsinhibit:
+            obs.discard(getrev(n))
+        return obs
+    obsolete.cachefuncs['obsolete'] = _computeobsoleteset
+    # drop divergence computation since it is incompatible with "light revive"
+    obsolete.cachefuncs['divergent'] = lambda repo: set()
+    # drop bumped computation since it is incompatible with "light revive"
+    obsolete.cachefuncs['bumped'] = lambda repo: set()
+    # wrap create marker to make it able to lift the inhibition
+    extensions.wrapfunction(obsolete, 'createmarkers', _createmarkers)
+    extensions.wrapfunction(repoview, '_getdynamicblockers', _accessvisible)
+    extensions.wrapfunction(revset, 'posttreebuilthook', _posttreebuilthook)
+    # wrap update to make sure that no obsolete commit is visible after an
+    # update
+    extensions.wrapcommand(commands.table, 'update', _update)
+    # There are two ways to save bookmark changes during a transation, we
+    # wrap both to add inhibition markers.
+    extensions.wrapfunction(bookmarks.bmstore, 'recordchange', _bookmarkchanged)
+    extensions.wrapfunction(bookmarks.bmstore, 'write', _bookmarkchanged)
+    # Add bookmark -D option
+    entry = extensions.wrapcommand(commands.table, 'bookmark', _bookmark)
+    entry[1].append(('D','prune',None,
+                    _('delete the bookmark and prune the commits underneath')))
+
+
+
+def gethashsymbols(tree):
+    # Returns the list of symbols of the tree that look like hashes
+    # for example for the revset 3::abe3ff it will return ('abe3ff')
+    if not tree:
+        return []
+
+    if len(tree) == 2 and tree[0] == "symbol":
+        try:
+            int(tree[1])
+            return []
+        except ValueError as e:
+            return [tree[1]]
+    elif len(tree) == 3:
+        return gethashsymbols(tree[1]) + gethashsymbols(tree[2])
+    else:
+        return []
+
+def _posttreebuilthook(orig, tree, repo):
+    # This is use to enabled direct hash access
+    # We extract the symbols that look like hashes and add them to the
+    # explicitaccess set
+    orig(tree, repo)
+    if repo is not None and repo.filtername == 'visible':
+        prelength = len(repo._explicitaccess)
+        repo.symbols = gethashsymbols(tree)
+        cl = repo.unfiltered().changelog
+        for node in repo.symbols:
+            try:
+                node = cl._partialmatch(node)
+            except error.LookupError:
+                node = None
+            if node is not None:
+                rev = cl.rev(node)
+                if rev not in repo.changelog:
+                    repo._explicitaccess.add(rev)
+        if prelength != len(repo._explicitaccess):
+            repo.invalidatevolatilesets()
+
+@command('debugobsinhibit', [], '')
+def cmddebugobsinhibit(ui, repo, *revs):
+    """inhibit obsolescence markers effect on a set of revs"""
+    nodes = (repo[r].node() for r in scmutil.revrange(repo, revs))
+    _inhibitmarkers(repo, nodes)
+
+# ensure revision accessed by hash are visible
+###############################################
+
+def _accessvisible(orig, repo):
+    """ensure accessed revs stay visible"""
+    blockers = orig(repo)
+    blockers.update(getattr(repo, '_explicitaccess', ()))
+    return blockers
--- a/hgext/obsolete.py	Mon May 04 10:58:14 2015 -0700
+++ b/hgext/obsolete.py	Mon May 04 10:56:06 2015 -0700
@@ -14,8 +14,6 @@
 
 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)')
 
@@ -40,6 +38,10 @@
     """
     if not repo.local():
         return
+    evolveopts = ui.configlist('experimental', 'evolution')
+    if not evolveopts:
+        evolveopts = 'all'
+        ui.setconfig('experimental', 'evolution', evolveopts)
     for arg in sys.argv:
         if 'debugc' in arg:
             break
--- a/hgext/pushexperiment.py	Mon May 04 10:58:14 2015 -0700
+++ b/hgext/pushexperiment.py	Mon May 04 10:56:06 2015 -0700
@@ -49,7 +49,8 @@
 
 def syncpush(orig, repo, remote):
     """wraper for obsolete.syncpush to use the fast way if possible"""
-    if not (obsolete._enabled and repo.obsstore):
+    if not (obsolete.isenabled(repo, obsolete.exchangeopt) and
+            repo.obsstore):
         return
     if remote.capable('_push_experiment_pushobsmarkers_0'):
         return # already pushed before changeset
@@ -75,7 +76,7 @@
     """push wrapped that call the wire protocol command"""
     if not remote.canpush():
         raise util.Abort(_("destination does not support push"))
-    if (obsolete._enabled and repo.obsstore
+    if (obsolete.isenabled(repo, obsolete.exchangeopt) and repo.obsstore
         and remote.capable('_push_experiment_pushobsmarkers_0')):
         # push marker early to limit damage of pushing too early.
         try:
@@ -94,7 +95,7 @@
 def capabilities(orig, repo, proto):
     """wrapper to advertise new capability"""
     caps = orig(repo, proto)
-    if obsolete._enabled:
+    if obsolete.isenabled(repo, obsolete.exchangeopt):
         caps += ' _push_experiment_pushobsmarkers_0'
     caps += ' _push_experiment_notifypushend_0'
     return caps
--- a/hgext/simple4server.py	Mon May 04 10:58:14 2015 -0700
+++ b/hgext/simple4server.py	Mon May 04 10:56:06 2015 -0700
@@ -12,7 +12,6 @@
 buglink = 'https://bitbucket.org/marmoute/mutable-history/issues'
 
 import mercurial.obsolete
-mercurial.obsolete._enabled = True
 
 import struct
 from mercurial import util
@@ -31,8 +30,6 @@
 gboptslist = gboptsmap = None
 try:
     from mercurial import obsolete
-    if not obsolete._enabled:
-        obsolete._enabled = True
     from mercurial import wireproto
     gboptslist = getattr(wireproto, 'gboptslist', None)
     gboptsmap = getattr(wireproto, 'gboptsmap', None)
@@ -247,7 +244,7 @@
     """wrapper to advertise new capability"""
     caps = orig(repo, proto)
     advertise = repo.ui.configbool('__temporary__', 'advertiseobsolete', True)
-    if obsolete._enabled and advertise:
+    if obsolete.isenabled(repo, obsolete.exchangeopt) and advertise:
         caps += ' _evoext_pushobsmarkers_0'
         caps += ' _evoext_pullobsmarkers_0'
         caps += ' _evoext_obshash_0'
@@ -303,3 +300,8 @@
     extensions.wrapfunction(pushkey, '_nslist', _nslist)
     pushkey._namespaces['namespaces'] = (lambda *x: False, pushkey._nslist)
 
+def reposetup(ui, repo):
+    evolveopts = ui.configlist('experimental', 'evolution')
+    if not evolveopts:
+        evolveopts = 'all'
+        ui.setconfig('experimental', 'evolution', evolveopts)
--- a/setup.py	Mon May 04 10:58:14 2015 -0700
+++ b/setup.py	Mon May 04 10:56:06 2015 -0700
@@ -1,6 +1,7 @@
 # Copied from histedit setup.py
 # Credit to Augie Fackler <durin42@gmail.com>
 
+import os
 from distutils.core import setup
 from os.path import dirname, join
 
@@ -14,6 +15,13 @@
           if "'" in line:
             return line.split("'")[1]
 
+py_modules = [
+    'hgext.evolve',
+]
+
+if os.environ.get('INCLUDE_INHIBIT'):
+    py_modules.append('hgext.inhibit')
+
 setup(
     name='hg-evolve',
     version=get_version('hgext/evolve.py'),
@@ -25,5 +33,5 @@
     long_description=open('README').read(),
     keywords='hg mercurial',
     license='GPLv2+',
-    py_modules=['hgext.evolve'],
+    py_modules=py_modules
 )
--- a/tests/killdaemons.py	Mon May 04 10:58:14 2015 -0700
+++ b/tests/killdaemons.py	Mon May 04 10:56:06 2015 -0700
@@ -1,25 +1,91 @@
 #!/usr/bin/env python
 
-import os, time, errno, signal
+import os, sys, time, errno, signal
+
+if os.name =='nt':
+    import ctypes
+
+    def _check(ret, expectederr=None):
+        if ret == 0:
+            winerrno = ctypes.GetLastError()
+            if winerrno == expectederr:
+                return True
+            raise ctypes.WinError(winerrno)
 
-# Kill off any leftover daemon processes
-try:
-    fp = open(os.environ['DAEMON_PIDS'])
-    for line in fp:
+    def kill(pid, logfn, tryhard=True):
+        logfn('# Killing daemon process %d' % pid)
+        PROCESS_TERMINATE = 1
+        PROCESS_QUERY_INFORMATION = 0x400
+        SYNCHRONIZE = 0x00100000
+        WAIT_OBJECT_0 = 0
+        WAIT_TIMEOUT = 258
+        handle = ctypes.windll.kernel32.OpenProcess(
+                PROCESS_TERMINATE|SYNCHRONIZE|PROCESS_QUERY_INFORMATION,
+                False, pid)
+        if handle == 0:
+            _check(0, 87) # err 87 when process not found
+            return # process not found, already finished
         try:
-            pid = int(line)
-        except ValueError:
-            continue
+            r = ctypes.windll.kernel32.WaitForSingleObject(handle, 100)
+            if r == WAIT_OBJECT_0:
+                pass # terminated, but process handle still available
+            elif r == WAIT_TIMEOUT:
+                _check(ctypes.windll.kernel32.TerminateProcess(handle, -1))
+            else:
+                _check(r)
+
+            # TODO?: forcefully kill when timeout
+            #        and ?shorter waiting time? when tryhard==True
+            r = ctypes.windll.kernel32.WaitForSingleObject(handle, 100)
+                                                       # timeout = 100 ms
+            if r == WAIT_OBJECT_0:
+                pass # process is terminated
+            elif r == WAIT_TIMEOUT:
+                logfn('# Daemon process %d is stuck')
+            else:
+                _check(r) # any error
+        except: #re-raises
+            ctypes.windll.kernel32.CloseHandle(handle) # no _check, keep error
+            raise
+        _check(ctypes.windll.kernel32.CloseHandle(handle))
+
+else:
+    def kill(pid, logfn, tryhard=True):
         try:
             os.kill(pid, 0)
+            logfn('# Killing daemon process %d' % pid)
             os.kill(pid, signal.SIGTERM)
-            for i in range(10):
-                time.sleep(0.05)
+            if tryhard:
+                for i in range(10):
+                    time.sleep(0.05)
+                    os.kill(pid, 0)
+            else:
+                time.sleep(0.1)
                 os.kill(pid, 0)
+            logfn('# Daemon process %d is stuck - really killing it' % pid)
             os.kill(pid, signal.SIGKILL)
         except OSError, err:
             if err.errno != errno.ESRCH:
                 raise
-    fp.close()
-except IOError:
-    pass
+
+def killdaemons(pidfile, tryhard=True, remove=False, logfn=None):
+    if not logfn:
+        logfn = lambda s: s
+    # Kill off any leftover daemon processes
+    try:
+        fp = open(pidfile)
+        for line in fp:
+            try:
+                pid = int(line)
+            except ValueError:
+                continue
+            kill(pid, logfn, tryhard)
+        fp.close()
+        if remove:
+            os.unlink(pidfile)
+    except IOError:
+        pass
+
+if __name__ == '__main__':
+    path, = sys.argv[1:]
+    killdaemons(path)
--- a/tests/test-evolve.t	Mon May 04 10:58:14 2015 -0700
+++ b/tests/test-evolve.t	Mon May 04 10:56:06 2015 -0700
@@ -69,9 +69,9 @@
   $ hg id -n
   5
   $ hg kill .
-  1 changesets pruned
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
   working directory now at fbb94e3a0ecf
+  1 changesets pruned
   $ hg qlog
   4 - fbb94e3a0ecf add e (draft)
   3 - 47d2a3944de8 add d (draft)
@@ -82,9 +82,9 @@
 test multiple kill
 
   $ hg kill 4 -r 3
-  2 changesets pruned
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
   working directory now at 7c3bad9141dc
+  2 changesets pruned
   $ hg qlog
   2 - 4538525df7e2 add c (draft)
   1 - 7c3bad9141dc add b (public)
@@ -97,9 +97,9 @@
   $ echo 4 > g
   $ hg add g
   $ hg kill .
-  1 changesets pruned
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
   working directory now at 7c3bad9141dc
+  1 changesets pruned
   $ hg st
   A g
 
--- a/tests/test-exchange-D2.t	Mon May 04 10:58:14 2015 -0700
+++ b/tests/test-exchange-D2.t	Mon May 04 10:56:06 2015 -0700
@@ -37,9 +37,9 @@
   created new head
   $ hg debugobsolete `getid 'desc(A0)'` `getid 'desc(A1)'`
   $ hg prune --date '0 0' .
-  1 changesets pruned
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
   working directory now at a9bdc8b26820
+  1 changesets pruned
   $ hg strip --hidden -q 'desc(A1)'
   $ hg log -G --hidden
   x  28b51eb45704 (draft): A0
--- a/tests/test-exchange-D3.t	Mon May 04 10:58:14 2015 -0700
+++ b/tests/test-exchange-D3.t	Mon May 04 10:56:06 2015 -0700
@@ -39,9 +39,9 @@
   $ mkcommit A1
   $ hg debugobsolete `getid 'desc(A0)'` `getid 'desc(A1)'`
   $ hg prune -d '0 0' .
-  1 changesets pruned
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
   working directory now at 35b183996678
+  1 changesets pruned
   $ hg strip --hidden -q 'desc(A1)'
   $ hg log -G --hidden
   @  35b183996678 (draft): B
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-inhibit.t	Mon May 04 10:56:06 2015 -0700
@@ -0,0 +1,541 @@
+  $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > logtemplate = {rev}:{node|short} {desc}\n
+  > [extensions]
+  > rebase=
+  > EOF
+  $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+  $ echo "inhibit=$(echo $(dirname $TESTDIR))/hgext/inhibit.py" >> $HGRCPATH
+  $ mkcommit() {
+  >    echo "$1" > "$1"
+  >    hg add "$1"
+  >    hg ci -m "add $1"
+  > }
+
+  $ hg init inhibit
+  $ cd inhibit
+  $ mkcommit cA
+  $ mkcommit cB
+  $ mkcommit cC
+  $ mkcommit cD
+  $ hg up 'desc(cA)'
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ mkcommit cE
+  created new head
+  $ mkcommit cG
+  $ mkcommit cH
+  $ mkcommit cJ
+  $ hg log -G
+  @  7:18214586bf78 add cJ
+  |
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  | o  3:2db36d8066ff add cD
+  | |
+  | o  2:7df62a38b9bf add cC
+  | |
+  | o  1:02bcbc3f6e56 add cB
+  |/
+  o  0:54ccbc537fc2 add cA
+  
+
+plain prune
+
+  $ hg prune 1::
+  3 changesets pruned
+  $ hg log -G
+  @  7:18214586bf78 add cJ
+  |
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+  $ hg debugobsinhibit --hidden 1::
+  $ hg log -G
+  @  7:18214586bf78 add cJ
+  |
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  | o  3:2db36d8066ff add cD
+  | |
+  | o  2:7df62a38b9bf add cC
+  | |
+  | o  1:02bcbc3f6e56 add cB
+  |/
+  o  0:54ccbc537fc2 add cA
+  
+  $ hg prune --hidden 1::
+  3 changesets pruned
+  $ hg log -G
+  @  7:18214586bf78 add cJ
+  |
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+
+after amend
+
+  $ echo babar > cJ
+  $ hg amend
+  $ hg log -G
+  @  9:55c73a90e4b4 add cJ
+  |
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+  $ hg debugobsinhibit --hidden 18214586bf78
+  $ hg log -G
+  @  9:55c73a90e4b4 add cJ
+  |
+  | o  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+
+and no divergence
+
+  $ hg summary
+  parent: 9:55c73a90e4b4 tip
+   add cJ
+  branch: default
+  commit: (clean)
+  update: 1 new changesets, 2 branch heads (merge)
+
+check public revision got cleared
+(when adding the second inhibitor, the first one is removed because it is public)
+
+  $ wc -m .hg/store/obsinhibit | sed -e 's/^[ \t]*//'
+  20 .hg/store/obsinhibit
+  $ hg prune 7
+  1 changesets pruned
+  $ hg debugobsinhibit --hidden 18214586bf78
+  $ wc -m .hg/store/obsinhibit | sed -e 's/^[ \t]*//'
+  20 .hg/store/obsinhibit
+  $ hg log -G
+  @  9:55c73a90e4b4 add cJ
+  |
+  | o  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+  $ hg phase --public 7
+  $ hg prune 9
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  working directory now at cf5c4f4554ce
+  1 changesets pruned
+  $ hg log -G
+  o  7:18214586bf78 add cJ
+  |
+  @  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+  $ hg debugobsinhibit --hidden 55c73a90e4b4
+  $ wc -m .hg/store/obsinhibit | sed -e 's/^[ \t]*//'
+  20 .hg/store/obsinhibit
+  $ hg log -G
+  o  9:55c73a90e4b4 add cJ
+  |
+  | o  7:18214586bf78 add cJ
+  |/
+  @  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+Update should inhibit all related unstable commits
+
+  $ hg update 2 --hidden
+  2 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ hg log -G
+  o  9:55c73a90e4b4 add cJ
+  |
+  | o  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  | @  2:7df62a38b9bf add cC
+  | |
+  | o  1:02bcbc3f6e56 add cB
+  |/
+  o  0:54ccbc537fc2 add cA
+  
+
+  $ hg update 9
+  4 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg log -G
+  @  9:55c73a90e4b4 add cJ
+  |
+  | o  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  | o  2:7df62a38b9bf add cC
+  | |
+  | o  1:02bcbc3f6e56 add cB
+  |/
+  o  0:54ccbc537fc2 add cA
+  
+  $ hg prune --hidden 1::
+  3 changesets pruned
+  $ hg log -G
+  @  9:55c73a90e4b4 add cJ
+  |
+  | o  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+
+Bookmark should inhibit all related unstable commits
+  $ hg bookmark -r 2 book1  --hidden
+  $ hg log -G
+  @  9:55c73a90e4b4 add cJ
+  |
+  | o  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  | o  2:7df62a38b9bf add cC
+  | |
+  | o  1:02bcbc3f6e56 add cB
+  |/
+  o  0:54ccbc537fc2 add cA
+  
+
+Removing a bookmark with bookmark -D should prune the changes underneath
+that are not reachable from another bookmark or head
+
+  $ hg bookmark -r 1 book2
+  $ hg bookmark -D book1  --config experimental.evolution=createmarkers #--config to make sure prune is not registered as a command.
+  bookmark 'book1' deleted
+  1 changesets pruned
+  $ hg log -G
+  @  9:55c73a90e4b4 add cJ
+  |
+  | o  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  | o  1:02bcbc3f6e56 add cB
+  |/
+  o  0:54ccbc537fc2 add cA
+  
+  $ hg bookmark -D book2
+  bookmark 'book2' deleted
+  1 changesets pruned
+  $ hg log -G
+  @  9:55c73a90e4b4 add cJ
+  |
+  | o  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+Test that direct access make changesets visible
+
+  $ hg export 2db36d8066ff 02bcbc3f6e56
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 2db36d8066ff50e8be3d3e6c2da1ebc0a8381d82
+  # Parent  7df62a38b9bf9daf968de235043ba88a8ef43393
+  add cD
+  
+  diff -r 7df62a38b9bf -r 2db36d8066ff cD
+  --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/cD	Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +cD
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 02bcbc3f6e56fb2928efec2c6e24472720bf5511
+  # Parent  54ccbc537fc2d6845a5d61337c1cfb80d1d2815e
+  add cB
+  
+  diff -r 54ccbc537fc2 -r 02bcbc3f6e56 cB
+  --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/cB	Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +cB
+
+But only with hash
+
+  $ hg export 2db36d8066ff::
+  # HG changeset patch
+  # User test
+  # Date 0 0
+  #      Thu Jan 01 00:00:00 1970 +0000
+  # Node ID 2db36d8066ff50e8be3d3e6c2da1ebc0a8381d82
+  # Parent  7df62a38b9bf9daf968de235043ba88a8ef43393
+  add cD
+  
+  diff -r 7df62a38b9bf -r 2db36d8066ff cD
+  --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/cD	Thu Jan 01 00:00:00 1970 +0000
+  @@ -0,0 +1,1 @@
+  +cD
+
+  $ hg export 1 3
+  abort: hidden revision '1'!
+  (use --hidden to access hidden revisions)
+  [255]
+
+
+With severals hidden sha, rebase of one hidden stack onto another one:
+  $ hg update -C 0
+  0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  $ mkcommit cK
+  created new head
+  $ mkcommit cL
+  $ hg update -C 9
+  4 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg log -G
+  o  11:53a94305e133 add cL
+  |
+  o  10:ad78ff7d621f add cK
+  |
+  | @  9:55c73a90e4b4 add cJ
+  | |
+  | | o  7:18214586bf78 add cJ
+  | |/
+  | o  6:cf5c4f4554ce add cH
+  | |
+  | o  5:5419eb264a33 add cG
+  | |
+  | o  4:98065434e5c6 add cE
+  |/
+  o  0:54ccbc537fc2 add cA
+  
+  $ hg prune 10:
+  2 changesets pruned
+  $ hg log -G
+  @  9:55c73a90e4b4 add cJ
+  |
+  | o  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+  $ hg rebase -s 10 -d 3 
+  abort: hidden revision '3'!
+  (use --hidden to access hidden revisions)
+  [255]
+  $ hg rebase -r ad78ff7d621f -r 53a94305e133 -d  2db36d8066ff
+  rebasing 10:ad78ff7d621f "add cK"
+  rebasing 11:53a94305e133 "add cL"
+  $ hg log -G
+  o  13:2f7b7704d714 add cL
+  |
+  o  12:fe1634cbe235 add cK
+  |
+  | o  11:53a94305e133 add cL
+  | |
+  | o  10:ad78ff7d621f add cK
+  | |
+  | | @  9:55c73a90e4b4 add cJ
+  | | |
+  | | | o  7:18214586bf78 add cJ
+  | | |/
+  | | o  6:cf5c4f4554ce add cH
+  | | |
+  | | o  5:5419eb264a33 add cG
+  | | |
+  | | o  4:98065434e5c6 add cE
+  | |/
+  o |  3:2db36d8066ff add cD
+  | |
+  o |  2:7df62a38b9bf add cC
+  | |
+  o |  1:02bcbc3f6e56 add cB
+  |/
+  o  0:54ccbc537fc2 add cA
+  
+Check that amending in the middle of a stack does not show obsolete revs
+
+  $ hg prune 1::
+  5 changesets pruned
+  $ hg prune 10::
+  2 changesets pruned
+  $ hg log -G
+  @  9:55c73a90e4b4 add cJ
+  |
+  | o  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+  $ hg up 7
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ mkcommit cL
+  $ mkcommit cM
+  $ mkcommit cN
+  $ hg log -G
+  @  16:a438c045eb37 add cN
+  |
+  o  15:2d66e189f5b5 add cM
+  |
+  o  14:d66ccb8c5871 add cL
+  |
+  | o  9:55c73a90e4b4 add cJ
+  | |
+  o |  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+  $ hg up 15
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ echo "mmm" >> cM
+  $ hg amend
+  $ hg log -G
+  @  18:210589181b14 add cM
+  |
+  | o  16:a438c045eb37 add cN
+  | |
+  | o  15:2d66e189f5b5 add cM
+  |/
+  o  14:d66ccb8c5871 add cL
+  |
+  | o  9:55c73a90e4b4 add cJ
+  | |
+  o |  7:18214586bf78 add cJ
+  |/
+  o  6:cf5c4f4554ce add cH
+  |
+  o  5:5419eb264a33 add cG
+  |
+  o  4:98065434e5c6 add cE
+  |
+  o  0:54ccbc537fc2 add cA
+  
+Check that rebasing a commit twice makes the commit visible again
+
+  $ hg rebase -d 18 -r 16 --keep
+  rebasing 16:a438c045eb37 "add cN"
+  $ hg log -r 14:: -G
+  o  19:104eed5354c7 add cN
+  |
+  @  18:210589181b14 add cM
+  |
+  | o  16:a438c045eb37 add cN
+  | |
+  | o  15:2d66e189f5b5 add cM
+  |/
+  o  14:d66ccb8c5871 add cL
+  |
+  $ hg prune -r 104eed5354c7
+  1 changesets pruned
+  $ hg rebase -d 18 -r 16 --keep
+  rebasing 16:a438c045eb37 "add cN"
+  $ hg log -r 14:: -G
+  o  19:104eed5354c7 add cN
+  |
+  @  18:210589181b14 add cM
+  |
+  | o  16:a438c045eb37 add cN
+  | |
+  | o  15:2d66e189f5b5 add cM
+  |/
+  o  14:d66ccb8c5871 add cL
+  |
+
+Test prunestrip
+
+  $ hg book foo -r 104eed5354c7
+  $ hg strip -r 210589181b14 --config experimental.prunestrip=True --config extensions.strip=
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  working directory now at d66ccb8c5871
+  2 changesets pruned
+  $ hg log -r 14:: -G -T '{rev}:{node|short} {desc|firstline} {bookmarks}\n'
+  o  16:a438c045eb37 add cN
+  |
+  o  15:2d66e189f5b5 add cM
+  |
+  @  14:d66ccb8c5871 add cL foo
+  |
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-options.t	Mon May 04 10:56:06 2015 -0700
@@ -0,0 +1,30 @@
+  $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > logtemplate={rev}:{node|short}[{bookmarks}] ({obsolete}/{phase}) {desc|firstline}\n
+  > [extensions]
+  > EOF
+  $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+
+  $ mkcommit() {
+  >    echo "$1" > "$1"
+  >    hg add "$1"
+  >    hg ci -m "add $1"
+  > }
+
+  $ hg init repo
+  $ cd repo
+  $ mkcommit a
+  $ mkcommit b
+
+test disabling commands
+
+  $ cat >> .hg/hgrc <<EOF
+  > [experimental]
+  > evolution=createmarkers
+  >   allowunstable
+  >   exchange
+  > EOF
+  $ hg prune | head -n 2
+  hg: unknown command 'prune'
+  Mercurial Distributed SCM
+  
--- a/tests/test-prune.t	Mon May 04 10:58:14 2015 -0700
+++ b/tests/test-prune.t	Mon May 04 10:56:06 2015 -0700
@@ -38,10 +38,10 @@
 prune current and tip changeset
 
   $ hg prune --user blah --date '1979-12-15' .
-  1 changesets pruned
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
   (leaving bookmark BABAR)
   working directory now at 47d2a3944de8
+  1 changesets pruned
   $ hg debugobsolete
   9d206ffc875e1bc304590549be293be36821e66c 0 {47d2a3944de8b013de3be9578e8e344ea2e6c097} (Sat Dec 15 00:00:00 1979 +0000) {'user': 'blah'}
 
@@ -57,9 +57,9 @@
 pruning multiple changeset at once
 
   $ hg prune 2:
-  2 changesets pruned
   0 files updated, 0 files merged, 3 files removed, 0 files unresolved
   working directory now at 1f0dee641bb7
+  2 changesets pruned
   $ hg debugobsolete
   9d206ffc875e1bc304590549be293be36821e66c 0 {47d2a3944de8b013de3be9578e8e344ea2e6c097} (Sat Dec 15 00:00:00 1979 +0000) {'user': 'blah'}
   7c3bad9141dcb46ff89abf5f61856facd56e476c 0 {1f0dee641bb7258c56bd60e93edfa2405381c41e} (*) {'user': 'test'} (glob)
@@ -118,9 +118,9 @@
   $ hg up 'desc("add ee")'
   4 files updated, 0 files merged, 4 files removed, 0 files unresolved
   $ hg prune 'desc("add ee")' -s 'desc("add nE")'
-  1 changesets pruned
   4 files updated, 0 files merged, 4 files removed, 0 files unresolved
   working directory now at 6e8148413dd5
+  1 changesets pruned
   $ hg debugobsolete
   9d206ffc875e1bc304590549be293be36821e66c 0 {47d2a3944de8b013de3be9578e8e344ea2e6c097} (Sat Dec 15 00:00:00 1979 +0000) {'user': 'blah'}
   7c3bad9141dcb46ff89abf5f61856facd56e476c 0 {1f0dee641bb7258c56bd60e93edfa2405381c41e} (*) {'user': 'test'} (glob)
@@ -208,9 +208,9 @@
   $ mkcommit n2
 
   $ hg prune 'desc("add n1")::desc("add n2")' -s 'desc("add nD")::desc("add nE")' --biject
-  2 changesets pruned
   0 files updated, 0 files merged, 2 files removed, 0 files unresolved
   working directory now at 1f0dee641bb7
+  2 changesets pruned
   $ hg debugobsolete
   9d206ffc875e1bc304590549be293be36821e66c 0 {47d2a3944de8b013de3be9578e8e344ea2e6c097} (Sat Dec 15 00:00:00 1979 +0000) {'user': 'blah'}
   7c3bad9141dcb46ff89abf5f61856facd56e476c 0 {1f0dee641bb7258c56bd60e93edfa2405381c41e} (*) {'user': 'test'} (glob)
@@ -223,6 +223,35 @@
   cb7f8f706a6532967b98cf8583a81baab79a0fa7 8ee176ff1d4b2034ce51e3efc579c2de346b631d 0 (*) {'user': 'test'} (glob)
   21b6f2f1cece8c10326e575dd38239189d467190 6e8148413dd541855b72a920a90c06fca127c7e7 0 (*) {'user': 'test'} (glob)
 
+test hg strip replacement
+
+  $ hg up 10
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ mkcommit n1
+  created new head
+  $ mkcommit n2
+  $ hg --config extensions.strip= --config experimental.prunestrip=True strip -r .
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  working directory now at c7e58696a948
+  1 changesets pruned
+  $ hg --config extensions.strip= --config experimental.prunestrip=True strip -r . --bundle
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  saved backup bundle to $TESTTMP/repo/.hg/strip-backup/c7e58696a948-69ca36d3-backup.hg (glob)
+
+test hg prune --keep
+  $ mkcommit n1
+  created new head
+  $ hg diff -r .^
+  diff -r aa96dc3f04c2 n1
+  --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/n1	* +0000 (glob)
+  @@ -0,0 +1,1 @@
+  +n1
+  $ hg prune -r . --keep
+  1 changesets pruned
+  $ hg status
+  ? n1
+
 test hg prune -B bookmark
 yoinked from test-mq-strip.t
 
@@ -243,11 +272,11 @@
   [255]
   $ hg tag --remove --local a
   $ hg prune -B todelete
-  1 changesets pruned
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   (leaving bookmark todelete)
   working directory now at d62d843c9a01
   bookmark 'todelete' deleted
+  1 changesets pruned
   $ hg id -ir dcbb326fdec2
   abort: hidden revision 'dcbb326fdec2'!
   (use --hidden to access hidden revisions)
@@ -258,8 +287,8 @@
      B                         10:ff43616e5d0f
      delete                    6:2702dd0c91e7
   $ hg prune -B delete
+  bookmark 'delete' deleted
   3 changesets pruned
-  bookmark 'delete' deleted
   $ hg tag --remove --local c
   $ hg id -ir 6:2702dd0c91e7
   abort: hidden revision '6'!
--- a/tests/test-simple4server-bundle2.t	Mon May 04 10:58:14 2015 -0700
+++ b/tests/test-simple4server-bundle2.t	Mon May 04 10:56:06 2015 -0700
@@ -140,7 +140,7 @@
 
   $ echo '[__temporary__]' >> server/.hg/hgrc
   $ echo 'advertiseobsolete=False' >> server/.hg/hgrc
-  $ $TESTDIR/killdaemons.py
+  $ $TESTDIR/killdaemons.py $DAEMON_PIDS
   $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
   $ cat hg.pid >> $DAEMON_PIDS
 
@@ -148,13 +148,9 @@
   bookmarks	
   namespaces	
   phases	
-  $ wget -q -O - http://localhost:$HGPORT/?cmd=hello
-  capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512%0Alistkeys%0Aobsmarkers%3DV0%2CV1%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024
-  $ wget -q -O - http://localhost:$HGPORT/?cmd=capabilities
-  lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512%0Alistkeys%0Aobsmarkers%3DV0%2CV1%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 (no-eol)
 
   $ echo 'advertiseobsolete=True' >> server/.hg/hgrc
-  $ $TESTDIR/killdaemons.py
+  $ $TESTDIR/killdaemons.py $DAEMON_PIDS
   $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
   $ cat hg.pid >> $DAEMON_PIDS
 
@@ -163,6 +159,7 @@
   namespaces	
   obsolete	
   phases	
+
   $ wget -q -O - http://localhost:$HGPORT/?cmd=hello
   capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512%0Alistkeys%0Aobsmarkers%3DV0%2CV1%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon
   $ wget -q -O - http://localhost:$HGPORT/?cmd=capabilities
--- a/tests/test-simple4server.t	Mon May 04 10:58:14 2015 -0700
+++ b/tests/test-simple4server.t	Mon May 04 10:56:06 2015 -0700
@@ -141,7 +141,7 @@
 
   $ echo '[__temporary__]' >> server/.hg/hgrc
   $ echo 'advertiseobsolete=False' >> server/.hg/hgrc
-  $ $TESTDIR/killdaemons.py
+  $ $TESTDIR/killdaemons.py $DAEMON_PIDS
   $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
   $ cat hg.pid >> $DAEMON_PIDS
 
@@ -155,7 +155,7 @@
   lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512%0Alistkeys%0Aobsmarkers%3DV0%2CV1%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 (no-eol)
 
   $ echo 'advertiseobsolete=True' >> server/.hg/hgrc
-  $ $TESTDIR/killdaemons.py
+  $ $TESTDIR/killdaemons.py $DAEMON_PIDS
   $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
   $ cat hg.pid >> $DAEMON_PIDS
 
--- a/tests/test-tutorial.t	Mon May 04 10:58:14 2015 -0700
+++ b/tests/test-tutorial.t	Mon May 04 10:56:06 2015 -0700
@@ -289,9 +289,9 @@
 not fit well in my standard shopping list)
 
   $ hg prune . # "." is for working directory parent
-  1 changesets pruned
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   working directory now at 41aff6a42b75
+  1 changesets pruned
 
 The silly changeset is gone.
 
@@ -804,9 +804,9 @@
 In the mean time I noticed you can't buy animals in a super market and I prune the animal changeset:
 
   $ hg prune ee942144f952
-  1 changesets pruned
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   working directory now at a44c85f957d3
+  1 changesets pruned
   1 new unstable changesets
 
 
--- a/tests/test-userguide.t	Mon May 04 10:58:14 2015 -0700
+++ b/tests/test-userguide.t	Mon May 04 10:56:06 2015 -0700
@@ -66,9 +66,9 @@
   $ echo 'debug hack' >> file1.c
   $ hg commit -m 'debug hack'
   $ hg prune .
-  1 changesets pruned
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   working directory now at 934359450037
+  1 changesets pruned
   $ hg parents --template '{rev}:{node|short}  {desc|firstline}\n'
   3:934359450037  implement feature Y
   $ hg --hidden shortlog -G -r 3: