[PATCH] rollback: avoid unsafe rollback when not at tip (issue2998)

Greg Ward greg-hg at gerg.ca
Fri Sep 30 20:59:51 CDT 2011


# HG changeset patch
# User Greg Ward <greg at gerg.ca>
# Date 1317434334 14400
# Node ID 59e8bc22506ec40f74ce192b6f0c36fb9e42d2c6
# Parent  de496752d9364cbc55e14900ccffa851c20d8b41
rollback: avoid unsafe rollback when not at tip (issue2998)

You can get into trouble if you commit, update back to an older
changeset, and then rollback. The update removes your valuable changes
from the working dir, then rollback removes them history. Oops: you've
just irretrievably lost data running nothing but core Mercurial
commands. (More subtly: rollback from a shared clone that was already
at an older changeset -- no update required, just rollback from the
wrong directory.)

The fix assumes that only "commit" transactions have irreplaceable
data, and allows rolling back non-commit transactions as always. But
when rolling back a commit, check that the working dir is checked out
to tip, i.e. the changeset we're about to destroy. If not, abort. You
can get back the old (dangerous) behaviour with --force.

diff --git a/hgext/keyword.py b/hgext/keyword.py
--- a/hgext/keyword.py
+++ b/hgext/keyword.py
@@ -590,12 +590,12 @@
                 kwt.restrict = restrict
             return n
 
-        def rollback(self, dryrun=False):
+        def rollback(self, dryrun=False, force=False):
             wlock = self.wlock()
             try:
                 if not dryrun:
                     changed = self['.'].files()
-                ret = super(kwrepo, self).rollback(dryrun)
+                ret = super(kwrepo, self).rollback(dryrun, force)
                 if not dryrun:
                     ctx = self['.']
                     modified, added = _preselect(self[None].status(), changed)
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -4612,7 +4612,8 @@
     finally:
         wlock.release()
 
- at command('rollback', dryrunopts)
+ at command('rollback', dryrunopts +
+         [('f', 'force', False, _('ignore safety measures'))])
 def rollback(ui, repo, **opts):
     """roll back the last transaction (dangerous)
 
@@ -4633,6 +4634,12 @@
     - push (with this repository as the destination)
     - unbundle
 
+    It's possible to lose data with rollback: commit, update back to
+    an older changeset, and then rollback. The update removes the
+    changes you committed from the working directory, and rollback
+    removes them from history. To avoid data loss, you must pass
+    --force in this case.
+
     This command is not intended for use on public repositories. Once
     changes are visible for pull by other users, rolling a transaction
     back locally is ineffective (someone else may already have pulled
@@ -4642,7 +4649,8 @@
 
     Returns 0 on success, 1 if no rollback data is available.
     """
-    return repo.rollback(opts.get('dry_run'))
+    return repo.rollback(dryrun=opts.get('dry_run'),
+                         force=opts.get('force'))
 
 @command('root', [])
 def root(ui, repo):
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -754,20 +754,20 @@
         finally:
             lock.release()
 
-    def rollback(self, dryrun=False):
+    def rollback(self, dryrun=False, force=False):
         wlock = lock = None
         try:
             wlock = self.wlock()
             lock = self.lock()
             if os.path.exists(self.sjoin("undo")):
-                return self._rollback(dryrun)
+                return self._rollback(dryrun, force)
             else:
                 self.ui.warn(_("no rollback information available\n"))
                 return 1
         finally:
             release(lock, wlock)
 
-    def _rollback(self, dryrun):
+    def _rollback(self, dryrun, force):
         ui = self.ui
         try:
             args = self.opener.read('undo.desc').splitlines()
@@ -786,6 +786,13 @@
                        % (oldtip, desc))
         except IOError:
             msg = _('rolling back unknown transaction\n')
+            desc = None
+
+        if not force and self['.'] != self['tip'] and desc == 'commit':
+            raise util.Abort(
+                _('rollback of last commit while not checked out '
+                  'may lose data (use -f to force)'))
+
         ui.status(msg)
         if dryrun:
             return 0
diff --git a/tests/test-debugcomplete.t b/tests/test-debugcomplete.t
--- a/tests/test-debugcomplete.t
+++ b/tests/test-debugcomplete.t
@@ -257,7 +257,7 @@
   rename: after, force, include, exclude, dry-run
   resolve: all, list, mark, unmark, no-status, tool, include, exclude
   revert: all, date, rev, no-backup, include, exclude, dry-run
-  rollback: dry-run
+  rollback: dry-run, force
   root: 
   showconfig: untrusted
   tag: force, local, rev, remove, edit, message, date, user
diff --git a/tests/test-import-bypass.t b/tests/test-import-bypass.t
--- a/tests/test-import-bypass.t
+++ b/tests/test-import-bypass.t
@@ -60,7 +60,7 @@
   |/
   @  0:07f494440405 test 0 0 - default - adda
   
-  $ hg rollback
+  $ hg rollback -f
   repository tip rolled back to revision 1 (undo commit)
 
 Test --import-branch
@@ -72,7 +72,7 @@
   |
   @  0:07f494440405 test 0 0 - default - adda
   
-  $ hg rollback
+  $ hg rollback -f
   repository tip rolled back to revision 1 (undo commit)
 
 Test --strip
@@ -94,7 +94,7 @@
   > +a
   > EOF
   applying patch from stdin
-  $ hg rollback
+  $ hg rollback -f
   repository tip rolled back to revision 1 (undo commit)
 
 Test unsupported combinations
diff --git a/tests/test-rollback.t b/tests/test-rollback.t
--- a/tests/test-rollback.t
+++ b/tests/test-rollback.t
@@ -83,7 +83,7 @@
   $ hg bookmark bar
   $ cat .hg/undo.branch ; echo
   test
-  $ hg rollback
+  $ hg rollback -f
   repository tip rolled back to revision 1 (undo commit)
   $ hg id -n
   0
@@ -146,3 +146,37 @@
   working directory now based on revision 0
   $ hg id default
   791dd2169706
+
+update to older changeset and then refuse rollback, because
+that would lose data (issue2998)
+  $ cd ../t
+  $ hg -q update
+  $ rm `hg status -un`
+  $ template='{rev}:{node|short}  [{branch}]  {desc|firstline}\n'
+  $ echo 'valuable new file' > b
+  $ echo 'valuable modification' >> a
+  $ hg commit -A -m'a valuable change'
+  adding b
+  $ hg update 0
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg rollback
+  abort: rollback of last commit while not checked out may lose data (use -f to force)
+  [255]
+  $ hg tip -q
+  2:4d9cd3795eea
+  $ hg rollback -f
+  repository tip rolled back to revision 1 (undo commit)
+  $ hg status
+  $ hg log --removed b   # yep, it's gone
+
+same again, but emulate an old client that doesn't write undo.desc
+  $ hg -q update
+  $ echo 'valuable modification redux' >> a
+  $ hg commit -m'a valuable change redux'
+  $ rm .hg/undo.desc
+  $ hg update 0
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg rollback
+  rolling back unknown transaction
+  $ cat a
+  a


More information about the Mercurial-devel mailing list