uncommit.py
318 lines
| 9.7 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, | ||
r43923 | pathutil, | |||
|
r35005 | pycompat, | ||
|
r34193 | registrar, | ||
|
r35244 | rewriteutil, | ||
|
r34193 | scmutil, | ||
) | ||||
cmdtable = {} | ||||
command = registrar.command(cmdtable) | ||||
|
r34759 | configtable = {} | ||
configitem = registrar.configitem(configtable) | ||||
|
r43346 | configitem( | ||
|
r46554 | b'experimental', | ||
b'uncommitondirtywdir', | ||||
default=False, | ||||
|
r34759 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'experimental', | ||
b'uncommit.keep', | ||||
default=False, | ||||
|
r41916 | ) | ||
|
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. | ||||
|
r43347 | testedwith = b'ships-with-hg-core' | ||
|
r34193 | |||
|
r43346 | |||
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()) | ||||
|
r44937 | exclude = {f for f in initialfiles if match(f)} | ||
|
r34193 | |||
# 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 | |||
|
r43346 | files = initialfiles - exclude | ||
|
r34193 | # Filter copies | ||
|
r41370 | copied = copiesmod.pathcopies(base, ctx) | ||
|
r44937 | copied = { | ||
dst: src for dst, src in pycompat.iteritems(copied) if dst in files | ||||
} | ||||
|
r43346 | |||
|
r34193 | def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()): | ||
if path not in contentctx: | ||||
return None | ||||
fctx = contentctx[path] | ||||
|
r43346 | mctx = context.memfilectx( | ||
repo, | ||||
memctx, | ||||
fctx.path(), | ||||
fctx.data(), | ||||
fctx.islink(), | ||||
fctx.isexec(), | ||||
copysource=copied.get(path), | ||||
) | ||||
|
r34193 | return mctx | ||
|
r41911 | if not files: | ||
|
r43347 | repo.ui.status(_(b"note: keeping empty commit\n")) | ||
|
r41911 | |||
|
r43172 | if message is None: | ||
message = ctx.description() | ||||
if not user: | ||||
user = ctx.user() | ||||
if not date: | ||||
date = ctx.date() | ||||
|
r43346 | new = context.memctx( | ||
repo, | ||||
parents=[base.node(), node.nullid], | ||||
text=message, | ||||
files=files, | ||||
filectxfn=filectxfn, | ||||
user=user, | ||||
date=date, | ||||
extra=ctx.extra(), | ||||
) | ||||
|
r38442 | return repo.commitctx(new) | ||
|
r34193 | |||
|
r43346 | |||
@command( | ||||
|
r43347 | b'uncommit', | ||
|
r43346 | [ | ||
|
r43347 | (b'', b'keep', None, _(b'allow an empty commit after uncommitting')), | ||
|
r43346 | ( | ||
|
r43347 | b'', | ||
b'allow-dirty-working-copy', | ||||
|
r43346 | False, | ||
|
r43347 | _(b'allow uncommit with outstanding changes'), | ||
|
r43346 | ), | ||
(b'n', b'note', b'', _(b'store a note on uncommit'), _(b'TEXT')), | ||||
] | ||||
+ commands.walkopts | ||||
+ commands.commitopts | ||||
+ commands.commitopts2 | ||||
|
r43173 | + commands.commitopts3, | ||
|
r43347 | _(b'[OPTION]... [FILE]...'), | ||
|
r43346 | 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(): | ||
|
r44042 | st = repo.status() | ||
m, a, r, d = st.modified, st.added, st.removed, st.deleted | ||||
|
r42025 | isdirtypath = any(set(m + a + r + d) & set(pats)) | ||
|
r43346 | allowdirtywcopy = opts[ | ||
|
r43347 | b'allow_dirty_working_copy' | ||
] or repo.ui.configbool(b'experimental', b'uncommitondirtywdir') | ||||
|
r42026 | if not allowdirtywcopy and (not pats or isdirtypath): | ||
|
r43346 | cmdutil.bailifchanged( | ||
repo, | ||||
|
r43387 | hint=_(b'requires --allow-dirty-working-copy to uncommit'), | ||
|
r43346 | ) | ||
|
r43347 | old = repo[b'.'] | ||
rewriteutil.precheck(repo, [old.rev()], b'uncommit') | ||||
|
r34193 | if len(old.parents()) > 1: | ||
|
r43347 | raise error.Abort(_(b"cannot uncommit merge changeset")) | ||
|
r34193 | |||
|
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: | ||||
r43923 | badfiles -= {f for f in pathutil.dirs(eligible)} | |||
|
r42218 | |||
for f in sorted(badfiles): | ||||
if f in s.clean: | ||||
|
r43346 | hint = _( | ||
|
r43387 | b"file was not changed in working directory parent" | ||
|
r43346 | ) | ||
|
r42218 | elif repo.wvfs.exists(f): | ||
hint = _(b"file was untracked in working directory parent") | ||||
else: | ||||
hint = _(b"file does not exist") | ||||
|
r43346 | raise error.Abort( | ||
_(b'cannot uncommit "%s"') % scmutil.getuipathfn(repo)(f), | ||||
hint=hint, | ||||
) | ||||
|
r42218 | |||
|
r43347 | with repo.transaction(b'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: | ||||
|
r43347 | if opts.get(b'keep') is not None: | ||
keepcommit = opts.get(b'keep') | ||||
|
r41916 | else: | ||
|
r43347 | keepcommit = ui.configbool( | ||
b'experimental', b'uncommit.keep' | ||||
) | ||||
|
r43346 | newid = _commitfiltered( | ||
repo, | ||||
old, | ||||
match, | ||||
keepcommit, | ||||
message=message, | ||||
user=opts.get(b'user'), | ||||
date=opts.get(b'date'), | ||||
) | ||||
|
r34193 | if newid is None: | ||
|
r43347 | ui.status(_(b"nothing to uncommit\n")) | ||
|
r34193 | 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 | |||
|
r43347 | scmutil.cleanupnodes(repo, mapping, b'uncommit', fixphase=True) | ||
|
r41371 | |||
|
r43346 | |||
|
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) | ||||
|
r43346 | |||
@command( | ||||
|
r43347 | b'unamend', | ||
|
r43346 | [], | ||
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() | ||||
|
r43347 | with repo.wlock(), repo.lock(), repo.transaction(b'unamend'): | ||
|
r35177 | |||
|
r35201 | # identify the commit from which to unamend | ||
|
r43347 | curctx = repo[b'.'] | ||
|
r35177 | |||
|
r43347 | rewriteutil.precheck(repo, [curctx.rev()], b'unamend') | ||
|
r35177 | |||
# identify the commit to which to unamend | ||||
markers = list(predecessormarkers(curctx)) | ||||
if len(markers) != 1: | ||||
|
r43347 | e = _(b"changeset must have one predecessor, found %i predecessors") | ||
|
r35177 | 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() | ||||
|
r43347 | extras[b'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 | ||||
|
r43346 | 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,)} | ||||
|
r43347 | scmutil.cleanupnodes(repo, mapping, b'unamend', fixphase=True) | ||