Mercurial > evolve
view hgext3rd/evolve/evocommands.py @ 2756:f4dd6e6d4c73
rewriteutil: create a rewriteutil module to host utility function
The ultimate goal of this module is to gather basic building block for rewriting
changesets. This is aimed at moving into core and being use by the core
extension.
author | Pierre-Yves David <pierre-yves.david@octobus.net> |
---|---|
date | Sat, 22 Jul 2017 23:40:28 +0200 |
parents | 4457aa1d81aa |
children | ddff53ecc00b |
line wrap: on
line source
# 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 from mercurial import ( cmdutil, commands, context, copies, error, lock as lockmod, node, obsolete, phases, scmutil, util, ) from mercurial.i18n import _ from . import ( exthelper, rewriteutil, ) 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)