Mercurial > evolve
changeset 2772:394b836e475b
commands: rewrite the 'evocommands' module to 'cmdrewrite'
The remaining commands should get their own module, so we rename the module for
clarity. We update the module comments to better fits its purpose.
author | Pierre-Yves David <pierre-yves.david@octobus.net> |
---|---|
date | Sun, 23 Jul 2017 17:28:02 +0200 |
parents | 6044bd16bfb7 |
children | 9e363e8c1c06 |
files | hgext3rd/evolve/__init__.py hgext3rd/evolve/cmdrewrite.py hgext3rd/evolve/evocommands.py hgext3rd/evolve/hack/inhibit.py |
diffstat | 4 files changed, 934 insertions(+), 936 deletions(-) [+] |
line wrap: on
line diff
--- a/hgext3rd/evolve/__init__.py Sun Jul 23 07:34:18 2017 +0200 +++ b/hgext3rd/evolve/__init__.py Sun Jul 23 17:28:02 2017 +0200 @@ -276,7 +276,7 @@ checkheads, compat, debugcmd, - evocommands, + cmdrewrite, exthelper, metadata, obscache, @@ -314,8 +314,8 @@ _unpack = struct.unpack aliases, entry = cmdutil.findcmd('commit', commands.table) -commitopts3 = evocommands.commitopts3 -interactiveopt = evocommands.interactiveopt +commitopts3 = cmdrewrite.commitopts3 +interactiveopt = cmdrewrite.interactiveopt _bookmarksupdater = rewriteutil.bookmarksupdater rewrite = rewriteutil.rewrite @@ -335,7 +335,7 @@ eh.merge(obshistory.eh) eh.merge(templatekw.eh) eh.merge(compat.eh) -eh.merge(evocommands.eh) +eh.merge(cmdrewrite.eh) uisetup = eh.final_uisetup extsetup = eh.final_extsetup reposetup = eh.final_reposetup @@ -1858,7 +1858,7 @@ with repo.dirstate.parentchange(): repo.dirstate.setparents(divergent.node(), node.nullid) oldlen = len(repo) - evocommands.amend(ui, repo, message='', logfile='') + cmdrewrite.amend(ui, repo, message='', logfile='') if oldlen == len(repo): new = divergent # no changes @@ -2164,7 +2164,7 @@ kwargs['new'] = [] kwargs['succ'] = [] kwargs['biject'] = False - return evocommands.cmdprune(ui, repo, *revs, **kwargs) + return cmdrewrite.cmdprune(ui, repo, *revs, **kwargs) @eh.wrapcommand('graft') def graftwrapper(orig, ui, repo, *revs, **kwargs):
--- /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)
--- a/hgext3rd/evolve/evocommands.py Sun Jul 23 07:34:18 2017 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,929 +0,0 @@ -# Module dedicated to host new commands added by the evolve extensions -# -# 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. -# -# Some command still live in evolve/__init__.py - -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)
--- a/hgext3rd/evolve/hack/inhibit.py Sun Jul 23 07:34:18 2017 +0200 +++ b/hgext3rd/evolve/hack/inhibit.py Sun Jul 23 17:28:02 2017 +0200 @@ -106,7 +106,7 @@ 'keep': None, 'biject': False, } - evolve.evocommands.cmdprune(ui, repo, **optsdict) + evolve.cmdrewrite.cmdprune(ui, repo, **optsdict) # obsolescence inhibitor ########################