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

login
register
mail settings
Submitter Durham Goode
Date May 3, 2013, 12:40 a.m.
Message ID <d2c73e3828940ae32ea6.1367541634@dev350.prn1.facebook.com>
Download mbox | patch
Permalink /patch/1537/
State RFC, archived
Headers show

Comments

Durham Goode - May 3, 2013, 12:40 a.m.
# HG changeset patch
# User Durham Goode <durham@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.
Pierre-Yves David - May 6, 2013, 10:26 p.m.
On 3 mai 2013, at 02:40, Durham Goode wrote:

> # HG changeset patch
> # User Durham Goode <durham@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

Lets discuss that on mumble tuersday at 16h UTC (in 15h30)
Durham Goode - May 7, 2013, 6:05 p.m.
On 5/6/13 3:26 PM, "Pierre-Yves David" <pierre-yves.david@ens-lyon.org>
wrote:

>
>On 3 mai 2013, at 02:40, Durham Goode wrote:
>
>> # HG changeset patch
>> # User Durham Goode <durham@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
>
>Lets discuss that on mumble tuersday at 16h UTC (in 15h30)

FYI, Pierre-Yves and I talked about it a bit this morning.  A few take
aways:

- A publicly available extension should probably not use 'hg amend' for
now, since we'll eventually want a real command there.

- The --fixup is too fragile right now because it breaks if you do another
amend before the --fixup. If we wanted it to be accessible to less
experienced users, we'd want to fix this.

- The 'amend --rebase' functionality is useful (and not as hacky as the
--fixup stuff), so it'd be nice to find a way to actually get that
upstream somehow.  'commit --amend --rebase' isn't great, but perhaps
something like 'histedit --amend' as a kind of shortcut for a common
history editing operation.

Patch

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)
+
+@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)