changeset 323:3edc829799d5

merge with stable doc update
author Pierre-Yves David <pierre-yves.david@logilab.fr>
date Wed, 27 Jun 2012 12:44:54 +0200
parents f4a00b2d8bfb (diff) 232990fbecb5 (current diff)
children ff070b9e22ef
files
diffstat 12 files changed, 977 insertions(+), 142 deletions(-) [+]
line wrap: on
line diff
--- a/docs/obs-implementation.rst	Wed Jun 20 18:04:50 2012 +0200
+++ b/docs/obs-implementation.rst	Wed Jun 27 12:44:54 2012 +0200
@@ -200,7 +200,7 @@
 * Use secret phase to remove from discovery obsolete and unstable changeset (to
   be improved soon)
 
-* alter rebase to use obsolete marker instead of stripping. (XXX break --keep for now)
+* alter rebase to use obsolete marker instead of stripping.
 
 * Have an experimental mq-like extension to rewrite history (more on that later)
 
--- a/enable.sh	Wed Jun 20 18:04:50 2012 +0200
+++ b/enable.sh	Wed Jun 27 12:44:54 2012 +0200
@@ -42,7 +42,7 @@
 pstatus=status --rev .^
 
 # diff with the previous amend
-odiff=diff --rev 'limit(obsparents(.),1)' --rev .
+odiff=diff --rev 'limit(precursors(.),1)' --rev .
 EOF
 
 cat << EOF >&2
--- a/hgext/evolve.py	Wed Jun 20 18:04:50 2012 +0200
+++ b/hgext/evolve.py	Wed Jun 27 12:44:54 2012 +0200
@@ -27,31 +27,29 @@
 
 ### util function
 #############################
+
 def noderange(repo, revsets):
     """The same as revrange but return node"""
     return map(repo.changelog.node,
                scmutil.revrange(repo, revsets))
 
-
-
-def warnunstable(orig, ui, repo, *args, **kwargs):
+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()'))
     #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
         #print orig, newunstables
         #print len(repo.revs('secret() - obsolete()'))
         if newunstables > 0:
             ui.warn(_('%i new unstables changesets\n') % newunstables)
-
-
-### extension check
-#############################
-
+        if newlatecomers > 0:
+            ui.warn(_('%i new latecomers changesets\n') % newlatecomers)
 
 ### changeset rewriting logic
 #############################
@@ -65,7 +63,7 @@
     if len(old.parents()) > 1: #XXX remove this unecessary limitation.
         raise error.Abort(_('cannot amend merge changesets'))
     base = old.p1()
-    bm = bookmarks.readcurrent(repo)
+    updatebookmarks = _bookmarksupdater(repo, old.node())
 
     wlock = repo.wlock()
     try:
@@ -136,21 +134,10 @@
         new = repo[newid]
         created = len(repo) != revcount
         if created:
-            # update the bookmark
-            if bm:
-                repo._bookmarks[bm] = newid
-                bookmarks.write(repo)
-
+            updatebookmarks(newid)
             # add evolution metadata
-            repo.addobsolete(new.node(), old.node())
-            for u in updates:
-                repo.addobsolete(u.node(), old.node())
-                repo.addobsolete(new.node(), u.node())
-            oldbookmarks = repo.nodebookmarks(old.node())
-            for book in oldbookmarks:
-                repo._bookmarks[book] = new.node()
-            if oldbookmarks:
-                bookmarks.write(repo)
+            collapsed = set([u.node() for u in updates] + [old.node()])
+            repo.addcollapsedobsolete(collapsed, new.node())
         else:
             # newid is an existing revision. It could make sense to
             # replace revisions with existing ones but probably not by
@@ -179,7 +166,8 @@
         else:
             rebase.rebasenode(repo, orig.node(), dest.node(),
                               {node.nullrev: node.nullrev})
-        nodenew = rebase.concludenode(repo, orig.node(), dest.node(), node.nullid)
+        nodenew = rebase.concludenode(repo, orig.node(), dest.node(),
+                                      node.nullid)
         oldbookmarks = repo.nodebookmarks(nodesrc)
         if nodenew is not None:
             phases.retractboundary(repo, destphase, [nodenew])
@@ -200,7 +188,6 @@
         repo.dirstate.invalidate()
         raise
 
-
 def stabilizableunstable(repo, pctx):
     """Return a changectx for an unstable changeset which can be
     stabilized on top of pctx or one of its descendants. None if none
@@ -220,17 +207,34 @@
             return unstables[0]
     return None
 
+def _bookmarksupdater(repo, oldid):
+    """Return a callable update(newid) updating the current bookmark
+    and bookmarks bound to oldid to newid.
+    """
+    bm = bookmarks.readcurrent(repo)
+    def updatebookmarks(newid):
+        dirty = False
+        if bm:
+            repo._bookmarks[bm] = newid
+            dirty = True
+        oldbookmarks = repo.nodebookmarks(oldid)
+        if oldbookmarks:
+            for b in oldbookmarks:
+                repo._bookmarks[b] = newid
+            dirty = True
+        if dirty:
+            bookmarks.write(repo)
+    return updatebookmarks
+
 ### new command
 #############################
 cmdtable = {}
 command = cmdutil.command(cmdtable)
 
 @command('^stabilize|evolve',
-    [
-     ('n', 'dry-run', False, 'Do nothing but printing what should be done'),
-     ('A', 'any', False, 'Stabilize unstable change on any topological branch'),
-    ],
-    '')
+    [('n', 'dry-run', False, 'do not perform actions, print what to be done'),
+    ('A', 'any', False, 'stabilize any unstable changeset'),],
+    _('[OPTIONS]...'))
 def stabilize(ui, repo, **opts):
     """rebase an unstable changeset to make it stable again
 
@@ -298,10 +302,10 @@
 shorttemplate = '[{rev}] {desc|firstline}\n'
 
 @command('^gdown',
-    [],
-    'update to working directory parent and display summary lines')
+         [],
+         '')
 def cmdgdown(ui, repo):
-    """update to working directory parent an display summary lines"""
+    """update to parent an display summary lines"""
     wkctx = repo[None]
     wparents = wkctx.parents()
     if len(wparents) != 1:
@@ -321,10 +325,10 @@
         return 1
 
 @command('^gup',
-    [],
-    'update to working directory children and display summary lines')
+         [],
+         '')
 def cmdup(ui, repo):
-    """update to working directory children an display summary lines"""
+    """update to child an display summary lines"""
     wkctx = repo[None]
     wparents = wkctx.parents()
     if len(wparents) != 1:
@@ -346,12 +350,9 @@
         ui.warn(_('Multiple non-obsolete children, explicitly update to one\n'))
         return 1
 
-
-@command('^kill|obsolete',
-    [
-    ('n', 'new', [], _("New changeset that justify this one to be killed"))
-    ],
-    '<revs>')
+@command('^kill|obsolete|prune',
+    [('n', 'new', [], _("successor changeset"))],
+    _('[OPTION] REV...'))
 def kill(ui, repo, *revs, **opts):
     """mark a changeset as obsolete
 
@@ -389,15 +390,11 @@
 @command('^amend|refresh',
     [('A', 'addremove', None,
      _('mark new/missing files as added/removed before committing')),
-    ('n', 'note', '',
-     _('use text as commit message for this update')),
-    ('c', 'change', '',
-     _('specifies the changeset to amend'), _('REV')),
-    ('e', 'edit', False,
-     _('edit commit message.'), _('')),
+    ('n', 'note', '', _('use text as commit message for this update')),
+    ('c', 'change', '', _('specifies the changesets to amend'), _('REV')),
+    ('e', 'edit', False, _('invoke editor on commit messages')),
     ] + walkopts + commitopts + commitopts2,
     _('[OPTION]... [FILE]...'))
-
 def amend(ui, repo, *pats, **opts):
     """combine a changeset with updates and replace it with a new one
 
@@ -408,11 +405,9 @@
 
     If you don't specify -m, the parent's message will be reused.
 
-    If you specify --change, amend additionally considers all changesets between
-    the indicated changeset and the working copy parent as updates to be subsumed.
-    This allows you to commit updates manually first. As a special shorthand you
-    can say `--amend .` instead of '--amend p1(p1())', which subsumes your latest
-    commit as an update of its parent.
+    If you specify --change, amend additionally considers all
+    changesets between the indicated changeset and the working copy
+    parent as updates to be subsumed.
 
     Behind the scenes, Mercurial first commits the update as a regular child
     of the current parent. Then it creates a new commit on the parent's parents
@@ -424,17 +419,15 @@
     """
 
     # determine updates to subsume
-    change = opts.get('change', '.')
-    if change == '.':
-        change = 'p1(p1())'
-    old = scmutil.revsingle(repo, change)
+    old = scmutil.revsingle(repo, opts.get('change') or '.')
 
     lock = repo.lock()
     try:
         wlock = repo.wlock()
         try:
-            if not old.phase():
-                raise util.Abort(_("can not rewrite immutable changeset %s") % old)
+            if old.phase() == phases.public:
+                raise util.Abort(_("can not rewrite immutable changeset %s")
+                                 % old)
             oldphase = old.phase()
             # commit current changes as update
             # code copied from commands.commit to avoid noisy messages
@@ -444,8 +437,8 @@
             ciopts['message'] = opts.get('note') or ('amends %s' % old.hex())
             e = cmdutil.commiteditor
             def commitfunc(ui, repo, message, match, opts):
-                return repo.commit(message, opts.get('user'), opts.get('date'), match,
-                                   editor=e)
+                return repo.commit(message, opts.get('user'), opts.get('date'),
+                                   match, editor=e)
             revcount = len(repo)
             tempid = cmdutil.commit(ui, repo, commitfunc, pats, ciopts)
             if len(repo) == revcount:
@@ -466,8 +459,6 @@
                     raise error.Abort(_('no updates found'))
             updates = [repo[n] for n in updatenodes]
 
-
-
             # perform amend
             if opts.get('edit'):
                 opts['force_editor'] = True
@@ -490,8 +481,142 @@
     finally:
         lock.release()
 
+def _commitfiltered(repo, ctx, match):
+    """Recommit ctx with changed files not in match. Return the new
+    node identifier, or None if nothing changed.
+    """
+    base = ctx.p1()
+    m, a, r = repo.status(base, ctx)[:3]
+    allfiles = set(m + a + r)
+    files = set(f for f in allfiles if not match(f))
+    if files == allfiles:
+        return None
 
+    # Filter copies
+    copied = copies.pathcopies(base, ctx)
+    copied = dict((src, dst) for src, dst in copied.iteritems()
+                  if dst in files)
+    def filectxfn(repo, memctx, path):
+        if path not in ctx:
+            raise IOError()
+        fctx = ctx[path]
+        flags = fctx.flags()
+        mctx = context.memfilectx(fctx.path(), fctx.data(),
+                                  islink='l' in flags,
+                                  isexec='x' in flags,
+                                  copied=copied.get(path))
+        return mctx
 
+    new = context.memctx(repo,
+                         parents=[base.node(), node.nullid],
+                         text=ctx.description(),
+                         files=files,
+                         filectxfn=filectxfn,
+                         user=ctx.user(),
+                         date=ctx.date(),
+                         extra=ctx.extra())
+    # commitctx always create a new revision, no need to check
+    newid = repo.commitctx(new)
+    return newid
+
+def _uncommitdirstate(repo, oldctx, match):
+    """Fix the dirstate after switching the working directory from
+    oldctx to a copy of oldctx not containing changed files matched by
+    match.
+    """
+    ctx = repo['.']
+    ds = repo.dirstate
+    copies = dict(ds.copies())
+    m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
+    for f in m:
+        if ds[f] == 'r':
+            # modified + removed -> removed
+            continue
+        ds.normallookup(f)
+
+    for f in a:
+        if ds[f] == 'r':
+            # added + removed -> unknown
+            ds.drop(f)
+        elif ds[f] != 'a':
+            ds.add(f)
+
+    for f in r:
+        if ds[f] == 'a':
+            # removed + added -> normal
+            ds.normallookup(f)
+        elif ds[f] != 'r':
+            ds.remove(f)
+
+    # Merge old parent and old working dir copies
+    oldcopies = {}
+    for f in (m + a):
+        src = oldctx[f].renamed()
+        if src:
+            oldcopies[f] = src[0]
+    oldcopies.update(copies)
+    copies = dict((dst, oldcopies.get(src, src))
+                  for dst, src in oldcopies.iteritems())
+    # Adjust the dirstate copies
+    for dst, src in copies.iteritems():
+        if (src not in ctx or dst in ctx or ds[dst] != 'a'):
+            src = None
+        ds.copy(src, dst)
+
+@command('^uncommit',
+    [('a', 'all', None, _('uncommit all changes when no arguments given')),
+     ] + commands.walkopts,
+    _('[OPTION]... [NAME]'))
+def uncommit(ui, repo, *pats, **opts):
+    """move changes from parent revision to working directory
+
+    Changes to selected files in parent revision appear again as
+    uncommitted changed in the working directory. A new revision
+    without selected changes is created, becomes the new parent and
+    obsoletes the previous one.
+
+    The --include option specify pattern to uncommit
+    The --exclude option specify pattern to keep in the commit
+
+    Return 0 if changed files are uncommitted.
+    """
+    lock = repo.lock()
+    try:
+        wlock = repo.wlock()
+        try:
+            wctx = repo[None]
+            if len(wctx.parents()) <= 0:
+                raise util.Abort(_("cannot uncommit null changeset"))
+            if len(wctx.parents()) > 1:
+                raise util.Abort(_("cannot uncommit while merging"))
+            old = repo['.']
+            if old.phase() == phases.public:
+                raise util.Abort(_("cannot rewrite immutable changeset"))
+            if len(old.parents()) > 1:
+                raise util.Abort(_("cannot uncommit merge changeset"))
+            oldphase = old.phase()
+            updatebookmarks = _bookmarksupdater(repo, old.node())
+            # Recommit the filtered changeset
+            newid = None
+            if (pats or opts.get('include') or opts.get('exclude')
+                or opts.get('all')):
+                match = scmutil.match(old, pats, opts)
+                newid = _commitfiltered(repo, old, match)
+            if newid is None:
+                raise util.Abort(_('nothing to uncommit'))
+            # Move local changes on filtered changeset
+            repo.addobsolete(newid, old.node())
+            phases.retractboundary(repo, oldphase, [newid])
+            repo.dirstate.setparents(newid, node.nullid)
+            _uncommitdirstate(repo, old, match)
+            updatebookmarks(newid)
+            if not repo[newid].files():
+                ui.warn(_("new changeset is empty\n"))
+                ui.status(_('(use "hg kill ." to remove it)\n'))
+        finally:
+            wlock.release()
+    finally:
+        lock.release()
 
 def commitwrapper(orig, ui, repo, *arg, **kwargs):
     lock = repo.lock()
@@ -547,16 +672,19 @@
         raise error.Abort(_('evolution extension require rebase extension.'))
 
     entry = extensions.wrapcommand(commands.table, 'commit', commitwrapper)
-    entry[1].append(('o', 'obsolete', [], _("this commit obsolet this revision")))
+    entry[1].append(('o', 'obsolete', [],
+                     _("make commit obsolete this revision")))
     entry = extensions.wrapcommand(commands.table, 'graft', graftwrapper)
-    entry[1].append(('o', 'obsolete', [], _("this graft obsolet this revision")))
-    entry[1].append(('O', 'old-obsolete', False, _("graft result obsolete graft source")))
+    entry[1].append(('o', 'obsolete', [],
+                     _("make graft obsoletes this revision")))
+    entry[1].append(('O', 'old-obsolete', False,
+                     _("make graft obsoletes its source")))
 
     # warning about more obsolete
-    for cmd in ['commit', 'push', 'pull', 'graft']:
-        entry = extensions.wrapcommand(commands.table, cmd, warnunstable)
-    for cmd in ['kill', 'amend']:
-        entry = extensions.wrapcommand(cmdtable, cmd, warnunstable)
+    for cmd in ['commit', 'push', 'pull', 'graft', 'phase', 'unbundle']:
+        entry = extensions.wrapcommand(commands.table, cmd, warnobserrors)
+    for cmd in ['amend', 'kill', 'uncommit']:
+        entry = extensions.wrapcommand(cmdtable, cmd, warnobserrors)
 
     if rebase is not None:
-        entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnunstable)
+        entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors)
--- a/hgext/obsolete.py	Wed Jun 20 18:04:50 2012 +0200
+++ b/hgext/obsolete.py	Wed Jun 27 12:44:54 2012 +0200
@@ -101,6 +101,7 @@
 from mercurial.lock import release
 from mercurial import localrepo
 from mercurial import cmdutil
+from mercurial import templatekw
 
 try:
     from mercurial.localrepo import storecache
@@ -137,10 +138,22 @@
 
 context.changectx.extinct = extinct
 
+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
+
 
 ### revset
 #############################
 
+def revsethidden(repo, subset, x):
+    """hidden changesets"""
+    args = revset.getargs(x, 0, 0, 'hidden takes no argument')
+    return [r for r in subset if r in repo.changelog.hiddenrevs]
 
 def revsetobsolete(repo, subset, x):
     """obsolete changesets"""
@@ -161,17 +174,21 @@
 
 def revsetsuspended(repo, subset, x):
     """obsolete changesets with non obsolete descendants"""
-    args = revset.getargs(x, 0, 0, 'unstable takes no arguments')
+    args = revset.getargs(x, 0, 0, 'suspended takes no arguments')
     return [r for r in subset if r in repo._suspendedset]
 
 def revsetextinct(repo, subset, x):
     """obsolete changesets without obsolete descendants"""
-    args = revset.getargs(x, 0, 0, 'unstable takes no arguments')
+    args = revset.getargs(x, 0, 0, 'extinct takes no arguments')
     return [r for r in subset if r in repo._extinctset]
 
+def revsetlatecomer(repo, subset, x):
+    """latecomer, Try to succeed to public change"""
+    args = revset.getargs(x, 0, 0, 'latecomer takes no arguments')
+    return [r for r in subset if r in repo._latecomerset]
 
-def _obsparents(repo, s):
-    """obsolete parents of a subset"""
+def _precursors(repo, s):
+    """Precursor of a changeset"""
     cs = set()
     nm = repo.changelog.nodemap
     markerbysubj = repo.obsoletestore.subjects
@@ -182,14 +199,14 @@
                 cs.add(pr)
     return cs
 
-def revsetobsparents(repo, subset, x):
-    """obsolete parents"""
+def revsetprecursors(repo, subset, x):
+    """precursors of a subset"""
     s = revset.getset(repo, range(len(repo)), x)
-    cs = _obsparents(repo, s)
+    cs = _precursors(repo, s)
     return [r for r in subset if r in cs]
 
-def _obsancestors(repo, s):
-    """obsolete ancestors of a subset"""
+def _allprecursors(repo, s):  # XXX we need a better naming
+    """transitive precursors of a subset"""
     toproceed = [repo[r].node() for r in s]
     seen = set()
     allsubjects = repo.obsoletestore.subjects
@@ -208,13 +225,73 @@
             cs.add(pr)
     return cs
 
-def revsetobsancestors(repo, subset, x):
+def revsetallprecursors(repo, subset, x):
     """obsolete parents"""
     s = revset.getset(repo, range(len(repo)), x)
-    cs = _obsancestors(repo, s)
+    cs = _allprecursors(repo, s)
+    return [r for r in subset if r in cs]
+
+def _successors(repo, s):
+    """Successors of a changeset"""
+    cs = set()
+    nm = repo.changelog.nodemap
+    markerbyobj = repo.obsoletestore.objects
+    for r in s:
+        for p in markerbyobj.get(repo[r].node(), ()):
+            for sub in p['subjects']:
+                sr = nm.get(sub)
+                if sr is not None:
+                    cs.add(sr)
+    return cs
+
+def revsetsuccessors(repo, subset, x):
+    """successors of a subset"""
+    s = revset.getset(repo, range(len(repo)), x)
+    cs = _successors(repo, s)
+    return [r for r in subset if r in cs]
+
+def _allsuccessors(repo, s):  # XXX we need a better naming
+    """transitive successors of a subset"""
+    toproceed = [repo[r].node() for r in s]
+    seen = set()
+    allobjects = repo.obsoletestore.objects
+    while toproceed:
+        nc = toproceed.pop()
+        for mark in allobjects.get(nc, ()):
+            for sub in mark['subjects']:
+                if sub not in seen:
+                    seen.add(sub)
+                    toproceed.append(sub)
+    nm = repo.changelog.nodemap
+    cs = set()
+    for s in seen:
+        sr = nm.get(s)
+        if sr is not None:
+            cs.add(sr)
+    return cs
+
+def revsetallsuccessors(repo, subset, x):
+    """obsolete parents"""
+    s = revset.getset(repo, range(len(repo)), x)
+    cs = _allsuccessors(repo, s)
     return [r for r in subset if r in cs]
 
 
+### template keywords
+#####################
+
+def obsoletekw(repo, ctx, templ, **args):
+    """:obsolete: String. The obsolescence level of the node, could be
+    ``stable``, ``unstable``, ``suspended`` or ``extinct``.
+    """
+    rev = ctx.rev()
+    if rev in repo._extinctset:
+        return 'extinct'
+    if rev in repo._suspendedset:
+        return 'suspended'
+    if rev in repo._unstableset:
+        return 'unstable'
+    return 'stable'
 
 ### Other Extension compat
 ############################
@@ -245,9 +322,8 @@
     return newrev
 
 def cmdrebase(orig, ui, repo, *args, **kwargs):
-    if kwargs.get('keep', False):
-        raise util.Abort(_('rebase --keep option is unsupported with obsolete '
-                           'extension'), hint=_("see 'hg help obsolete'"))
+
+    reallykeep = kwargs.get('keep', False)
     kwargs = dict(kwargs)
     kwargs['keep'] = True
 
@@ -260,33 +336,41 @@
     # added from this state after a successful call.
     repo._rebasestate = {}
     repo._rebasetarget = None
-    maxrev = len(repo) - 1
     try:
         res = orig(ui, repo, *args, **kwargs)
-        if not res and not kwargs.get('abort') and repo._rebasetarget:
-            # We have to tell rewritten revisions from removed
-            # ones. When collapsing, removed revisions are considered
-            # to be collapsed onto the final one, while in the normal
-            # case their are marked obsolete without successor.
-            emptynode = nullid
-            if kwargs.get('collapse'):
-                emptynode = repo[max(repo._rebasestate.values())].node()
-            # Rebased revisions are assumed to be descendants of
-            # targetrev. If a source revision is mapped to targetrev
-            # or to another rebased revision, it must have been
-            # removed.
-            targetrev = repo[repo._rebasetarget].rev()
-            newrevs = set([targetrev])
-            for rev, newrev in sorted(repo._rebasestate.items()):
-                if newrev == -2:  # nullmerge
-                    continue
-                oldnode = repo[rev].node()
-                if newrev not in newrevs and newrev >= 0:
-                    newnode = repo[newrev].node()
-                    newrevs.add(newrev)
+        if not reallykeep:
+            # Filter nullmerge or unrebased entries
+            repo._rebasestate = dict(p for p in repo._rebasestate.iteritems()
+                                     if p[1] >= 0)
+            if not res and not kwargs.get('abort') and repo._rebasestate:
+                # Rebased revisions are assumed to be descendants of
+                # targetrev. If a source revision is mapped to targetrev
+                # or to another rebased revision, it must have been
+                # removed.
+                targetrev = repo[repo._rebasetarget].rev()
+                newrevs = set([targetrev])
+                replacements = {}
+                for rev, newrev in sorted(repo._rebasestate.items()):
+                    oldnode = repo[rev].node()
+                    if newrev not in newrevs:
+                        newnode = repo[newrev].node()
+                        newrevs.add(newrev)
+                    else:
+                        newnode = nullid
+                    replacements[oldnode] = newnode
+
+                if kwargs.get('collapse'):
+                    newnodes = set(n for n in replacements.values() if n != nullid)
+                    if newnodes:
+                        # Collapsing into more than one revision?
+                        assert len(newnodes) == 1, newnodes
+                        newnode = newnodes.pop()
+                    else:
+                        newnode = nullid
+                    repo.addcollapsedobsolete(replacements, newnode)
                 else:
-                    newnode = emptynode
-                repo.addobsolete(newnode, oldnode)
+                    for oldnode, newnode in replacements.iteritems():
+                        repo.addobsolete(newnode, oldnode)
         return res
     finally:
         delattr(repo, '_rebasestate')
@@ -295,13 +379,20 @@
 
 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["obsparents"] = revsetobsparents
-    revset.symbols["obsancestors"] = revsetobsancestors
+    revset.symbols["latecomer"] = revsetlatecomer
+    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
 
     try:
         rebase = extensions.find('rebase')
@@ -311,7 +402,7 @@
             extensions.wrapfunction(rebase, 'concludenode', concludenode)
             extensions.wrapcommand(rebase.cmdtable, "rebase", cmdrebase)
     except KeyError:
-        pass # rebase not found
+        pass  # rebase not found
 
 # Pushkey mechanism for mutable
 #########################################
@@ -382,6 +473,9 @@
         if ctx.obsolete():
             raise util.Abort(_("Trying to push obsolete changeset: %s!") % ctx,
                              hint=hint)
+        if ctx.latecomer():
+            raise util.Abort(_("Trying to push latecomer changeset: %s!") % ctx,
+                             hint=hint)
     ### patch remote branch map
     # do not read it this burn eyes
     try:
@@ -436,6 +530,12 @@
     else:
         return None # break recursion
 
+def wrapclearcache(orig, repo, *args, **kwargs):
+    try:
+        return orig(repo, *args, **kwargs)
+    finally:
+        repo._clearobsoletecache()
+
 
 ### New commands
 #############################
@@ -527,12 +627,35 @@
     repo._turn_extinct_secret()
     return orig(repo)
 
+def wrapcmdutilamend(orig, ui, repo, commitfunc, old, *args, **kwargs):
+    oldnode = old.node()
+    new = orig(ui, repo, commitfunc, old, *args, **kwargs)
+    if new != oldnode:
+        lock = repo.lock()
+        try:
+            newmarker = {
+                'subjects':  [new],
+                'object': oldnode,
+                'date':  util.makedate(),
+                'user': ui.username(),
+                'reason': 'commit --amend',
+                }
+            repo.obsoletestore.new(newmarker)
+            repo._clearobsoletecache()
+            repo._turn_extinct_secret()
+        finally:
+            lock.release()
+    return new
+
 def uisetup(ui):
     extensions.wrapcommand(commands.table, "update", wrapmayobsoletewc)
     extensions.wrapcommand(commands.table, "pull", wrapmayobsoletewc)
+    if util.safehasattr(cmdutil, 'amend'):
+        extensions.wrapfunction(cmdutil, 'amend', wrapcmdutilamend)
     extensions.wrapfunction(discovery, 'findcommonoutgoing', wrapfindcommonoutgoing)
     extensions.wrapfunction(discovery, 'checkheads', wrapcheckheads)
     extensions.wrapfunction(phases, 'visibleheads', noextinctsvisibleheads)
+    extensions.wrapfunction(phases, 'advanceboundary', wrapclearcache)
     if util.safehasattr(phases, 'visiblebranchmap'):
         extensions.wrapfunction(phases, 'visiblebranchmap', wrapvisiblebranchmap)
 
@@ -767,6 +890,11 @@
             """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"""
+            return set(self.revs('allsuccessors(public()) - obsolete()'))
+
         def _clearobsoletecache(self):
             if '_obsoleteset' in vars(self):
                 del self._obsoleteset
@@ -783,6 +911,8 @@
                 del self._suspendedset
             if '_extinctset' in vars(self):
                 del self._extinctset
+            if '_latecomerset' in vars(self):
+                del self._latecomerset
 
         def addobsolete(self, sub, obj):
             """Add a relation marking that node <sub> is a new version of <obj>"""
@@ -812,12 +942,22 @@
             finally:
                 lock.release()
 
+        def addcollapsedobsolete(self, oldnodes, newnode):
+            """Mark oldnodes as collapsed into newnode."""
+            # Assume oldnodes are all descendants of a single rev
+            rootrevs = self.revs('roots(%ln)', oldnodes)
+            assert len(rootrevs) == 1, rootrevs
+            rootnode = self[rootrevs[0]].node()
+            for n in oldnodes:
+                self.addobsolete(newnode, n)
+
         def _turn_extinct_secret(self):
             """ensure all extinct changeset are secret"""
             self._clearobsoletecache()
             # this is mainly for safety purpose
             # both pull and push
-            expobs = [c.node() for c in repo.set('extinct() - secret()')]
+            query = '(obsolete() - obsolete()::(unstable() - secret())) - secret()'
+            expobs = [c.node() for c in repo.set(query)]
             phases.retractboundary(repo, 2, expobs)
 
         ### Disk IO
@@ -954,13 +1094,3 @@
             return c
 
     repo.__class__ = obsoletingrepo
-
-    if False:
-        expobs = [c.node() for c in repo.set('extinct() - secret()')]
-        if expobs: # do not lock in nothing move. locking for peanut make hgview reload on any command
-            lock = repo.lock()
-            try:
-                expobs = [c.node() for c in repo.set('extinct() - secret()')]
-                phases.retractboundary(repo, 2, expobs)
-            finally:
-                lock.release()
--- a/tests/test-amend.t	Wed Jun 20 18:04:50 2012 +0200
+++ b/tests/test-amend.t	Wed Jun 27 12:44:54 2012 +0200
@@ -26,7 +26,6 @@
   $ hg amend
   $ hg debugsuccessors
   07f494440405 a34b93d251e4
-  07f494440405 bd19cbe78fbf
   bd19cbe78fbf a34b93d251e4
   $ hg branch
   foo
@@ -65,7 +64,6 @@
   [255]
   $ hg debugsuccessors
   07f494440405 a34b93d251e4
-  07f494440405 bd19cbe78fbf
   bd19cbe78fbf a34b93d251e4
   $ hg phase 2
   2: draft
@@ -92,7 +90,6 @@
   [255]
   $ hg debugsuccessors
   07f494440405 a34b93d251e4
-  07f494440405 bd19cbe78fbf
   7384bbcba36f 000000000000
   bd19cbe78fbf a34b93d251e4
   $ glog
--- a/tests/test-evolve.t	Wed Jun 20 18:04:50 2012 +0200
+++ b/tests/test-evolve.t	Wed Jun 27 12:44:54 2012 +0200
@@ -23,6 +23,10 @@
   >    hg ci -m "add $1"
   > }
 
+  $ glog() {
+  >   hg glog --template '{rev}:{node|short}@{branch}({phase}) {desc|firstline}\n' "$@"
+  > }
+
 various init
 
   $ hg init local
@@ -52,7 +56,9 @@
 
 test simple kill
 
-  $ hg kill 5
+  $ hg id -n
+  5
+  $ hg kill .
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
   working directory now at fbb94e3a0ecf
   $ hg qlog
@@ -71,6 +77,18 @@
   2 - 4538525df7e2 add c (draft)
   1 - 7c3bad9141dc add b (public)
   0 - 1f0dee641bb7 add a (public)
+
+test kill with dirty changes
+
+  $ hg up 2
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo 4 > g
+  $ hg add g
+  $ hg kill .
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  working directory now at 7c3bad9141dc
+  $ hg st
+  A g
   $ cd ..
 
 ##########################
@@ -209,8 +227,27 @@
   4	feature-B: another feature - test
   1	: a nifty feature - test
   0	: base - test
-  $ hg up -q 1
-  Working directory parent is obsolete
+  $ hg up -q 0
+  $ glog --hidden
+  o  6:23409eba69a0@default(draft) a nifty feature
+  |
+  | o  5:e416e48b2742@default(secret) french looks better
+  | |
+  | | o  4:f8111a076f09@default(draft) another feature
+  | |/
+  | | o  3:524e478d4811@default(secret) fix spelling of Zwei
+  | | |
+  | | o  2:7b36850622b2@default(secret) another feature
+  | |/
+  | o  1:568a468b60fc@default(draft) a nifty feature
+  |/
+  @  0:e55e0562ee93@default(draft) base
+  
+  $ hg debugsuccessors
+  524e478d4811 f8111a076f09
+  568a468b60fc 23409eba69a0
+  7b36850622b2 f8111a076f09
+  e416e48b2742 23409eba69a0
   $ hg stabilize
   move:[4] another feature
   atop:[6] a nifty feature
@@ -234,6 +271,12 @@
   8	feature-B: another feature that rox - test
   6	feature-A: a nifty feature - test
   0	: base - test
+
+phase change turning obsolete changeset public issue a latecomer warning
+
+  $ hg phase --public 7
+  1 new latecomers changesets
+
   $ cd ..
 
 enable general delta
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-obsolete-push.t	Wed Jun 27 12:44:54 2012 +0200
@@ -0,0 +1,48 @@
+  $ cat >> $HGRCPATH <<EOF
+  > [defaults]
+  > amend=-d "0 0"
+  > [extensions]
+  > hgext.rebase=
+  > hgext.graphlog=
+  > EOF
+  $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH
+  $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+
+  $ template='{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n'
+  $ glog() {
+  >   hg glog --template "$template" "$@"
+  > }
+
+Test outgoing, common A is suspended, B unstable and C secret, remote
+has A and B, neither A or C should be in outgoing.
+
+  $ hg init source
+  $ cd source
+  $ echo a > a
+  $ hg ci -qAm A a
+  $ echo b > b
+  $ hg ci -qAm B b
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ echo c > c
+  $ hg ci -qAm C c
+  $ hg phase --secret --force .
+  $ hg kill 0 1
+  1 new unstables changesets
+  $ glog --hidden
+  @  2:244232c2222a@default(unstable/secret) C
+  |
+  | o  1:6c81ed0049f8@default(extinct/secret) B
+  |/
+  o  0:1994f17a630e@default(suspended/secret) A
+  
+  $ hg init ../clone
+  $ cat >  ../clone/.hg/hgrc <<EOF
+  > [phases]
+  > publish = false
+  > EOF
+  $ hg outgoing ../clone --template "$template"
+  comparing with ../clone
+  searching for changes
+  no changes found (ignored 2 secret changesets)
+  [1]
--- a/tests/test-obsolete-rebase.t	Wed Jun 20 18:04:50 2012 +0200
+++ b/tests/test-obsolete-rebase.t	Wed Jun 27 12:44:54 2012 +0200
@@ -30,11 +30,43 @@
   created new head
   $ echo e > e
   $ hg ci -Am adde e
-  $ hg rebase -d 1 -r . --detach --keep  
-  abort: rebase --keep option is unsupported with obsolete extension
-  (see 'hg help obsolete')
-  [255]
-  $ hg rebase -d 1 -r . --detach
+  $ hg rebase -d 1 -r 3 --detach --keep  
+  $ glog
+  @  4:9c5494949763@default(draft) adde
+  |
+  | o  3:98e4a024635e@default(draft) adde
+  | |
+  | o  2:102a90ea7b4a@default(draft) addb
+  | |
+  o |  1:540395c44225@default(draft) changea
+  |/
+  o  0:07f494440405@default(draft) adda
+  
+  $ glog --hidden
+  @  4:9c5494949763@default(draft) adde
+  |
+  | o  3:98e4a024635e@default(draft) adde
+  | |
+  | o  2:102a90ea7b4a@default(draft) addb
+  | |
+  o |  1:540395c44225@default(draft) changea
+  |/
+  o  0:07f494440405@default(draft) adda
+  
+  $ hg debugsuccessors
+  $ hg --config extensions.hgext.mq= strip tip
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9c5494949763-backup.hg
+  $ hg rebase -d 1 -r 3 --detach
+  $ glog
+  @  4:9c5494949763@default(draft) adde
+  |
+  | o  2:102a90ea7b4a@default(draft) addb
+  | |
+  o |  1:540395c44225@default(draft) changea
+  |/
+  o  0:07f494440405@default(draft) adda
+  
   $ glog --hidden
   @  4:9c5494949763@default(draft) adde
   |
--- a/tests/test-obsolete.t	Wed Jun 20 18:04:50 2012 +0200
+++ b/tests/test-obsolete.t	Wed Jun 27 12:44:54 2012 +0200
@@ -5,7 +5,7 @@
   > [phases]
   > publish=False
   > [alias]
-  > odiff=diff --rev 'limit(obsparents(.),1)' --rev .
+  > odiff=diff --rev 'limit(precursors(.),1)' --rev .
   > [extensions]
   > hgext.graphlog=
   > EOF
@@ -54,7 +54,7 @@
 
 Test that obsolete parent a properly computed
 
-  $ qlog -r 'obsparents(.)' --hidden
+  $ qlog -r 'precursors(.)' --hidden
   2
   - 4538525df7e2
   $ qlog -r .
@@ -72,6 +72,12 @@
   @@ -0,0 +1,1 @@
   +obsol_c
 
+Test that obsolete successors a properly computed
+
+  $ qlog -r 'successors(2)' --hidden
+  3
+  - 0d3f46688ccc
+
 test obsolete changeset with no-obsolete descendant
   $ hg up 1 -q
   $ mkcommit "obsol_c'" # 4 (on 1)
@@ -89,11 +95,16 @@
   - 4538525df7e2
   3
   - 0d3f46688ccc
-  $ qlog -r 'obsancestors(4)' --hidden
+  $ qlog -r 'allprecursors(4)' --hidden
   2
   - 4538525df7e2
   3
   - 0d3f46688ccc
+  $ qlog -r 'allsuccessors(2)' --hidden
+  3
+  - 0d3f46688ccc
+  4
+  - 725c380fe99b
   $ hg up 3 -q
   Working directory parent is obsolete
   $ mkcommit d # 5 (on 3)
@@ -111,6 +122,23 @@
   5
   - a7a6f2b5d8a5
 
+Test obsolete keyword
+
+  $ hg glog --template '{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' \
+  >   --hidden
+  @  5:a7a6f2b5d8a5@default(unstable/secret) add d
+  |
+  | o  4:725c380fe99b@default(stable/draft) add obsol_c'
+  | |
+  o |  3:0d3f46688ccc@default(suspended/secret) add obsol_c
+  |/
+  | o  2:4538525df7e2@default(extinct/secret) add c
+  |/
+  o  1:7c3bad9141dc@default(stable/draft) add b
+  |
+  o  0:1f0dee641bb7@default(stable/public) add a
+  
+
 Test communication of obsolete relation with a compatible client
 
   $ hg init ../other-new
@@ -454,3 +482,99 @@
   adding file changes
   added 1 changesets with 1 changes to 1 files (+1 heads)
   $ cd ..
+
+check latecomer detection
+(make an obsolete changeset public)
+
+  $ cd local
+  $ hg phase --public 11
+  $ hg --config extensions.graphlog=glog glog --template='{rev} - ({phase}) {node|short} {desc}\n'
+  @  12 - (draft) 6db5e282cb91 add obsol_d'''
+  |
+  | o  11 - (public) 9468a5f5d8b2 add obsol_d''
+  |/
+  o  10 - (public) 2033b4e49474 add obsol_c
+  |
+  o  4 - (public) 725c380fe99b add obsol_c'
+  |
+  o  1 - (public) 7c3bad9141dc add b
+  |
+  o  0 - (public) 1f0dee641bb7 add a
+  
+  $ hg log -r 'latecomer()'
+  changeset:   12:6db5e282cb91
+  tag:         tip
+  parent:      10:2033b4e49474
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     add obsol_d'''
+  
+  $ hg push ../other-new/
+  pushing to ../other-new/
+  searching for changes
+  abort: Trying to push latecomer changeset: 6db5e282cb91!
+  (use 'hg stabilize' to get a stable history (or --force to proceed))
+  [255]
+
+Check hg commit --amend compat
+
+  $ hg up 'desc(obsol_c)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit f
+  created new head
+  $ echo 42 >> f
+  $ hg commit --amend --traceback
+  saved backup bundle to $TESTTMP/local/.hg/strip-backup/0b1b6dd009c0-amend-backup.hg
+  $ hg glog
+  @  changeset:   13:3734a65252e6
+  |  tag:         tip
+  |  parent:      10:2033b4e49474
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     add f
+  |
+  | o  changeset:   12:6db5e282cb91
+  |/   parent:      10:2033b4e49474
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     add obsol_d'''
+  |
+  | o  changeset:   11:9468a5f5d8b2
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     add obsol_d''
+  |
+  o  changeset:   10:2033b4e49474
+  |  parent:      4:725c380fe99b
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     add obsol_c
+  |
+  o  changeset:   4:725c380fe99b
+  |  parent:      1:7c3bad9141dc
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     add obsol_c'
+  |
+  o  changeset:   1:7c3bad9141dc
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     add b
+  |
+  o  changeset:   0:1f0dee641bb7
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     add a
+  
+  $ hg debugsuccessors
+  0b1b6dd009c0 3734a65252e6
+  0d3f46688ccc 2033b4e49474
+  0d3f46688ccc 725c380fe99b
+  159dfc9fa5d3 9468a5f5d8b2
+  1f0dee641bb7 83b5778897ad
+  4538525df7e2 0d3f46688ccc
+  83b5778897ad 000000000000
+  909a0fb57e5d 159dfc9fa5d3
+  9468a5f5d8b2 6db5e282cb91
+  95de7fc6918d 909a0fb57e5d
+  a7a6f2b5d8a5 95de7fc6918d
--- a/tests/test-stabilize-order.t	Wed Jun 20 18:04:50 2012 +0200
+++ b/tests/test-stabilize-order.t	Wed Jun 27 12:44:54 2012 +0200
@@ -106,9 +106,9 @@
    3a4a591493f8 f5ff10856e5a
    3ca0ded0dc50 ab8cbb6d87ff
   +7a7552255fb5 5e819fbb0d27
-   93418d2c0979 3a4a591493f8
    93418d2c0979 f5ff10856e5a
    ab8cbb6d87ff 6bf44048e43f
+   ef23d6ef94d6 ab8cbb6d87ff
   [1]
   $ glog
   @  9:5e819fbb0d27@default(draft) addc
--- a/tests/test-stabilize-result.t	Wed Jun 20 18:04:50 2012 +0200
+++ b/tests/test-stabilize-result.t	Wed Jun 27 12:44:54 2012 +0200
@@ -47,6 +47,5 @@
   
   $ hg debugsuccessors
   102a90ea7b4a 1447e1c4828d
-  102a90ea7b4a 41ad4fe8c795
   41ad4fe8c795 1447e1c4828d
   cce2c55b8965 000000000000
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-uncommit.t	Wed Jun 27 12:44:54 2012 +0200
@@ -0,0 +1,334 @@
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > hgext.rebase=
+  > hgext.graphlog=
+  > EOF
+  $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH
+  $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+
+  $ glog() {
+  >   hg glog --template '{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' "$@"
+  > }
+
+  $ hg init repo
+  $ cd repo
+
+Cannot uncommit null changeset
+
+  $ hg uncommit
+  abort: cannot rewrite immutable changeset
+  [255]
+
+Cannot uncommit public changeset
+
+  $ echo a > a
+  $ hg ci -Am adda a
+  $ hg phase --public .
+  $ hg uncommit
+  abort: cannot rewrite immutable changeset
+  [255]
+  $ hg phase --force --draft .
+
+Cannot uncommit merge
+
+  $ hg up -q null
+  $ echo b > b
+  $ echo c > c
+  $ echo d > d
+  $ echo f > f
+  $ echo g > g
+  $ echo j > j
+  $ echo m > m
+  $ echo n > n
+  $ echo o > o
+  $ hg ci -Am addmore
+  adding b
+  adding c
+  adding d
+  adding f
+  adding g
+  adding j
+  adding m
+  adding n
+  adding o
+  created new head
+  $ hg merge
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg uncommit
+  abort: cannot uncommit while merging
+  [255]
+  $ hg ci -m merge
+  $ hg uncommit
+  abort: cannot uncommit merge changeset
+  [255]
+
+Prepare complicated changeset
+
+  $ hg branch bar
+  marked working directory as branch bar
+  (branches are permanent and global, did you want a bookmark?)
+  $ hg cp a aa
+  $ echo b >> b
+  $ hg rm c
+  $ echo d >> d
+  $ echo e > e
+  $ hg mv f ff
+  $ hg mv g h
+  $ echo j >> j
+  $ echo k > k
+  $ echo l > l
+  $ hg rm m
+  $ hg rm n
+  $ echo o >> o
+  $ hg ci -Am touncommit
+  adding e
+  adding k
+  adding l
+  $ hg st --copies --change .
+  M b
+  M d
+  M j
+  M o
+  A aa
+    a
+  A e
+  A ff
+    f
+  A h
+    g
+  A k
+  A l
+  R c
+  R f
+  R g
+  R m
+  R n
+  $ hg man -r .
+  a
+  aa
+  b
+  d
+  e
+  ff
+  h
+  j
+  k
+  l
+  o
+
+Add a couple of bookmarks
+
+  $ glog --hidden
+  @  3:5eb72dbe0cb4@bar(stable/draft) touncommit
+  |
+  o    2:f63b90038565@default(stable/draft) merge
+  |\
+  | o  1:f15c744d48e8@default(stable/draft) addmore
+  |
+  o  0:07f494440405@default(stable/draft) adda
+  
+  $ hg bookmark -r 2 unrelated
+  $ hg bookmark touncommit-bm
+  $ hg bookmark --inactive touncommit-bm-inactive
+  $ hg bookmarks
+   * touncommit-bm             3:5eb72dbe0cb4
+     touncommit-bm-inactive    3:5eb72dbe0cb4
+     unrelated                 2:f63b90038565
+
+Prepare complicated working directory
+
+  $ hg branch foo
+  marked working directory as branch foo
+  (branches are permanent and global, did you want a bookmark?)
+  $ hg mv ff f
+  $ hg mv h i
+  $ hg rm j
+  $ hg rm k
+  $ echo l >> l
+  $ echo m > m
+  $ echo o > o
+
+Test uncommit without argument, should be a no-op
+
+  $ hg uncommit
+  abort: nothing to uncommit
+  [255]
+  $ hg bookmarks
+   * touncommit-bm             3:5eb72dbe0cb4
+     touncommit-bm-inactive    3:5eb72dbe0cb4
+     unrelated                 2:f63b90038565
+
+Test no matches
+
+  $ hg uncommit --include nothere
+  abort: nothing to uncommit
+  [255]
+
+Enjoy uncommit
+
+  $ hg uncommit aa b c f ff g h j k l m o
+  $ hg branch
+  foo
+  $ hg st --copies
+  M b
+  A aa
+    a
+  A i
+    g
+  A l
+  R c
+  R g
+  R j
+  R m
+  $ cat aa
+  a
+  $ cat b
+  b
+  b
+  $ cat l
+  l
+  l
+  $ cat m
+  m
+  $ test -f c && echo 'error: c was removed!'
+  [1]
+  $ test -f j && echo 'error: j was removed!'
+  [1]
+  $ test -f k && echo 'error: k was removed!'
+  [1]
+  $ hg st --copies --change .
+  M d
+  A e
+  R n
+  $ hg man -r .
+  a
+  b
+  c
+  d
+  e
+  f
+  g
+  j
+  m
+  o
+  $ hg cat -r . d
+  d
+  d
+  $ hg cat -r . e
+  e
+  $ glog --hidden
+  @  4:e8db4aa611f6@bar(stable/draft) touncommit
+  |
+  | o  3:5eb72dbe0cb4@bar(extinct/secret) touncommit
+  |/
+  o    2:f63b90038565@default(stable/draft) merge
+  |\
+  | o  1:f15c744d48e8@default(stable/draft) addmore
+  |
+  o  0:07f494440405@default(stable/draft) adda
+  
+  $ hg bookmarks
+   * touncommit-bm             4:e8db4aa611f6
+     touncommit-bm-inactive    4:e8db4aa611f6
+     unrelated                 2:f63b90038565
+  $ hg debugsuccessors
+  5eb72dbe0cb4 e8db4aa611f6
+
+Test phase is preserved, no local changes
+
+  $ hg up -C 3
+  8 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  Working directory parent is obsolete
+  $ hg --config extensions.purge= purge
+  $ hg uncommit -I 'set:added() and e'
+  $ hg st --copies
+  A e
+  $ hg st --copies --change .
+  M b
+  M d
+  M j
+  M o
+  A aa
+  A ff
+    f
+  A h
+    g
+  A k
+  A l
+  R c
+  R f
+  R g
+  R m
+  R n
+  $ glog --hidden
+  @  5:c706fe2c12f8@bar(stable/secret) touncommit
+  |
+  | o  4:e8db4aa611f6@bar(stable/draft) touncommit
+  |/
+  | o  3:5eb72dbe0cb4@bar(extinct/secret) touncommit
+  |/
+  o    2:f63b90038565@default(stable/draft) merge
+  |\
+  | o  1:f15c744d48e8@default(stable/draft) addmore
+  |
+  o  0:07f494440405@default(stable/draft) adda
+  
+  $ hg debugsuccessors
+  5eb72dbe0cb4 c706fe2c12f8
+  5eb72dbe0cb4 e8db4aa611f6
+
+Test --all
+
+  $ hg up -C 3
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  Working directory parent is obsolete
+  $ hg --config extensions.purge= purge
+  $ hg uncommit --all -X e
+  $ hg st --copies
+  M b
+  M d
+  M j
+  M o
+  A aa
+    a
+  A ff
+    f
+  A h
+    g
+  A k
+  A l
+  R c
+  R f
+  R g
+  R m
+  R n
+  $ hg st --copies --change .
+  A e
+
+  $ hg debugsuccessors
+  5eb72dbe0cb4 c4cbebac3751
+  5eb72dbe0cb4 c706fe2c12f8
+  5eb72dbe0cb4 e8db4aa611f6
+
+Display a warning if nothing left
+
+  $ hg uncommit e
+  new changeset is empty
+  (use "hg kill ." to remove it)
+  $ hg debugsuccessors
+  5eb72dbe0cb4 c4cbebac3751
+  5eb72dbe0cb4 c706fe2c12f8
+  5eb72dbe0cb4 e8db4aa611f6
+  c4cbebac3751 4f1c269eab68
+
+Test instability warning
+
+  $ hg ci -m touncommit
+  $ echo unrelated > unrelated
+  $ hg ci -Am addunrelated unrelated
+  $ hg gdown
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  [8] touncommit
+  $ hg uncommit aa
+  1 new unstables changesets