[PATCH 2 of 2 RFC] rollback: try to prevent problems with shared clones

Greg Ward greg-hg at gerg.ca
Mon Sep 12 19:16:27 CDT 2011


# HG changeset patch
# User Greg Ward <greg-hg at gerg.ca>
# Date 1315790054 14400
# Node ID 652cef658e59e6922694fc58d59d3858b3590d7b
# Parent  2d12345fdecc19eff9d0498f9ee98b15669182c9
rollback: try to prevent problems with shared clones.

Trouble happens with shared clones (multiple working dirs all sharing
the same repo store) if you commit in one working dir and then
rollback that transaction in a different working dir: rollback uses
partial info (.hg/store/undo but no .hg/undo.*) and generally makes a
mess of things. Try to avoid that by refusing to rollback unless we're
in the same working dir where the last transaction was created from.

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -4388,7 +4388,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)
 
@@ -4418,7 +4419,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
@@ -718,7 +718,16 @@
         return tr
 
     def _writejournal(self, desc):
-        # save dirstate for rollback
+        # save pre-transaction state for rollback
+        # - path to working dir (in case of shared clones)
+        # - content of dirstate
+        # - current branch
+        # - current len(repo) (tip rev + 1), description of transaction
+        #   (N.B. if desc contains a newline, then undo.desc is effectively
+        #   a 3-tuple, not a 2-tuple!)
+        # - current bookmark
+
+        self.sopener.write('journal.wdir', '%s\n' % self.root)
         try:
             ds = self.opener.read("dirstate")
         except IOError:
@@ -735,8 +744,11 @@
         else:
             self.opener.write('journal.bookmarks', '')
 
-        return (self.sjoin('journal'), self.join('journal.dirstate'),
-                self.join('journal.branch'), self.join('journal.desc'),
+        return (self.sjoin('journal'), 
+                self.sjoin('journal.wdir'),
+                self.join('journal.dirstate'),
+                self.join('journal.branch'), 
+                self.join('journal.desc'),
                 self.join('journal.bookmarks'))
 
     def recover(self):
@@ -754,20 +766,33 @@
         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):
+        # check preconditions
+        try:
+            wdir = self.sopener.read('undo.wdir').strip()
+        except IOError:
+            pass
+        else:
+            if not force and wdir != self.root:
+                raise util.Abort(
+                    _('attempt to rollback from a different working dir '
+                      'than where this transaction was created (%s) '
+                      '(use -f to force: this will make a mess)')
+                    % wdir)
+
         try:
             args = self.opener.read("undo.desc").splitlines()
             if len(args) >= 3 and self.ui.verbose:
diff --git a/tests/test-rollback-safe.t b/tests/test-rollback-safe.t
new file mode 100644
--- /dev/null
+++ b/tests/test-rollback-safe.t
@@ -0,0 +1,36 @@
+setup: repo with two changesets
+  $ hg init repo1
+  $ cd repo1
+  $ echo a > a
+  $ hg -q commit -A -m'rev 0: add a'
+  $ echo b > b
+  $ hg -q commit -A -m'rev 1: add b'
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > share =
+  > EOF
+
+keep a backup copy for repeated rollback attempts
+  $ cd ..
+  $ tar -cf repo1.tar repo1
+
+attempt rollback in a shared clone
+  $ hg share repo1 share
+  updating working directory
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd share
+  $ hg rollback
+  abort: attempt to rollback from a different working dir than where this transaction was created ($TESTTMP/repo1) (use -f to force: this will make a mess)
+  [255]
+  $ hg tip -q
+  1:d7cd497f77e1
+  $ hg rollback -f
+  rolling back unknown transaction
+  abort: No such file or directory
+  [255]
+  $ hg tip -q
+  0:b9795f9c9ebd
+  $ hg status
+  warning: ignoring unknown working parent d7cd497f77e1!
+  M a
+  M b


More information about the Mercurial-devel mailing list