[PATCH 1 of 2 standalone-strip-short-version] mq: extract strip as a standalone extension (issue3824)
pierre-yves.david at ens-lyon.org
pierre-yves.david at ens-lyon.org
Thu Sep 26 22:04:04 UTC 2013
# HG changeset patch
# User Pierre-Yves David <pierre-yves.david at ens-lyon.org>
# Date 1380232768 -7200
# Thu Sep 26 23:59:28 2013 +0200
# Node ID 6fb90ba27626df7897bdc1e456deea078459d72e
# 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 command. 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) prefer 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 command. 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