mercurial/strip.py
changeset 45865 d7a508a75d72
parent 45514 93a0f3ba36bb
child 45942 89a2afe31e82
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/strip.py	Sun Nov 08 16:23:35 2020 -0500
@@ -0,0 +1,277 @@
+from __future__ import absolute_import
+
+from .i18n import _
+from .pycompat import getattr
+from . import (
+    bookmarks as bookmarksmod,
+    cmdutil,
+    error,
+    hg,
+    lock as lockmod,
+    mergestate as mergestatemod,
+    node as nodemod,
+    pycompat,
+    registrar,
+    repair,
+    scmutil,
+    util,
+)
+
+nullid = nodemod.nullid
+release = lockmod.release
+
+cmdtable = {}
+command = registrar.command(cmdtable)
+
+
+def checklocalchanges(repo, force=False):
+    s = repo.status()
+    if not force:
+        cmdutil.checkunfinished(repo)
+        cmdutil.bailifchanged(repo)
+    else:
+        cmdutil.checkunfinished(repo, skipmerge=True)
+    return s
+
+
+def _findupdatetarget(repo, nodes):
+    unode, p2 = repo.changelog.parents(nodes[0])
+    currentbranch = repo[None].branch()
+
+    if (
+        util.safehasattr(repo, b'mq')
+        and p2 != nullid
+        and p2 in [x.node for x in repo.mq.applied]
+    ):
+        unode = p2
+    elif currentbranch != repo[unode].branch():
+        pwdir = b'parents(wdir())'
+        revset = b'max(((parents(%ln::%r) + %r) - %ln::%r) and branch(%s))'
+        branchtarget = repo.revs(
+            revset, nodes, pwdir, pwdir, nodes, pwdir, currentbranch
+        )
+        if branchtarget:
+            cl = repo.changelog
+            unode = cl.node(branchtarget.first())
+
+    return unode
+
+
+def strip(
+    ui,
+    repo,
+    revs,
+    update=True,
+    backup=True,
+    force=None,
+    bookmarks=None,
+    soft=False,
+):
+    with repo.wlock(), repo.lock():
+
+        if update:
+            checklocalchanges(repo, force=force)
+            urev = _findupdatetarget(repo, revs)
+            hg.clean(repo, urev)
+            repo.dirstate.write(repo.currenttransaction())
+
+        if soft:
+            repair.softstrip(ui, repo, revs, backup)
+        else:
+            repair.strip(ui, repo, revs, backup)
+
+        repomarks = repo._bookmarks
+        if bookmarks:
+            with repo.transaction(b'strip') as tr:
+                if repo._activebookmark in bookmarks:
+                    bookmarksmod.deactivate(repo)
+                repomarks.applychanges(repo, tr, [(b, None) for b in bookmarks])
+            for bookmark in sorted(bookmarks):
+                ui.write(_(b"bookmark '%s' deleted\n") % bookmark)
+
+
+@command(
+    b"debugstrip",
+    [
+        (
+            b'r',
+            b'rev',
+            [],
+            _(
+                b'strip specified revision (optional, '
+                b'can specify revisions without this '
+                b'option)'
+            ),
+            _(b'REV'),
+        ),
+        (
+            b'f',
+            b'force',
+            None,
+            _(
+                b'force removal of changesets, discard '
+                b'uncommitted changes (no backup)'
+            ),
+        ),
+        (b'', b'no-backup', None, _(b'do not save backup bundle')),
+        (b'', b'nobackup', None, _(b'do not save backup bundle (DEPRECATED)'),),
+        (b'n', b'', None, _(b'ignored  (DEPRECATED)')),
+        (
+            b'k',
+            b'keep',
+            None,
+            _(b"do not modify working directory during strip"),
+        ),
+        (
+            b'B',
+            b'bookmark',
+            [],
+            _(b"remove revs only reachable from given bookmark"),
+            _(b'BOOKMARK'),
+        ),
+        (
+            b'',
+            b'soft',
+            None,
+            _(b"simply drop changesets from visible history (EXPERIMENTAL)"),
+        ),
+    ],
+    _(b'hg debugstrip [-k] [-f] [-B bookmark] [-r] REV...'),
+    helpcategory=command.CATEGORY_MAINTENANCE,
+)
+def debugstrip(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.
+    """
+    opts = pycompat.byteskwargs(opts)
+    backup = True
+    if opts.get(b'no_backup') or opts.get(b'nobackup'):
+        backup = False
+
+    cl = repo.changelog
+    revs = list(revs) + opts.get(b'rev')
+    revs = set(scmutil.revrange(repo, revs))
+
+    with repo.wlock():
+        bookmarks = set(opts.get(b'bookmark'))
+        if bookmarks:
+            repomarks = repo._bookmarks
+            if not bookmarks.issubset(repomarks):
+                raise error.Abort(
+                    _(b"bookmark '%s' not found")
+                    % b','.join(sorted(bookmarks - set(repomarks.keys())))
+                )
+
+            # 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.
+            nodetobookmarks = {}
+            for mark, node in pycompat.iteritems(repomarks):
+                nodetobookmarks.setdefault(node, []).append(mark)
+            for marks in nodetobookmarks.values():
+                if bookmarks.issuperset(marks):
+                    rsrevs = scmutil.bookmarkrevs(repo, marks[0])
+                    revs.update(set(rsrevs))
+            if not revs:
+                with repo.lock(), repo.transaction(b'bookmark') as tr:
+                    bmchanges = [(b, None) for b in bookmarks]
+                    repomarks.applychanges(repo, tr, bmchanges)
+                for bookmark in sorted(bookmarks):
+                    ui.write(_(b"bookmark '%s' deleted\n") % bookmark)
+
+        if not revs:
+            raise error.Abort(_(b'empty revision set'))
+
+        descendants = set(cl.descendants(revs))
+        strippedrevs = revs.union(descendants)
+        roots = revs.difference(descendants)
+
+        # if one of the wdir parent is stripped we'll need
+        # to update away to an earlier revision
+        update = any(
+            p != nullid and cl.rev(p) in strippedrevs
+            for p in repo.dirstate.parents()
+        )
+
+        rootnodes = {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(b'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(b'keep'):
+            urev = _findupdatetarget(repo, revs)
+            uctx = repo[urev]
+
+            # only reset the dirstate for files that would actually change
+            # between the working context and uctx
+            descendantrevs = repo.revs(b"only(., %d)", 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] != b'n']
+            changedfiles.extend(dirchanges)
+
+            repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
+            repo.dirstate.write(repo.currenttransaction())
+
+            # clear resolve state
+            mergestatemod.mergestate.clean(repo)
+
+            update = False
+
+        strip(
+            ui,
+            repo,
+            revs,
+            backup=backup,
+            update=update,
+            force=opts.get(b'force'),
+            bookmarks=bookmarks,
+            soft=opts[b'soft'],
+        )
+
+    return 0