diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py --- a/mercurial/cmdutil.py +++ b/mercurial/cmdutil.py @@ -10,7 +10,7 @@ from i18n import _ 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 @@ def commit(ui, repo, commitfunc, pats, o 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() diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -1163,6 +1163,7 @@ def clone(ui, source, dest=None, **opts) _('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): @@ -1183,6 +1184,20 @@ 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. @@ -1198,31 +1213,70 @@ def commit(ui, repo, *pats, **opts): # 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])) - else: + 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')) + + 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 it's empty, + # 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) + + node = cmdutil.amend(ui, repo, commitfunc, old, extra, pats, opts) + if node == old.node(): ui.status(_("nothing changed\n")) - return 1 + return 1 + 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 diff --git a/tests/test-commit-amend.t b/tests/test-commit-amend.t new file mode 100644 --- /dev/null +++ b/tests/test-commit-amend.t @@ -0,0 +1,292 @@ + $ 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] diff --git a/tests/test-debugcomplete.t b/tests/test-debugcomplete.t --- a/tests/test-debugcomplete.t +++ b/tests/test-debugcomplete.t @@ -193,7 +193,7 @@ Show all commands + options 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 diff --git a/tests/test-qrecord.t b/tests/test-qrecord.t --- a/tests/test-qrecord.t +++ b/tests/test-qrecord.t @@ -59,6 +59,7 @@ help record (record) 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