Mercurial > evolve
view hgext3rd/evolve/evocommands.py @ 2729:69fe16428b0f
uncommit: add support for -U and -D
author | Pierre-Yves David <pierre-yves.david@octobus.net> |
---|---|
date | Tue, 11 Jul 2017 12:00:45 +0200 |
parents | 3c371aa16cb9 |
children | 7fbb7a5d359f |
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, ) 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')]] def _bookmarksupdater(repo, oldid, tr): """Return a callable update(newid) updating the current bookmark and bookmarks bound to oldid to newid. """ def updatebookmarks(newid): dirty = False oldbookmarks = repo.nodebookmarks(oldid) if oldbookmarks: for b in oldbookmarks: repo._bookmarks[b] = newid dirty = True if dirty: repo._bookmarks.recordchange(tr) return updatebookmarks @eh.command( 'amend|refresh', [('A', 'addremove', None, _('mark new/missing files as added/removed before committing')), ('e', 'edit', False, _('invoke editor on commit messages')), ('', '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. Returns 0 on success, 1 if nothing changed. """ opts = opts.copy() 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 = _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)