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

pierre-yves.david at ens-lyon.org pierre-yves.david at ens-lyon.org
Wed Sep 25 16:53:41 CDT 2013


# HG changeset patch
# User Pierre-Yves David <pierre-yves.david at 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.

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)
-
-
- at 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)
+
+
+ at 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


More information about the Mercurial-devel mailing list