[PATCH 4 of 5 V2] histedit: add execute function (issue4036)

Olle Lundberg olle.lundberg at gmail.com
Thu Mar 6 05:26:17 CST 2014


# HG changeset patch
# User Olle Lundberg <geek at nerd.sh>
# Date 1394067184 -3600
#      Thu Mar 06 01:53:04 2014 +0100
# Node ID 655a8eecb29f6e58d56eb480e0b871139f0f134e
# Parent  ab2d2ac49a0293653398995ef2de73f31485341a
histedit: add execute function (issue4036)

The basic contract is that it receives a clean working copy and
is expected to leave a clean working copy if it exits 0.
If either the command leaves the working copy dirty, or it exits non-0,
histedit aborts. If we get a clean working copy we try to gather any
eventual new children that might have spawned due to new commits by
an executed command.

We also make the surrounding functions aware of the new workflow,

verifyrules skips the repo and hash related logic since the command
we run most likely will never be a valid hash in the repo.

continuebootstrap checks for any uncommited changes/merges in the
working directory and bails if we find any.
Else set the currentnode to the working directories
first parent and let histedit do its magic. Unless the current
node ctx matches the parent ctx, then we can short circuit the
logic since there are no changes and we can just return the
parent with an empty replacements list.

diff --git a/hgext/histedit.py b/hgext/histedit.py
--- a/hgext/histedit.py
+++ b/hgext/histedit.py
@@ -36,10 +36,11 @@
  #  p, pick = use commit
  #  e, edit = use commit, but stop for amending
  #  f, fold = use commit, but combine it with the one above
  #  d, drop = remove commit from history
  #  m, mess = edit message without changing commit content
+ #  x, exec = execute the given command
  #
 
 In this file, lines beginning with ``#`` are ignored. You must specify a rule
 for each revision in your history. For example, if you had meant to add gamma
 before beta, and then wanted to add delta in the same revision as beta, you
@@ -57,10 +58,11 @@
  #  p, pick = use commit
  #  e, edit = use commit, but stop for amending
  #  f, fold = use commit, but combine it with the one above
  #  d, drop = remove commit from history
  #  m, mess = edit message without changing commit content
+ #  x, exec = execute the given command
  #
 
 At which point you close the editor and ``histedit`` starts working. When you
 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
 those revisions together, offering you a chance to clean up the commit message::
@@ -101,10 +103,16 @@
 
 The ``message`` operation will give you a chance to revise a commit
 message without changing the contents. It's a shortcut for doing
 ``edit`` immediately followed by `hg histedit --continue``.
 
+The ``exec`` operation will let you execute arbitrary commands. With the
+working directory updated to the last given revision. The command receive a
+clean working copy and is expected to leave a clean working copy if the
+executed command exits 0. If the command leaves the working copy dirty or
+exits non-zero you are droped back to a command prompt to clean it up.
+
 If ``histedit`` encounters a conflict when moving a revision (while
 handling ``pick`` or ``fold``), it'll stop in a similar manner to
 ``edit`` with the difference that it won't prompt you for a commit
 message when done. If you decide at this point that you don't like how
 much work it will be to rearrange history, or that you made a mistake,
@@ -179,10 +187,11 @@
 #  p, pick = use commit
 #  e, edit = use commit, but stop for amending
 #  f, fold = use commit, but combine it with the one above
 #  d, drop = remove commit from history
 #  m, mess = edit message without changing commit content
+#  x, exec = execute the given command
 #
 """)
 
 def commitfuncfor(repo, src):
     """Build a commit function for the replacement of <src>
@@ -329,10 +338,43 @@
     applychanges(ui, repo, oldctx, opts)
     raise error.InterventionRequired(
         _('Make changes as needed, you may commit or record as needed now.\n'
           'When you are finished, run hg histedit --continue to resume.'))
 
+def execute(ui, repo, ctx, cmd, opts):
+    ha = ctx.node()
+    revision = node.hex(ha)
+    replacements = []
+    hg.update(repo, ha)
+    _toprepolock.releaselocks()
+    rc = util.system(cmd, environ={'HGREVISION': revision,
+                                   'HG_NODE': revision })
+    _toprepolock.takelocks()
+    if rc != 0:
+        raise error.InterventionRequired(
+            _('Command exited with %i. Fix up the change and run '
+            'hg histedit --continue') % rc)
+    if util.any(repo.status()[:4]):
+        raise error.InterventionRequired(
+            _('Working copy dirty, to see the state of the working copy run '
+              'hg status.\n'
+              'When you are finished, run hg histedit --continue to resume.'))
+
+    repoctx = repo[None].p1()
+
+    if repoctx != ctx:
+        ha = repoctx.node()
+        newchildren = gatherchildren(repo, ctx)
+        if ha not in newchildren:
+            # note: new children may be empty when the changeset is dropped.
+            # this happen e.g during conflicting pick where we revert content
+            # to parent.
+            replacements.append((ha, tuple(newchildren)))
+        if newchildren:
+            ctx = repoctx
+    return ctx, replacements
+
 def fold(ui, repo, ctx, ha, opts):
     oldctx = repo[ha]
     hg.update(repo, ctx.node())
     stats = applychanges(ui, repo, oldctx, opts)
     if stats and stats[3] > 0:
@@ -461,10 +503,12 @@
                'fold': fold,
                'd': drop,
                'drop': drop,
                'm': message,
                'mess': message,
+               'x': execute,
+               'exec': execute,
                }
 
 @command('histedit',
     [('', 'commands', '',
       _('Read history edits from the specified file.')),
@@ -680,10 +724,28 @@
         newchildren.pop(0)  # remove ctx
     return newchildren
 
 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
     action, currentnode = rules.pop(0)
+
+    # track replacements
+    replacements = []
+
+    if action in ('x', 'exec'):
+        # TODO: Do we want to auto-commit anything that the exec did for us?
+        # That would be useful in cases where there is an external tool
+        # modifying commits for us. The auto-commit behaviour is present in
+        # the case when a used have used edit to split/add commits. Whatever
+        # is present in the working dir gets commited.
+        # If the first parent of the working direcroty is the same as the
+        # parentctx from the histedit state, we can short circuit the logic
+        # and just return the parentctx with no replacements.
+        cmdutil.bailifchanged(repo)
+        currentnode = repo[None].p1()
+        if currentnode == parentctx:
+            return parentctx, replacements
+
     ctx = repo[currentnode]
 
     newchildren = gatherchildren(repo, parentctx)
 
     # Commit dirty working directory if necessary
@@ -704,12 +766,10 @@
                      date=ctx.date(), extra=ctx.extra(),
                      editor=editor)
         if new is not None:
             newchildren.append(new)
 
-    replacements = []
-    # track replacements
     if ctx.node() not in newchildren:
         # note: new children may be empty when the changeset is dropped.
         # this happen e.g during conflicting pick where we revert content
         # to parent.
         replacements.append((ctx.node(), tuple(newchildren)))
@@ -788,24 +848,28 @@
     seen = set()
     for r in rules:
         if ' ' not in r:
             raise util.Abort(_('malformed line "%s"') % r)
         action, rest = r.split(' ', 1)
-        ha = rest.strip().split(' ', 1)[0]
-        try:
-            ha = str(repo[ha])  # ensure its a short hash
-        except error.RepoError:
-            raise util.Abort(_('unknown changeset %s listed') % ha)
-        if ha not in expected:
-            raise util.Abort(
-                _('may not use changesets other than the ones listed'))
-        if ha in seen:
-            raise util.Abort(_('duplicated command for changeset %s') % ha)
-        seen.add(ha)
+        if action not in ('x', 'exec'):
+            args = rest.strip().split(' ', 1)[0]
+            try:
+                args = str(repo[args])  # ensure its a short hash
+            except error.RepoError:
+                raise util.Abort(_('unknown changeset %s listed') % args)
+            if args not in expected:
+                raise util.Abort(
+                    _('may not use changesets other than the ones listed'))
+            if args in seen:
+                raise util.Abort(
+                    _('duplicated command for changeset %s') % args)
+            seen.add(args)
+        else:
+            args = rest
         if action not in actiontable:
             raise util.Abort(_('unknown action "%s"') % action)
-        parsed.append([action, ha])
+        parsed.append([action, args])
     missing = sorted(expected - seen)  # sort to stabilize output
     if missing:
         raise util.Abort(_('missing rules for changeset %s') % missing[0],
                          hint=_('do you want to use the drop action?'))
     return parsed
diff --git a/tests/test-histedit-arguments.t b/tests/test-histedit-arguments.t
--- a/tests/test-histedit-arguments.t
+++ b/tests/test-histedit-arguments.t
@@ -57,10 +57,11 @@
   #  p, pick = use commit
   #  e, edit = use commit, but stop for amending
   #  f, fold = use commit, but combine it with the one above
   #  d, drop = remove commit from history
   #  m, mess = edit message without changing commit content
+  #  x, exec = execute the given command
   #
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
 
 Run on a revision not ancestors of the current working directory.
 --------------------------------------------------------------------
diff --git a/tests/test-histedit-bookmark-motion.t b/tests/test-histedit-bookmark-motion.t
--- a/tests/test-histedit-bookmark-motion.t
+++ b/tests/test-histedit-bookmark-motion.t
@@ -73,10 +73,11 @@
   #  p, pick = use commit
   #  e, edit = use commit, but stop for amending
   #  f, fold = use commit, but combine it with the one above
   #  d, drop = remove commit from history
   #  m, mess = edit message without changing commit content
+  #  x, exec = execute the given command
   #
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg histedit 1 --commands - --verbose << EOF | grep histedit
   > pick 177f92b77385 2 c
   > drop d2ae7f538514 1 b
@@ -133,10 +134,11 @@
   #  p, pick = use commit
   #  e, edit = use commit, but stop for amending
   #  f, fold = use commit, but combine it with the one above
   #  d, drop = remove commit from history
   #  m, mess = edit message without changing commit content
+  #  x, exec = execute the given command
   #
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg histedit 1 --commands - --verbose << EOF | grep histedit
   > pick b346ab9a313d 1 c
   > pick cacdfd884a93 3 f
diff --git a/tests/test-histedit-commute.t b/tests/test-histedit-commute.t
--- a/tests/test-histedit-commute.t
+++ b/tests/test-histedit-commute.t
@@ -67,10 +67,11 @@
   #  p, pick = use commit
   #  e, edit = use commit, but stop for amending
   #  f, fold = use commit, but combine it with the one above
   #  d, drop = remove commit from history
   #  m, mess = edit message without changing commit content
+  #  x, exec = execute the given command
   #
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
 
 edit the history
 (use a hacky editor to check histedit-last-edit.txt backup)
diff --git a/tests/test-histedit-exec.t b/tests/test-histedit-exec.t
new file mode 100644
--- /dev/null
+++ b/tests/test-histedit-exec.t
@@ -0,0 +1,290 @@
+  $ . "$TESTDIR/histedit-helpers.sh"
+
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > histedit=
+  > EOF
+
+  $ initrepo ()
+  > {
+  >     hg init r
+  >     cd r
+  >     for x in a b c d e f ; do
+  >         echo $x > $x
+  >         hg add $x
+  >         hg ci -m $x
+  >     done
+  > }
+
+  $ initrepo
+
+log before exec
+  $ hg log --graph
+  @  changeset:   5:652413bf663e
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     f
+  |
+  o  changeset:   4:e860deea161a
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     e
+  |
+  o  changeset:   3:055a42cdd887
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     d
+  |
+  o  changeset:   2:177f92b77385
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     c
+  |
+  o  changeset:   1:d2ae7f538514
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     b
+  |
+  o  changeset:   0:cb9a9f314b8b
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     a
+  
+
+execute a command with a zero exit code
+  $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
+  > pick 177f92b77385 c
+  > pick 055a42cdd887 d
+  > pick e860deea161a e
+  > exec exit 0
+  > pick 652413bf663e f
+  > EOF
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+log after exec
+  $ hg log --graph
+  @  changeset:   5:652413bf663e
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     f
+  |
+  o  changeset:   4:e860deea161a
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     e
+  |
+  o  changeset:   3:055a42cdd887
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     d
+  |
+  o  changeset:   2:177f92b77385
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     c
+  |
+  o  changeset:   1:d2ae7f538514
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     b
+  |
+  o  changeset:   0:cb9a9f314b8b
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     a
+  
+
+execute a command with a non-zero exit code
+  $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
+  > pick 177f92b77385 c
+  > pick 055a42cdd887 d
+  > pick e860deea161a e
+  > exec exit 1
+  > pick 652413bf663e f
+  > EOF
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  Command exited with 1. Fix up the change and run hg histedit --continue
+
+  $ hg summary
+  parent: 4:e860deea161a 
+   e
+  branch: default
+  commit: (clean)
+  update: 1 new changesets (update)
+  hist:   2 remaining (histedit --continue)
+
+  $ hg histedit --continue
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+  $ hg log --graph
+  @  changeset:   5:652413bf663e
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     f
+  |
+  o  changeset:   4:e860deea161a
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     e
+  |
+  o  changeset:   3:055a42cdd887
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     d
+  |
+  o  changeset:   2:177f92b77385
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     c
+  |
+  o  changeset:   1:d2ae7f538514
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     b
+  |
+  o  changeset:   0:cb9a9f314b8b
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     a
+  
+
+execute a command that modifies the working copy
+  $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
+  > pick 177f92b77385 c
+  > pick 055a42cdd887 d
+  > pick e860deea161a e
+  > exec echo foo >> e
+  > pick 652413bf663e f
+  > EOF
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  Working copy dirty, to see the state of the working copy run hg status.
+  When you are finished, run hg histedit --continue to resume.
+
+  $ hg summary
+  parent: 4:e860deea161a 
+   e
+  branch: default
+  commit: 1 modified (new branch head)
+  update: 1 new changesets (update)
+  hist:   2 remaining (histedit --continue)
+
+  $ hg commit -m "foo"
+  created new head
+
+  $ hg histedit --continue
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  saved backup bundle to $TESTTMP/r/.hg/strip-backup/652413bf663e-backup.hg (glob)
+
+  $ hg cat e
+  e
+  foo
+
+  $ hg log --graph
+  @  changeset:   6:a8b916d59ea3
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     f
+  |
+  o  changeset:   5:baf2479ff303
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     foo
+  |
+  o  changeset:   4:e860deea161a
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     e
+  |
+  o  changeset:   3:055a42cdd887
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     d
+  |
+  o  changeset:   2:177f92b77385
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     c
+  |
+  o  changeset:   1:d2ae7f538514
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     b
+  |
+  o  changeset:   0:cb9a9f314b8b
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     a
+  
+execute a command that adds a commit
+  $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
+  > pick 177f92b77385 c
+  > pick 055a42cdd887 d
+  > pick e860deea161a e
+  > exec echo g > g; hg ci -Am "g" g
+  > pick baf2479ff303 foo
+  > pick a8b916d59ea3 f
+  > EOF
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  created new head
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+
+  $ hg summary
+  parent: 7:5652d6d85ba5 tip
+   f
+  branch: default
+  commit: (clean)
+  update: (current)
+
+  $ hg cat g
+  g
+
+  $ hg log --graph
+  @  changeset:   7:5652d6d85ba5
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     f
+  |
+  o  changeset:   6:c009deb54b16
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     foo
+  |
+  o  changeset:   5:caf70cdd2c86
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     g
+  |
+  o  changeset:   4:e860deea161a
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     e
+  |
+  o  changeset:   3:055a42cdd887
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     d
+  |
+  o  changeset:   2:177f92b77385
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     c
+  |
+  o  changeset:   1:d2ae7f538514
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     b
+  |
+  o  changeset:   0:cb9a9f314b8b
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     a
+  
diff --git a/tests/test-histedit-obsolete.t b/tests/test-histedit-obsolete.t
--- a/tests/test-histedit-obsolete.t
+++ b/tests/test-histedit-obsolete.t
@@ -57,10 +57,11 @@
   #  p, pick = use commit
   #  e, edit = use commit, but stop for amending
   #  f, fold = use commit, but combine it with the one above
   #  d, drop = remove commit from history
   #  m, mess = edit message without changing commit content
+  #  x, exec = execute the given command
   #
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg histedit 1 --commands - --verbose <<EOF | grep histedit
   > pick 177f92b77385 2 c
   > drop d2ae7f538514 1 b
diff --git a/tests/test-histedit-outgoing.t b/tests/test-histedit-outgoing.t
--- a/tests/test-histedit-outgoing.t
+++ b/tests/test-histedit-outgoing.t
@@ -49,10 +49,11 @@
   #  p, pick = use commit
   #  e, edit = use commit, but stop for amending
   #  f, fold = use commit, but combine it with the one above
   #  d, drop = remove commit from history
   #  m, mess = edit message without changing commit content
+  #  x, exec = execute the given command
   #
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ cd ..
 
 show the error from unrelated repos
@@ -80,10 +81,11 @@
   #  p, pick = use commit
   #  e, edit = use commit, but stop for amending
   #  f, fold = use commit, but combine it with the one above
   #  d, drop = remove commit from history
   #  m, mess = edit message without changing commit content
+  #  x, exec = execute the given command
   #
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ cd ..
 
 test sensitivity to branch in URL:
@@ -103,10 +105,11 @@
   #  p, pick = use commit
   #  e, edit = use commit, but stop for amending
   #  f, fold = use commit, but combine it with the one above
   #  d, drop = remove commit from history
   #  m, mess = edit message without changing commit content
+  #  x, exec = execute the given command
   #
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
 
 test to check number of roots in outgoing revisions
 


More information about the Mercurial-devel mailing list