hgext/evolve.py
changeset 312 a7b5989d1d92
parent 309 de21685d22d1
child 313 47d10459fa24
--- 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: