[PATCH RFC] amend: add extension which extends amend functionality

Durham Goode durham at fb.com
Thu May 2 19:40:34 CDT 2013


# HG changeset patch
# User Durham Goode <durham at fb.com>
# Date 1367539819 25200
#      Thu May 02 17:10:19 2013 -0700
# Node ID d2c73e3828940ae32ea67803235c9b7caf53a7b6
# Parent  6406230e4a8ae8ebc412621c86ab464092d99653
amend: add extension which extends amend functionality

I don't expect this extension to be appropriate for upstream, but I wanted to
put it out there for people to see and perhaps influence any future amend
command. It's basically a poor man's evolve, allowing me to amend commits with
children then run a single command to rebase the children. I intend to allow
our users to enable this extension to ease the pain of stacked commits without
the knowledge cost of mq and the perf cost of evolve. It also makes dealing
with patch stacks for mercurial-devel@ much easier.

This creates an amend extension which does the following:

- adds 'hg amend' command
- allows amending commits with children
- adds 'hg amend --rebase' to automatically rebase children on to the new commit
- adds 'hg amend --fixup' to automatically rebase children that were left behind
by a previous commit.
- adds 'hg amend -e/--edit' to edit the commit message as part of the amend
- 'hg commit --amend' gains the same options and abilities, except --edit

When amending a commit with children, it prints a message saying:

  warning: the commit's children were left behind (use hg amend --fixup to
  rebase them)

When amending a commit with children it leaves the original commit around with
a bookmark with the format "foo(preamend)" if amending while foo is active. If
no bookmark is active the format is "DEADBEEF1234(preamend)". This commit is
cleaned up when --fixup is used.

diff --git a/hgext/amend.py b/hgext/amend.py
new file mode 100644
--- /dev/null
+++ b/hgext/amend.py
@@ -0,0 +1,156 @@
+# amend.py - improved amend functionality
+#
+# Copyright 2013 Facebook, Inc.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+"""extends the existing commit amend functionality
+
+Adds an hg amend command that amends the current parent commit with the
+changes in the working copy.  Similiar to the existing hg commit --amend
+except it doesn't prompt for the commit message unless --edit is provided.
+
+Allows amending commits that have children and can automatically rebase
+the children onto the new version of the commit
+
+"""
+
+from hgext import rebase
+from mercurial import util, cmdutil, phases, commands, bookmarks, repair
+from mercurial import merge, extensions
+from mercurial.node import hex
+from mercurial.i18n import _
+import errno, os, re
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+testedwith = 'internal'
+
+amendopts = [('', 'rebase', None, _('rebases children commits after the amend')),
+    ('', 'fixup', None, _('rebase children commits from a previous amend')),
+]
+
+def uisetup(ui):
+    entry = extensions.wrapcommand(commands.table, 'commit', commit)
+    for opt in amendopts:
+        opt = (opt[0], opt[1], opt[2], "(with --amend) " + opt[3])
+        entry[1].append(opt)
+
+def commit(orig, ui, repo, *pats, **opts):
+    if opts.get("amend"):
+        # commit --amend default behavior is to prompt for edit
+        opts['edit'] = True
+        return amend(ui, repo, *pats, **opts)
+    else:
+        return orig(ui, repo, *pats, **opts)
+
+ at command('^amend', [
+        ('e', 'edit', None, _('prompt to edit the commit message')),
+    ] + amendopts + commands.walkopts + commands.commitopts,
+    _('hg amend [OPTION]...'))
+def amend(ui, repo, *pats, **opts):
+    '''amend the current commit with more changes
+    '''
+    rebase = opts.get('rebase')
+    fixup = opts.get('fixup')
+    edit = opts.get('edit')
+
+    if fixup:
+        fixupamend(ui, repo)
+        return
+
+    old = repo['.']
+    if old.phase() == phases.public:
+        raise util.Abort(_('cannot amend public changesets'))
+    if len(repo[None].parents()) > 1:
+        raise util.Abort(_('cannot amend while merging'))
+
+    haschildren = len(old.children()) > 0
+
+    if not edit:
+        opts['message'] = old.description()
+
+    def commitfunc(ui, repo, message, match, opts):
+        e = cmdutil.commiteditor
+        return repo.commit(message,
+                           old.user(),
+                           old.date(),
+                           match,
+                           editor=e,
+                           extra={})
+
+    current = repo._bookmarkcurrent
+    oldbookmarks = old.bookmarks()
+    node = cmdutil.amend(ui, repo, commitfunc, old, {}, pats, opts, keep=haschildren)
+
+    if node == old.node():
+        ui.status(_("nothing changed\n"))
+        return 1
+
+    if haschildren and not rebase:
+        ui.status("warning: the commit's children were left behind " +
+                  "(use hg amend --fixup to rebase them)\n")
+
+    # move bookmarks
+    newbookmarks = repo._bookmarks
+    for bm in oldbookmarks:
+        newbookmarks[bm] = node
+
+    # create preamend bookmark
+    if current:
+        bookmarks.setcurrent(repo, current)
+        if haschildren:
+            newbookmarks[current + "(preamend)"] = old.node()
+    else:
+        # no active bookmark
+        if haschildren:
+            newbookmarks[hex(node)[:12] + "(preamend)"] = old.node()
+
+    newbookmarks.write()
+
+    if rebase and haschildren:
+        fixupamend(ui, repo)
+
+def fixupamend(ui, repo):
+    """rebases any children found on the preamend commit and strips the
+    preamend commit
+    """
+    current = repo['.']
+    preamendname = None
+    active = repo._bookmarkcurrent
+    if active:
+        preamendname = active + "(preamend)"
+
+    if not preamendname:
+        preamendname = hex(current.node())[:12] + "(preamend)"
+
+    if not preamendname in repo._bookmarks:
+        if active:
+            raise util.Abort(_('no %s(preamend) bookmark' % active))
+        else:
+            raise util.Abort(_('no %s(preamend) bookmark - is your bookmark not active?' %
+                               hex(current.node())[:12]))
+
+    ui.status("rebasing the children of %s\n" % (preamendname))
+
+    old = repo[preamendname]
+    oldbookmarks = old.bookmarks()
+
+    opts = {
+        'rev' : [str.join(',', [str(c.rev()) for c in old.descendants()])],
+        'dest' : active
+    }
+    if opts['rev'][0]:
+        rebase.rebase(ui, repo, **opts)
+
+    repair.strip(ui, repo, old.node(), topic='preamend-backup')
+
+    for bookmark in oldbookmarks:
+        repo._bookmarks.pop(bookmark)
+
+    repo._bookmarks.write()
+
+    merge.update(repo, current.node(), False, True, False)
+    if active:
+        bookmarks.setcurrent(repo, active)


More information about the Mercurial-devel mailing list