mercurial/cmdutil.py
changeset 16304 a740fa28d718
parent 16283 6c4dbe28dda3
child 16380 84ba30e8c790
--- a/mercurial/cmdutil.py	Tue Mar 27 16:17:46 2012 -0500
+++ b/mercurial/cmdutil.py	Wed Mar 28 11:42:17 2012 +0200
@@ -1325,6 +1325,183 @@
 
     return text
 
+def revert(ui, repo, ctx, parents, *pats, **opts):
+    parent, p2 = parents
+    node = ctx.node()
+
+    mf = ctx.manifest()
+    if node == parent:
+        pmf = mf
+    else:
+        pmf = None
+
+    # need all matching names in dirstate and manifest of target rev,
+    # so have to walk both. do not print errors if files exist in one
+    # but not other.
+
+    names = {}
+
+    wlock = repo.wlock()
+    try:
+        # walk dirstate.
+
+        m = scmutil.match(repo[None], pats, opts)
+        m.bad = lambda x, y: False
+        for abs in repo.walk(m):
+            names[abs] = m.rel(abs), m.exact(abs)
+
+        # walk target manifest.
+
+        def badfn(path, msg):
+            if path in names:
+                return
+            if path in repo[node].substate:
+                ui.warn("%s: %s\n" % (m.rel(path),
+                    'reverting subrepos is unsupported'))
+                return
+            path_ = path + '/'
+            for f in names:
+                if f.startswith(path_):
+                    return
+            ui.warn("%s: %s\n" % (m.rel(path), msg))
+
+        m = scmutil.match(repo[node], pats, opts)
+        m.bad = badfn
+        for abs in repo[node].walk(m):
+            if abs not in names:
+                names[abs] = m.rel(abs), m.exact(abs)
+
+        m = scmutil.matchfiles(repo, names)
+        changes = repo.status(match=m)[:4]
+        modified, added, removed, deleted = map(set, changes)
+
+        # if f is a rename, also revert the source
+        cwd = repo.getcwd()
+        for f in added:
+            src = repo.dirstate.copied(f)
+            if src and src not in names and repo.dirstate[src] == 'r':
+                removed.add(src)
+                names[src] = (repo.pathto(src, cwd), True)
+
+        def removeforget(abs):
+            if repo.dirstate[abs] == 'a':
+                return _('forgetting %s\n')
+            return _('removing %s\n')
+
+        revert = ([], _('reverting %s\n'))
+        add = ([], _('adding %s\n'))
+        remove = ([], removeforget)
+        undelete = ([], _('undeleting %s\n'))
+
+        disptable = (
+            # dispatch table:
+            #   file state
+            #   action if in target manifest
+            #   action if not in target manifest
+            #   make backup if in target manifest
+            #   make backup if not in target manifest
+            (modified, revert, remove, True, True),
+            (added, revert, remove, True, False),
+            (removed, undelete, None, False, False),
+            (deleted, revert, remove, False, False),
+            )
+
+        for abs, (rel, exact) in sorted(names.items()):
+            mfentry = mf.get(abs)
+            target = repo.wjoin(abs)
+            def handle(xlist, dobackup):
+                xlist[0].append(abs)
+                if (dobackup and not opts.get('no_backup') and
+                    os.path.lexists(target)):
+                    bakname = "%s.orig" % rel
+                    ui.note(_('saving current version of %s as %s\n') %
+                            (rel, bakname))
+                    if not opts.get('dry_run'):
+                        util.rename(target, bakname)
+                if ui.verbose or not exact:
+                    msg = xlist[1]
+                    if not isinstance(msg, basestring):
+                        msg = msg(abs)
+                    ui.status(msg % rel)
+            for table, hitlist, misslist, backuphit, backupmiss in disptable:
+                if abs not in table:
+                    continue
+                # file has changed in dirstate
+                if mfentry:
+                    handle(hitlist, backuphit)
+                elif misslist is not None:
+                    handle(misslist, backupmiss)
+                break
+            else:
+                if abs not in repo.dirstate:
+                    if mfentry:
+                        handle(add, True)
+                    elif exact:
+                        ui.warn(_('file not managed: %s\n') % rel)
+                    continue
+                # file has not changed in dirstate
+                if node == parent:
+                    if exact:
+                        ui.warn(_('no changes needed to %s\n') % rel)
+                    continue
+                if pmf is None:
+                    # only need parent manifest in this unlikely case,
+                    # so do not read by default
+                    pmf = repo[parent].manifest()
+                if abs in pmf and mfentry:
+                    # if version of file is same in parent and target
+                    # manifests, do nothing
+                    if (pmf[abs] != mfentry or
+                        pmf.flags(abs) != mf.flags(abs)):
+                        handle(revert, False)
+                else:
+                    handle(remove, False)
+
+        if not opts.get('dry_run'):
+            def checkout(f):
+                fc = ctx[f]
+                repo.wwrite(f, fc.data(), fc.flags())
+
+            audit_path = scmutil.pathauditor(repo.root)
+            for f in remove[0]:
+                if repo.dirstate[f] == 'a':
+                    repo.dirstate.drop(f)
+                    continue
+                audit_path(f)
+                try:
+                    util.unlinkpath(repo.wjoin(f))
+                except OSError:
+                    pass
+                repo.dirstate.remove(f)
+
+            normal = None
+            if node == parent:
+                # We're reverting to our parent. If possible, we'd like status
+                # to report the file as clean. We have to use normallookup for
+                # merges to avoid losing information about merged/dirty files.
+                if p2 != nullid:
+                    normal = repo.dirstate.normallookup
+                else:
+                    normal = repo.dirstate.normal
+            for f in revert[0]:
+                checkout(f)
+                if normal:
+                    normal(f)
+
+            for f in add[0]:
+                checkout(f)
+                repo.dirstate.add(f)
+
+            normal = repo.dirstate.normallookup
+            if node == parent and p2 == nullid:
+                normal = repo.dirstate.normal
+            for f in undelete[0]:
+                checkout(f)
+                normal(f)
+
+    finally:
+        wlock.release()
+
 def command(table):
     '''returns a function object bound to table which can be used as
     a decorator for populating table as a command table'''