Patchwork [1,of,2,standalone-strip] mq: extract strip as a standalone extension (issue3824)

login
register
mail settings
Submitter Pierre-Yves David
Date Sept. 25, 2013, 9:53 p.m.
Message ID <f2d6590c1873f5d9fbd9.1380146021@vulgaris>
Download mbox | patch
Permalink /patch/2640/
State Superseded
Headers show

Comments

Pierre-Yves David - Sept. 25, 2013, 9:53 p.m.
# HG changeset patch
# User Pierre-Yves David <pierre-yves.david@ens-lyon.org>
# Date 1380144102 -7200
#      Wed Sep 25 23:21:42 2013 +0200
# Node ID f2d6590c1873f5d9fbd903a2ea467a19de4d4c13
# Parent  72982741c525e1d0b06096bc7eafc780a4f2274f
mq: extract strip as a standalone extension (issue3824)

Strip now lives in its own extension. The extension is surprisingly called `strip`.
The `mq` extension force the use of the strip extension when its enabled. This
is both necessary for backward compatibility (people expect `mq` to comes with strip) and
become some utility function used by `mq` are now in the strip extension.
Martin Geisler - Sept. 26, 2013, 9:48 p.m.
pierre-yves.david@ens-lyon.org writes:

> # HG changeset patch
> # User Pierre-Yves David <pierre-yves.david@ens-lyon.org>
> # Date 1380144102 -7200
> #      Wed Sep 25 23:21:42 2013 +0200
> # Node ID f2d6590c1873f5d9fbd903a2ea467a19de4d4c13
> # Parent  72982741c525e1d0b06096bc7eafc780a4f2274f
> mq: extract strip as a standalone extension (issue3824)

This is a great idea! Thanks for making my life easier when I explain
new users how to remove changesets.

> Strip now lives in its own extension. The extension is surprisingly called `strip`.
> The `mq` extension force the use of the strip extension when its enabled. This
> is both necessary for backward compatibility (people expect `mq` to comes with strip) and
> become some utility function used by `mq` are now in the strip extension.

The commit message above is wrapped inconsistently -- maybe by hand?

> diff --git a/hgext/mq.py b/hgext/mq.py
> --- a/hgext/mq.py
> +++ b/hgext/mq.py
> @@ -55,17 +55,20 @@ discarded. Setting::
>    keepchanges = True
>  
>  make them behave as if --keep-changes were passed, and non-conflicting
>  local changes will be tolerated and preserved. If incompatible options
>  such as -f/--force or --exact are passed, this setting is ignored.
> +
> +This extension used to provide a strip commands. This command now lives

Should be "a strip command", without the "s".

[...]

> +# force load strip extension formely included in mq and import some utility
> +try:
> +    stripext = extensions.find('strip')
> +except KeyError:
> +    # note: load is lazy so we could avoid the try-except.
> +    # but I (marmoute) prefers this explicite code.
> +    class dummyui(object):
> +        def debug(self, msg):
> +            pass
> +    stripext = extensions.load(dummyui(), 'strip', '')
> +
> +strip = stripext.strip
> +checksubstate = stripext.checksubstate
> +checklocalchanges = stripext.checklocalchanges

I guess you don't simply do 'from hgext import strip as stripext' since
you want to avoid loading the extension twice in case the user already
enabled 'strip'?

If it isn't a big problem to load it twice, then I would just import it.
Those who already use mq will probably not enable strip too, so they
will only import the module once. New users will hopefully only enable
the strip extension, so they also only get it once.

> diff --git a/hgext/strip.py b/hgext/strip.py
> new file mode 100644
> --- /dev/null
> +++ b/hgext/strip.py
> @@ -0,0 +1,217 @@
> +"""This extension contains the strip commands.

Command should be singular here again.
Pierre-Yves David - Sept. 26, 2013, 10:03 p.m.
On 09/26/2013 11:48 PM, Martin Geisler wrote:
> pierre-yves.david@ens-lyon.org writes:
>
>> # HG changeset patch
>> # User Pierre-Yves David<pierre-yves.david@ens-lyon.org>
>> # Date 1380144102 -7200
>> #      Wed Sep 25 23:21:42 2013 +0200
>> # Node ID f2d6590c1873f5d9fbd903a2ea467a19de4d4c13
>> # Parent  72982741c525e1d0b06096bc7eafc780a4f2274f
>> mq: extract strip as a standalone extension (issue3824)
> This is a great idea! Thanks for making my life easier when I explain
> new users how to remove changesets.
>
>> Strip now lives in its own extension. The extension is surprisingly called `strip`.
>> The `mq` extension force the use of the strip extension when its enabled. This
>> is both necessary for backward compatibility (people expect `mq` to comes with strip) and
>> become some utility function used by `mq` are now in the strip extension.
> The commit message above is wrapped inconsistently -- maybe by hand?
>
>> diff --git a/hgext/mq.py b/hgext/mq.py
>> --- a/hgext/mq.py
>> +++ b/hgext/mq.py
>> @@ -55,17 +55,20 @@ discarded. Setting::
>>     keepchanges = True
>>
>>   make them behave as if --keep-changes were passed, and non-conflicting
>>   local changes will be tolerated and preserved. If incompatible options
>>   such as -f/--force or --exact are passed, this setting is ignored.
>> +
>> +This extension used to provide a strip commands. This command now lives
> Should be "a strip command", without the "s".
>
> [...]
>
>> +# force load strip extension formely included in mq and import some utility
>> +try:
>> +    stripext = extensions.find('strip')
>> +except KeyError:
>> +    # note: load is lazy so we could avoid the try-except.
>> +    # but I (marmoute) prefers this explicite code.
>> +    class dummyui(object):
>> +        def debug(self, msg):
>> +            pass
>> +    stripext = extensions.load(dummyui(), 'strip', '')
>> +
>> +strip = stripext.strip
>> +checksubstate = stripext.checksubstate
>> +checklocalchanges = stripext.checklocalchanges
> I guess you don't simply do 'from hgext import strip as stripext'

I prefer to use the Mercurial code dedicated to load extension when I 
have to load extension. In particular because I actually NEED to have to 
properly loaded by mercurial so the strip command is registred and 
extension callback are called.

>   since you want to avoid loading the extension twice in case the user already
> enabled 'strip'?
Mercurial already have its own way to prevent that (because extension 
can be enable in multiple hgrc file and you do not want them to be 
loaded multiple time in that case)
So there is no issue in that direction.
Martin Geisler - Sept. 26, 2013, 10:50 p.m.
Pierre-Yves David <pierre-yves.david@ens-lyon.org> writes:

> On 09/26/2013 11:48 PM, Martin Geisler wrote:
>> pierre-yves.david@ens-lyon.org writes:
>>
>>> +# force load strip extension formely included in mq and import some utility
>>> +try:
>>> +    stripext = extensions.find('strip')
>>> +except KeyError:
>>> +    # note: load is lazy so we could avoid the try-except.
>>> +    # but I (marmoute) prefers this explicite code.
>>> +    class dummyui(object):
>>> +        def debug(self, msg):
>>> +            pass
>>> +    stripext = extensions.load(dummyui(), 'strip', '')
>>> +
>>> +strip = stripext.strip
>>> +checksubstate = stripext.checksubstate
>>> +checklocalchanges = stripext.checklocalchanges
>> I guess you don't simply do 'from hgext import strip as stripext'
>
> I prefer to use the Mercurial code dedicated to load extension when I
> have to load extension. In particular because I actually NEED to have
> to properly loaded by mercurial so the strip command is registred and
> extension callback are called.

Good point. As far as I can see, though, uisetup and extsetup wont be
called when you use extensions.load -- it is extensions.loadall that
calls those functions.

If that is true, then perhaps you can use extensions.loadall still: it
loads the extensions based on the configuration, so you could set
ui.config('extensions', 'strip') and then call loadall. It should only
load and initialize the new extensions, i.e., strip.

>>   since you want to avoid loading the extension twice in case the
>> user already enabled 'strip'?
> Mercurial already have its own way to prevent that (because extension
> can be enable in multiple hgrc file and you do not want them to be
> loaded multiple time in that case) So there is no issue in that
> direction.

I think you misunderstood me. I was talking about the hgext.strip module
being loaded twice: once when you do

  from hgext import strip

and once when you run

  extensions.load(ui, 'strip', None)

I had the impression that extensions.load would mangle the module name
so that the module would end up twice in sys.modules. That was wrong,
the module name is only changed if you supply a path. So calling

  extensions.load(ui, 'strip', None)

should give the same result as importing it the normal way, except that
extensions.load will also store a reference in it's _extensions dict.
Pierre-Yves David - Sept. 26, 2013, 10:56 p.m.
On 09/27/2013 12:50 AM, Martin Geisler wrote:
> Pierre-Yves David<pierre-yves.david@ens-lyon.org>  writes:
>
>> On 09/26/2013 11:48 PM, Martin Geisler wrote:
>>> pierre-yves.david@ens-lyon.org writes:
>>>
>>>> +# force load strip extension formely included in mq and import some utility
>>>> +try:
>>>> +    stripext = extensions.find('strip')
>>>> +except KeyError:
>>>> +    # note: load is lazy so we could avoid the try-except.
>>>> +    # but I (marmoute) prefers this explicite code.
>>>> +    class dummyui(object):
>>>> +        def debug(self, msg):
>>>> +            pass
>>>> +    stripext = extensions.load(dummyui(), 'strip', '')
>>>> +
>>>> +strip = stripext.strip
>>>> +checksubstate = stripext.checksubstate
>>>> +checklocalchanges = stripext.checklocalchanges
>>> I guess you don't simply do 'from hgext import strip as stripext'
>> I prefer to use the Mercurial code dedicated to load extension when I
>> have to load extension. In particular because I actually NEED to have
>> to properly loaded by mercurial so the strip command is registred and
>> extension callback are called.
> Good point. As far as I can see, though, uisetup and extsetup wont be
> called when you use extensions.load -- it is extensions.loadall that
> calls those functions.

No, extensions load insert it in the _order list so that loadall call 
those function.

loadall is run
    load(mq)
      load(strip)
    Extension hook run for everything.


> If that is true, then perhaps you can use extensions.loadall still: it
> loads the extensions based on the configuration, so you could set
> ui.config('extensions', 'strip') and then call loadall. It should only
> load and initialize the new extensions, i.e., strip.

No, I will not run loadall in the middle of loadall. I'm certain the 
code breaks if I do that.

>
>>>    since you want to avoid loading the extension twice in case the
>>> user already enabled 'strip'?
>> Mercurial already have its own way to prevent that (because extension
>> can be enable in multiple hgrc file and you do not want them to be
>> loaded multiple time in that case) So there is no issue in that
>> direction.
> I think you misunderstood me. I was talking about the hgext.strip module
> being loaded twice: once when you do
>
>    from hgext import strip
>
> and once when you run
>
>    extensions.load(ui, 'strip', None)

The nice part is that you do not run `from hgext import strip` the same 
as you do not run `from hgext import random-extension` because that's 
not the way they meant to be used.

> I had the impression that extensions.load would mangle the module name
> so that the module would end up twice in sys.modules. That was wrong,
> the module name is only changed if you supply a path. So calling
>
>    extensions.load(ui, 'strip', None)
>
> should give the same result as importing it the normal way, except that
> extensions.load will also store a reference in it's _extensions dict.

We are discussing the generic implementation of extensions here. There 
is nothing wrong down there and that's far out of scope.

Patch

diff --git a/hgext/mq.py b/hgext/mq.py
--- a/hgext/mq.py
+++ b/hgext/mq.py
@@ -55,17 +55,20 @@  discarded. Setting::
   keepchanges = True
 
 make them behave as if --keep-changes were passed, and non-conflicting
 local changes will be tolerated and preserved. If incompatible options
 such as -f/--force or --exact are passed, this setting is ignored.
+
+This extension used to provide a strip commands. This command now lives
+in the strip extension.
 '''
 
 from mercurial.i18n import _
 from mercurial.node import bin, hex, short, nullid, nullrev
 from mercurial.lock import release
 from mercurial import commands, cmdutil, hg, scmutil, util, revset
-from mercurial import repair, extensions, error, phases, bookmarks
+from mercurial import extensions, error, phases
 from mercurial import patch as patchmod
 from mercurial import localrepo
 from mercurial import subrepo
 import os, re, errno, shutil
 
@@ -75,10 +78,26 @@  seriesopts = [('s', 'summary', None, _('
 
 cmdtable = {}
 command = cmdutil.command(cmdtable)
 testedwith = 'internal'
 
+# force load strip extension formely included in mq and import some utility
+try:
+    stripext = extensions.find('strip')
+except KeyError:
+    # note: load is lazy so we could avoid the try-except.
+    # but I (marmoute) prefers this explicite code.
+    class dummyui(object):
+        def debug(self, msg):
+            pass
+    stripext = extensions.load(dummyui(), 'strip', '')
+
+strip = stripext.strip
+checksubstate = stripext.checksubstate
+checklocalchanges = stripext.checklocalchanges
+
+
 # Patch names looks like unix-file names.
 # They must be joinable with queue directory and result in the patch path.
 normname = util.normpath
 
 class statusentry(object):
@@ -2894,212 +2913,10 @@  def save(ui, repo, **opts):
         del q.applied[:]
         q.applieddirty = True
         q.savedirty()
     return 0
 
-def checksubstate(repo, baserev=None):
-    '''return list of subrepos at a different revision than substate.
-    Abort if any subrepos have uncommitted changes.'''
-    inclsubs = []
-    wctx = repo[None]
-    if baserev:
-        bctx = repo[baserev]
-    else:
-        bctx = wctx.parents()[0]
-    for s in sorted(wctx.substate):
-        if wctx.sub(s).dirty(True):
-            raise util.Abort(
-                _("uncommitted changes in subrepository %s") % s)
-        elif s not in bctx.substate or bctx.sub(s).dirty():
-            inclsubs.append(s)
-    return inclsubs
-
-def checklocalchanges(repo, force=False, excsuffix=''):
-    cmdutil.checkunfinished(repo)
-    m, a, r, d = repo.status()[:4]
-    if not force:
-        if (m or a or r or d):
-            _("local changes found") # i18n tool detection
-            raise util.Abort(_("local changes found" + excsuffix))
-        if checksubstate(repo):
-            _("local changed subrepos found") # i18n tool detection
-            raise util.Abort(_("local changed subrepos found" + excsuffix))
-    return m, a, r, d
-
-def strip(ui, repo, revs, update=True, backup="all", force=None):
-    wlock = lock = None
-    try:
-        wlock = repo.wlock()
-        lock = repo.lock()
-
-        if update:
-            checklocalchanges(repo, force=force)
-            urev, p2 = repo.changelog.parents(revs[0])
-            if p2 != nullid and p2 in [x.node for x in repo.mq.applied]:
-                urev = p2
-            hg.clean(repo, urev)
-            repo.dirstate.write()
-
-        repair.strip(ui, repo, revs, backup)
-    finally:
-        release(lock, wlock)
-
-
-@command("strip",
-         [
-          ('r', 'rev', [], _('strip specified revision (optional, '
-                               'can specify revisions without this '
-                               'option)'), _('REV')),
-          ('f', 'force', None, _('force removal of changesets, discard '
-                                 'uncommitted changes (no backup)')),
-          ('b', 'backup', None, _('bundle only changesets with local revision'
-                                  ' number greater than REV which are not'
-                                  ' descendants of REV (DEPRECATED)')),
-          ('', 'no-backup', None, _('no backups')),
-          ('', 'nobackup', None, _('no backups (DEPRECATED)')),
-          ('n', '', None, _('ignored  (DEPRECATED)')),
-          ('k', 'keep', None, _("do not modify working copy during strip")),
-          ('B', 'bookmark', '', _("remove revs only reachable from given"
-                                  " bookmark"))],
-          _('hg strip [-k] [-f] [-n] [-B bookmark] [-r] REV...'))
-def stripcmd(ui, repo, *revs, **opts):
-    """strip changesets and all their descendants from the repository
-
-    The strip command removes the specified changesets and all their
-    descendants. If the working directory has uncommitted changes, the
-    operation is aborted unless the --force flag is supplied, in which
-    case changes will be discarded.
-
-    If a parent of the working directory is stripped, then the working
-    directory will automatically be updated to the most recent
-    available ancestor of the stripped parent after the operation
-    completes.
-
-    Any stripped changesets are stored in ``.hg/strip-backup`` as a
-    bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can
-    be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`,
-    where BUNDLE is the bundle file created by the strip. Note that
-    the local revision numbers will in general be different after the
-    restore.
-
-    Use the --no-backup option to discard the backup bundle once the
-    operation completes.
-
-    Strip is not a history-rewriting operation and can be used on
-    changesets in the public phase. But if the stripped changesets have
-    been pushed to a remote repository you will likely pull them again.
-
-    Return 0 on success.
-    """
-    backup = 'all'
-    if opts.get('backup'):
-        backup = 'strip'
-    elif opts.get('no_backup') or opts.get('nobackup'):
-        backup = 'none'
-
-    cl = repo.changelog
-    revs = list(revs) + opts.get('rev')
-    revs = set(scmutil.revrange(repo, revs))
-
-    if opts.get('bookmark'):
-        mark = opts.get('bookmark')
-        marks = repo._bookmarks
-        if mark not in marks:
-            raise util.Abort(_("bookmark '%s' not found") % mark)
-
-        # If the requested bookmark is not the only one pointing to a
-        # a revision we have to only delete the bookmark and not strip
-        # anything. revsets cannot detect that case.
-        uniquebm = True
-        for m, n in marks.iteritems():
-            if m != mark and n == repo[mark].node():
-                uniquebm = False
-                break
-        if uniquebm:
-            rsrevs = repo.revs("ancestors(bookmark(%s)) - "
-                               "ancestors(head() and not bookmark(%s)) - "
-                               "ancestors(bookmark() and not bookmark(%s))",
-                               mark, mark, mark)
-            revs.update(set(rsrevs))
-        if not revs:
-            del marks[mark]
-            marks.write()
-            ui.write(_("bookmark '%s' deleted\n") % mark)
-
-    if not revs:
-        raise util.Abort(_('empty revision set'))
-
-    descendants = set(cl.descendants(revs))
-    strippedrevs = revs.union(descendants)
-    roots = revs.difference(descendants)
-
-    update = False
-    # if one of the wdir parent is stripped we'll need
-    # to update away to an earlier revision
-    for p in repo.dirstate.parents():
-        if p != nullid and cl.rev(p) in strippedrevs:
-            update = True
-            break
-
-    rootnodes = set(cl.node(r) for r in roots)
-
-    q = getattr(repo, 'mq', None)
-    if q is not None and q.applied:
-        # refresh queue state if we're about to strip
-        # applied patches
-        if cl.rev(repo.lookup('qtip')) in strippedrevs:
-            q.applieddirty = True
-            start = 0
-            end = len(q.applied)
-            for i, statusentry in enumerate(q.applied):
-                if statusentry.node in rootnodes:
-                    # if one of the stripped roots is an applied
-                    # patch, only part of the queue is stripped
-                    start = i
-                    break
-            del q.applied[start:end]
-            q.savedirty()
-
-    revs = sorted(rootnodes)
-    if update and opts.get('keep'):
-        wlock = repo.wlock()
-        try:
-            urev, p2 = repo.changelog.parents(revs[0])
-            if (util.safehasattr(repo, 'mq') and p2 != nullid
-                and p2 in [x.node for x in repo.mq.applied]):
-                urev = p2
-            uctx = repo[urev]
-
-            # only reset the dirstate for files that would actually change
-            # between the working context and uctx
-            descendantrevs = repo.revs("%s::." % uctx.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(urev, uctx.manifest(), changedfiles)
-            repo.dirstate.write()
-            update = False
-        finally:
-            wlock.release()
-
-    if opts.get('bookmark'):
-        if mark == repo._bookmarkcurrent:
-            bookmarks.setcurrent(repo, None)
-        del marks[mark]
-        marks.write()
-        ui.write(_("bookmark '%s' deleted\n") % mark)
-
-    strip(ui, repo, revs, backup=backup, update=update, force=opts.get('force'))
-
-    return 0
 
 @command("qselect",
          [('n', 'none', None, _('disable all guards')),
           ('s', 'series', None, _('list all guards in series file')),
           ('', 'pop', None, _('pop to before first guarded applied patch')),
diff --git a/hgext/strip.py b/hgext/strip.py
new file mode 100644
--- /dev/null
+++ b/hgext/strip.py
@@ -0,0 +1,217 @@ 
+"""This extension contains the strip commands.
+
+This extensions allows to strip changesets and all their descendants from the
+repository. See the command help for details.
+"""
+from mercurial.i18n import _
+from mercurial.node import nullid
+from mercurial.lock import release
+from mercurial import cmdutil, hg, scmutil, util
+from mercurial import repair, bookmarks
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+testedwith = 'internal'
+
+def checksubstate(repo, baserev=None):
+    '''return list of subrepos at a different revision than substate.
+    Abort if any subrepos have uncommitted changes.'''
+    inclsubs = []
+    wctx = repo[None]
+    if baserev:
+        bctx = repo[baserev]
+    else:
+        bctx = wctx.parents()[0]
+    for s in sorted(wctx.substate):
+        if wctx.sub(s).dirty(True):
+            raise util.Abort(
+                _("uncommitted changes in subrepository %s") % s)
+        elif s not in bctx.substate or bctx.sub(s).dirty():
+            inclsubs.append(s)
+    return inclsubs
+
+def checklocalchanges(repo, force=False, excsuffix=''):
+    cmdutil.checkunfinished(repo)
+    m, a, r, d = repo.status()[:4]
+    if not force:
+        if (m or a or r or d):
+            _("local changes found") # i18n tool detection
+            raise util.Abort(_("local changes found" + excsuffix))
+        if checksubstate(repo):
+            _("local changed subrepos found") # i18n tool detection
+            raise util.Abort(_("local changed subrepos found" + excsuffix))
+    return m, a, r, d
+
+def strip(ui, repo, revs, update=True, backup="all", force=None):
+    wlock = lock = None
+    try:
+        wlock = repo.wlock()
+        lock = repo.lock()
+
+        if update:
+            checklocalchanges(repo, force=force)
+            urev, p2 = repo.changelog.parents(revs[0])
+            if p2 != nullid and p2 in [x.node for x in repo.mq.applied]:
+                urev = p2
+            hg.clean(repo, urev)
+            repo.dirstate.write()
+
+        repair.strip(ui, repo, revs, backup)
+    finally:
+        release(lock, wlock)
+
+
+@command("strip",
+         [
+          ('r', 'rev', [], _('strip specified revision (optional, '
+                               'can specify revisions without this '
+                               'option)'), _('REV')),
+          ('f', 'force', None, _('force removal of changesets, discard '
+                                 'uncommitted changes (no backup)')),
+          ('b', 'backup', None, _('bundle only changesets with local revision'
+                                  ' number greater than REV which are not'
+                                  ' descendants of REV (DEPRECATED)')),
+          ('', 'no-backup', None, _('no backups')),
+          ('', 'nobackup', None, _('no backups (DEPRECATED)')),
+          ('n', '', None, _('ignored  (DEPRECATED)')),
+          ('k', 'keep', None, _("do not modify working copy during strip")),
+          ('B', 'bookmark', '', _("remove revs only reachable from given"
+                                  " bookmark"))],
+          _('hg strip [-k] [-f] [-n] [-B bookmark] [-r] REV...'))
+def stripcmd(ui, repo, *revs, **opts):
+    """strip changesets and all their descendants from the repository
+
+    The strip command removes the specified changesets and all their
+    descendants. If the working directory has uncommitted changes, the
+    operation is aborted unless the --force flag is supplied, in which
+    case changes will be discarded.
+
+    If a parent of the working directory is stripped, then the working
+    directory will automatically be updated to the most recent
+    available ancestor of the stripped parent after the operation
+    completes.
+
+    Any stripped changesets are stored in ``.hg/strip-backup`` as a
+    bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can
+    be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`,
+    where BUNDLE is the bundle file created by the strip. Note that
+    the local revision numbers will in general be different after the
+    restore.
+
+    Use the --no-backup option to discard the backup bundle once the
+    operation completes.
+
+    Strip is not a history-rewriting operation and can be used on
+    changesets in the public phase. But if the stripped changesets have
+    been pushed to a remote repository you will likely pull them again.
+
+    Return 0 on success.
+    """
+    backup = 'all'
+    if opts.get('backup'):
+        backup = 'strip'
+    elif opts.get('no_backup') or opts.get('nobackup'):
+        backup = 'none'
+
+    cl = repo.changelog
+    revs = list(revs) + opts.get('rev')
+    revs = set(scmutil.revrange(repo, revs))
+
+    if opts.get('bookmark'):
+        mark = opts.get('bookmark')
+        marks = repo._bookmarks
+        if mark not in marks:
+            raise util.Abort(_("bookmark '%s' not found") % mark)
+
+        # If the requested bookmark is not the only one pointing to a
+        # a revision we have to only delete the bookmark and not strip
+        # anything. revsets cannot detect that case.
+        uniquebm = True
+        for m, n in marks.iteritems():
+            if m != mark and n == repo[mark].node():
+                uniquebm = False
+                break
+        if uniquebm:
+            rsrevs = repo.revs("ancestors(bookmark(%s)) - "
+                               "ancestors(head() and not bookmark(%s)) - "
+                               "ancestors(bookmark() and not bookmark(%s))",
+                               mark, mark, mark)
+            revs.update(set(rsrevs))
+        if not revs:
+            del marks[mark]
+            marks.write()
+            ui.write(_("bookmark '%s' deleted\n") % mark)
+
+    if not revs:
+        raise util.Abort(_('empty revision set'))
+
+    descendants = set(cl.descendants(revs))
+    strippedrevs = revs.union(descendants)
+    roots = revs.difference(descendants)
+
+    update = False
+    # if one of the wdir parent is stripped we'll need
+    # to update away to an earlier revision
+    for p in repo.dirstate.parents():
+        if p != nullid and cl.rev(p) in strippedrevs:
+            update = True
+            break
+
+    rootnodes = set(cl.node(r) for r in roots)
+
+    q = getattr(repo, 'mq', None)
+    if q is not None and q.applied:
+        # refresh queue state if we're about to strip
+        # applied patches
+        if cl.rev(repo.lookup('qtip')) in strippedrevs:
+            q.applieddirty = True
+            start = 0
+            end = len(q.applied)
+            for i, statusentry in enumerate(q.applied):
+                if statusentry.node in rootnodes:
+                    # if one of the stripped roots is an applied
+                    # patch, only part of the queue is stripped
+                    start = i
+                    break
+            del q.applied[start:end]
+            q.savedirty()
+
+    revs = sorted(rootnodes)
+    if update and opts.get('keep'):
+        wlock = repo.wlock()
+        try:
+            urev, p2 = repo.changelog.parents(revs[0])
+            if (util.safehasattr(repo, 'mq') and p2 != nullid
+                and p2 in [x.node for x in repo.mq.applied]):
+                urev = p2
+            uctx = repo[urev]
+
+            # only reset the dirstate for files that would actually change
+            # between the working context and uctx
+            descendantrevs = repo.revs("%s::." % uctx.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(urev, uctx.manifest(), changedfiles)
+            repo.dirstate.write()
+            update = False
+        finally:
+            wlock.release()
+
+    if opts.get('bookmark'):
+        if mark == repo._bookmarkcurrent:
+            bookmarks.setcurrent(repo, None)
+        del marks[mark]
+        marks.write()
+        ui.write(_("bookmark '%s' deleted\n") % mark)
+
+    strip(ui, repo, revs, backup=backup, update=update, force=opts.get('force'))
+
+    return 0
diff --git a/tests/test-extension.t b/tests/test-extension.t
--- a/tests/test-extension.t
+++ b/tests/test-extension.t
@@ -404,17 +404,20 @@  Issue811: Problem loading extensions twi
   > cmdtable = {"debugextensions": (debugextensions, (), "hg debugextensions")}
   > commands.norepo += " debugextensions"
   > EOF
   $ echo "debugissue811 = $debugpath" >> $HGRCPATH
   $ echo "mq=" >> $HGRCPATH
+  $ echo "strip=" >> $HGRCPATH
   $ echo "hgext.mq=" >> $HGRCPATH
   $ echo "hgext/mq=" >> $HGRCPATH
 
 Show extensions:
+(note that mq force load strip, also checking it's not loaded twice)
 
   $ hg debugextensions
   debugissue811
+  strip
   mq
 
 Disabled extension commands:
 
   $ ORGHGRCPATH=$HGRCPATH
diff --git a/tests/test-mq.t b/tests/test-mq.t
--- a/tests/test-mq.t
+++ b/tests/test-mq.t
@@ -65,10 +65,13 @@  help
   
   make them behave as if --keep-changes were passed, and non-conflicting local
   changes will be tolerated and preserved. If incompatible options such as
   -f/--force or --exact are passed, this setting is ignored.
   
+  This extension used to provide a strip commands. This command now lives in the
+  strip extension.
+  
   list of commands:
   
    qapplied      print the patches already applied
    qclone        clone main and patch repository at same time
    qdelete       remove patches from queue
@@ -89,11 +92,10 @@  help
    qrename       rename a patch
    qselect       set or print guarded patches to push
    qseries       print the entire series file
    qtop          print the name of the current patch
    qunapplied    print the patches not yet applied
-   strip         strip changesets and all their descendants from the repository
   
   use "hg -v help mq" to show builtin aliases and global options
 
   $ hg init a
   $ cd a