# HG changeset patch # User Patrick Mezard # Date 1340727151 -7200 # Node ID a7b5989d1d92e3f7bf483f48f21554ef99de8830 # Parent 5eecfda0a5c748c0e89f3ad09419eea93923912e evolve: add uncommit command diff -r 5eecfda0a5c7 -r a7b5989d1d92 hgext/evolve.py --- 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: diff -r 5eecfda0a5c7 -r a7b5989d1d92 tests/test-uncommit.t --- /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 < [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