--- 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()
--- a/tests/test-evolution.t Wed Sep 14 22:29:19 2011 +0200
+++ b/tests/test-evolution.t Mon Sep 19 01:11:10 2011 +0200
@@ -4,6 +4,9 @@
> allow_push = *
> [alias]
> qlog = log --template='{rev} - {node|short} {desc} ({state})\n'
+ > [diff]
+ > git = 1
+ > unified = 0
> [extensions]
> EOF
$ echo "states=$(echo $(dirname $TESTDIR))/hgext/states.py" >> $HGRCPATH
@@ -59,3 +62,131 @@
2 - 4538525df7e2 add c (ready)
1 - 7c3bad9141dc add b (published)
0 - 1f0dee641bb7 add a (published)
+ $ cd ..
+
+##########################
+importing Parren test
+##########################
+
+ $ cat << EOF >> $HGRCPATH
+ > [ui]
+ > logtemplate = "{rev}\t{bookmarks}: {desc|firstline} - {author|user}\n"
+ > EOF
+
+Creating And Updating Changeset
+===============================
+
+Setup the Base Repo
+-------------------
+
+We start with a plain base repo::
+
+ $ hg init main; cd main
+ $ hg states ready
+ $ cat >main-file-1 <<-EOF
+ > One
+ >
+ > Two
+ >
+ > Three
+ > EOF
+ $ echo Two >main-file-2
+ $ hg add
+ adding main-file-1
+ adding main-file-2
+ $ hg commit --message base
+ $ cd ..
+
+and clone this into a new repo where we do our work::
+
+ $ hg clone main work
+ updating to branch default
+ 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ cd work
+ $ hg states ready
+
+
+Create First Patch
+------------------
+
+To begin with, we just do the changes that will be the initial version of the changeset::
+
+ $ echo One >file-from-A
+ $ sed -i '' -e s/One/Eins/ main-file-1
+ $ hg add file-from-A
+
+So this is what we would like our changeset to be::
+
+ $ hg diff
+ diff --git a/file-from-A b/file-from-A
+ new file mode 100644
+ --- /dev/null
+ +++ b/file-from-A
+ @@ -0,0 +1,1 @@
+ +One
+ diff --git a/main-file-1 b/main-file-1
+ --- a/main-file-1
+ +++ b/main-file-1
+ @@ -1,1 +1,1 @@
+ -One
+ +Eins
+
+To commit it we just - commit it::
+
+ $ hg commit --message "a nifty feature"
+
+and place a bookmark so we can easily refer to it again (which we could have done before the commit)::
+
+ $ hg book feature-A
+
+
+Create Second Patch
+-------------------
+
+Let's do this again for the second changeset::
+
+ $ echo Two >file-from-B
+ $ sed -i '' -e s/Two/Zwie/ main-file-1
+ $ hg add file-from-B
+
+Before committing, however, we need to switch to a new bookmark for the second
+changeset. Otherwise we would inadvertently move the bookmark for our first changeset.
+It is therefore advisable to always set the bookmark before committing::
+
+ $ hg book feature-B
+ $ hg commit --message "another feature"
+
+So here we are::
+
+ $ hg book
+ feature-A 1:568a468b60fc
+ * feature-B 2:7b36850622b2
+
+
+Fix The Second Patch
+--------------------
+
+There's a typo in feature-B. We spelled *Zwie* instead of *Zwei*::
+
+ $ hg diff --change tip | grep -F Zwie
+ +Zwie
+
+Fixing this is very easy. Just change::
+
+ $ sed -i '' -e s/Zwie/Zwei/ main-file-1
+
+and **amend**::
+
+ $ hg amend --note "fix spelling of Zwei"
+
+The `--note` is our commit message for the *update* only. So its only purpose
+is to document the evolution of the changeset. If we use `--message` with
+`amend`, it replaces the commit message of the changeset itself.
+
+This results in a new single changeset for our amended changeset, and the old
+changeset plus the updating changeset are hidden from view by default::
+
+ $ hg log
+ 4 feature-B: another feature - test
+ 1 feature-A: a nifty feature - test
+ 0 : base - test