diff hgext/strip.py @ 19826:4b1cbcfdabf7

mq: extract strip function as its standalone extension (issue3824) Strip now lives in its own extension reminder: 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.
author Pierre-Yves David <pierre-yves.david@ens-lyon.org>
date Thu, 26 Sep 2013 23:57:21 +0200
parents 4b4997068143
children 0a986233ecbf
line wrap: on
line diff
--- a/hgext/strip.py	Thu Sep 26 23:43:00 2013 +0200
+++ b/hgext/strip.py	Thu Sep 26 23:57:21 2013 +0200
@@ -1,8 +1,13 @@
+"""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 import cmdutil, hg, util
 from mercurial.node import nullid
 from mercurial.lock import release
-from mercurial import repair
+from mercurial import cmdutil, hg, scmutil, util
+from mercurial import repair, bookmarks
 
 cmdtable = {}
 command = cmdutil.command(cmdtable)
@@ -55,3 +60,158 @@
     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