Commits

idank  committed 84bd9d3

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.

It is currently not possible to amend merge commits or recursively,
this can be added at a later time.

  • Participants
  • Parent commits f86dd67

Comments (0)

Files changed (4)

File mercurial/commands.py

 from lock import release
 from i18n import _, gettext
 import os, re, difflib, time, tempfile, errno
-import hg, scmutil, util, revlog, extensions, copies, error, bookmarks
+import hg, scmutil, util, revlog, extensions, copies, error, bookmarks, repair
 import patch, help, url, encoding, templatekw, discovery
 import archival, changegroup, cmdutil, hbisect
 import sshserver, hgweb, hgweb.server, commandserver
      _('mark new/missing files as added/removed before committing')),
     ('', 'close-branch', None,
      _('mark a branch as closed, hiding it from the branch list')),
+    ('', 'amend', None, _('amend the parent of the working dir')),
     ] + walkopts + commitopts + commitopts2 + subrepoopts,
     _('[OPTION]... [FILE]...'))
 def commit(ui, repo, *pats, **opts):
     commit fails, you will find a backup of your message in
     ``.hg/last-message.txt``.
 
+    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.
+
     See :hg:`help dates` for a list of formats valid for -d/--date.
 
     Returns 0 on success, 1 if nothing changed.
             # current branch, so it's sufficient to test branchheads
             raise util.Abort(_('can only close branch heads'))
         extra['close'] = 1
-    e = cmdutil.commiteditor
-    if opts.get('force_editor'):
-        e = cmdutil.commitforceeditor
-
-    def commitfunc(ui, repo, message, match, opts):
-        return repo.commit(message, opts.get('user'), opts.get('date'), match,
-                           editor=e, extra=extra)
 
     branch = repo[None].branch()
     bheads = repo.branchheads(branch)
 
-    node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
-    if not node:
-        stat = repo.status(match=scmutil.match(repo[None], pats, opts))
-        if stat[3]:
-            ui.status(_("nothing changed (%d missing files, see 'hg status')\n")
-                      % len(stat[3]))
+    if opts.get('amend'):
+        if ui.config('ui', 'commitsubrepos'):
+            raise util.Abort(_('cannot amend recursively'))
+
+        old = repo['.']
+        if old.phase() == phases.public:
+            raise util.Abort(_('cannot amend public changesets'))
+        if len(old.parents()) > 1:
+            raise util.Abort(_('cannot amend merge changesets'))
+        if len(repo[None].parents()) > 1:
+            raise util.Abort(_('cannot amend while merging'))
+        if old.children():
+            raise util.Abort(_('cannot amend changeset with children'))
+        ui.note(_('amending changeset %s\n') % old)
+
+        e = cmdutil.commiteditor
+        if opts.get('force_editor'):
+            e = cmdutil.commitforceeditor
+
+        def commitfunc(ui, repo, message, match, opts):
+            editor = e
+            # message contains text from -m or -l, if not, open the editor with
+            # the old message
+            if not message:
+                message = old.description()
+                editor = cmdutil.commitforceeditor
+            return repo.commit(message,
+                               opts.get('user') or old.user(),
+                               opts.get('date') or old.date(),
+                               match,
+                               editor=editor,
+                               extra=extra)
+
+        base = old.p1()
+
+        # Fix up dirstate for copies and renames
+        cmdutil.duplicatecopies(repo, None, base.node())
+
+        # First, do a regular commit to record all changes in the working
+        # directory (if there are any)
+        node = cmdutil.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 = cmdutil.logmessage(ui, opts)
+            if not message:
+                cctx = context.workingctx(repo, old.description(), user, date,
+                                          extra, old.changes(base))
+                message = cmdutil.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 not newid or newid == old.node():
             ui.status(_("nothing changed\n"))
-        return 1
+            return 1
+
+        wlock = repo.wlock()
+        try:
+            # 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)
+        finally:
+            wlock.release()
+
+        # 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()
+
+        node = newid
+    else:
+        e = cmdutil.commiteditor
+        if opts.get('force_editor'):
+            e = cmdutil.commitforceeditor
+
+        def commitfunc(ui, repo, message, match, opts):
+            return repo.commit(message, opts.get('user'), opts.get('date'),
+                               match, editor=e, extra=extra)
+
+        node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
+
+        if not node:
+            stat = repo.status(match=scmutil.match(repo[None], pats, opts))
+            if stat[3]:
+                ui.status(_("nothing changed (%d missing files, see "
+                            "'hg status')\n") % len(stat[3]))
+            else:
+                ui.status(_("nothing changed\n"))
+            return 1
 
     ctx = repo[node]
     parents = ctx.parents()
 
-    if (bheads and node not in bheads and not
+    if (not opts.get('amend') and bheads and node not in bheads and not
         [x for x in parents if x.node() in bheads and x.branch() == branch]):
         ui.status(_('created new head\n'))
         # The message is not printed for initial roots. For the other

File tests/test-commit-amend.t

+  $ hg init
+
+Setup:
+
+  $ echo a >> a
+  $ hg ci -Am 'base'
+  adding a
+
+Refuse to amend public csets:
+
+  $ hg phase -r . -p
+  $ hg ci --amend
+  abort: cannot amend public changesets
+  [255]
+  $ hg phase -r . -f -d
+
+  $ echo a >> a
+  $ hg ci -Am 'base1'
+
+Nothing to amend:
+
+  $ hg ci --amend
+  nothing changed
+  [1]
+
+Amending changeset with changes in working dir:
+
+  $ echo a >> a
+  $ hg ci --amend -m 'amend base1'
+  saved backup bundle to $TESTTMP/.hg/strip-backup/489edb5b847d-amend-backup.hg
+  $ hg diff -c .
+  diff -r ad120869acf0 -r 9cd25b479c51 a
+  --- a/a	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a	Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,1 +1,3 @@
+   a
+  +a
+  +a
+  $ hg log
+  changeset:   1:9cd25b479c51
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     amend base1
+  
+  changeset:   0:ad120869acf0
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     base
+  
+
+Add new file:
+
+  $ echo b > b
+  $ hg ci --amend -Am 'amend base1 new file'
+  adding b
+  saved backup bundle to $TESTTMP/.hg/strip-backup/9cd25b479c51-amend-backup.hg
+
+Remove file that was added in amended commit:
+
+  $ hg rm b
+  $ hg ci --amend -m 'amend base1 remove new file'
+  saved backup bundle to $TESTTMP/.hg/strip-backup/e2bb3ecffd2f-amend-backup.hg
+
+  $ hg cat b
+  b: no such file in rev 664a9b2d60cd
+  [1]
+
+No changes, just a different message:
+
+  $ hg ci -v --amend -m 'no changes, new message'
+  amending changeset 664a9b2d60cd
+  copying changeset 664a9b2d60cd to ad120869acf0
+  a
+  stripping amended changeset 664a9b2d60cd
+  1 changesets found
+  saved backup bundle to $TESTTMP/.hg/strip-backup/664a9b2d60cd-amend-backup.hg
+  1 changesets found
+  adding branch
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  committed changeset 1:ea6e356ff2ad
+  $ hg diff -c .
+  diff -r ad120869acf0 -r ea6e356ff2ad a
+  --- a/a	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a	Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,1 +1,3 @@
+   a
+  +a
+  +a
+  $ hg log
+  changeset:   1:ea6e356ff2ad
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     no changes, new message
+  
+  changeset:   0:ad120869acf0
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     base
+  
+
+Disable default date on commit so when -d isn't given, the old date is preserved:
+
+  $ echo '[defaults]' >> $HGRCPATH
+  $ echo 'commit=' >> $HGRCPATH
+
+Test -u/-d:
+
+  $ hg ci --amend -u foo -d '1 0'
+  saved backup bundle to $TESTTMP/.hg/strip-backup/ea6e356ff2ad-amend-backup.hg
+  $ echo a >> a
+  $ hg ci --amend -u foo -d '1 0'
+  saved backup bundle to $TESTTMP/.hg/strip-backup/377b91ce8b56-amend-backup.hg
+  $ hg log -r .
+  changeset:   1:2c94e4a5756f
+  tag:         tip
+  user:        foo
+  date:        Thu Jan 01 00:00:01 1970 +0000
+  summary:     no changes, new message
+  
+
+Open editor with old commit message if a message isn't given otherwise:
+
+  $ cat > editor << '__EOF__'
+  > #!/bin/sh
+  > cat $1
+  > echo "another precious commit message" > "$1"
+  > __EOF__
+  $ chmod +x editor
+  $ HGEDITOR="'`pwd`'"/editor hg commit --amend -v
+  amending changeset 2c94e4a5756f
+  copying changeset 2c94e4a5756f to ad120869acf0
+  no changes, new message
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: foo
+  HG: branch 'default'
+  HG: changed a
+  a
+  stripping amended changeset 2c94e4a5756f
+  1 changesets found
+  saved backup bundle to $TESTTMP/.hg/strip-backup/2c94e4a5756f-amend-backup.hg
+  1 changesets found
+  adding branch
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  committed changeset 1:ffb49186f961
+
+Same, but with changes in working dir (different code path):
+
+  $ echo a >> a
+  $ HGEDITOR="'`pwd`'"/editor hg commit --amend -v
+  amending changeset ffb49186f961
+  another precious commit message
+  
+  
+  HG: Enter commit message.  Lines beginning with 'HG:' are removed.
+  HG: Leave message empty to abort commit.
+  HG: --
+  HG: user: foo
+  HG: branch 'default'
+  HG: changed a
+  a
+  copying changeset 27f3aacd3011 to ad120869acf0
+  a
+  stripping intermediate changeset 27f3aacd3011
+  stripping amended changeset ffb49186f961
+  2 changesets found
+  saved backup bundle to $TESTTMP/.hg/strip-backup/ffb49186f961-amend-backup.hg
+  1 changesets found
+  adding branch
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  committed changeset 1:fb6cca43446f
+
+  $ rm editor
+  $ hg log -r .
+  changeset:   1:fb6cca43446f
+  tag:         tip
+  user:        foo
+  date:        Thu Jan 01 00:00:01 1970 +0000
+  summary:     another precious commit message
+  
+
+Moving bookmarks, preserve active bookmark:
+
+  $ hg book book1
+  $ hg book book2
+  $ hg ci --amend -m 'move bookmarks'
+  saved backup bundle to $TESTTMP/.hg/strip-backup/fb6cca43446f-amend-backup.hg
+  $ hg book
+     book1                     1:0cf1c7a51bcf
+   * book2                     1:0cf1c7a51bcf
+  $ echo a >> a
+  $ hg ci --amend -m 'move bookmarks'
+  saved backup bundle to $TESTTMP/.hg/strip-backup/0cf1c7a51bcf-amend-backup.hg
+  $ hg book
+     book1                     1:7344472bd951
+   * book2                     1:7344472bd951
+
+  $ echo '[defaults]' >> $HGRCPATH
+  $ echo "commit=-d '0 0'" >> $HGRCPATH
+
+Moving branches:
+
+  $ hg branch foo
+  marked working directory as branch foo
+  (branches are permanent and global, did you want a bookmark?)
+  $ echo a >> a
+  $ hg ci -m 'branch foo'
+  $ hg branch default -f
+  marked working directory as branch default
+  (branches are permanent and global, did you want a bookmark?)
+  $ hg ci --amend -m 'back to default'
+  saved backup bundle to $TESTTMP/.hg/strip-backup/1661ca36a2db-amend-backup.hg
+  $ hg branches
+  default                        2:f24ee5961967
+
+Close branch:
+
+  $ hg up -q 0
+  $ echo b >> b
+  $ hg branch foo
+  marked working directory as branch foo
+  (branches are permanent and global, did you want a bookmark?)
+  $ hg ci -Am 'fork'
+  adding b
+  $ echo b >> b
+  $ hg ci -mb
+  $ hg ci --amend --close-branch -m 'closing branch foo'
+  saved backup bundle to $TESTTMP/.hg/strip-backup/c962248fa264-amend-backup.hg
+
+Same thing, different code path:
+
+  $ echo b >> b
+  $ hg ci -m 'reopen branch'
+  reopening closed branch head 4
+  $ echo b >> b
+  $ hg ci --amend --close-branch
+  saved backup bundle to $TESTTMP/.hg/strip-backup/5e302dcc12b8-amend-backup.hg
+  $ hg branches
+  default                        2:f24ee5961967
+
+Refuse to amend merges:
+
+  $ hg up -q default
+  $ hg merge foo
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg ci --amend
+  abort: cannot amend while merging
+  [255]
+  $ hg ci -m 'merge'
+  $ hg ci --amend
+  abort: cannot amend merge changesets
+  [255]
+
+Follow copies/renames:
+
+  $ hg mv b c
+  $ hg ci -m 'b -> c'
+  $ hg mv c d
+  $ hg ci --amend -m 'b -> d'
+  saved backup bundle to $TESTTMP/.hg/strip-backup/9c207120aa98-amend-backup.hg
+  $ hg st --rev .^ --copies d
+  A d
+    b
+  $ hg cp d e
+  $ hg ci -m 'e = d'
+  $ hg cp e f
+  $ hg ci --amend -m 'f = d'
+  saved backup bundle to $TESTTMP/.hg/strip-backup/fda2b3b27b22-amend-backup.hg
+  $ hg st --rev .^ --copies f
+  A f
+    d
+
+Can't rollback an amend:
+
+  $ hg rollback
+  no rollback information available
+  [1]

File tests/test-debugcomplete.t

   add: include, exclude, subrepos, dry-run
   annotate: rev, follow, no-follow, text, user, file, date, number, changeset, line-number, ignore-all-space, ignore-space-change, ignore-blank-lines, include, exclude
   clone: noupdate, updaterev, rev, branch, pull, uncompressed, ssh, remotecmd, insecure
-  commit: addremove, close-branch, include, exclude, message, logfile, date, user, subrepos
+  commit: addremove, close-branch, amend, include, exclude, message, logfile, date, user, subrepos
   diff: rev, change, text, git, nodates, show-function, reverse, ignore-all-space, ignore-space-change, ignore-blank-lines, unified, stat, include, exclude, subrepos
   export: output, switch-parent, rev, text, git, nodates
   forget: include, exclude

File tests/test-qrecord.t

                             committing
       --close-branch        mark a branch as closed, hiding it from the branch
                             list
+      --amend               amend the parent of the working dir
    -I --include PATTERN [+] include names matching the given patterns
    -X --exclude PATTERN [+] exclude names matching the given patterns
    -m --message TEXT        use text as commit message