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