uncommit.py
269 lines
| 9.6 KiB
| text/x-python
|
PythonLexer
/ hgext / uncommit.py
|
r34193 | # uncommit - undo the actions of a commit | ||
# | ||||
# Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com> | ||||
# Logilab SA <contact@logilab.fr> | ||||
# Pierre-Yves David <pierre-yves.david@ens-lyon.org> | ||||
# Patrick Mezard <patrick@mezard.eu> | ||||
# Copyright 2016 Facebook, Inc. | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
"""uncommit part or all of a local changeset (EXPERIMENTAL) | ||||
This command undoes the effect of a local commit, returning the affected | ||||
files to their uncommitted state. This means that files modified, added or | ||||
removed in the changeset will be left unchanged, and so will remain modified, | ||||
added and removed in the working directory. | ||||
""" | ||||
from __future__ import absolute_import | ||||
from mercurial.i18n import _ | ||||
from mercurial import ( | ||||
|
r34285 | cmdutil, | ||
|
r34193 | commands, | ||
context, | ||||
|
r41370 | copies as copiesmod, | ||
|
r34193 | error, | ||
node, | ||||
|
r35177 | obsutil, | ||
|
r35005 | pycompat, | ||
|
r34193 | registrar, | ||
|
r35244 | rewriteutil, | ||
|
r34193 | scmutil, | ||
|
r42218 | util, | ||
|
r34193 | ) | ||
cmdtable = {} | ||||
command = registrar.command(cmdtable) | ||||
|
r34759 | configtable = {} | ||
configitem = registrar.configitem(configtable) | ||||
configitem('experimental', 'uncommitondirtywdir', | ||||
default=False, | ||||
) | ||||
|
r41916 | configitem('experimental', 'uncommit.keep', | ||
default=False, | ||||
) | ||||
|
r34759 | |||
|
r34193 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
testedwith = 'ships-with-hg-core' | ||||
|
r43172 | def _commitfiltered(repo, ctx, match, keepcommit, message=None, user=None, | ||
date=None): | ||||
|
r34193 | """Recommit ctx with changed files not in match. Return the new | ||
node identifier, or None if nothing changed. | ||||
""" | ||||
base = ctx.p1() | ||||
# ctx | ||||
initialfiles = set(ctx.files()) | ||||
exclude = set(f for f in initialfiles if match(f)) | ||||
# No files matched commit, so nothing excluded | ||||
if not exclude: | ||||
return None | ||||
# return the p1 so that we don't create an obsmarker later | ||||
|
r36992 | if not keepcommit: | ||
|
r41442 | return ctx.p1().node() | ||
|
r34193 | |||
|
r41911 | files = (initialfiles - exclude) | ||
|
r34193 | # Filter copies | ||
|
r41370 | copied = copiesmod.pathcopies(base, ctx) | ||
|
r34193 | copied = dict((dst, src) for dst, src in copied.iteritems() | ||
if dst in files) | ||||
def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()): | ||||
if path not in contentctx: | ||||
return None | ||||
fctx = contentctx[path] | ||||
|
r35401 | mctx = context.memfilectx(repo, memctx, fctx.path(), fctx.data(), | ||
|
r34193 | fctx.islink(), | ||
fctx.isexec(), | ||||
|
r42161 | copysource=copied.get(path)) | ||
|
r34193 | return mctx | ||
|
r41911 | if not files: | ||
repo.ui.status(_("note: keeping empty commit\n")) | ||||
|
r43172 | if message is None: | ||
message = ctx.description() | ||||
if not user: | ||||
user = ctx.user() | ||||
if not date: | ||||
date = ctx.date() | ||||
|
r34193 | new = context.memctx(repo, | ||
parents=[base.node(), node.nullid], | ||||
|
r43172 | text=message, | ||
|
r34193 | files=files, | ||
filectxfn=filectxfn, | ||||
|
r43172 | user=user, | ||
date=date, | ||||
|
r34193 | extra=ctx.extra()) | ||
|
r38442 | return repo.commitctx(new) | ||
|
r34193 | |||
@command('uncommit', | ||||
|
r41916 | [('', 'keep', None, _('allow an empty commit after uncommiting')), | ||
|
r42026 | ('', 'allow-dirty-working-copy', False, | ||
|
r43206 | _('allow uncommit with outstanding changes')), | ||
(b'n', b'note', b'', _(b'store a note on uncommit'), _(b'TEXT')) | ||||
|
r43173 | ] + commands.walkopts + commands.commitopts + commands.commitopts2 | ||
+ commands.commitopts3, | ||||
|
r40329 | _('[OPTION]... [FILE]...'), | ||
helpcategory=command.CATEGORY_CHANGE_MANAGEMENT) | ||||
|
r34193 | def uncommit(ui, repo, *pats, **opts): | ||
"""uncommit part or all of a local changeset | ||||
This command undoes the effect of a local commit, returning the affected | ||||
files to their uncommitted state. This means that files modified or | ||||
deleted in the changeset will be left unchanged, and so will remain | ||||
modified in the working directory. | ||||
|
r36991 | |||
If no files are specified, the commit will be pruned, unless --keep is | ||||
given. | ||||
|
r34193 | """ | ||
|
r35005 | opts = pycompat.byteskwargs(opts) | ||
|
r34193 | |||
|
r43206 | cmdutil.checknotesize(ui, opts) | ||
|
r43173 | cmdutil.resolvecommitoptions(ui, opts) | ||
|
r34193 | with repo.wlock(), repo.lock(): | ||
|
r42025 | m, a, r, d = repo.status()[:4] | ||
isdirtypath = any(set(m + a + r + d) & set(pats)) | ||||
|
r42026 | allowdirtywcopy = (opts['allow_dirty_working_copy'] or | ||
repo.ui.configbool('experimental', 'uncommitondirtywdir')) | ||||
if not allowdirtywcopy and (not pats or isdirtypath): | ||||
|
r42025 | cmdutil.bailifchanged(repo, hint=_('requires ' | ||
|
r42026 | '--allow-dirty-working-copy to uncommit')) | ||
|
r34193 | old = repo['.'] | ||
|
r35244 | rewriteutil.precheck(repo, [old.rev()], 'uncommit') | ||
|
r34193 | if len(old.parents()) > 1: | ||
raise error.Abort(_("cannot uncommit merge changeset")) | ||||
|
r42218 | match = scmutil.match(old, pats, opts) | ||
# Check all explicitly given files; abort if there's a problem. | ||||
if match.files(): | ||||
s = old.status(old.p1(), match, listclean=True) | ||||
eligible = set(s.added) | set(s.modified) | set(s.removed) | ||||
badfiles = set(match.files()) - eligible | ||||
# Naming a parent directory of an eligible file is OK, even | ||||
# if not everything tracked in that directory can be | ||||
# uncommitted. | ||||
if badfiles: | ||||
|
r42224 | badfiles -= {f for f in util.dirs(eligible)} | ||
|
r42218 | |||
for f in sorted(badfiles): | ||||
if f in s.clean: | ||||
hint = _(b"file was not changed in working directory " | ||||
b"parent") | ||||
elif repo.wvfs.exists(f): | ||||
hint = _(b"file was untracked in working directory parent") | ||||
else: | ||||
hint = _(b"file does not exist") | ||||
raise error.Abort(_(b'cannot uncommit "%s"') | ||||
% scmutil.getuipathfn(repo)(f), hint=hint) | ||||
|
r34193 | with repo.transaction('uncommit'): | ||
|
r43172 | if not (opts[b'message'] or opts[b'logfile']): | ||
opts[b'message'] = old.description() | ||||
|
r43222 | message = cmdutil.logmessage(ui, opts) | ||
|
r43172 | |||
|
r41916 | keepcommit = pats | ||
if not keepcommit: | ||||
if opts.get('keep') is not None: | ||||
keepcommit = opts.get('keep') | ||||
else: | ||||
keepcommit = ui.configbool('experimental', 'uncommit.keep') | ||||
|
r43172 | newid = _commitfiltered(repo, old, match, keepcommit, | ||
message=message, user=opts.get(b'user'), | ||||
date=opts.get(b'date')) | ||||
|
r34193 | if newid is None: | ||
ui.status(_("nothing to uncommit\n")) | ||||
return 1 | ||||
mapping = {} | ||||
if newid != old.p1().node(): | ||||
# Move local changes on filtered changeset | ||||
mapping[old.node()] = (newid,) | ||||
else: | ||||
# Fully removed the old commit | ||||
mapping[old.node()] = () | ||||
with repo.dirstate.parentchange(): | ||||
|
r42103 | scmutil.movedirstate(repo, repo[newid], match) | ||
|
r35177 | |||
|
r41371 | scmutil.cleanupnodes(repo, mapping, 'uncommit', fixphase=True) | ||
|
r35177 | def predecessormarkers(ctx): | ||
"""yields the obsolete markers marking the given changeset as a successor""" | ||||
for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()): | ||||
yield obsutil.marker(ctx.repo(), data) | ||||
|
r40331 | @command('unamend', [], helpcategory=command.CATEGORY_CHANGE_MANAGEMENT, | ||
helpbasic=True) | ||||
|
r35177 | def unamend(ui, repo, **opts): | ||
|
r35827 | """undo the most recent amend operation on a current changeset | ||
|
r35177 | |||
This command will roll back to the previous version of a changeset, | ||||
leaving working directory in state in which it was before running | ||||
`hg amend` (e.g. files modified as part of an amend will be | ||||
marked as modified `hg status`) | ||||
""" | ||||
unfi = repo.unfiltered() | ||||
|
r35201 | with repo.wlock(), repo.lock(), repo.transaction('unamend'): | ||
|
r35177 | |||
|
r35201 | # identify the commit from which to unamend | ||
curctx = repo['.'] | ||||
|
r35177 | |||
|
r35451 | rewriteutil.precheck(repo, [curctx.rev()], 'unamend') | ||
|
r35177 | |||
# identify the commit to which to unamend | ||||
markers = list(predecessormarkers(curctx)) | ||||
if len(markers) != 1: | ||||
e = _("changeset must have one predecessor, found %i predecessors") | ||||
raise error.Abort(e % len(markers)) | ||||
prednode = markers[0].prednode() | ||||
predctx = unfi[prednode] | ||||
# add an extra so that we get a new hash | ||||
# note: allowing unamend to undo an unamend is an intentional feature | ||||
extras = predctx.extra() | ||||
|
r35201 | extras['unamend_source'] = curctx.hex() | ||
|
r35177 | |||
def filectxfn(repo, ctx_, path): | ||||
try: | ||||
return predctx.filectx(path) | ||||
except KeyError: | ||||
return None | ||||
# Make a new commit same as predctx | ||||
newctx = context.memctx(repo, | ||||
parents=(predctx.p1(), predctx.p2()), | ||||
text=predctx.description(), | ||||
files=predctx.files(), | ||||
filectxfn=filectxfn, | ||||
user=predctx.user(), | ||||
date=predctx.date(), | ||||
extra=extras) | ||||
|
r38442 | newprednode = repo.commitctx(newctx) | ||
|
r35177 | newpredctx = repo[newprednode] | ||
dirstate = repo.dirstate | ||||
with dirstate.parentchange(): | ||||
|
r42103 | scmutil.movedirstate(repo, newpredctx) | ||
|
r35177 | |||
mapping = {curctx.node(): (newprednode,)} | ||||
|
r38442 | scmutil.cleanupnodes(repo, mapping, 'unamend', fixphase=True) | ||