hgext3rd/evolve/cmdrewrite.py
changeset 2772 394b836e475b
parent 2771 6044bd16bfb7
child 2773 9e363e8c1c06
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/cmdrewrite.py	Sun Jul 23 17:28:02 2017 +0200
@@ -0,0 +1,927 @@
+# Module dedicated to host history rewriting commands
+#
+# Copyright 2017 Octobus <contact@octobus.net>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+# Status: Stabilization of the API in progress
+#
+#   The final set of command should go into core.
+
+from __future__ import absolute_import
+
+import random
+
+from mercurial import (
+    bookmarks as bookmarksmod,
+    cmdutil,
+    commands,
+    context,
+    copies,
+    error,
+    hg,
+    lock as lockmod,
+    node,
+    obsolete,
+    phases,
+    scmutil,
+    util,
+)
+
+from mercurial.i18n import _
+
+from . import (
+    compat,
+    exthelper,
+    rewriteutil,
+    utility,
+)
+
+eh = exthelper.exthelper()
+
+walkopts = commands.walkopts
+commitopts = commands.commitopts
+commitopts2 = commands.commitopts2
+mergetoolopts = commands.mergetoolopts
+
+# option added by evolve
+
+def _resolveoptions(ui, opts):
+    """modify commit options dict to handle related options
+
+    For now, all it does is figure out the commit date: respect -D unless
+    -d was supplied.
+    """
+    # N.B. this is extremely similar to setupheaderopts() in mq.py
+    if not opts.get('date') and opts.get('current_date'):
+        opts['date'] = '%d %d' % util.makedate()
+    if not opts.get('user') and opts.get('current_user'):
+        opts['user'] = ui.username()
+
+commitopts3 = [
+    ('D', 'current-date', None,
+     _('record the current date as commit date')),
+    ('U', 'current-user', None,
+     _('record the current user as committer')),
+]
+
+interactiveopt = [['i', 'interactive', None, _('use interactive mode')]]
+
+@eh.command(
+    'amend|refresh',
+    [('A', 'addremove', None,
+      _('mark new/missing files as added/removed before committing')),
+     ('a', 'all', False, _("match all files")),
+     ('e', 'edit', False, _('invoke editor on commit messages')),
+     ('', 'extract', False, _('extract changes from the commit to the working copy')),
+     ('', 'close-branch', None,
+      _('mark a branch as closed, hiding it from the branch list')),
+     ('s', 'secret', None, _('use the secret phase for committing')),
+    ] + walkopts + commitopts + commitopts2 + commitopts3 + interactiveopt,
+    _('[OPTION]... [FILE]...'))
+def amend(ui, repo, *pats, **opts):
+    """combine a changeset with updates and replace it with a new one
+
+    Commits a new changeset incorporating both the changes to the given files
+    and all the changes from the current parent changeset into the repository.
+
+    See :hg:`commit` for details about committing changes.
+
+    If you don't specify -m, the parent's message will be reused.
+
+    If --extra is specified, the behavior of `hg amend` is reversed: Changes
+    to selected files in the checked out revision appear again as uncommitted
+    changed in the working directory.
+
+    Returns 0 on success, 1 if nothing changed.
+    """
+    opts = opts.copy()
+    if opts.get('extract'):
+        if opts.pop('interactive', False):
+            msg = _('not support for --interactive with --extract yet')
+            raise error.Abort(msg)
+        return uncommit(ui, repo, *pats, **opts)
+    else:
+        if opts.pop('all', False):
+            # add an include for all
+            include = list(opts.get('include'))
+            include.append('re:.*')
+        edit = opts.pop('edit', False)
+        log = opts.get('logfile')
+        opts['amend'] = True
+        if not (edit or opts['message'] or log):
+            opts['message'] = repo['.'].description()
+        _resolveoptions(ui, opts)
+        _alias, commitcmd = cmdutil.findcmd('commit', commands.table)
+        return commitcmd[0](ui, repo, *pats, **opts)
+
+def _touchedbetween(repo, source, dest, match=None):
+    touched = set()
+    for files in repo.status(source, dest, match=match)[:3]:
+        touched.update(files)
+    return touched
+
+def _commitfiltered(repo, ctx, match, target=None, message=None, user=None,
+                    date=None):
+    """Recommit ctx with changed files not in match. Return the new
+    node identifier, or None if nothing changed.
+    """
+    base = ctx.p1()
+    if target is None:
+        target = base
+    # ctx
+    initialfiles = _touchedbetween(repo, base, ctx)
+    if base == target:
+        affected = set(f for f in initialfiles if match(f))
+        newcontent = set()
+    else:
+        affected = _touchedbetween(repo, target, ctx, match=match)
+        newcontent = _touchedbetween(repo, target, base, match=match)
+    # The commit touchs all existing files
+    # + all file that needs a new content
+    # - the file affected bny uncommit with the same content than base.
+    files = (initialfiles - affected) | newcontent
+    if not newcontent and files == initialfiles:
+        return None
+
+    # Filter copies
+    copied = copies.pathcopies(target, ctx)
+    copied = dict((dst, src) for dst, src in copied.iteritems()
+                  if dst in files)
+
+    def filectxfn(repo, memctx, path, contentctx=ctx, redirect=newcontent):
+        if path in redirect:
+            return filectxfn(repo, memctx, path, contentctx=target, redirect=())
+        if path not in contentctx:
+            return None
+        fctx = contentctx[path]
+        flags = fctx.flags()
+        mctx = context.memfilectx(repo, fctx.path(), fctx.data(),
+                                  islink='l' in flags,
+                                  isexec='x' in flags,
+                                  copied=copied.get(path))
+        return mctx
+
+    if message is None:
+        message = ctx.description()
+    if not user:
+        user = ctx.user()
+    if not date:
+        date = ctx.date()
+    new = context.memctx(repo,
+                         parents=[base.node(), node.nullid],
+                         text=message,
+                         files=files,
+                         filectxfn=filectxfn,
+                         user=user,
+                         date=date,
+                         extra=ctx.extra())
+    # commitctx always create a new revision, no need to check
+    newid = repo.commitctx(new)
+    return newid
+
+def _uncommitdirstate(repo, oldctx, match):
+    """Fix the dirstate after switching the working directory from
+    oldctx to a copy of oldctx not containing changed files matched by
+    match.
+    """
+    ctx = repo['.']
+    ds = repo.dirstate
+    copies = dict(ds.copies())
+    m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
+    for f in m:
+        if ds[f] == 'r':
+            # modified + removed -> removed
+            continue
+        ds.normallookup(f)
+
+    for f in a:
+        if ds[f] == 'r':
+            # added + removed -> unknown
+            ds.drop(f)
+        elif ds[f] != 'a':
+            ds.add(f)
+
+    for f in r:
+        if ds[f] == 'a':
+            # removed + added -> normal
+            ds.normallookup(f)
+        elif ds[f] != 'r':
+            ds.remove(f)
+
+    # Merge old parent and old working dir copies
+    oldcopies = {}
+    for f in (m + a):
+        src = oldctx[f].renamed()
+        if src:
+            oldcopies[f] = src[0]
+    oldcopies.update(copies)
+    copies = dict((dst, oldcopies.get(src, src))
+                  for dst, src in oldcopies.iteritems())
+    # Adjust the dirstate copies
+    for dst, src in copies.iteritems():
+        if (src not in ctx or dst in ctx or ds[dst] != 'a'):
+            src = None
+        ds.copy(src, dst)
+
+@eh.command(
+    '^uncommit',
+    [('a', 'all', None, _('uncommit all changes when no arguments given')),
+     ('r', 'rev', '', _('revert commit content to REV instead')),
+     ] + commands.walkopts + commitopts + commitopts2 + commitopts3,
+    _('[OPTION]... [NAME]'))
+def uncommit(ui, repo, *pats, **opts):
+    """move changes from parent revision to working directory
+
+    Changes to selected files in the checked out revision appear again as
+    uncommitted changed in the working directory. A new revision
+    without the selected changes is created, becomes the checked out
+    revision, and obsoletes the previous one.
+
+    The --include option specifies patterns to uncommit.
+    The --exclude option specifies patterns to keep in the commit.
+
+    The --rev argument let you change the commit file to a content of another
+    revision. It still does not change the content of your file in the working
+    directory.
+
+    Return 0 if changed files are uncommitted.
+    """
+
+    _resolveoptions(ui, opts) # process commitopts3
+    wlock = lock = tr = None
+    try:
+        wlock = repo.wlock()
+        lock = repo.lock()
+        wctx = repo[None]
+        if len(wctx.parents()) <= 0:
+            raise error.Abort(_("cannot uncommit null changeset"))
+        if len(wctx.parents()) > 1:
+            raise error.Abort(_("cannot uncommit while merging"))
+        old = repo['.']
+        if old.phase() == phases.public:
+            raise error.Abort(_("cannot rewrite immutable changeset"))
+        if len(old.parents()) > 1:
+            raise error.Abort(_("cannot uncommit merge changeset"))
+        oldphase = old.phase()
+
+        rev = None
+        if opts.get('rev'):
+            rev = scmutil.revsingle(repo, opts.get('rev'))
+            ctx = repo[None]
+            if ctx.p1() == rev or ctx.p2() == rev:
+                raise error.Abort(_("cannot uncommit to parent changeset"))
+
+        onahead = old.rev() in repo.changelog.headrevs()
+        disallowunstable = not obsolete.isenabled(repo,
+                                                  obsolete.allowunstableopt)
+        if disallowunstable and not onahead:
+            raise error.Abort(_("cannot uncommit in the middle of a stack"))
+
+        # Recommit the filtered changeset
+        tr = repo.transaction('uncommit')
+        updatebookmarks = rewriteutil.bookmarksupdater(repo, old.node(), tr)
+        newid = None
+        includeorexclude = opts.get('include') or opts.get('exclude')
+        if (pats or includeorexclude or opts.get('all')):
+            match = scmutil.match(old, pats, opts)
+            if not (opts['message'] or opts['logfile']):
+                opts['message'] = old.description()
+            message = cmdutil.logmessage(ui, opts)
+            newid = _commitfiltered(repo, old, match, target=rev,
+                                    message=message, user=opts.get('user'),
+                                    date=opts.get('date'))
+        if newid is None:
+            raise error.Abort(_('nothing to uncommit'),
+                              hint=_("use --all to uncommit all files"))
+        # Move local changes on filtered changeset
+        obsolete.createmarkers(repo, [(old, (repo[newid],))])
+        phases.retractboundary(repo, tr, oldphase, [newid])
+        with repo.dirstate.parentchange():
+            repo.dirstate.setparents(newid, node.nullid)
+            _uncommitdirstate(repo, old, match)
+        updatebookmarks(newid)
+        if not repo[newid].files():
+            ui.warn(_("new changeset is empty\n"))
+            ui.status(_("(use 'hg prune .' to remove it)\n"))
+        tr.close()
+    finally:
+        lockmod.release(tr, lock, wlock)
+
+@eh.command(
+    '^fold|squash',
+    [('r', 'rev', [], _("revision to fold")),
+     ('', 'exact', None, _("only fold specified revisions")),
+     ('', 'from', None, _("fold revisions linearly to working copy parent"))
+    ] + commitopts + commitopts2 + commitopts3,
+    _('hg fold [OPTION]... [-r] REV'))
+def fold(ui, repo, *revs, **opts):
+    """fold multiple revisions into a single one
+
+    With --from, folds all the revisions linearly between the given revisions
+    and the parent of the working directory.
+
+    With --exact, folds only the specified revisions while ignoring the
+    parent of the working directory. In this case, the given revisions must
+    form a linear unbroken chain.
+
+    .. container:: verbose
+
+     Some examples:
+
+     - Fold the current revision with its parent::
+
+         hg fold --from .^
+
+     - Fold all draft revisions with working directory parent::
+
+         hg fold --from 'draft()'
+
+       See :hg:`help phases` for more about draft revisions and
+       :hg:`help revsets` for more about the `draft()` keyword
+
+     - Fold revisions between 3 and 6 with the working directory parent::
+
+         hg fold --from 3::6
+
+     - Fold revisions 3 and 4:
+
+        hg fold "3 + 4" --exact
+
+     - Only fold revisions linearly between foo and @::
+
+         hg fold foo::@ --exact
+    """
+    _resolveoptions(ui, opts)
+    revs = list(revs)
+    revs.extend(opts['rev'])
+    if not revs:
+        raise error.Abort(_('no revisions specified'))
+
+    revs = scmutil.revrange(repo, revs)
+
+    if opts['from'] and opts['exact']:
+        raise error.Abort(_('cannot use both --from and --exact'))
+    elif opts['from']:
+        # Try to extend given revision starting from the working directory
+        extrevs = repo.revs('(%ld::.) or (.::%ld)', revs, revs)
+        discardedrevs = [r for r in revs if r not in extrevs]
+        if discardedrevs:
+            msg = _("cannot fold non-linear revisions")
+            hint = _("given revisions are unrelated to parent of working"
+                     " directory")
+            raise error.Abort(msg, hint=hint)
+        revs = extrevs
+    elif opts['exact']:
+        # Nothing to do; "revs" is already set correctly
+        pass
+    else:
+        raise error.Abort(_('must specify either --from or --exact'))
+
+    if not revs:
+        raise error.Abort(_('specified revisions evaluate to an empty set'),
+                          hint=_('use different revision arguments'))
+    elif len(revs) == 1:
+        ui.write_err(_('single revision specified, nothing to fold\n'))
+        return 1
+
+    wlock = lock = None
+    try:
+        wlock = repo.wlock()
+        lock = repo.lock()
+
+        root, head = rewriteutil.foldcheck(repo, revs)
+
+        tr = repo.transaction('fold')
+        try:
+            commitopts = opts.copy()
+            allctx = [repo[r] for r in revs]
+            targetphase = max(c.phase() for c in allctx)
+
+            if commitopts.get('message') or commitopts.get('logfile'):
+                commitopts['edit'] = False
+            else:
+                msgs = ["HG: This is a fold of %d changesets." % len(allctx)]
+                msgs += ["HG: Commit message of changeset %s.\n\n%s\n" %
+                         (c.rev(), c.description()) for c in allctx]
+                commitopts['message'] = "\n".join(msgs)
+                commitopts['edit'] = True
+
+            newid, unusedvariable = rewriteutil.rewrite(repo, root, allctx,
+                                                        head,
+                                                        [root.p1().node(),
+                                                         root.p2().node()],
+                                                        commitopts=commitopts)
+            phases.retractboundary(repo, tr, targetphase, [newid])
+            obsolete.createmarkers(repo, [(ctx, (repo[newid],))
+                                   for ctx in allctx])
+            tr.close()
+        finally:
+            tr.release()
+        ui.status('%i changesets folded\n' % len(revs))
+        if repo['.'].rev() in revs:
+            hg.update(repo, newid)
+    finally:
+        lockmod.release(lock, wlock)
+
+@eh.command(
+    '^metaedit',
+    [('r', 'rev', [], _("revision to edit")),
+     ('', 'fold', None, _("also fold specified revisions into one")),
+    ] + commitopts + commitopts2 + commitopts3,
+    _('hg metaedit [OPTION]... [-r] [REV]'))
+def metaedit(ui, repo, *revs, **opts):
+    """edit commit information
+
+    Edits the commit information for the specified revisions. By default, edits
+    commit information for the working directory parent.
+
+    With --fold, also folds multiple revisions into one if necessary. In this
+    case, the given revisions must form a linear unbroken chain.
+
+    .. container:: verbose
+
+     Some examples:
+
+     - Edit the commit message for the working directory parent::
+
+         hg metaedit
+
+     - Change the username for the working directory parent::
+
+         hg metaedit --user 'New User <new-email@example.com>'
+
+     - Combine all draft revisions that are ancestors of foo but not of @ into
+       one::
+
+         hg metaedit --fold 'draft() and only(foo,@)'
+
+       See :hg:`help phases` for more about draft revisions, and
+       :hg:`help revsets` for more about the `draft()` and `only()` keywords.
+    """
+    _resolveoptions(ui, opts)
+    revs = list(revs)
+    revs.extend(opts['rev'])
+    if not revs:
+        if opts['fold']:
+            raise error.Abort(_('revisions must be specified with --fold'))
+        revs = ['.']
+
+    wlock = lock = None
+    try:
+        wlock = repo.wlock()
+        lock = repo.lock()
+
+        revs = scmutil.revrange(repo, revs)
+        if not opts['fold'] and len(revs) > 1:
+            # TODO: handle multiple revisions. This is somewhat tricky because
+            # if we want to edit a series of commits:
+            #
+            #   a ---- b ---- c
+            #
+            # we need to rewrite a first, then directly rewrite b on top of the
+            # new a, then rewrite c on top of the new b. So we need to handle
+            # revisions in topological order.
+            raise error.Abort(_('editing multiple revisions without --fold is '
+                                'not currently supported'))
+
+        if opts['fold']:
+            root, head = rewriteutil.foldcheck(repo, revs)
+        else:
+            if repo.revs("%ld and public()", revs):
+                raise error.Abort(_('cannot edit commit information for public '
+                                    'revisions'))
+            newunstable = rewriteutil.disallowednewunstable(repo, revs)
+            if newunstable:
+                msg = _('cannot edit commit information in the middle'
+                        ' of a stack')
+                hint = _('%s will become unstable and new unstable changes'
+                         ' are not allowed')
+                hint %= repo[newunstable.first()]
+                raise error.Abort(msg, hint=hint)
+            root = head = repo[revs.first()]
+
+        wctx = repo[None]
+        p1 = wctx.p1()
+        tr = repo.transaction('metaedit')
+        newp1 = None
+        try:
+            commitopts = opts.copy()
+            allctx = [repo[r] for r in revs]
+            targetphase = max(c.phase() for c in allctx)
+
+            if commitopts.get('message') or commitopts.get('logfile'):
+                commitopts['edit'] = False
+            else:
+                if opts['fold']:
+                    msgs = ["HG: This is a fold of %d changesets." % len(allctx)]
+                    msgs += ["HG: Commit message of changeset %s.\n\n%s\n" %
+                             (c.rev(), c.description()) for c in allctx]
+                else:
+                    msgs = [head.description()]
+                commitopts['message'] = "\n".join(msgs)
+                commitopts['edit'] = True
+
+            # TODO: if the author and message are the same, don't create a new
+            # hash. Right now we create a new hash because the date can be
+            # different.
+            newid, created = rewriteutil.rewrite(repo, root, allctx, head,
+                                                 [root.p1().node(),
+                                                  root.p2().node()],
+                                                 commitopts=commitopts)
+            if created:
+                if p1.rev() in revs:
+                    newp1 = newid
+                phases.retractboundary(repo, tr, targetphase, [newid])
+                obsolete.createmarkers(repo, [(ctx, (repo[newid],))
+                                              for ctx in allctx])
+            else:
+                ui.status(_("nothing changed\n"))
+            tr.close()
+        finally:
+            tr.release()
+
+        if opts['fold']:
+            ui.status('%i changesets folded\n' % len(revs))
+        if newp1 is not None:
+            hg.update(repo, newp1)
+    finally:
+        lockmod.release(lock, wlock)
+
+metadataopts = [
+    ('d', 'date', '',
+     _('record the specified date in metadata'), _('DATE')),
+    ('u', 'user', '',
+     _('record the specified user in metadata'), _('USER')),
+]
+
+def _getmetadata(**opts):
+    metadata = {}
+    date = opts.get('date')
+    user = opts.get('user')
+    if date:
+        metadata['date'] = '%i %i' % util.parsedate(date)
+    if user:
+        metadata['user'] = user
+    return metadata
+
+@eh.command(
+    '^prune|obsolete',
+    [('n', 'new', [], _("successor changeset (DEPRECATED)")),
+     ('s', 'succ', [], _("successor changeset")),
+     ('r', 'rev', [], _("revisions to prune")),
+     ('k', 'keep', None, _("does not modify working copy during prune")),
+     ('', 'biject', False, _("do a 1-1 map between rev and successor ranges")),
+     ('', 'fold', False,
+      _("record a fold (multiple precursors, one successors)")),
+     ('', 'split', False,
+      _("record a split (on precursor, multiple successors)")),
+     ('B', 'bookmark', [], _("remove revs only reachable from given"
+                             " bookmark"))] + metadataopts,
+    _('[OPTION] [-r] REV...'))
+# XXX -U  --noupdate option to prevent wc update and or bookmarks update ?
+def cmdprune(ui, repo, *revs, **opts):
+    """hide changesets by marking them obsolete
+
+    Pruned changesets are obsolete with no successors. If they also have no
+    descendants, they are hidden (invisible to all commands).
+
+    Non-obsolete descendants of pruned changesets become "unstable". Use :hg:`evolve`
+    to handle this situation.
+
+    When you prune the parent of your working copy, Mercurial updates the working
+    copy to a non-obsolete parent.
+
+    You can use ``--succ`` to tell Mercurial that a newer version (successor) of the
+    pruned changeset exists. Mercurial records successor revisions in obsolescence
+    markers.
+
+    You can use the ``--biject`` option to specify a 1-1 mapping (bijection) between
+    revisions to pruned (precursor) and successor changesets. This option may be
+    removed in a future release (with the functionality provided automatically).
+
+    If you specify multiple revisions in ``--succ``, you are recording a "split" and
+    must acknowledge it by passing ``--split``. Similarly, when you prune multiple
+    changesets with a single successor, you must pass the ``--fold`` option.
+    """
+    revs = scmutil.revrange(repo, list(revs) + opts.get('rev'))
+    succs = opts['new'] + opts['succ']
+    bookmarks = set(opts.get('bookmark'))
+    metadata = _getmetadata(**opts)
+    biject = opts.get('biject')
+    fold = opts.get('fold')
+    split = opts.get('split')
+
+    options = [o for o in ('biject', 'fold', 'split') if opts.get(o)]
+    if 1 < len(options):
+        raise error.Abort(_("can only specify one of %s") % ', '.join(options))
+
+    if bookmarks:
+        reachablefrombookmark = rewriteutil.reachablefrombookmark
+        repomarks, revs = reachablefrombookmark(repo, revs, bookmarks)
+        if not revs:
+            # no revisions to prune - delete bookmark immediately
+            rewriteutil.deletebookmark(repo, repomarks, bookmarks)
+
+    if not revs:
+        raise error.Abort(_('nothing to prune'))
+
+    wlock = lock = tr = None
+    try:
+        wlock = repo.wlock()
+        lock = repo.lock()
+        tr = repo.transaction('prune')
+        # defines pruned changesets
+        precs = []
+        revs.sort()
+        for p in revs:
+            cp = repo[p]
+            if not cp.mutable():
+                # note: createmarkers() would have raised something anyway
+                raise error.Abort('cannot prune immutable changeset: %s' % cp,
+                                  hint="see 'hg help phases' for details")
+            precs.append(cp)
+        if not precs:
+            raise error.Abort('nothing to prune')
+
+        if rewriteutil.disallowednewunstable(repo, revs):
+            raise error.Abort(_("cannot prune in the middle of a stack"),
+                              hint=_("new unstable changesets are not allowed"))
+
+        # defines successors changesets
+        sucs = scmutil.revrange(repo, succs)
+        sucs.sort()
+        sucs = tuple(repo[n] for n in sucs)
+        if not biject and len(sucs) > 1 and len(precs) > 1:
+            msg = "Can't use multiple successors for multiple precursors"
+            hint = _("use --biject to mark a series as a replacement"
+                     " for another")
+            raise error.Abort(msg, hint=hint)
+        elif biject and len(sucs) != len(precs):
+            msg = "Can't use %d successors for %d precursors" \
+                % (len(sucs), len(precs))
+            raise error.Abort(msg)
+        elif (len(precs) == 1 and len(sucs) > 1) and not split:
+            msg = "please add --split if you want to do a split"
+            raise error.Abort(msg)
+        elif len(sucs) == 1 and len(precs) > 1 and not fold:
+            msg = "please add --fold if you want to do a fold"
+            raise error.Abort(msg)
+        elif biject:
+            relations = [(p, (s,)) for p, s in zip(precs, sucs)]
+        else:
+            relations = [(p, sucs) for p in precs]
+
+        wdp = repo['.']
+
+        if len(sucs) == 1 and len(precs) == 1 and wdp in precs:
+            # '.' killed, so update to the successor
+            newnode = sucs[0]
+        else:
+            # update to an unkilled parent
+            newnode = wdp
+
+            while newnode in precs or newnode.obsolete():
+                newnode = newnode.parents()[0]
+
+        if newnode.node() != wdp.node():
+            if opts.get('keep', False):
+                # This is largely the same as the implementation in
+                # strip.stripcmd(). We might want to refactor this somewhere
+                # common at some point.
+
+                # only reset the dirstate for files that would actually change
+                # between the working context and uctx
+                descendantrevs = repo.revs("%d::." % newnode.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(newnode.node(), newnode.manifest(),
+                                      changedfiles)
+                dirstate.write(tr)
+            else:
+                bookactive = repo._activebookmark
+                # Active bookmark that we don't want to delete (with -B option)
+                # we deactivate and move it before the update and reactivate it
+                # after
+                movebookmark = bookactive and not bookmarks
+                if movebookmark:
+                    bookmarksmod.deactivate(repo)
+                    bmchanges = [(bookactive, newnode.node())]
+                    compat.bookmarkapplychanges(repo, tr, bmchanges)
+                commands.update(ui, repo, newnode.rev())
+                ui.status(_('working directory now at %s\n')
+                          % ui.label(str(newnode), 'evolve.node'))
+                if movebookmark:
+                    bookmarksmod.activate(repo, bookactive)
+
+        # update bookmarks
+        if bookmarks:
+            rewriteutil.deletebookmark(repo, repomarks, bookmarks)
+
+        # create markers
+        obsolete.createmarkers(repo, relations, metadata=metadata)
+
+        # informs that changeset have been pruned
+        ui.status(_('%i changesets pruned\n') % len(precs))
+
+        for ctx in repo.unfiltered().set('bookmark() and %ld', precs):
+            # used to be:
+            #
+            #   ldest = list(repo.set('max((::%d) - obsolete())', ctx))
+            #   if ldest:
+            #      c = ldest[0]
+            #
+            # but then revset took a lazy arrow in the knee and became much
+            # slower. The new forms makes as much sense and a much faster.
+            for dest in ctx.ancestors():
+                if not dest.obsolete():
+                    bookmarksupdater = rewriteutil.bookmarksupdater
+                    updatebookmarks = bookmarksupdater(repo, ctx.node(), tr)
+                    updatebookmarks(dest.node())
+                    break
+
+        tr.close()
+    finally:
+        lockmod.release(tr, lock, wlock)
+
+@eh.command(
+    '^split',
+    [('r', 'rev', [], _("revision to split")),
+    ] + commitopts + commitopts2 + commitopts3,
+    _('hg split [OPTION]... [-r] REV'))
+def cmdsplit(ui, repo, *revs, **opts):
+    """split a changeset into smaller changesets
+
+    By default, split the current revision by prompting for all its hunks to be
+    redistributed into new changesets.
+
+    Use --rev to split a given changeset instead.
+    """
+    _resolveoptions(ui, opts)
+    tr = wlock = lock = None
+    newcommits = []
+
+    revarg = (list(revs) + opts.get('rev')) or ['.']
+    if len(revarg) != 1:
+        msg = _("more than one revset is given")
+        hnt = _("use either `hg split <rs>` or `hg split --rev <rs>`, not both")
+        raise error.Abort(msg, hint=hnt)
+
+    rev = scmutil.revsingle(repo, revarg[0])
+    try:
+        wlock = repo.wlock()
+        lock = repo.lock()
+        cmdutil.bailifchanged(repo)
+        tr = repo.transaction('split')
+        ctx = repo[rev]
+        r = ctx.rev()
+        disallowunstable = not obsolete.isenabled(repo,
+                                                  obsolete.allowunstableopt)
+        if disallowunstable:
+            # XXX We should check head revs
+            if repo.revs("(%d::) - %d", rev, rev):
+                raise error.Abort(_("cannot split commit: %s not a head") % ctx)
+
+        if len(ctx.parents()) > 1:
+            raise error.Abort(_("cannot split merge commits"))
+        prev = ctx.p1()
+        bmupdate = rewriteutil.bookmarksupdater(repo, ctx.node(), tr)
+        bookactive = repo._activebookmark
+        if bookactive is not None:
+            repo.ui.status(_("(leaving bookmark %s)\n") % repo._activebookmark)
+        bookmarksmod.deactivate(repo)
+
+        # Prepare the working directory
+        rewriteutil.presplitupdate(repo, ui, prev, ctx)
+
+        def haschanges():
+            modified, added, removed, deleted = repo.status()[:4]
+            return modified or added or removed or deleted
+        msg = ("HG: This is the original pre-split commit message. "
+               "Edit it as appropriate.\n\n")
+        msg += ctx.description()
+        opts['message'] = msg
+        opts['edit'] = True
+        if not opts['user']:
+            opts['user'] = ctx.user()
+        while haschanges():
+            pats = ()
+            cmdutil.dorecord(ui, repo, commands.commit, 'commit', False,
+                             cmdutil.recordfilter, *pats, **opts)
+            # TODO: Does no seem like the best way to do this
+            # We should make dorecord return the newly created commit
+            newcommits.append(repo['.'])
+            if haschanges():
+                if ui.prompt('Done splitting? [yN]', default='n') == 'y':
+                    commands.commit(ui, repo, **opts)
+                    newcommits.append(repo['.'])
+                    break
+            else:
+                ui.status(_("no more change to split\n"))
+
+        if newcommits:
+            tip = repo[newcommits[-1]]
+            bmupdate(tip.node())
+            if bookactive is not None:
+                bookmarksmod.activate(repo, bookactive)
+            obsolete.createmarkers(repo, [(repo[r], newcommits)])
+        tr.close()
+    finally:
+        lockmod.release(tr, lock, wlock)
+
+@eh.command(
+    '^touch',
+    [('r', 'rev', [], 'revision to update'),
+     ('D', 'duplicate', False,
+      'do not mark the new revision as successor of the old one'),
+     ('A', 'allowdivergence', False,
+      'mark the new revision as successor of the old one potentially creating '
+      'divergence')],
+    # allow to choose the seed ?
+    _('[-r] revs'))
+def touch(ui, repo, *revs, **opts):
+    """create successors that are identical to their predecessors except
+    for the changeset ID
+
+    This is used to "resurrect" changesets
+    """
+    duplicate = opts['duplicate']
+    allowdivergence = opts['allowdivergence']
+    revs = list(revs)
+    revs.extend(opts['rev'])
+    if not revs:
+        revs = ['.']
+    revs = scmutil.revrange(repo, revs)
+    if not revs:
+        ui.write_err('no revision to touch\n')
+        return 1
+    if not duplicate and repo.revs('public() and %ld', revs):
+        raise error.Abort("can't touch public revision")
+    tmpl = utility.shorttemplate
+    displayer = cmdutil.show_changeset(ui, repo, {'template': tmpl})
+    wlock = lock = tr = None
+    try:
+        wlock = repo.wlock()
+        lock = repo.lock()
+        tr = repo.transaction('touch')
+        revs.sort() # ensure parent are run first
+        newmapping = {}
+        for r in revs:
+            ctx = repo[r]
+            extra = ctx.extra().copy()
+            extra['__touch-noise__'] = random.randint(0, 0xffffffff)
+            # search for touched parent
+            p1 = ctx.p1().node()
+            p2 = ctx.p2().node()
+            p1 = newmapping.get(p1, p1)
+            p2 = newmapping.get(p2, p2)
+
+            if not (duplicate or allowdivergence):
+                # The user hasn't yet decided what to do with the revived
+                # cset, let's ask
+                sset = compat.successorssets(repo, ctx.node())
+                nodivergencerisk = (len(sset) == 0 or
+                                    (len(sset) == 1 and
+                                     len(sset[0]) == 1 and
+                                     repo[sset[0][0]].rev() == ctx.rev()
+                                    ))
+                if nodivergencerisk:
+                    duplicate = False
+                else:
+                    displayer.show(ctx)
+                    index = ui.promptchoice(
+                        _("reviving this changeset will create divergence"
+                          " unless you make a duplicate.\n(a)llow divergence or"
+                          " (d)uplicate the changeset? $$ &Allowdivergence $$ "
+                          "&Duplicate"), 0)
+                    choice = ['allowdivergence', 'duplicate'][index]
+                    if choice == 'allowdivergence':
+                        duplicate = False
+                    else:
+                        duplicate = True
+
+            extradict = {'extra': extra}
+            new, unusedvariable = rewriteutil.rewrite(repo, ctx, [], ctx,
+                                                      [p1, p2],
+                                                      commitopts=extradict)
+            # store touched version to help potential children
+            newmapping[ctx.node()] = new
+
+            if not duplicate:
+                obsolete.createmarkers(repo, [(ctx, (repo[new],))])
+            phases.retractboundary(repo, tr, ctx.phase(), [new])
+            if ctx in repo[None].parents():
+                with repo.dirstate.parentchange():
+                    repo.dirstate.setparents(new, node.nullid)
+        tr.close()
+    finally:
+        lockmod.release(tr, lock, wlock)