[PATCH 3 of 3 evolve-ext RFC] commands: introduce a new command to edit commit metadata

Siddharth Agarwal sid0 at fb.com
Thu Mar 10 04:24:19 EST 2016


# HG changeset patch
# User Siddharth Agarwal <sid0 at fb.com>
# Date 1457601780 28800
#      Thu Mar 10 01:23:00 2016 -0800
# Branch stable
# Node ID baf6b4129735a703fd356077e0c73c49c0d13f20
# Parent  acb48e8d739cec3b824a088843d8c432a34d2fd6
commands: introduce a new command to edit commit metadata

The eventual goal of this command is to allow editing one or more changesets'
metadata without being on the particular changeset.

This implementation is by no means complete, but it does allow folding multiple
commits into one. Crucially, it is different from 'hg fold --exact' in that it
also allows 'folding' a single commit and rewriting its metadata. This is
really useful to have as a single logical operation, for example while
preparing a series of multiple local changesets that will need to be pushed as
a single changeset.

diff --git a/hgext/evolve.py b/hgext/evolve.py
--- a/hgext/evolve.py
+++ b/hgext/evolve.py
@@ -2997,6 +2997,127 @@ def fold(ui, repo, *revs, **opts):
     finally:
         lockmod.release(lock, wlock)
 
+ at command('^metaedit',
+         [('r', 'rev', [], _("revisions to edit")),
+          ('', 'fold', None, _("also fold specified revisions into one"))
+         ] + commitopts + commitopts2,
+         _('hg metaedit [OPTION]... [-r] [REV]'))
+def metaedit(ui, repo, *revs, **opts):
+    """edit commit information
+
+    Edits the commit information for the specified revisions. By default, edits
+    commit information for the working directory parent.
+
+    With --fold, also folds multiple revisions into one if necessary. In this
+    case, the given revisions must form a linear unbroken chain.
+
+    .. container:: verbose
+
+     Some examples:
+
+     - Edit the commit message for the working directory parent::
+
+         hg metaedit
+
+     - Change the username for the working directory parent::
+
+         hg metaedit --user 'New User <new-email at example.com>'
+
+     - Combine all draft revisions that are ancestors of foo but not of @ into
+       one::
+
+         hg metaedit --fold 'draft() and only(foo,@)'
+
+       See :hg:`help phases` for more about draft revisions and
+       :hg:`help revsets` for more about the `draft()` and `only()` keywords
+    """
+    revs = list(revs)
+    revs.extend(opts['rev'])
+    if not revs:
+        if opts['fold']:
+            raise error.Abort(_('revisions must be specified with --fold'))
+        revs = ['.']
+
+    revs = scmutil.revrange(repo, revs)
+    if not opts['fold'] and len(revs) > 1:
+        # TODO: handle the non-fold case with multiple revisions. This is
+        # somewhat tricky because if we want to edit a series of commits:
+        #
+        #   a ---- b ---- c
+        #
+        # we need to rewrite a first, then directly rewrite b on top of the new
+        # a, then rewrite c on top of the new b. So we need to handle revisions
+        # in topological order.
+        raise error.Abort(_('editing multiple revisions without --fold is not '
+                            'currently supported'))
+
+    if opts['fold']:
+        root, head = _foldcheck(repo, revs)
+    else:
+        if _willcreatedisallowedunstable(repo, revs):
+            raise error.Abort(_('cannot edit commit information in the middle '
+                                'of a stack'))
+        # check above ensures only one revision
+        root = head = repo[revs.first()]
+        if root.phase() == phases.public:
+            raise error.Abort(_('cannot edit commit information for public '
+                                'revisions'))
+
+    wlock = lock = None
+    try:
+        wlock = repo.wlock()
+        lock = repo.lock()
+        wctx = repo[None]
+        p1, p2 = wctx.p1(), wctx.p2()
+        tr = repo.transaction('metaedit')
+        newp1 = None
+        try:
+            commitopts = opts.copy()
+            allctx = [repo[r] for r in revs]
+            targetphase = max(c.phase() for c in allctx)
+
+            if commitopts.get('message') or commitopts.get('logfile'):
+                commitopts['edit'] = False
+            else:
+                if opts['fold']:
+                    msgs = ["HG: This is a fold of %d changesets." % len(allctx)]
+                    msgs += ["HG: Commit message of changeset %s.\n\n%s\n" %
+                             (c.rev(), c.description()) for c in allctx]
+                else:
+                    msgs = [head.description()]
+                commitopts['message'] =  "\n".join(msgs)
+                commitopts['edit'] = True
+
+            # TODO: don't create a new commit if there are no non-trivial
+            # changes
+            newid, created = rewrite(repo, root, allctx, head,
+                                     [root.p1().node(), root.p2().node()],
+                                     commitopts=commitopts)
+            # Optimization: if the working copy parent is a *head* (not root,
+            # not in between) of a commit or commit series that got rewritten,
+            # just use localrepo.setparents and avoid any working copy
+            # updates. It's easier to do this if we don't also have to worry
+            # about p2.
+            if not p2 and head == p1:
+                newp1 = newid
+            if created:
+                phases.retractboundary(repo, tr, targetphase, [newid])
+                obsolete.createmarkers(repo, [(ctx, (repo[newid],))
+                                              for ctx in allctx])
+            else:
+                ui.status(_("nothing changed\n"))
+            tr.close()
+        finally:
+            tr.release()
+        if opts['fold']:
+            ui.status('%i changesets folded\n' % len(revs))
+        if newp1 is not None:
+            repo.setparents(newp1)
+        elif p1.rev() in revs:
+            hg.update(repo, newid)
+    finally:
+        lockmod.release(lock, wlock)
+
 def _foldcheck(repo, revs):
     roots = repo.revs('roots(%ld)', revs)
     if len(roots) > 1:
diff --git a/tests/test-evolve.t b/tests/test-evolve.t
--- a/tests/test-evolve.t
+++ b/tests/test-evolve.t
@@ -2,6 +2,7 @@
   > [defaults]
   > amend=-d "0 0"
   > fold=-d "0 0"
+  > metaedit=-d "0 0"
   > [web]
   > push_ssl = false
   > allow_push = *
@@ -1449,3 +1450,113 @@ Check that dirstate changes are kept at 
 
   $ hg status newlyadded
   A newlyadded
+
+hg metaedit
+-----------
+
+deliberately leave the working copy with dirty merges so that we know there are
+no updates going on
+  $ hg update .
+  abort: outstanding merge conflicts
+  [255]
+check that metaedit respects allowunstable
+  $ hg metaedit '36 + 42' --fold
+  abort: cannot fold non-linear revisions (multiple roots given)
+  [255]
+  $ hg metaedit '36::39 + 41' --fold
+  abort: cannot fold non-linear revisions (multiple heads given)
+  [255]
+  $ hg metaedit -r 0
+  abort: cannot edit commit information for public revisions
+  [255]
+  $ hg metaedit -r 0 --fold
+  abort: cannot fold public revisions
+  [255]
+  $ hg metaedit '18::20' --fold --config 'experimental.evolution=createmarkers, allnewcommands'
+  abort: cannot fold chain not ending with a head or with branching
+  [255]
+  $ hg metaedit '.^' --config 'experimental.evolution=createmarkers, allnewcommands'
+  abort: cannot edit commit information in the middle of a stack
+  [255]
+  $ hg metaedit --fold
+  abort: revisions must be specified with --fold
+  [255]
+  $ hg metaedit --user foobar
+  $ hg log --template '{rev}: {author}\n' -r '42:' --hidden
+  42: test
+  43: foobar
+  $ hg log --template '{rev}: {author}\n' -r .
+  43: foobar
+  $ hg status newlyadded
+  A newlyadded
+  $ hg resolve --list
+  U newfile
+
+TODO: support this
+  $ hg metaedit '.^::.'
+  abort: editing multiple revisions without --fold is not currently supported
+  [255]
+
+  $ HGEDITOR=cat hg metaedit '.^::.' --fold
+  HG: This is a fold of 2 changesets.
+  HG: Commit message of changeset 41.
+  
+  amended
+  
+  HG: Commit message of changeset 43.
+  
+  will be evolved safely
+  
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: changed a
+  HG: changed newfile
+  2 changesets folded
+
+  $ glog -r .
+  @  44:41bf1183869c at default(draft) amended
+  |
+
+no new commit is created here because the date is the same
+  $ HGEDITOR=cat hg metaedit
+  amended
+  
+  
+  will be evolved safely
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: test
+  HG: branch 'default'
+  HG: changed a
+  HG: changed newfile
+  nothing changed
+
+  $ glog -r '.^::.'
+  @  44:41bf1183869c at default(draft) amended
+  |
+  o  36:43c3f5ef149f at default(draft) add uu
+  |
+
+'fold' one commit
+  $ hg metaedit 39 --fold --user foobar2
+  1 changesets folded
+  $ hg log -r 45 --template '{rev}: {author}\n'
+  45: foobar2
+
+TODO: don't create a new commit in this case
+  $ hg metaedit --config defaults.metaedit=
+  $ hg log -r '.^::.' --template '{rev}: {desc|firstline}\n'
+  36: add uu
+  46: amended
+  $ hg status newlyadded
+  A newlyadded
+  $ hg resolve --list
+  U newfile


More information about the Mercurial-devel mailing list