hgext3rd/evolve/rewind.py
author Raphaël Gomès <rgomes@octobus.net>
Tue, 06 Aug 2019 15:06:38 +0200
changeset 4814 48b30ff742cb
parent 4794 7b5d08e84c4d
child 4821 d8e36e60aea0
permissions -rw-r--r--
python3: use format-source to run byteify-strings in .py files Using the format-source extension smooth out the pain of merging after auto-formatting. This change makes all of the Evolve test suite pass under python3 and has added benefit of being 100% automated using mercurial's `byteify-strings` script version 1.0 (revision 11498aa91c036c6d70f7ac5ee5af2664a84a1130). How to benefit from the help of format-source is explained in the README.

from __future__ import absolute_import

import collections
import hashlib

from mercurial import (
    cmdutil,
    error,
    hg,
    obsolete,
    obsutil,
    scmutil,
)

from mercurial.i18n import _

from . import (
    exthelper,
    rewriteutil,
    compat,
)

eh = exthelper.exthelper()

# flag in obsolescence markers to link to identical version
identicalflag = 4

@eh.command(
    b'rewind|undo',
    [(b'', b'to', [], _(b"rewind to these revisions"), _(b'REV')),
     (b'', b'as-divergence', None, _(b"preserve current latest successors")),
     (b'', b'exact', None, _(b"only rewind explicitly selected revisions")),
     (b'', b'from', [],
      _(b"rewind these revisions to their predecessors"), _(b'REV')),
     ],
    _(b'[--as-divergence] [--exact] [--to REV]... [--from REV]...'),
    helpbasic=True)
def rewind(ui, repo, **opts):
    """rewind a stack of changesets to a previous state

    This command can be used to restore stacks of changesets to an obsolete
    state, creating identical copies.

    There are two main ways to select the rewind target. Rewinding "from"
    changesets will restore the direct predecessors of these changesets (and
    obsolete the changeset you rewind from). Rewinding "to" will restore the
    changeset you have selected (and obsolete their latest successors).

    By default, we rewind from the working directory parents, restoring its
    predecessor.

    When we rewind to an obsolete version, we also rewind to all its obsolete
    ancestors. To only rewind to the explicitly selected changesets use the
    `--exact` flag. Using the `--exact` flag can restore some changesets as
    orphan.

    The latest successors of the obsolete changesets will be superseded by
    these new copies. This behavior can be disabled using `--as-divergence`,
    the current latest successors won't be affected and content-divergence will
    appear between them and the restored version of the obsolete changesets.

    Current rough edges:

      * fold: rewinding to only some of the initially folded changesets will be
              problematic. The fold result is marked obsolete and the part not
              rewinded to are "lost".  Please use --as-divergence when you
              need to perform such operation.

      * :hg:`rewind` might affect changesets outside the current stack. Without
              --exact, we also restore ancestors of the rewind target,
              obsoleting their latest successors (unless --as-divergent is
              provided). In some case, these latest successors will be on
              branches unrelated to the changeset you rewind from.
              (We plan to automatically detect this case in the future)

    """
    unfi = repo.unfiltered()

    successorsmap = collections.defaultdict(set)
    rewindmap = {}
    sscache = {}
    with repo.wlock(), repo.lock():
        # stay on the safe side: prevent local case in case we need to upgrade
        cmdutil.bailifchanged(repo)

        rewinded = _select_rewinded(repo, opts)

        if not opts['as_divergence']:
            for rev in rewinded:
                ctx = unfi[rev]
                ssets = obsutil.successorssets(repo, ctx.node(), sscache)
                if 1 < len(ssets):
                    msg = _(b'rewind confused by divergence on %s') % ctx
                    hint = _(b'solve divergence first or use "--as-divergence"')
                    raise error.Abort(msg, hint=hint)
                if ssets and ssets[0]:
                    for succ in ssets[0]:
                        successorsmap[succ].add(ctx.node())

        # Check that we can rewind these changesets
        with repo.transaction(b'rewind'):
            for rev in sorted(rewinded):
                ctx = unfi[rev]
                rewindmap[ctx.node()] = _revive_revision(unfi, rev, rewindmap)

            relationships = []
            cl = unfi.changelog
            wctxp = repo[None].p1()
            update_target = None
            for (source, dest) in sorted(successorsmap.items()):
                newdest = [rewindmap[d] for d in sorted(dest, key=cl.rev)]
                rel = (unfi[source], tuple(unfi[d] for d in newdest))
                relationships.append(rel)
                if wctxp.node() == source:
                    update_target = newdest[-1]
            obsolete.createmarkers(unfi, relationships, operation=b'rewind')
            if update_target is not None:
                hg.updaterepo(repo, update_target, False)

    repo.ui.status(_(b'rewinded to %d changesets\n') % len(rewinded))
    if relationships:
        repo.ui.status(_(b'(%d changesets obsoleted)\n') % len(relationships))
    if update_target is not None:
        ui.status(_(b'working directory is now at %s\n') % repo[b'.'])

def _select_rewinded(repo, opts):
    """select the revision we shoudl rewind to
    """
    unfi = repo.unfiltered()
    rewinded = set()
    revsto = opts.get('to')
    revsfrom = opts.get('from')
    if not (revsto or revsfrom):
        revsfrom.append(b'.')
    if revsto:
        rewinded.update(scmutil.revrange(repo, revsto))
    if revsfrom:
        succs = scmutil.revrange(repo, revsfrom)
        rewinded.update(unfi.revs(b'predecessors(%ld)', succs))

    if not rewinded:
        raise error.Abort(b'no revision to rewind to')

    if not opts['exact']:
        rewinded = unfi.revs(b'obsolete() and ::%ld', rewinded)

    return sorted(rewinded)

def _revive_revision(unfi, rev, rewindmap):
    """rewind a single revision rev.
    """
    ctx = unfi[rev]
    extra = ctx.extra().copy()
    # rewind hash should be unique over multiple rewind.
    user = unfi.ui.config(b'devel', b'user.obsmarker')
    if not user:
        user = unfi.ui.username()
    date = unfi.ui.configdate(b'devel', b'default-date')
    if date is None:
        date = compat.makedate()
    noise = b"%s\0%s\0%d\0%d" % (ctx.node(), user, date[0], date[1])
    extra[b'__rewind-hash__'] = hashlib.sha256(noise).hexdigest()

    p1 = ctx.p1().node()
    p1 = rewindmap.get(p1, p1)
    p2 = ctx.p2().node()
    p2 = rewindmap.get(p2, p2)

    updates = []
    if len(ctx.parents()) > 1:
        updates = ctx.parents()
    extradict = {b'extra': extra}

    new, unusedvariable = rewriteutil.rewrite(unfi, ctx, updates, ctx,
                                              [p1, p2],
                                              commitopts=extradict)

    obsolete.createmarkers(unfi, [(ctx, (unfi[new],))],
                           flag=identicalflag, operation=b'rewind')

    return new