--- a/hgext/evolve.py Tue Jun 26 14:35:09 2012 +0200
+++ b/hgext/evolve.py Tue Jun 26 18:12:31 2012 +0200
@@ -471,6 +471,136 @@
finally:
lock.release()
+def _commitfiltered(repo, ctx, match):
+ """Recommit ctx with changed files not in match. Return the new
+ node identifier, or None if nothing changed.
+ """
+ base = ctx.p1()
+ m, a, r = repo.status(base, ctx)[:3]
+ allfiles = set(m + a + r)
+ files = set(f for f in allfiles if not match(f))
+ if files == allfiles:
+ return None
+
+ # Filter copies
+ copied = copies.pathcopies(base, ctx)
+ copied = dict((src, dst) for src, dst in copied.iteritems()
+ if dst in files)
+ def filectxfn(repo, memctx, path):
+ if path not in ctx:
+ raise IOError()
+ fctx = ctx[path]
+ flags = fctx.flags()
+ mctx = context.memfilectx(fctx.path(), fctx.data(),
+ islink='l' in flags,
+ isexec='x' in flags,
+ copied=copied.get(path))
+ return mctx
+
+ new = context.memctx(repo,
+ parents=[base.node(), node.nullid],
+ text=ctx.description(),
+ files=files,
+ filectxfn=filectxfn,
+ user=ctx.user(),
+ date=ctx.date(),
+ extra=ctx.extra())
+ # commitctx always create a new revision, no need to check
+ newid = repo.commitctx(new)
+ return newid
+
+def _uncommitdirstate(repo, oldctx, match):
+ """Fix the dirstate after switching the working directory from
+ oldctx to a copy of oldctx not containing changed files matched by
+ match.
+ """
+ ctx = repo['.']
+ ds = repo.dirstate
+ copies = dict(ds.copies())
+ m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
+ for f in m:
+ if ds[f] == 'r':
+ # modified + removed -> removed
+ continue
+ ds.normallookup(f)
+
+ for f in a:
+ if ds[f] == 'r':
+ # added + removed -> unknown
+ ds.drop(f)
+ elif ds[f] != 'a':
+ ds.add(f)
+
+ for f in r:
+ if ds[f] == 'a':
+ # removed + added -> normal
+ ds.normallookup(f)
+ elif ds[f] != 'r':
+ ds.remove(f)
+
+ # Merge old parent and old working dir copies
+ oldcopies = {}
+ for f in (m + a):
+ src = oldctx[f].renamed()
+ if src:
+ oldcopies[f] = src[0]
+ oldcopies.update(copies)
+ copies = dict((dst, oldcopies.get(src, src))
+ for dst, src in oldcopies.iteritems())
+ # Adjust the dirstate copies
+ for dst, src in copies.iteritems():
+ if (src not in ctx or dst in ctx or ds[dst] != 'a'):
+ src = None
+ ds.copy(src, dst)
+
+@command('^uncommit',
+ [] + commands.walkopts,
+ _('[OPTION]... [NAME]'))
+def uncommit(ui, repo, *pats, **opts):
+ """move changes from parent revision to working directory
+
+ Changes to selected files in parent revision appear again as
+ uncommitted changed in the working directory. A new revision
+ without selected changes is created, becomes the new parent and
+ obsoletes the previous one.
+
+ The --include option specify pattern to uncommit
+ The --exclude option specify pattern to keep in the commit
+
+ Return 0 if changed files are uncommitted.
+ """
+ lock = repo.lock()
+ try:
+ wlock = repo.wlock()
+ try:
+ wctx = repo[None]
+ if len(wctx.parents()) <= 0:
+ raise util.Abort(_("cannot uncommit null changeset"))
+ if len(wctx.parents()) > 1:
+ raise util.Abort(_("cannot uncommit while merging"))
+ old = repo['.']
+ if old.phase() == phases.public:
+ raise util.Abort(_("cannot rewrite immutable changeset"))
+ if len(old.parents()) > 1:
+ raise util.Abort(_("cannot uncommit merge changeset"))
+ oldphase = old.phase()
+ # Recommit the filtered changeset
+ newid = None
+ if pats or opts.get('include') or opts.get('exclude'):
+ match = scmutil.match(old, pats, opts)
+ newid = _commitfiltered(repo, old, match)
+ if newid is None:
+ raise util.Abort(_('nothing to uncommit'))
+ # Move local changes on filtered changeset
+ repo.addobsolete(newid, old.node())
+ phases.retractboundary(repo, oldphase, [newid])
+ repo.dirstate.setparents(newid, node.nullid)
+ _uncommitdirstate(repo, old, match)
+ finally:
+ wlock.release()
+ finally:
+ lock.release()
+
def commitwrapper(orig, ui, repo, *arg, **kwargs):
lock = repo.lock()
try:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-uncommit.t Tue Jun 26 18:12:31 2012 +0200
@@ -0,0 +1,252 @@
+ $ cat >> $HGRCPATH <<EOF
+ > [extensions]
+ > hgext.rebase=
+ > hgext.graphlog=
+ > EOF
+ $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+
+ $ glog() {
+ > hg glog --template '{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' "$@"
+ > }
+
+ $ hg init repo
+ $ cd repo
+
+Cannot uncommit null changeset
+
+ $ hg uncommit
+ abort: cannot rewrite immutable changeset
+ [255]
+
+Cannot uncommit public changeset
+
+ $ echo a > a
+ $ hg ci -Am adda a
+ $ hg phase --public .
+ $ hg uncommit
+ abort: cannot rewrite immutable changeset
+ [255]
+ $ hg phase --force --draft .
+
+Cannot uncommit merge
+
+ $ hg up -q null
+ $ echo b > b
+ $ echo c > c
+ $ echo d > d
+ $ echo f > f
+ $ echo g > g
+ $ echo j > j
+ $ echo m > m
+ $ echo n > n
+ $ echo o > o
+ $ hg ci -Am addmore
+ adding b
+ adding c
+ adding d
+ adding f
+ adding g
+ adding j
+ adding m
+ adding n
+ adding o
+ created new head
+ $ hg merge
+ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (branch merge, don't forget to commit)
+ $ hg uncommit
+ abort: cannot uncommit while merging
+ [255]
+ $ hg ci -m merge
+ $ hg uncommit
+ abort: cannot uncommit merge changeset
+ [255]
+
+Prepare complicated changeset
+
+ $ hg branch bar
+ marked working directory as branch bar
+ (branches are permanent and global, did you want a bookmark?)
+ $ hg cp a aa
+ $ echo b >> b
+ $ hg rm c
+ $ echo d >> d
+ $ echo e > e
+ $ hg mv f ff
+ $ hg mv g h
+ $ echo j >> j
+ $ echo k > k
+ $ echo l > l
+ $ hg rm m
+ $ hg rm n
+ $ echo o >> o
+ $ hg ci -Am touncommit
+ adding e
+ adding k
+ adding l
+ $ hg st --copies --change .
+ M b
+ M d
+ M j
+ M o
+ A aa
+ a
+ A e
+ A ff
+ f
+ A h
+ g
+ A k
+ A l
+ R c
+ R f
+ R g
+ R m
+ R n
+ $ hg man -r .
+ a
+ aa
+ b
+ d
+ e
+ ff
+ h
+ j
+ k
+ l
+ o
+
+Prepare complicated working directory
+
+ $ hg branch foo
+ marked working directory as branch foo
+ (branches are permanent and global, did you want a bookmark?)
+ $ hg mv ff f
+ $ hg mv h i
+ $ hg rm j
+ $ hg rm k
+ $ echo l >> l
+ $ echo m > m
+ $ echo o > o
+
+Test uncommit without argument, should be a no-op
+
+ $ hg uncommit
+ abort: nothing to uncommit
+ [255]
+
+Test no matches
+
+ $ hg uncommit --include nothere
+ abort: nothing to uncommit
+ [255]
+
+Enjoy uncommit
+
+ $ hg uncommit aa b c f ff g h j k l m o
+ $ hg branch
+ foo
+ $ hg st --copies
+ M b
+ A aa
+ a
+ A i
+ g
+ A l
+ R c
+ R g
+ R j
+ R m
+ $ cat aa
+ a
+ $ cat b
+ b
+ b
+ $ cat l
+ l
+ l
+ $ cat m
+ m
+ $ test -f c && echo 'error: c was removed!'
+ [1]
+ $ test -f j && echo 'error: j was removed!'
+ [1]
+ $ test -f k && echo 'error: k was removed!'
+ [1]
+ $ hg st --copies --change .
+ M d
+ A e
+ R n
+ $ hg man -r .
+ a
+ b
+ c
+ d
+ e
+ f
+ g
+ j
+ m
+ o
+ $ hg cat -r . d
+ d
+ d
+ $ hg cat -r . e
+ e
+ $ glog --hidden
+ @ 4:e8db4aa611f6@bar(stable/draft) touncommit
+ |
+ | o 3:5eb72dbe0cb4@bar(extinct/secret) touncommit
+ |/
+ o 2:f63b90038565@default(stable/draft) merge
+ |\
+ | o 1:f15c744d48e8@default(stable/draft) addmore
+ |
+ o 0:07f494440405@default(stable/draft) adda
+
+ $ hg debugsuccessors
+ 5eb72dbe0cb4 e8db4aa611f6
+
+Test phase is preserved, no local changes
+
+ $ hg up -C 3
+ 8 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ Working directory parent is obsolete
+ $ hg --config extensions.purge= purge
+ $ hg uncommit -I 'set:added() and e'
+ $ hg st --copies
+ A e
+ $ hg st --copies --change .
+ M b
+ M d
+ M j
+ M o
+ A aa
+ A ff
+ f
+ A h
+ g
+ A k
+ A l
+ R c
+ R f
+ R g
+ R m
+ R n
+ $ glog --hidden
+ @ 5:c706fe2c12f8@bar(stable/secret) touncommit
+ |
+ | o 4:e8db4aa611f6@bar(stable/draft) touncommit
+ |/
+ | o 3:5eb72dbe0cb4@bar(extinct/secret) touncommit
+ |/
+ o 2:f63b90038565@default(stable/draft) merge
+ |\
+ | o 1:f15c744d48e8@default(stable/draft) addmore
+ |
+ o 0:07f494440405@default(stable/draft) adda
+
+ $ hg debugsuccessors
+ 5eb72dbe0cb4 c706fe2c12f8
+ 5eb72dbe0cb4 e8db4aa611f6