diff mercurial/commands.py @ 2029:d436b21b20dc

rewrite revert command. fix issues 93, 123, 147. new version does these things: - saves backup copies of modified files (issue 147) - prints output like other commands, and errors when files not found (issue 123) - marks files added/removed (issue 93)
author Vadim Gelfer <vadim.gelfer@gmail.com>
date Fri, 31 Mar 2006 10:37:25 -0800
parents a59da8cc35e4
children e3280d350792 c9226bcc288d
line wrap: on
line diff
--- a/mercurial/commands.py	Fri Mar 31 03:25:35 2006 -0600
+++ b/mercurial/commands.py	Fri Mar 31 10:37:25 2006 -0800
@@ -43,16 +43,17 @@
     return util.cmdmatcher(repo.root, cwd, pats or ['.'], opts.get('include'),
                            opts.get('exclude'), head)
 
-def makewalk(repo, pats, opts, node=None, head=''):
+def makewalk(repo, pats, opts, node=None, head='', badmatch=None):
     files, matchfn, anypats = matchpats(repo, pats, opts, head)
     exact = dict(zip(files, files))
     def walk():
-        for src, fn in repo.walk(node=node, files=files, match=matchfn):
+        for src, fn in repo.walk(node=node, files=files, match=matchfn,
+                                 badmatch=None):
             yield src, fn, util.pathto(repo.getcwd(), fn), fn in exact
     return files, matchfn, walk()
 
-def walk(repo, pats, opts, node=None, head=''):
-    files, matchfn, results = makewalk(repo, pats, opts, node, head)
+def walk(repo, pats, opts, node=None, head='', badmatch=None):
+    files, matchfn, results = makewalk(repo, pats, opts, node, head, badmatch)
     for r in results:
         yield r
 
@@ -2003,7 +2004,7 @@
     performed before any further updates are allowed.
     """
     return update(ui, repo, node=node, merge=True, **opts)
-    
+
 def outgoing(ui, repo, dest="default-push", **opts):
     """show changesets not found in destination
 
@@ -2088,7 +2089,7 @@
         ui.status(_("(run 'hg heads' to see heads, 'hg merge' to merge)\n"))
     else:
         ui.status(_("(run 'hg update' to get a working copy)\n"))
-    
+
 def pull(ui, repo, source="default", **opts):
     """pull changes from the specified source
 
@@ -2286,6 +2287,10 @@
     to the named files or directories.  This restores the contents of
     the affected files to an unmodified state.
 
+    Modified files have backup copies saved before revert.  To disable
+    backups, use --no-backup.  To change the name of backup files, use
+    --backup to give a format string.
+
     Using the -r option, it reverts the given files or directories to
     their state as of an earlier revision.  This can be helpful to "roll
     back" some or all of a change that should not have been committed.
@@ -2300,15 +2305,92 @@
 
     If no arguments are given, all files in the repository are reverted.
     """
-    node = opts['rev'] and repo.lookup(opts['rev']) or \
-           repo.dirstate.parents()[0]
-
-    files, choose, anypats = matchpats(repo, pats, opts)
-    modified, added, removed, deleted, unknown = repo.changes(match=choose)
-    repo.forget(added)
-    repo.undelete(removed)
-
-    return repo.update(node, False, True, choose, False)
+    parent = repo.dirstate.parents()[0]
+    node = opts['rev'] and repo.lookup(opts['rev']) or parent
+    mf = repo.manifest.read(repo.changelog.read(node)[0])
+
+    def backup(name, exact):
+        bakname = make_filename(repo, repo.changelog,
+                                opts['backup_name'] or '%p.orig',
+                                node=parent, pathname=name)
+        if os.path.exists(name):
+            # if backup already exists and is same as backup we want
+            # to make, do nothing
+            if os.path.exists(bakname):
+                if repo.wread(name) == repo.wread(bakname):
+                    return
+                raise util.Abort(_('cannot save current version of %s - '
+                                   '%s exists and differs') %
+                                 (name, bakname))
+            ui.status(('saving current version of %s as %s\n') %
+                      (name, bakname))
+            shutil.copyfile(name, bakname)
+            shutil.copymode(name, bakname)
+
+    wlock = repo.wlock()
+
+    entries = []
+    names = {}
+    for src, abs, rel, exact in walk(repo, pats, opts, badmatch=mf.has_key):
+        names[abs] = True
+        entries.append((abs, rel, exact))
+
+    changes = repo.changes(match=names.has_key, wlock=wlock)
+    modified, added, removed, deleted, unknown = map(dict.fromkeys, changes)
+
+    revert = ([], _('reverting %s\n'))
+    add = ([], _('adding %s\n'))
+    remove = ([], _('removing %s\n'))
+    forget = ([], _('forgetting %s\n'))
+    undelete = ([], _('undeleting %s\n'))
+    update = {}
+
+    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, forget, True, True),
+        (removed, undelete, None, False, False),
+        (deleted, revert, remove, False, False),
+        (unknown, add, None, True, False),
+        )
+
+    for abs, rel, exact in entries:
+        def handle(xlist, dobackup):
+            xlist[0].append(abs)
+            if dobackup and not opts['no_backup']:
+                backup(rel, exact)
+            if ui.verbose or not exact:
+                ui.status(xlist[1] % rel)
+        for table, hitlist, misslist, backuphit, backupmiss in disptable:
+            if abs not in table: continue
+            # file has changed in dirstate
+            if abs in mf:
+                handle(hitlist, backuphit)
+            elif misslist is not None:
+                handle(misslist, backupmiss)
+            else:
+                if exact: ui.warn(_('file not managed: %s\n' % rel))
+            break
+        else:
+            # file has not changed in dirstate
+            if node == parent:
+                if exact: ui.warn(_('no changes needed to %s\n' % rel))
+                continue
+            if abs not in mf:
+                remove[0].append(abs)
+        update[abs] = True
+
+    repo.dirstate.forget(forget[0])
+    r = repo.update(node, False, True, update.has_key, False, wlock=wlock)
+    repo.dirstate.update(add[0], 'a')
+    repo.dirstate.update(undelete[0], 'n')
+    repo.dirstate.update(remove[0], 'r')
+    return r
 
 def root(ui, repo):
     """print the root (top) of the current working dir
@@ -2929,8 +3011,10 @@
     "^revert":
         (revert,
          [('r', 'rev', '', _('revision to revert to')),
-          ('I', 'include', [], _('include names matching the given patterns')),
-          ('X', 'exclude', [], _('exclude names matching the given patterns'))],
+          ('', 'backup-name', '', _('save backup with formatted name')),
+          ('', 'no-backup', None, _('do not save backup copies of files')),
+          ('I', 'include', [], _('include names matching given patterns')),
+          ('X', 'exclude', [], _('exclude names matching given patterns'))],
          _('hg revert [-r REV] [NAME]...')),
     "root": (root, [], _('hg root')),
     "^serve":