diff mercurial/cmdutil.py @ 16458:55982f62651f

commit: add option to amend the working dir parent The --amend flag can be used to amend the parent of the working directory with a new commit that contains the changes in the parent in addition to those currently reported by "hg status", if there are any. The old commit is stored in a backup bundle in ".hg/strip-backup"(see "hg help bundle" and "hg help unbundle" on how to restore it). Message, user and date are taken from the amended commit unless specified. When a message isn't specified on the command line, the editor will open with the message of the amended commit. It is not possible to amend public changesets (see "hg help phases") or changesets that have children. Behind the scenes, first commit the update (if there is one) as a regular child of the current parent. Then create a new commit on the parent's parent with the updated contents. Then change the working copy parent to this new combined changeset. Finally, strip the amended commit and update commit created in the beginning. An alternative (cleaner?) approach of doing this is suggested here: http://selenic.com/pipermail/mercurial-devel/2012-March/038540.html It is currently not possible to amend merge commits or recursively, this can be added at a later time.
author Idan Kamara <idankk86@gmail.com>
date Wed, 18 Apr 2012 01:20:16 +0300
parents 6883c2363f44
children e596a631210e
line wrap: on
line diff
--- a/mercurial/cmdutil.py	Mon Apr 16 22:41:03 2012 -0700
+++ b/mercurial/cmdutil.py	Wed Apr 18 01:20:16 2012 +0300
@@ -10,7 +10,7 @@
 import os, sys, errno, re, tempfile
 import util, scmutil, templater, patch, error, templatekw, revlog, copies
 import match as matchmod
-import subrepo
+import subrepo, context, repair, bookmarks
 
 def parsealiases(cmd):
     return cmd.lstrip("^").split("|")
@@ -1285,6 +1285,123 @@
     return commitfunc(ui, repo, message,
                       scmutil.match(repo[None], pats, opts), opts)
 
+def amend(ui, repo, commitfunc, old, extra, pats, opts):
+    ui.note(_('amending changeset %s\n') % old)
+    base = old.p1()
+
+    wlock = repo.wlock()
+    try:
+        # Fix up dirstate for copies and renames
+        duplicatecopies(repo, None, base.node())
+
+        # First, do a regular commit to record all changes in the working
+        # directory (if there are any)
+        node = commit(ui, repo, commitfunc, pats, opts)
+        ctx = repo[node]
+
+        # Participating changesets:
+        #
+        # node/ctx o - new (intermediate) commit that contains changes from
+        #          |   working dir to go into amending commit (or a workingctx
+        #          |   if there were no changes)
+        #          |
+        # old      o - changeset to amend
+        #          |
+        # base     o - parent of amending changeset
+
+        files = set(old.files())
+
+        # Second, we use either the commit we just did, or if there were no
+        # changes the parent of the working directory as the version of the
+        # files in the final amend commit
+        if node:
+            ui.note(_('copying changeset %s to %s\n') % (ctx, base))
+
+            user = ctx.user()
+            date = ctx.date()
+            message = ctx.description()
+            extra = ctx.extra()
+
+            # Prune files which were reverted by the updates: if old introduced
+            # file X and our intermediate commit, node, renamed that file, then
+            # those two files are the same and we can discard X from our list
+            # of files. Likewise if X was deleted, it's no longer relevant
+            files.update(ctx.files())
+
+            def samefile(f):
+                if f in ctx.manifest():
+                    a = ctx.filectx(f)
+                    if f in base.manifest():
+                        b = base.filectx(f)
+                        return (a.data() == b.data()
+                                and a.flags() == b.flags()
+                                and a.renamed() == b.renamed())
+                    else:
+                        return False
+                else:
+                    return f not in base.manifest()
+            files = [f for f in files if not samefile(f)]
+
+            def filectxfn(repo, ctx_, path):
+                try:
+                    return ctx.filectx(path)
+                except KeyError:
+                    raise IOError()
+        else:
+            ui.note(_('copying changeset %s to %s\n') % (old, base))
+
+            # Use version of files as in the old cset
+            def filectxfn(repo, ctx_, path):
+                try:
+                    return old.filectx(path)
+                except KeyError:
+                    raise IOError()
+
+            # See if we got a message from -m or -l, if not, open the editor
+            # with the message of the changeset to amend
+            user = opts.get('user') or old.user()
+            date = opts.get('date') or old.date()
+            message = logmessage(ui, opts)
+            if not message:
+                cctx = context.workingctx(repo, old.description(), user, date,
+                                          extra,
+                                          repo.status(base.node(), old.node()))
+                message = commitforceeditor(repo, cctx, [])
+
+        new = context.memctx(repo,
+                             parents=[base.node(), nullid],
+                             text=message,
+                             files=files,
+                             filectxfn=filectxfn,
+                             user=user,
+                             date=date,
+                             extra=extra)
+        newid = repo.commitctx(new)
+        if newid != old.node():
+            # Reroute the working copy parent to the new changeset
+            repo.dirstate.setparents(newid, nullid)
+
+            # Move bookmarks from old parent to amend commit
+            bms = repo.nodebookmarks(old.node())
+            if bms:
+                for bm in bms:
+                    repo._bookmarks[bm] = newid
+                bookmarks.write(repo)
+
+            # Strip the intermediate commit (if there was one) and the amended
+            # commit
+            lock = repo.lock()
+            try:
+                if node:
+                    ui.note(_('stripping intermediate changeset %s\n') % ctx)
+                ui.note(_('stripping amended changeset %s\n') % old)
+                repair.strip(ui, repo, old.node(), topic='amend-backup')
+            finally:
+                lock.release()
+    finally:
+        wlock.release()
+    return newid
+
 def commiteditor(repo, ctx, subs):
     if ctx.description():
         return ctx.description()