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

Greg Ward greg-hg at gerg.ca
Thu Sep 29 21:55:16 CDT 2011


# HG changeset patch
# User Greg Ward <greg at gerg.ca>
# Date 1317350673 14400
# Node ID c10af212a55cbfa84b5d5182ee41028c4baa33c6
# Parent  f295542f482d4b567fa6229b93c1854aca78792c
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.  (Or, 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 rollbacks can be considered "safe" or "unsafe". A
safe rollback is one that aborts a transaction whose data already
exists somewhere else: pull, unbundle, or push. Don't be so picky
about rolling back those transactions, since the repo or bundle from
which they came presumably still exists. Every other rollback, most
typically of a commit transaction, is considered unsafe. Finally, you
can get back the existing dangerous behaviour with --force.

This is PRELIMINARY for review only. I still have some broken tests to
deal with (looks like some tests rely on the existing dangerous
behaviour). More importantly, it's an open issue whether we should do
anything similar with strip. In this case it seems unnecessary, since
strip by default backs up the changesets that it destroys. But I want
to send another "make rollback safer" change -- prevent rollback from
the wrong shared clone -- and that probably should affect strip.

So: should we factor out a pre-destroy sanity check that can be shared
by strip and rollback? Same sort of idea as repo.destroyed(), except
it would be called before rollback or strip do anything you might
regret.

diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -786,6 +786,16 @@
                        % (oldtip, desc))
         except IOError:
             msg = _('rolling back unknown transaction\n')
+            desc = None
+
+        # if the changesets added in the last transaction came from
+        # somewhere else, consider the rollback "safe"
+        safe = desc in ('pull', 'push', 'unbundle')
+        nottip = self.dirstate.parents()[0] != self.changelog.tip()
+        if not force and not safe and nottip:
+            raise util.Abort(_('attempt to rollback a commit when not at tip '
+                               '(use -f to force and lose data)'))
+
         ui.status(msg)
         if dryrun:
             return 0
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,32 @@
   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'
+#  $ hg --config extensions.graphlog= glog --template="$template"
+  $ echo 'valuable new file' > b
+  $ echo 'valuable modification' >> a
+  $ hg commit -A -m'a valuable change'
+  adding b
+#  $ hg --config extensions.graphlog= glog  --template="$template"
+  $ hg status
+  $ hg update 0
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg rollback
+  abort: attempt to rollback a commit when not at tip (use -f to force and lose data)
+  [255]
+  $ hg tip -q
+  2:4d9cd3795eea
+  $ hg parents -q
+  0:23b0221f3370
+  $ hg rollback -f
+  repository tip rolled back to revision 1 (undo commit)
+  $ hg parents -q
+  0:23b0221f3370
+  $ hg status
+  $ hg log --removed b   # yep, it's gone


More information about the Mercurial-devel mailing list