hgext3rd/evolve/rewind.py
author Pierre-Yves David <pierre-yves.david@octobus.net>
Sun, 17 Jun 2018 02:32:29 +0200
changeset 3868 1742254d1190
parent 3866 de42d00d6ee2
child 3869 bbfbaf46f7b0
permissions -rw-r--r--
rewind: automatically rewind entire stack We now rewind the full stack, avoiding creating orphans. A `--exact` flag is added to force rewinding only the explicitly specified changesets.

from __future__ import absolute_import

import collections
import hashlib

from mercurial import (
    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(
    '^rewind',
    [('', 'to', [], _("rewind to these revision")),
     ('', 'as-divergence', None, _("preserve current latest successors")),
     ('', 'exact', None, _("only rewind explicitly selected revisions")),
    ],
    _(''))
def rewind(ui, repo, **opts):
    """rewind stacks of changeset to a previous content

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

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

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

    Current rought 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 too are "lost".  Please use --as-divergence when you
              need to perform such operation.
    """
    unfi = repo.unfiltered()

    if not opts.get('to'):
        raise error.Abort('no revision to rewind to')

    successorsmap = collections.defaultdict(set)
    rewindmap = {}
    sscache = {}
    with repo.wlock(), repo.lock():

        rewinded = scmutil.revrange(repo, opts.get('to'))

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

        if not opts['as_divergence']:
            for rev in rewinded:
                ctx = unfi[rev]
                ssets = obsutil.successorssets(repo, ctx.node(), sscache)
                if 1 < len(ssets):
                    msg = _('rewind confused by divergence on %s') % ctx
                    hint = _('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('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='rewind')
            if update_target is not None:
                hg.updaterepo(repo, update_target, False)

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

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('devel', 'user.obsmarker')
    if not user:
        user = unfi.ui.username()
    date = unfi.ui.configdate('devel', 'default-date')
    if date is None:
        date = compat.makedate()
    noise = "%s\0%s\0%d\0%d" % (ctx.node(), user, date[0], date[1])
    extra['__rewind-hash__'] = hashlib.sha256(noise).hexdigest()

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

    extradict = {'extra': extra}

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

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

    return new