[evolution] imported hg amend from parren works
authorPierre-Yves David <pierre-yves.david@ens-lyon.org>
Mon, 19 Sep 2011 01:11:10 +0200
changeset 82 8108d566a8b5
parent 81 5cec25ce019c
child 83 97a5c943db19
[evolution] imported hg amend from parren works (this rocks heavily)
hgext/evolution.py
tests/test-evolution.t
--- 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