# HG changeset patch # User Pierre-Yves David # Date 1316387470 -7200 # Node ID 8108d566a8b5681c1de1459dccabe517a056dcb6 # Parent 5cec25ce019cb1a4c0bb59b559372c5f293611b6 [evolution] imported hg amend from parren works (this rocks heavily) diff -r 5cec25ce019c -r 8108d566a8b5 hgext/evolution.py --- 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() diff -r 5cec25ce019c -r 8108d566a8b5 tests/test-evolution.t --- 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