[PATCH evolve-ext-V2] uncommit: add support for interactive selection

Laurent Charignon lcharignon at fb.com
Wed May 27 18:39:32 CDT 2015


# HG changeset patch
# User Laurent Charignon <lcharignon at fb.com>
# Date 1432747476 25200
#      Wed May 27 10:24:36 2015 -0700
# Node ID 2ba8f8322588f8940ab93e3801fd23c0e1fa8456
# Parent  69e5de3e6129185469c2cbf98383ac6d58260d0c
uncommit: add support for interactive selection

This patch adds a --interactive flag to the uncommit command. This allows
the user to interactively (record and crecord) select changes to be uncommited.

diff --git a/hgext/evolve.py b/hgext/evolve.py
--- a/hgext/evolve.py
+++ b/hgext/evolve.py
@@ -28,8 +28,10 @@
 from StringIO import StringIO
 import struct
 import re
+import inspect
 import socket
 import errno
+import cStringIO
 sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
 
 import mercurial
@@ -1985,6 +1987,69 @@
         touched.update(files)
     return touched
 
+def _commitfilteredinteractive(repo, ctx, match, target=None):
+    """Recommit ctx with changed files not in match. Return the new
+    node identifier, or None if nothing changed.
+    """
+
+    # In older versions of mercurial context.makememctx does not accept
+    # the extra argument. If that is not the case we fail early
+    if not 'extra' in inspect.getargspec(context.makememctx).args:
+        raise util.Abort('Your version of mercurial does not support this'
+                         'feature, please update it and try again or do not'
+                         'use the interactive mode')
+    base = ctx.p1()
+    ui = repo.ui
+    branch = base.branch()
+    store = patch.filestore()
+    files = set()
+
+    if target is None:
+        target = base
+
+    try:
+        # Filter the hunks
+        diffopts = patch.difffeatureopts(ui, whitespace=True)
+        diffopts.nodates = True
+        diffopts.git = True
+
+        originaldiff = patch.diff(repo, target.node(), ctx.node(), match,
+                                  opts=diffopts)
+        originaldiff =  list(originaldiff)
+        originalchunks = patch.parsepatch(originaldiff)
+        filterfn = cmdutil.recordfilter
+        chunks = filterfn(ui, originalchunks)
+        if not chunks:
+            raise util.Abort('no change selected')
+
+        # Write the patch
+        fp = cStringIO.StringIO()
+        for c in chunks:
+            c.write(fp)
+        fp.seek(0)
+
+        # Apply the patch
+        tmpname, message, _, _, _, _, _, _ = patch.extract(ui, fp)
+        patch.patchrepo(ui, repo, target, store, tmpname,
+                        strip=1,
+                        prefix='',
+                        files=files,
+                        eolmode=None)
+        new = context.makememctx(repo, (target, node.nullid),
+                                    ctx.description(),
+                                    ctx.user(),
+                                    ctx.date(),
+                                    branch, files, store,
+                                    editor=None,
+                                    extra=ctx.extra())
+        return repo.commitctx(new)
+
+    except patch.PatchError, err:
+        raise util.Abort('error parsing patch: %s' % err)
+
+    finally:
+        store.close()
+
 def _commitfiltered(repo, ctx, match, target=None):
     """Recommit ctx with changed files not in match. Return the new
     node identifier, or None if nothing changed.
@@ -2082,6 +2147,7 @@
 
 @command('^uncommit',
     [('a', 'all', None, _('uncommit all changes when no arguments given')),
+     ('i', 'interactive', None, _('interactive mode')),
      ('r', 'rev', '', _('revert commit content to REV instead')),
      ] + commands.walkopts,
     _('[OPTION]... [NAME]'))
@@ -2131,10 +2197,21 @@
         # Recommit the filtered changeset
         tr = repo.transaction('uncommit')
         newid = None
+        interactive = opts.get('interactive')
         includeorexclude = opts.get('include') or opts.get('exclude')
-        if (pats or includeorexclude or opts.get('all')):
+        if (pats or includeorexclude or opts.get('all') or interactive):
+
+            if interactive and not includeorexclude:
+                # Interactive with no include or exclude implies looking at
+                # all the changes
+                opts['all'] = True
+
             match = scmutil.match(old, pats, opts)
-            newid = _commitfiltered(repo, old, match, target=rev)
+            if interactive:
+                newid = _commitfilteredinteractive(repo, old, match,
+                                                   target=rev)
+            else:
+                newid = _commitfiltered(repo, old, match, target=rev)
         if newid is None:
             raise util.Abort(_('nothing to uncommit'),
                              hint=_("use --all to uncommit all files"))
diff --git a/tests/test-uncommit.t b/tests/test-uncommit.t
--- a/tests/test-uncommit.t
+++ b/tests/test-uncommit.t
@@ -7,6 +7,13 @@
   $ glog() {
   >   hg glog --template '{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' "$@"
   > }
+  $ mkcommit() {
+  >    echo "$1" > "$1"
+  >    hg add "$1"
+  >    hg ci -m "add $1"
+  > }
+
+
 
   $ hg init repo
   $ cd repo
@@ -360,3 +367,205 @@
   $ hg cat b --rev .
   b
   b
+
+Setup for test of uncommit interactive
+
+  $ cd ..
+  $ hg init interactiverepo
+  $ cd interactiverepo
+  $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > interactive = true
+  > [diff]
+  > git = 1
+  > unified = 0
+  > EOF
+  $ mkcommit aa
+  $ mkcommit bb
+  $ mkcommit cc
+  $ echo "1" >> bb
+  $ echo "2" >> aa
+  $ hg commit --amend
+
+Uncommit interactive and select nothing: abort or it would create an empty commit
+
+  $ hg uncommit -i << EOF
+  > n
+  > n
+  > n
+  > n
+  > EOF
+  diff --git a/aa b/aa
+  1 hunks, 1 lines changed
+  examine changes to 'aa'? [Ynesfdaq?] n
+  
+  diff --git a/bb b/bb
+  1 hunks, 1 lines changed
+  examine changes to 'bb'? [Ynesfdaq?] n
+  
+  diff --git a/cc b/cc
+  new file mode 100644
+  examine changes to 'cc'? [Ynesfdaq?] n
+  
+  abort: no change selected
+  [255]
+
+
+Uncommit interactive and keep only one change
+
+  $ hg uncommit -i << EOF
+  > y
+  > y
+  > n
+  > n
+  > EOF
+  diff --git a/aa b/aa
+  1 hunks, 1 lines changed
+  examine changes to 'aa'? [Ynesfdaq?] y
+  
+  @@ -1,0 +2,1 @@
+  +2
+  record change 1/3 to 'aa'? [Ynesfdaq?] y
+  
+  diff --git a/bb b/bb
+  1 hunks, 1 lines changed
+  examine changes to 'bb'? [Ynesfdaq?] n
+  
+  diff --git a/cc b/cc
+  new file mode 100644
+  examine changes to 'cc'? [Ynesfdaq?] n
+  
+  $ glog
+  @  5:ce99b0615028 at default(stable/draft) add cc
+  |
+  o  1:c5e444d44616 at default(stable/draft) add bb
+  |
+  o  0:58663bb03074 at default(stable/draft) add aa
+  
+ 
+  $ hg diff -c .
+  diff --git a/aa b/aa
+  --- a/aa
+  +++ b/aa
+  @@ -1,0 +2,1 @@
+  +2
+
+  $ hg diff
+  diff --git a/bb b/bb
+  --- a/bb
+  +++ b/bb
+  @@ -1,0 +2,1 @@
+  +1
+  diff --git a/cc b/cc
+  new file mode 100644
+  --- /dev/null
+  +++ b/cc
+  @@ -0,0 +1,1 @@
+  +cc
+
+  $ hg commit --amend
+
+Uncommit interactive and keep all of the changes
+  $ hg uncommit -i << EOF
+  > y
+  > y
+  > y
+  > y
+  > y
+  > y
+  > EOF
+  diff --git a/aa b/aa
+  1 hunks, 1 lines changed
+  examine changes to 'aa'? [Ynesfdaq?] y
+  
+  @@ -1,0 +2,1 @@
+  +2
+  record change 1/3 to 'aa'? [Ynesfdaq?] y
+  
+  diff --git a/bb b/bb
+  1 hunks, 1 lines changed
+  examine changes to 'bb'? [Ynesfdaq?] y
+  
+  @@ -1,0 +2,1 @@
+  +1
+  record change 2/3 to 'bb'? [Ynesfdaq?] y
+  
+  diff --git a/cc b/cc
+  new file mode 100644
+  examine changes to 'cc'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +cc
+  record change 3/3 to 'cc'? [Ynesfdaq?] y
+  
+  abort: changeset d8903e0fc592 cannot obsolete itself
+  [255]
+
+Uncommit interactive: editing a hunk in the commit and keep all the rest
+
+1) Create a dummy editor changing cc to 42
+  $ cat > $TESTTMP/editor.sh << '__EOF__'
+  > cat "$1"  | sed "s/+cc/+42/g"  > tt
+  > mv tt  "$1"
+  > __EOF__
+
+2) Run the uncommit command
+  $ HGEDITOR="\"sh\" \"\${TESTTMP}/editor.sh\"" hg uncommit -i << EOF
+  > y
+  > y
+  > y
+  > y
+  > y
+  > e
+  > EOF
+  diff --git a/aa b/aa
+  1 hunks, 1 lines changed
+  examine changes to 'aa'? [Ynesfdaq?] y
+  
+  @@ -1,0 +2,1 @@
+  +2
+  record change 1/3 to 'aa'? [Ynesfdaq?] y
+  
+  diff --git a/bb b/bb
+  1 hunks, 1 lines changed
+  examine changes to 'bb'? [Ynesfdaq?] y
+  
+  @@ -1,0 +2,1 @@
+  +1
+  record change 2/3 to 'bb'? [Ynesfdaq?] y
+  
+  diff --git a/cc b/cc
+  new file mode 100644
+  examine changes to 'cc'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +cc
+  record change 3/3 to 'cc'? [Ynesfdaq?] e
+  
+ 
+  $ hg diff -c . 
+  diff --git a/aa b/aa
+  --- a/aa
+  +++ b/aa
+  @@ -1,0 +2,1 @@
+  +2
+  diff --git a/bb b/bb
+  --- a/bb
+  +++ b/bb
+  @@ -1,0 +2,1 @@
+  +1
+  diff --git a/cc b/cc
+  new file mode 100644
+  --- /dev/null
+  +++ b/cc
+  @@ -0,0 +1,1 @@
+  +42
+
+3) As expected the workdir still contains the change of the old commit
+  $ hg diff
+  diff --git a/cc b/cc
+  new file mode 100644
+  --- /dev/null
+  +++ b/cc
+  @@ -0,0 +1,1 @@
+  +cc


More information about the Mercurial-devel mailing list