D5792: uncommit: added interactive mode(issue6062)
taapas1128 (Taapas Agrawal)
phabricator at mercurial-scm.org
Sat Feb 2 13:11:59 EST 2019
taapas1128 updated this revision to Diff 13701.
REPOSITORY
rHG Mercurial
CHANGES SINCE LAST UPDATE
https://phab.mercurial-scm.org/D5792?vs=13669&id=13701
REVISION DETAIL
https://phab.mercurial-scm.org/D5792
AFFECTED FILES
hgext/uncommit.py
tests/test-uncommit.t
CHANGE DETAILS
diff --git a/tests/test-uncommit.t b/tests/test-uncommit.t
--- a/tests/test-uncommit.t
+++ b/tests/test-uncommit.t
@@ -1,6 +1,8 @@
Test uncommit - set up the config
$ cat >> $HGRCPATH <<EOF
+ > [ui]
+ > interactive = true
> [experimental]
> evolution.createmarkers=True
> evolution.allowunstable=True
@@ -34,6 +36,7 @@
options ([+] can be repeated):
+ -i --interactive interactive mode to uncommit
--keep allow an empty commit after uncommiting
-I --include PATTERN [+] include names matching the given patterns
-X --exclude PATTERN [+] exclude names matching the given patterns
@@ -398,3 +401,15 @@
|/
o 0:ea4e33293d4d274a2ba73150733c2612231f398c a 1
+Test for interactive mode
+ $ hg init repo3
+ $ touch x
+ $ hg add x
+ $ hg commit -m "added x"
+ $ hg uncommit -i<<EOF
+ > y
+ > EOF
+ diff --git a/x b/x
+ new file mode 100644
+ examine changes to 'x'? [Ynesfdaq?] y
+
diff --git a/hgext/uncommit.py b/hgext/uncommit.py
--- a/hgext/uncommit.py
+++ b/hgext/uncommit.py
@@ -28,11 +28,14 @@
copies,
error,
node,
+ obsolete,
obsutil,
+ patch,
pycompat,
registrar,
rewriteutil,
scmutil,
+ util,
)
cmdtable = {}
@@ -45,6 +48,8 @@
default=False,
)
+stringio = util.stringio
+
# 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
@@ -135,8 +140,107 @@
src = None
ds.copy(src, dst)
+
+def _uncommitdirstate(repo, oldctx, match, interactive):
+ """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())
+ if interactive:
+ # In interactive cases, we will find the status between oldctx and ctx
+ # and considering only the files which are changed between oldctx and
+ # ctx, and the status of what changed between oldctx and ctx will help
+ # us in defining the exact behavior
+ m, a, r = repo.status(oldctx, ctx, match=match)[:3]
+ for f in m:
+ # These are files which are modified between oldctx and ctx which
+ # contains two cases: 1) Were modified in oldctx and some
+ # modifications are uncommitted
+ # 2) Were added in oldctx but some part is uncommitted (this cannot
+ # contain the case when added files are uncommitted completely as
+ # that will result in status as removed not modified.)
+ # Also any modifications to a removed file will result the status as
+ # added, so we have only two cases. So in either of the cases, the
+ # resulting status can be modified or clean.
+ if ds[f] == 'r':
+ # But the file is removed in the working directory, leaving that
+ # as removed
+ continue
+ ds.normallookup(f)
+
+ for f in a:
+ # These are the files which are added between oldctx and ctx(new
+ # one), which means the files which were removed in oldctx
+ # but uncommitted completely while making the ctx
+ # This file should be marked as removed if the working directory
+ # does not adds it back. If it's adds it back, we do a normallookup.
+ # The file can't be removed in working directory, because it was
+ # removed in oldctx
+ if ds[f] == 'a':
+ ds.normallookup(f)
+ continue
+ ds.remove(f)
+
+ for f in r:
+ # These are files which are removed between oldctx and ctx, which
+ # means the files which were added in oldctx and were completely
+ # uncommitted in ctx. If a added file is partially uncommitted, that
+ # would have resulted in modified status, not removed.
+ # So a file added in a commit, and uncommitting that addition must
+ # result in file being stated as unknown.
+ if ds[f] == 'r':
+ # The working directory say it's removed, so lets make the file
+ # unknown
+ ds.drop(f)
+ continue
+ ds.add(f)
+ else:
+ 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 = {}
+ if interactive:
+ # Interactive had different meaning of the variables so restoring the
+ # original meaning to use them
+ m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
+ 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)
+
@command('uncommit',
- [('', 'keep', False, _('allow an empty commit after uncommiting')),
+ [('i', 'interactive', False, _('interactive mode to uncommit')),
+ ('', 'keep', False, _('allow an empty commit after uncommiting')),
] + commands.walkopts,
_('[OPTION]... [FILE]...'),
helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
@@ -152,6 +256,7 @@
given.
"""
opts = pycompat.byteskwargs(opts)
+ interactive = opts.get('interactive')
with repo.wlock(), repo.lock():
@@ -167,6 +272,10 @@
match = scmutil.match(old, pats, opts)
keepcommit = opts.get('keep') or pats
newid = _commitfiltered(repo, old, match, keepcommit)
+ if interactive:
+ match = scmutil.match(old, pats, opts)
+ newid = _interactiveuncommit(ui, repo, old, match)
+
if newid is None:
ui.status(_("nothing to uncommit\n"))
return 1
@@ -183,8 +292,101 @@
with repo.dirstate.parentchange():
repo.dirstate.setparents(newid, node.nullid)
- s = old.p1().status(old, match=match)
- _fixdirstate(repo, old, repo[newid], s)
+ _uncommitdirstate(repo, old, match, interactive)
+
+def _interactiveuncommit(ui, repo, old, match):
+ """ The function which contains all the logic for interactively uncommiting
+ a commit. This function makes a temporary commit with the chunks which user
+ selected to uncommit. After that the diff of the parent and that commit is
+ applied to the working directory and committed again which results in the
+ new commit which should be one after uncommitted.
+ """
+
+ # create a temporary commit with hunks user selected
+ tempnode = _createtempcommit(ui, repo, old, match)
+
+ diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
+ diffopts.nodates = True
+ diffopts.git = True
+ fp = stringio()
+ for chunk, label in patch.diffui(repo, tempnode, old.node(), None,
+ opts=diffopts):
+ fp.write(chunk)
+
+ fp.seek(0)
+ newnode = _patchtocommit(ui, repo, old, fp)
+ # creating obs marker temp -> ()
+ obsolete.createmarkers(repo, [(repo[tempnode], ())], operation="uncommit")
+ return newnode
+def _createtempcommit(ui, repo, old, match):
+ """ Creates a temporary commit for `uncommit --interative` which contains
+ the hunks which were selected by the user to uncommit.
+ """
+
+ pold = old.p1()
+ # The logic to interactively selecting something copied from
+ # cmdutil.revert()
+ diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
+ diffopts.nodates = True
+ diffopts.git = True
+ diff = patch.diff(repo, pold.node(), old.node(), match, opts=diffopts)
+ originalchunks = patch.parsepatch(diff)
+ # XXX: The interactive selection is buggy and does not let you
+ # uncommit a removed file partially.
+ # TODO: wrap the operations in mercurial/patch.py and mercurial/crecord.py
+ # to add uncommit as an operation taking care of BC.
+ chunks, opts = cmdutil.recordfilter(repo.ui, originalchunks,
+ operation='discard')
+ if not chunks:
+ raise error.Abort(_("nothing selected to uncommit"))
+ fp = stringio()
+ for c in chunks:
+ c.write(fp)
+
+ fp.seek(0)
+ oldnode = node.hex(old.node())[:12]
+ message = 'temporary commit for uncommiting %s' % oldnode
+ tempnode = _patchtocommit(ui, repo, old, fp, message, oldnode)
+ return tempnode
+
+def _patchtocommit(ui, repo, old, fp, message=None, extras=None):
+ """ A function which will apply the patch to the working directory and
+ make a commit whose parents are same as that of old argument. The message
+ argument tells us whether to use the message of the old commit or a
+ different message which is passed. Returns the node of new commit made.
+ """
+ pold = old.p1()
+ parents = (old.p1().node(), old.p2().node())
+ date = old.date()
+ branch = old.branch()
+ user = old.user()
+ extra = old.extra()
+ if extras:
+ extra['uncommit_source'] = extras
+ if not message:
+ message = old.description()
+ store = patch.filestore()
+ try:
+ files = set()
+ try:
+ patch.patchrepo(ui, repo, pold, store, fp, 1, '',
+ files=files, eolmode=None)
+ except patch.PatchError as err:
+ raise error.Abort(str(err))
+
+ finally:
+ del fp
+
+ memctx = context.memctx(repo, parents, message, files=files,
+ filectxfn=store,
+ user=user,
+ date=date,
+ branch=branch,
+ extra=extra)
+ newcm = memctx.commit()
+ finally:
+ store.close()
+ return newcm
def predecessormarkers(ctx):
"""yields the obsolete markers marking the given changeset as a successor"""
To: taapas1128, #hg-reviewers
Cc: pulkit, lothiraldan, mercurial-devel
More information about the Mercurial-devel
mailing list