Mercurial > evolve
changeset 2941:b0458b9e1b47
uncommit: add an interactive option to uncommit
This patchs adds an interactive flag to uncommit command which lets you choose
hunks interactively. The implementation is as follows:
1) prompt user to select hunks to uncommit interactively
2) make a temporary commit of the hunks user selected
3) calculate the diff of temporary commit and changeset to be uncommitted
4) commit that diff which results in the changeset which should be there after
uncommiting
5) create obsmarkers as required, phases and bookmarks handling
6) reroute the working directory
author | Pulkit Goyal <7895pulkit@gmail.com> |
---|---|
date | Thu, 14 Sep 2017 17:23:24 +0530 |
parents | 89b205e5271e |
children | 10194206acc7 |
files | hgext3rd/evolve/cmdrewrite.py tests/test-uncommit-interactive.t |
diffstat | 2 files changed, 847 insertions(+), 2 deletions(-) [+] |
line wrap: on
line diff
--- a/hgext3rd/evolve/cmdrewrite.py Mon Sep 04 15:54:39 2017 +0200 +++ b/hgext3rd/evolve/cmdrewrite.py Thu Sep 14 17:23:24 2017 +0530 @@ -24,6 +24,7 @@ lock as lockmod, node, obsolete, + patch, phases, scmutil, util, @@ -44,6 +45,7 @@ commitopts = commands.commitopts commitopts2 = commands.commitopts2 mergetoolopts = commands.mergetoolopts +stringio = util.stringio # option added by evolve @@ -234,6 +236,7 @@ @eh.command( '^uncommit', [('a', 'all', None, _('uncommit all changes when no arguments given')), + ('i', 'interactive', False, _('interactive mode to uncommit (EXPERIMENTAL)')), ('r', 'rev', '', _('revert commit content to REV instead')), ] + commands.walkopts + commitopts + commitopts2 + commitopts3, _('[OPTION]... [NAME]')) @@ -252,10 +255,16 @@ revision. It still does not change the content of your file in the working directory. + .. container:: verbose + + The --interactive option lets you select hunks interactively to uncommit. + You can uncommit parts of file using this option. + Return 0 if changed files are uncommitted. """ _resolveoptions(ui, opts) # process commitopts3 + interactive = opts.get('interactive') wlock = lock = tr = None try: wlock = repo.wlock() @@ -287,7 +296,11 @@ # Recommit the filtered changeset tr = repo.transaction('uncommit') updatebookmarks = rewriteutil.bookmarksupdater(repo, old.node(), tr) - if True: + if interactive: + opts['all'] = True + match = scmutil.match(old, pats, opts) + newid = _interactiveuncommit(ui, repo, old, match) + else: newid = None includeorexclude = opts.get('include') or opts.get('exclude') if (pats or includeorexclude or opts.get('all')): @@ -301,7 +314,7 @@ 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(): @@ -315,6 +328,101 @@ finally: lockmod.release(tr, lock, wlock) +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], ())]) + 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 + @eh.command( '^fold|squash', [('r', 'rev', [], _("revision to fold")),
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-uncommit-interactive.t Thu Sep 14 17:23:24 2017 +0530 @@ -0,0 +1,737 @@ +================================================ +|| The test for `hg uncommit --interactive` || +================================================ + +Repo Setup +============ + + $ cat >> $HGRCPATH <<EOF + > [ui] + > interactive = true + > [extensions] + > EOF + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH + + $ glog() { + > hg log -G --template '{rev}:{node|short}@{branch}({separate("/", obsolete, phase)}) {desc|firstline}\n' "$@" + > } + + $ hg init repo + $ cd repo + + $ touch a + $ cat >> a << EOF + > 1 + > 2 + > 3 + > 4 + > 5 + > EOF + + $ hg add a + $ hg ci -m "The base commit" + +Make sure aborting the interactive selection does no magic +---------------------------------------------------------- + + $ hg status + $ hg uncommit -i<<EOF + > q + > EOF + diff --git a/a b/a + new file mode 100644 + examine changes to 'a'? [Ynesfdaq?] q + + abort: user quit + [255] + $ hg status + +Make a commit with multiple hunks +--------------------------------- + + $ cat > a << EOF + > -2 + > -1 + > 0 + > 1 + > 2 + > 3 + > foo + > bar + > 4 + > 5 + > babar + > EOF + + $ hg diff + diff -r 7733902a8d94 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,5 +1,11 @@ + +-2 + +-1 + +0 + 1 + 2 + 3 + +foo + +bar + 4 + 5 + +babar + + $ hg ci -m "another one" + +Not deselecting anything to uncommit +==================================== + + $ hg uncommit -i<<EOF + > y + > n + > n + > n + > EOF + diff --git a/a b/a + 3 hunks, 6 lines changed + examine changes to 'a'? [Ynesfdaq?] y + + @@ -1,3 +1,6 @@ + +-2 + +-1 + +0 + 1 + 2 + 3 + discard change 1/3 to 'a'? [Ynesfdaq?] n + + @@ -1,5 +4,7 @@ + 1 + 2 + 3 + +foo + +bar + 4 + 5 + discard change 2/3 to 'a'? [Ynesfdaq?] n + + @@ -4,2 +9,3 @@ + 4 + 5 + +babar + discard change 3/3 to 'a'? [Ynesfdaq?] n + + abort: nothing selected to uncommit + [255] + $ hg status + +Uncommit a chunk +================ + + $ hg uncommit -i<<EOF + > y + > y + > n + > n + > EOF + diff --git a/a b/a + 3 hunks, 6 lines changed + examine changes to 'a'? [Ynesfdaq?] y + + @@ -1,3 +1,6 @@ + +-2 + +-1 + +0 + 1 + 2 + 3 + discard change 1/3 to 'a'? [Ynesfdaq?] y + + @@ -1,5 +4,7 @@ + 1 + 2 + 3 + +foo + +bar + 4 + 5 + discard change 2/3 to 'a'? [Ynesfdaq?] n + + @@ -4,2 +9,3 @@ + 4 + 5 + +babar + discard change 3/3 to 'a'? [Ynesfdaq?] n + +The unselected part should be in the diff +----------------------------------------- + + $ hg diff + diff -r 678a59e5ff90 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,3 +1,6 @@ + +-2 + +-1 + +0 + 1 + 2 + 3 + +The commit should contain the rest of part +------------------------------------------ + + $ hg exp + # HG changeset patch + # User test + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 678a59e5ff90754d5e94719bd82ad169be773c21 + # Parent 7733902a8d94c789ca81d866bea1893d79442db6 + another one + + diff -r 7733902a8d94 -r 678a59e5ff90 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,5 +1,8 @@ + 1 + 2 + 3 + +foo + +bar + 4 + 5 + +babar + +Uncommiting on dirty working directory +====================================== + + $ hg status + M a + $ hg diff + diff -r 678a59e5ff90 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,3 +1,6 @@ + +-2 + +-1 + +0 + 1 + 2 + 3 + + $ hg uncommit -i<<EOF + > y + > n + > y + > EOF + diff --git a/a b/a + 2 hunks, 3 lines changed + examine changes to 'a'? [Ynesfdaq?] y + + @@ -1,5 +1,7 @@ + 1 + 2 + 3 + +foo + +bar + 4 + 5 + discard change 1/2 to 'a'? [Ynesfdaq?] n + + @@ -4,2 +6,3 @@ + 4 + 5 + +babar + discard change 2/2 to 'a'? [Ynesfdaq?] y + + patching file a + Hunk #1 succeeded at 2 with fuzz 1 (offset 0 lines). + + $ hg diff + diff -r 46e35360be47 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,3 +1,6 @@ + +-2 + +-1 + +0 + 1 + 2 + 3 + @@ -5,3 +8,4 @@ + bar + 4 + 5 + +babar + + $ hg exp + # HG changeset patch + # User test + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 46e35360be473bf761bedf3d05de4a68ffd9d9f8 + # Parent 7733902a8d94c789ca81d866bea1893d79442db6 + another one + + diff -r 7733902a8d94 -r 46e35360be47 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,5 +1,7 @@ + 1 + 2 + 3 + +foo + +bar + 4 + 5 + +Checking the obsolescence history +================================ + + $ hg obslog + @ 46e35360be47 (5) another one + | + x 678a59e5ff90 (3) another one + | rewritten(content) as 46e35360be47 by test (Thu Jan 01 00:00:00 1970 +0000) + | + x f70fb463d5bf (1) another one + rewritten(content) as 678a59e5ff90 by test (Thu Jan 01 00:00:00 1970 +0000) + + +Push the changes back to the commit and more commits for more testing +===================================================================== + + $ hg amend + $ glog + @ 6:905eb2a23ea2@default(draft) another one + | + o 0:7733902a8d94@default(draft) The base commit + + $ touch foo + $ echo "hey" >> foo + $ hg ci -Am "Added foo" + adding foo + +Testing uncommiting a whole changeset and also for a file addition +================================================================== + + $ hg uncommit -i<<EOF + > y + > y + > EOF + diff --git a/foo b/foo + new file mode 100644 + examine changes to 'foo'? [Ynesfdaq?] y + + @@ -0,0 +1,1 @@ + +hey + discard this change to 'foo'? [Ynesfdaq?] y + + new changeset is empty + (use 'hg prune .' to remove it) + + $ hg status + A foo + $ hg diff + diff -r 857367499298 foo + --- /dev/null Thu Jan 01 00:00:00 1970 +0000 + +++ b/foo Thu Jan 01 00:00:00 1970 +0000 + @@ -0,0 +1,1 @@ + +hey + + $ hg exp + # HG changeset patch + # User test + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 857367499298e999b5841bb01df65f73088b5d3b + # Parent 905eb2a23ea2d92073419d0e19165b90d36ea223 + Added foo + + $ hg amend + +Testing to uncommit removed files completely +============================================ + + $ hg rm a + $ hg ci -m "Removed a" + $ hg exp + # HG changeset patch + # User test + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 219cfe20964e93f8bb9bd82ceaa54d3b776046db + # Parent 42cc15efbec26c14d96d805dee2766ba91d1fd31 + Removed a + + diff -r 42cc15efbec2 -r 219cfe20964e a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 + @@ -1,11 +0,0 @@ + --2 + --1 + -0 + -1 + -2 + -3 + -foo + -bar + -4 + -5 + -babar + +Not examining the file +---------------------- + + $ hg uncommit -i<<EOF + > n + > EOF + diff --git a/a b/a + deleted file mode 100644 + examine changes to 'a'? [Ynesfdaq?] n + + abort: nothing selected to uncommit + [255] + +Examining the file +------------------ +XXX: there is a bug in interactive selection as it is not letting to examine the +file. Tried with curses too. In the curses UI, if you just unselect the hunks +and the not file mod thing at the top, it will show the same "nothing unselected +to uncommit" message which is a bug in interactive selection. + + $ hg uncommit -i<<EOF + > y + > EOF + diff --git a/a b/a + deleted file mode 100644 + examine changes to 'a'? [Ynesfdaq?] y + + new changeset is empty + (use 'hg prune .' to remove it) + + $ hg diff + diff -r 737487f1e5f8 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 + @@ -1,11 +0,0 @@ + --2 + --1 + -0 + -1 + -2 + -3 + -foo + -bar + -4 + -5 + -babar + $ hg status + R a + $ hg exp + # HG changeset patch + # User test + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 737487f1e5f853e55decb73ea31522c63e7f5980 + # Parent 42cc15efbec26c14d96d805dee2766ba91d1fd31 + Removed a + + + $ hg prune . + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + working directory now at 42cc15efbec2 + 1 changesets pruned + $ hg revert --all + undeleting a + + $ glog + @ 10:42cc15efbec2@default(draft) Added foo + | + o 6:905eb2a23ea2@default(draft) another one + | + o 0:7733902a8d94@default(draft) The base commit + + +Testing when a new file is added in the last commit +=================================================== + +XXX: This is buggy, look for the diff below + + $ echo "foo" >> foo + $ touch x + $ echo "abcd" >> x + $ hg add x + $ hg ci -m "Added x" + $ hg uncommit -i<<EOF + > y + > y + > y + > n + > EOF + diff --git a/foo b/foo + 1 hunks, 1 lines changed + examine changes to 'foo'? [Ynesfdaq?] y + + @@ -1,1 +1,2 @@ + hey + +foo + discard change 1/2 to 'foo'? [Ynesfdaq?] y + + diff --git a/x b/x + new file mode 100644 + examine changes to 'x'? [Ynesfdaq?] y + + @@ -0,0 +1,1 @@ + +abcd + discard change 2/2 to 'x'? [Ynesfdaq?] n + + + $ hg exp + # HG changeset patch + # User test + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 25a080d13cb23dbd014839f54d99a96e57ba7e9b + # Parent 42cc15efbec26c14d96d805dee2766ba91d1fd31 + Added x + + diff -r 42cc15efbec2 -r 25a080d13cb2 x + --- /dev/null Thu Jan 01 00:00:00 1970 +0000 + +++ b/x Thu Jan 01 00:00:00 1970 +0000 + @@ -0,0 +1,1 @@ + +abcd + +XXX: The diff here should not contain the file 'x' as it is in the commit, there +is a bug related to dirstate handling of files which are added in the commit +which we are trying to uncommit. This should be fixed + + $ hg diff + diff -r 25a080d13cb2 foo + --- a/foo Thu Jan 01 00:00:00 1970 +0000 + +++ b/foo Thu Jan 01 00:00:00 1970 +0000 + @@ -1,1 +1,2 @@ + hey + +foo + diff -r 25a080d13cb2 x + --- /dev/null Thu Jan 01 00:00:00 1970 +0000 + +++ b/x Thu Jan 01 00:00:00 1970 +0000 + @@ -0,0 +1,1 @@ + +abcd + + $ hg status + M foo + A x + + $ hg revert --all + reverting foo + forgetting x + +Testing between the stack and with dirty working copy +===================================================== + + $ glog + @ 16:25a080d13cb2@default(draft) Added x + | + o 10:42cc15efbec2@default(draft) Added foo + | + o 6:905eb2a23ea2@default(draft) another one + | + o 0:7733902a8d94@default(draft) The base commit + + $ hg up 6 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + + $ touch bar + $ echo "foo" >> bar + $ hg add bar + $ hg status + A bar + ? foo.orig + + $ hg exp + # HG changeset patch + # User test + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 905eb2a23ea2d92073419d0e19165b90d36ea223 + # Parent 7733902a8d94c789ca81d866bea1893d79442db6 + another one + + diff -r 7733902a8d94 -r 905eb2a23ea2 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,5 +1,11 @@ + +-2 + +-1 + +0 + 1 + 2 + 3 + +foo + +bar + 4 + 5 + +babar + + $ hg uncommit -i<<EOF + > y + > n + > n + > y + > EOF + diff --git a/a b/a + 3 hunks, 6 lines changed + examine changes to 'a'? [Ynesfdaq?] y + + @@ -1,3 +1,6 @@ + +-2 + +-1 + +0 + 1 + 2 + 3 + discard change 1/3 to 'a'? [Ynesfdaq?] n + + @@ -1,5 +4,7 @@ + 1 + 2 + 3 + +foo + +bar + 4 + 5 + discard change 2/3 to 'a'? [Ynesfdaq?] n + + @@ -4,2 +9,3 @@ + 4 + 5 + +babar + discard change 3/3 to 'a'? [Ynesfdaq?] y + + patching file a + Hunk #1 succeeded at 1 with fuzz 1 (offset -1 lines). + 2 new orphan changesets + + $ hg diff + diff -r 676366511f95 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -8,3 +8,4 @@ + bar + 4 + 5 + +babar + diff -r 676366511f95 bar + --- /dev/null Thu Jan 01 00:00:00 1970 +0000 + +++ b/bar Thu Jan 01 00:00:00 1970 +0000 + @@ -0,0 +1,1 @@ + +foo + + $ hg exp + # HG changeset patch + # User test + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 676366511f95ca4122413dcf79b45eaab61fb387 + # Parent 7733902a8d94c789ca81d866bea1893d79442db6 + another one + + diff -r 7733902a8d94 -r 676366511f95 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,5 +1,10 @@ + +-2 + +-1 + +0 + 1 + 2 + 3 + +foo + +bar + 4 + 5 + $ hg status + M a + A bar + ? foo.orig + +More uncommit on the same dirty working copy +============================================= + + $ hg uncommit -i<<EOF + > y + > y + > n + > EOF + diff --git a/a b/a + 2 hunks, 5 lines changed + examine changes to 'a'? [Ynesfdaq?] y + + @@ -1,3 +1,6 @@ + +-2 + +-1 + +0 + 1 + 2 + 3 + discard change 1/2 to 'a'? [Ynesfdaq?] y + + @@ -1,5 +4,7 @@ + 1 + 2 + 3 + +foo + +bar + 4 + 5 + discard change 2/2 to 'a'? [Ynesfdaq?] n + + + $ hg exp + # HG changeset patch + # User test + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 62d907d0c4fa13b4b8bfeed05f13751035daf963 + # Parent 7733902a8d94c789ca81d866bea1893d79442db6 + another one + + diff -r 7733902a8d94 -r 62d907d0c4fa a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,5 +1,7 @@ + 1 + 2 + 3 + +foo + +bar + 4 + 5 + + $ hg diff + diff -r 62d907d0c4fa a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,3 +1,6 @@ + +-2 + +-1 + +0 + 1 + 2 + 3 + @@ -5,3 +8,4 @@ + bar + 4 + 5 + +babar + diff -r 62d907d0c4fa bar + --- /dev/null Thu Jan 01 00:00:00 1970 +0000 + +++ b/bar Thu Jan 01 00:00:00 1970 +0000 + @@ -0,0 +1,1 @@ + +foo + + $ hg status + M a + A bar + ? foo.orig