--- a/hgext/evolution.py Wed Sep 14 22:29:19 2011 +0200
+++ b/hgext/evolution.py Mon Sep 19 01:11:10 2011 +0200
@@ -15,7 +15,10 @@
from mercurial import error
from mercurial import extensions
from mercurial import commands
+from mercurial import bookmarks
+from mercurial import context
from mercurial.i18n import _
+from mercurial.commands import walkopts, commitopts, commitopts2, logopts
### util function
#############################
@@ -33,6 +36,74 @@
except KeyError:
raise error.Abort(_('evolution extension require obsolete extension.'))
+### changeset rewriting logic
+#############################
+
+def rewrite(repo, old, updates, head, newbases, commitopts):
+ if len(old.parents()) > 1: #XXX remove this unecessary limitation.
+ raise error.Abort(_('cannot amend merge changesets'))
+ base = old.p1()
+ bm = bookmarks.readcurrent(repo)
+
+ wlock = repo.wlock()
+ try:
+
+ # commit a new version of the old changeset, including the update
+ # collect all files which might be affected
+ files = set(old.files())
+ for u in updates:
+ files.update(u.files())
+ # prune files which were reverted by the updates
+ def samefile(f):
+ if f in head.manifest():
+ a = head.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)]
+ # commit version of these files as defined by head
+ headmf = head.manifest()
+ def filectxfn(repo, ctx, path):
+ if path in headmf:
+ return head.filectx(path)
+ raise IOError()
+ new = context.memctx(repo,
+ parents=newbases,
+ text=commitopts.get('message') or old.description(),
+ files=files,
+ filectxfn=filectxfn,
+ user=commitopts.get('user') or None,
+ date=commitopts.get('date') or None,
+ extra=commitopts.get('extra') or None)
+ newid = repo.commitctx(new)
+ new = repo[newid]
+
+ # update the bookmark
+ if bm:
+ repo._bookmarks[bm] = newid
+ bookmarks.write(repo)
+
+ # hide obsolete csets
+ repo.changelog.hiddeninit = False
+
+ # add evolution metadata
+ repo.addobsolete(new.node(), old.node())
+ for u in updates:
+ repo.addobsolete(u.node(), old.node())
+ repo.addobsolete(new.node(), u.node())
+
+ finally:
+ wlock.release()
+
+ return newid
+
+
### new command
#############################
cmdtable = {}
@@ -68,3 +139,78 @@
finally:
wlock.release()
+@command('^amend',
+ [('A', 'addremove', None,
+ _('mark new/missing files as added/removed before committing')),
+ ('n', 'note', '',
+ _('use text as commit message for this update')),
+ ('c', 'change', '',
+ _('specifies the changeset to amend'), _('REV'))
+ ] + walkopts + commitopts + commitopts2,
+ _('[OPTION]... [FILE]...'))
+
+def amend(ui, repo, *pats, **opts):
+ """combine a changeset with updates and replace it with a new one
+
+ Commits a new changeset incorporating both the changes to the given files
+ and all the changes from the current parent changeset into the repository.
+
+ See :hg:`commit` for details about committing changes.
+
+ If you don't specify -m, the parent's message will be reused.
+
+ If you specify --change, amend additionally considers all changesets between
+ the indicated changeset and the working copy parent as updates to be subsumed.
+ This allows you to commit updates manually first. As a special shorthand you
+ can say `--amend .` instead of '--amend p1(p1())', which subsumes your latest
+ commit as an update of its parent.
+
+ Behind the scenes, Mercurial first commits the update as a regular child
+ of the current parent. Then it creates a new commit on the parent's parents
+ with the updated contents. Then it changes the working copy parent to this
+ new combined changeset. Finally, the old changeset and its update are hidden
+ from :hg:`log` (unless you use --hidden with log).
+
+ Returns 0 on success, 1 if nothing changed.
+ """
+
+ # determine updates to subsume
+ change = opts.get('change')
+ if change == '.':
+ change = 'p1(p1())'
+ old = scmutil.revsingle(repo, change)
+
+ wlock = repo.wlock()
+ try:
+
+ # commit current changes as update
+ # code copied from commands.commit to avoid noisy messages
+ ciopts = dict(opts)
+ ciopts['message'] = opts.get('note') or ('amends %s' % old.hex())
+ e = cmdutil.commiteditor
+ if ciopts.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)
+ cmdutil.commit(ui, repo, commitfunc, pats, ciopts)
+
+ # find all changesets to be considered updates
+ cl = repo.changelog
+ head = repo['.']
+ updatenodes = set(cl.nodesbetween(roots=[old.node()],
+ heads=[head.node()])[0])
+ updatenodes.remove(old.node())
+ if not updatenodes:
+ raise error.Abort(_('no updates found'))
+ updates = [repo[n] for n in updatenodes]
+
+ # perform amend
+ newid = rewrite(repo, old, updates, head,
+ [old.p1().node(), old.p2().node()], opts)
+
+ # reroute the working copy parent to the new changeset
+ repo.dirstate.setparents(newid, node.nullid)
+
+ finally:
+ wlock.release()