Rev 5636: (jameinel) Add 'bzr reset-workingtree' as a way to fix a corrupted dirstate in file:///home/pqm/archives/thelove/bzr/%2Btrunk/

Canonical.com Patch Queue Manager pqm at pqm.ubuntu.com
Thu Jan 27 17:45:27 UTC 2011


At file:///home/pqm/archives/thelove/bzr/%2Btrunk/

------------------------------------------------------------
revno: 5636 [merge]
revision-id: pqm at pqm.ubuntu.com-20110127174524-89zg98o11grbj26x
parent: pqm at pqm.ubuntu.com-20110127170513-w1gozoytzqeerfbd
parent: john at arbash-meinel.com-20110127162705-50yqwnbkvcrw0yfl
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Thu 2011-01-27 17:45:24 +0000
message:
  (jameinel) Add 'bzr reset-workingtree' as a way to fix a corrupted dirstate
   file. (John A Meinel)
added:
  bzrlib/tests/blackbox/test_repair_workingtree.py test_reset_workingtr-20110124212823-0kwmrmaesin4dnbv-1
  bzrlib/tests/per_workingtree/test_check_state.py test_check_state.py-20110124214803-mmm65pi0pdgdfhtw-1
modified:
  bzrlib/builtins.py             builtins.py-20050830033751-fc01482b9ca23183
  bzrlib/dirstate.py             dirstate.py-20060728012006-d6mvoihjb3je9peu-1
  bzrlib/tests/blackbox/__init__.py __init__.py-20051128053524-eba30d8255e08dc3
  bzrlib/tests/per_workingtree/__init__.py __init__.py-20060203003124-b2aa5aca21a8bfad
  bzrlib/tests/test_dirstate.py  test_dirstate.py-20060728012006-d6mvoihjb3je9peu-2
  bzrlib/workingtree.py          workingtree.py-20050511021032-29b6ec0a681e02e3
  bzrlib/workingtree_4.py        workingtree_4.py-20070208044105-5fgpc5j3ljlh5q6c-1
  doc/en/release-notes/bzr-2.4.txt bzr2.4.txt-20110114053217-k7ym9jfz243fddjm-1
=== modified file 'bzrlib/builtins.py'
--- a/bzrlib/builtins.py	2011-01-25 13:59:32 +0000
+++ b/bzrlib/builtins.py	2011-01-27 17:45:24 +0000
@@ -482,6 +482,59 @@
             d.destroy_workingtree()
 
 
+class cmd_repair_workingtree(Command):
+    __doc__ = """Reset the working tree state file.
+
+    This is not meant to be used normally, but more as a way to recover from
+    filesystem corruption, etc. This rebuilds the working inventory back to a
+    'known good' state. Any new modifications (adding a file, renaming, etc)
+    will be lost, though modified files will still be detected as such.
+
+    Most users will want something more like "bzr revert" or "bzr update"
+    unless the state file has become corrupted.
+
+    By default this attempts to recover the current state by looking at the
+    headers of the state file. If the state file is too corrupted to even do
+    that, you can supply --revision to force the state of the tree.
+    """
+
+    takes_options = ['revision', 'directory',
+        Option('force',
+               help='Reset the tree even if it doesn\'t appear to be'
+                    ' corrupted.'),
+    ]
+    hidden = True
+
+    def run(self, revision=None, directory='.', force=False):
+        tree, _ = WorkingTree.open_containing(directory)
+        self.add_cleanup(tree.lock_tree_write().unlock)
+        if not force:
+            try:
+                tree.check_state()
+            except errors.BzrError:
+                pass # There seems to be a real error here, so we'll reset
+            else:
+                # Refuse
+                raise errors.BzrCommandError(
+                    'The tree does not appear to be corrupt. You probably'
+                    ' want "bzr revert" instead. Use "--force" if you are'
+                    ' sure you want to reset the working tree.')
+        if revision is None:
+            revision_ids = None
+        else:
+            revision_ids = [r.as_revision_id(tree.branch) for r in revision]
+        try:
+            tree.reset_state(revision_ids)
+        except errors.BzrError, e:
+            if revision_ids is None:
+                extra = (', the header appears corrupt, try passing -r -1'
+                         ' to set the state to the last commit')
+            else:
+                extra = ''
+            raise errors.BzrCommandError('failed to reset the tree state'
+                                         + extra)
+
+
 class cmd_revno(Command):
     __doc__ = """Show current revision number.
 

=== modified file 'bzrlib/dirstate.py'
--- a/bzrlib/dirstate.py	2010-09-13 06:36:59 +0000
+++ b/bzrlib/dirstate.py	2011-01-25 22:54:08 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2006-2010 Canonical Ltd
+# Copyright (C) 2006-2011 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -2681,6 +2681,23 @@
         if tracing:
             trace.mutter("set_state_from_inventory complete.")
 
+    def set_state_from_scratch(self, working_inv, parent_trees, parent_ghosts):
+        """Wipe the currently stored state and set it to something new.
+
+        This is a hard-reset for the data we are working with.
+        """
+        # Technically, we really want a write lock, but until we write, we
+        # don't really need it.
+        self._requires_lock()
+        # root dir and root dir contents with no children. We have to have a
+        # root for set_state_from_inventory to work correctly.
+        empty_root = (('', '', inventory.ROOT_ID),
+                      [('d', '', 0, False, DirState.NULLSTAT)])
+        empty_tree_dirblocks = [('', [empty_root]), ('', [])]
+        self._set_data([], empty_tree_dirblocks)
+        self.set_state_from_inventory(working_inv)
+        self.set_parent_trees(parent_trees, parent_ghosts)
+
     def _make_absent(self, current_old):
         """Mark current_old - an entry - as absent for tree 0.
 

=== modified file 'bzrlib/tests/blackbox/__init__.py'
--- a/bzrlib/tests/blackbox/__init__.py	2010-11-12 02:12:40 +0000
+++ b/bzrlib/tests/blackbox/__init__.py	2011-01-27 17:45:24 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2005-2010 Canonical Ltd
+# Copyright (C) 2005-2011 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -99,6 +99,7 @@
                      'test_remove',
                      'test_re_sign',
                      'test_remove_tree',
+                     'test_repair_workingtree',
                      'test_resolve',
                      'test_revert',
                      'test_revno',

=== added file 'bzrlib/tests/blackbox/test_repair_workingtree.py'
--- a/bzrlib/tests/blackbox/test_repair_workingtree.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/blackbox/test_repair_workingtree.py	2011-01-27 16:27:05 +0000
@@ -0,0 +1,97 @@
+# Copyright (C) 2011 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+from bzrlib import (
+    workingtree,
+    )
+from bzrlib.tests import TestCaseWithTransport
+
+
+class TestRepairWorkingTree(TestCaseWithTransport):
+
+    def break_dirstate(self, tree, completely=False):
+        """Write garbage into the dirstate file."""
+        # This test assumes that the format uses a DirState file, which we then
+        # manually corrupt. If we change the way to get at that dirstate file,
+        # then we can update how this is done
+        self.assertIsNot(None, getattr(tree, 'current_dirstate', None))
+        tree.lock_read()
+        try:
+            dirstate = tree.current_dirstate()
+            dirstate_path = dirstate._filename
+            self.failUnlessExists(dirstate_path)
+        finally:
+            tree.unlock()
+        # We have to have the tree unlocked at this point, so we can safely
+        # mutate the state file on all platforms.
+        if completely:
+            f = open(dirstate_path, 'wb')
+        else:
+            f = open(dirstate_path, 'ab')
+        try:
+            f.write('garbage-at-end-of-file\n')
+        finally:
+            f.close()
+
+    def make_initial_tree(self):
+        tree = self.make_branch_and_tree('tree')
+        self.build_tree(['tree/foo', 'tree/dir/', 'tree/dir/bar'])
+        tree.add(['foo', 'dir', 'dir/bar'])
+        tree.commit('first')
+        return tree
+
+    def test_repair_refuses_uncorrupted(self):
+        tree = self.make_initial_tree()
+        # If the tree doesn't appear to be corrupt, we refuse, but prompt the
+        # user to let them know that:
+        # a) they may want to use 'bzr revert' instead of repair-workingtree
+        # b) they can use --force if they really want to do this
+        self.run_bzr_error(['The tree does not appear to be corrupt',
+                            '"bzr revert"',
+                            '--force'],
+                           'repair-workingtree -d tree')
+
+    def test_repair_forced(self):
+        tree = self.make_initial_tree()
+        tree.rename_one('dir', 'alt_dir')
+        self.assertIsNot(None, tree.path2id('alt_dir'))
+        self.run_bzr('repair-workingtree -d tree --force')
+        # This requires the tree has reloaded the working state
+        self.assertIs(None, tree.path2id('alt_dir'))
+        self.failUnlessExists('tree/alt_dir')
+
+    def test_repair_corrupted_dirstate(self):
+        tree = self.make_initial_tree()
+        self.break_dirstate(tree)
+        self.run_bzr('repair-workingtree -d tree')
+        tree = workingtree.WorkingTree.open('tree')
+        # At this point, check should be happy
+        tree.check_state()
+
+    def test_repair_naive_destroyed_fails(self):
+        tree = self.make_initial_tree()
+        self.break_dirstate(tree, completely=True)
+        self.run_bzr_error(['the header appears corrupt, try passing'],
+                           'repair-workingtree -d tree')
+
+    def test_repair_destroyed_with_revs_passes(self):
+        tree = self.make_initial_tree()
+        self.break_dirstate(tree, completely=True)
+        self.run_bzr('repair-workingtree -d tree -r -1')
+        tree = workingtree.WorkingTree.open('tree')
+        # At this point, check should be happy
+        tree.check_state()

=== modified file 'bzrlib/tests/per_workingtree/__init__.py'
--- a/bzrlib/tests/per_workingtree/__init__.py	2010-10-26 13:53:31 +0000
+++ b/bzrlib/tests/per_workingtree/__init__.py	2011-01-24 21:49:59 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2006-2010 Canonical Ltd
+# Copyright (C) 2006-2011 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -83,6 +83,7 @@
         'break_lock',
         'changes_from',
         'check',
+        'check_state',
         'content_filters',
         'commit',
         'eol_conversion',

=== added file 'bzrlib/tests/per_workingtree/test_check_state.py'
--- a/bzrlib/tests/per_workingtree/test_check_state.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/per_workingtree/test_check_state.py	2011-01-25 23:21:46 +0000
@@ -0,0 +1,110 @@
+# Copyright (C) 2011 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Tests for WorkingTree.check_state."""
+
+from bzrlib import (
+    errors,
+    tests,
+    )
+from bzrlib.tests.per_workingtree import TestCaseWithWorkingTree
+
+
+
+class TestCaseWithState(TestCaseWithWorkingTree):
+
+    def make_tree_with_broken_dirstate(self, path):
+        tree = self.make_branch_and_tree(path)
+        self.break_dirstate(tree)
+        return tree
+
+    def break_dirstate(self, tree, completely=False):
+        """Write garbage into the dirstate file."""
+        if getattr(tree, 'current_dirstate', None) is None:
+            raise tests.TestNotApplicable(
+                'Only applies to dirstate-based trees')
+        tree.lock_read()
+        try:
+            dirstate = tree.current_dirstate()
+            dirstate_path = dirstate._filename
+            self.failUnlessExists(dirstate_path)
+        finally:
+            tree.unlock()
+        # We have to have the tree unlocked at this point, so we can safely
+        # mutate the state file on all platforms.
+        if completely:
+            f = open(dirstate_path, 'wb')
+        else:
+            f = open(dirstate_path, 'ab')
+        try:
+            f.write('garbage-at-end-of-file\n')
+        finally:
+            f.close()
+
+
+class TestCheckState(TestCaseWithState):
+
+    def test_check_state(self):
+        tree = self.make_branch_and_tree('tree')
+        # Everything should be fine with an unmodified tree, no exception
+        # should be raised.
+        tree.check_state()
+
+    def test_check_broken_dirstate(self):
+        tree = self.make_tree_with_broken_dirstate('tree')
+        self.assertRaises(errors.BzrError, tree.check_state)
+
+
+class TestResetState(TestCaseWithState):
+
+    def make_initial_tree(self):
+        tree = self.make_branch_and_tree('tree')
+        self.build_tree(['tree/foo', 'tree/dir/', 'tree/dir/bar'])
+        tree.add(['foo', 'dir', 'dir/bar'])
+        tree.commit('initial')
+        return tree
+
+    def test_reset_state_forgets_changes(self):
+        tree = self.make_initial_tree()
+        foo_id = tree.path2id('foo')
+        tree.rename_one('foo', 'baz')
+        self.assertEqual(None, tree.path2id('foo'))
+        self.assertEqual(foo_id, tree.path2id('baz'))
+        tree.reset_state()
+        # After reset, we should have forgotten about the rename, but we won't
+        # have
+        self.assertEqual(foo_id, tree.path2id('foo'))
+        self.assertEqual(None, tree.path2id('baz'))
+        self.failIfExists('tree/foo')
+        self.failUnlessExists('tree/baz')
+
+    def test_reset_state_handles_corrupted_dirstate(self):
+        tree = self.make_initial_tree()
+        rev_id = tree.last_revision()
+        self.break_dirstate(tree)
+        tree.reset_state()
+        tree.check_state()
+        self.assertEqual(rev_id, tree.last_revision())
+
+    def test_reset_state_handles_destroyed_dirstate(self):
+        # If you pass the revision_id, we can handle a completely destroyed
+        # dirstate file.
+        tree = self.make_initial_tree()
+        rev_id = tree.last_revision()
+        self.break_dirstate(tree, completely=True)
+        tree.reset_state(revision_ids=[rev_id])
+        tree.check_state()
+        self.assertEqual(rev_id, tree.last_revision())

=== modified file 'bzrlib/tests/test_dirstate.py'
--- a/bzrlib/tests/test_dirstate.py	2011-01-12 01:01:53 +0000
+++ b/bzrlib/tests/test_dirstate.py	2011-01-25 22:54:08 +0000
@@ -725,6 +725,14 @@
 
 class TestDirStateManipulations(TestCaseWithDirState):
 
+    def make_minimal_tree(self):
+        tree1 = self.make_branch_and_memory_tree('tree1')
+        tree1.lock_write()
+        self.addCleanup(tree1.unlock)
+        tree1.add('')
+        revid1 = tree1.commit('foo')
+        return tree1, revid1
+
     def test_update_minimal_updates_id_index(self):
         state = self.create_dirstate_with_root_and_subdir()
         self.addCleanup(state.unlock)
@@ -743,15 +751,9 @@
 
     def test_set_state_from_inventory_no_content_no_parents(self):
         # setting the current inventory is a slow but important api to support.
-        tree1 = self.make_branch_and_memory_tree('tree1')
-        tree1.lock_write()
-        try:
-            tree1.add('')
-            revid1 = tree1.commit('foo').encode('utf8')
-            root_id = tree1.get_root_id()
-            inv = tree1.inventory
-        finally:
-            tree1.unlock()
+        tree1, revid1 = self.make_minimal_tree()
+        inv = tree1.inventory
+        root_id = inv.path2id('')
         expected_result = [], [
             (('', '', root_id), [
              ('d', '', 0, False, dirstate.DirState.NULLSTAT)])]
@@ -769,6 +771,50 @@
             # This will unlock it
             self.check_state_with_reopen(expected_result, state)
 
+    def test_set_state_from_scratch_no_parents(self):
+        tree1, revid1 = self.make_minimal_tree()
+        inv = tree1.inventory
+        root_id = inv.path2id('')
+        expected_result = [], [
+            (('', '', root_id), [
+             ('d', '', 0, False, dirstate.DirState.NULLSTAT)])]
+        state = dirstate.DirState.initialize('dirstate')
+        try:
+            state.set_state_from_scratch(inv, [], [])
+            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
+                             state._header_state)
+            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
+                             state._dirblock_state)
+        except:
+            state.unlock()
+            raise
+        else:
+            # This will unlock it
+            self.check_state_with_reopen(expected_result, state)
+
+    def test_set_state_from_scratch_identical_parent(self):
+        tree1, revid1 = self.make_minimal_tree()
+        inv = tree1.inventory
+        root_id = inv.path2id('')
+        rev_tree1 = tree1.branch.repository.revision_tree(revid1)
+        d_entry = ('d', '', 0, False, dirstate.DirState.NULLSTAT)
+        parent_entry = ('d', '', 0, False, revid1)
+        expected_result = [revid1], [
+            (('', '', root_id), [d_entry, parent_entry])]
+        state = dirstate.DirState.initialize('dirstate')
+        try:
+            state.set_state_from_scratch(inv, [(revid1, rev_tree1)], [])
+            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
+                             state._header_state)
+            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
+                             state._dirblock_state)
+        except:
+            state.unlock()
+            raise
+        else:
+            # This will unlock it
+            self.check_state_with_reopen(expected_result, state)
+
     def test_set_state_from_inventory_preserves_hashcache(self):
         # https://bugs.launchpad.net/bzr/+bug/146176
         # set_state_from_inventory should preserve the stat and hash value for

=== modified file 'bzrlib/workingtree.py'
--- a/bzrlib/workingtree.py	2011-01-26 20:02:52 +0000
+++ b/bzrlib/workingtree.py	2011-01-27 17:45:24 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2005-2010 Canonical Ltd
+# Copyright (C) 2005-2011 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -2675,6 +2675,34 @@
         """
         return
 
+    @needs_read_lock
+    def check_state(self):
+        """Check that the working state is/isn't valid."""
+        check_refs = self._get_check_refs()
+        refs = {}
+        for ref in check_refs:
+            kind, value = ref
+            if kind == 'trees':
+                refs[ref] = self.branch.repository.revision_tree(value)
+        self._check(refs)
+
+    @needs_tree_write_lock
+    def reset_state(self, revision_ids=None):
+        """Reset the state of the working tree.
+
+        This does a hard-reset to a last-known-good state. This is a way to
+        fix if something got corrupted (like the .bzr/checkout/dirstate file)
+        """
+        if revision_ids is None:
+            revision_ids = self.get_parent_ids()
+        if not revision_ids:
+            rt = self.branch.repository.revision_tree(
+                _mod_revision.NULL_REVISION)
+        else:
+            rt = self.branch.repository.revision_tree(revision_ids[0])
+        self._write_inventory(rt.inventory)
+        self.set_parent_ids(revision_ids)
+
     def _get_rules_searcher(self, default_searcher):
         """See Tree._get_rules_searcher."""
         if self._rules_searcher is None:

=== modified file 'bzrlib/workingtree_4.py'
--- a/bzrlib/workingtree_4.py	2010-12-02 10:41:05 +0000
+++ b/bzrlib/workingtree_4.py	2011-01-25 23:21:46 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2007-2010 Canonical Ltd
+# Copyright (C) 2007-2011 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -1293,6 +1293,27 @@
             self._inventory = inv
         self.flush()
 
+    @needs_tree_write_lock
+    def reset_state(self, revision_ids=None):
+        """Reset the state of the working tree.
+
+        This does a hard-reset to a last-known-good state. This is a way to
+        fix if something got corrupted (like the .bzr/checkout/dirstate file)
+        """
+        if revision_ids is None:
+            revision_ids = self.get_parent_ids()
+        if not revision_ids:
+            base_tree = self.branch.repository.revision_tree(
+                _mod_revision.NULL_REVISION)
+            trees = []
+        else:
+            trees = zip(revision_ids,
+                        self.branch.repository.revision_trees(revision_ids))
+            base_tree = trees[0][1]
+        state = self.current_dirstate()
+        # We don't support ghosts yet
+        state.set_state_from_scratch(base_tree.inventory, trees, [])
+
 
 class ContentFilterAwareSHA1Provider(dirstate.SHA1Provider):
 

=== modified file 'doc/en/release-notes/bzr-2.4.txt'
--- a/doc/en/release-notes/bzr-2.4.txt	2011-01-26 20:07:50 +0000
+++ b/doc/en/release-notes/bzr-2.4.txt	2011-01-27 17:45:24 +0000
@@ -32,6 +32,10 @@
 .. Improvements to existing commands, especially improved performance 
    or memory usage, or better results.
 
+* A new hidden command ``bzr repair-workingtree``. This is a way to force
+  the dirstate file to be rebuilt, rather than using a ``bzr checkout``
+  workaround. (John Arbash Meinel)
+
 * ``bzr cat-revision`` no longer requires a working tree. (Jelmer Vernooij, #704405)
 
 Bug Fixes
@@ -99,3 +103,5 @@
    suite.  This can include new facilities for writing tests, fixes to 
    spurious test failures and changes to the way things should be tested.
 
+..
+   vim: tw=74 ft=rst ff=unix




More information about the bazaar-commits mailing list