[MERGE] Implement KnitRepository._find_inconsistent_revision_parents

Andrew Bennetts andrew at canonical.com
Mon Sep 24 05:05:07 BST 2007


Andrew Bennetts wrote:
> This adds a method to KnitRepository that can detect if the knit index graph
> disagrees with the revision texts about what the parents of a revision are.
> It's not used by anything yet, aside from the test, but I'm about to hook it
> into check.

This builds on that patch to actually hook it into check and reconcile.  It
teaches reconcile to abort in the face of this inconsistency, to be safe.
Ideally reconcile would correct it, but until we implement that it shouldn't do
operations that potentially trust that data.

Aaron wrote in his earlier review:
] Looks reasonable.   I wonder whether the tests should blacklist weaves rather
] than whitelisting knits?

Robert had a similar concern, which is that packs will have the same possibility
for errors here, so should not be silently excluded from these tests.

So I've added a “revision_graph_can_have_wrong_parents” method to repository
objects so they can declare if they are capable of having this sort of
corruption.

Separately we should improve per-repository test parameterisation to provide a
paramaterisable way to corrupt a repo.  Until then my tests are still tied to
knits (because it pokes at knit internals), but at least it has clearer XXXs
about that fact now.

-Andrew.

-------------- next part --------------
# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: andrew.bennetts at canonical.com-20070924034521-\
#   hbp0u7drdwzgx8b0
# target_branch: http://bazaar-vcs.org/bzr/bzr.dev
# testament_sha1: b287ea74cf824ff8375659fd1f39917285e89ba2
# timestamp: 2007-09-24 14:03:53 +1000
# source_branch: http://people.ubuntu.com/~andrew/bzr/find-\
#   inconsistent-parents
# base_revision_id: pqm at pqm.ubuntu.com-20070914004213-xraql0v7q1p63j81
# 
# Begin patch
=== added file 'bzrlib/tests/repository_implementations/helpers.py'
--- bzrlib/tests/repository_implementations/helpers.py	1970-01-01 00:00:00 +0000
+++ bzrlib/tests/repository_implementations/helpers.py	2007-09-24 03:45:21 +0000
@@ -0,0 +1,70 @@
+# Copyright (C) 2007 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""Helper classes for repository implementation tests."""
+
+
+from bzrlib import (
+    inventory,
+    revision as _mod_revision,
+    )
+from bzrlib.repofmt.knitrepo import RepositoryFormatKnit
+from bzrlib.tests.repository_implementations import TestCaseWithRepository
+from bzrlib.tests import TestNotApplicable
+
+
+class TestCaseWithBrokenRevisionIndex(TestCaseWithRepository):
+
+    def make_repo_with_extra_ghost_index(self):
+        """Make a corrupt repository.
+        
+        It will contain one revision, 'revision-id'.  The knit index will claim
+        that it has one parent, 'incorrect-parent', but the revision text will
+        claim it has no parents.
+
+        Note: only the *cache* of the knit index is corrupted.  Thus the
+        corruption will only last while the repository is locked.  For this
+        reason, the returned repo is locked.
+        """
+        if not isinstance(self.repository_format, RepositoryFormatKnit):
+            # XXX: Broken revision graphs can happen to weaves too, but they're
+            # pretty deprecated.  Ideally these tests should apply to any repo
+            # where repo.revision_graph_can_have_wrong_parents() is True, but
+            # at the moment we only know how to corrupt knit repos.
+            raise TestNotApplicable(
+                "%s isn't a knit format" % self.repository_format)
+
+        repo = self.make_repository('broken')
+        inv = inventory.Inventory(revision_id='revision-id')
+        inv.root.revision = 'revision-id'
+        repo.add_inventory('revision-id', inv, [])
+        revision = _mod_revision.Revision('revision-id',
+            committer='jrandom at example.com', timestamp=0,
+            inventory_sha1='', timezone=0, message='message', parent_ids=[])
+        repo.add_revision('revision-id',revision, inv)
+
+        # Change the knit index's record of the parents for 'revision-id' to
+        # claim it has a parent, 'incorrect-parent', that doesn't exist in this
+        # knit at all.
+        repo.lock_write()
+        self.addCleanup(repo.unlock)
+        rev_knit = repo._get_revision_vf()
+        index_cache = rev_knit._index._cache
+        cached_index_entry = list(index_cache['revision-id'])
+        cached_index_entry[4] = ['incorrect-parent']
+        index_cache['revision-id'] = tuple(cached_index_entry)
+        return repo
+

=== added file 'bzrlib/tests/repository_implementations/test_check.py'
--- bzrlib/tests/repository_implementations/test_check.py	1970-01-01 00:00:00 +0000
+++ bzrlib/tests/repository_implementations/test_check.py	2007-09-24 03:45:21 +0000
@@ -0,0 +1,73 @@
+# Copyright (C) 2007 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+"""Test operations that check the repository for corruption"""
+
+
+from bzrlib import errors
+from bzrlib.tests.repository_implementations.helpers import (
+    TestCaseWithBrokenRevisionIndex,
+    )
+from bzrlib.tests import TestNotApplicable
+
+
+class TestFindInconsistentRevisionParents(TestCaseWithBrokenRevisionIndex):
+
+    def test__find_inconsistent_revision_parents(self):
+        """_find_inconsistent_revision_parents finds revisions with broken
+        parents.
+        """
+        repo = self.make_repo_with_extra_ghost_index()
+        self.assertEqual(
+            [('revision-id', ['incorrect-parent'], [])],
+            list(repo._find_inconsistent_revision_parents()))
+
+    def test__check_for_inconsistent_revision_parents(self):
+        """_check_for_inconsistent_revision_parents raises BzrCheckError if
+        there are any revisions with inconsistent parents.
+        """
+        repo = self.make_repo_with_extra_ghost_index()
+        self.assertRaises(
+            errors.BzrCheckError,
+            repo._check_for_inconsistent_revision_parents)
+
+    def test__check_for_inconsistent_revision_parents_on_clean_repo(self):
+        """_check_for_inconsistent_revision_parents does nothing if there are
+        no broken revisions.
+        """
+        repo = self.make_repository('empty-repo')
+        if not repo.revision_graph_can_have_wrong_parents():
+            raise TestNotApplicable(
+                '%r cannot have corrupt revision index.' % repo)
+        repo._check_for_inconsistent_revision_parents()  # nothing happens
+
+    def test_check_reports_bad_ancestor(self):
+        repo = self.make_repo_with_extra_ghost_index()
+        # XXX: check requires a non-empty revision IDs list, but it ignores the
+        # contents of it!
+        check_object = repo.check(['ignored'])
+        check_object.report_results(verbose=False)
+        log = self._get_log(keep_log_file=True)
+        self.assertContainsRe(
+            log, '1 revisions have incorrect parents in the revision index')
+        check_object.report_results(verbose=True)
+        log = self._get_log(keep_log_file=True)
+        self.assertContainsRe(
+            log,
+            "revision-id has wrong parents in index: "
+            r"\['incorrect-parent'\] should be \[\]")
+

=== modified file 'bzrlib/check.py'
--- bzrlib/check.py	2007-08-22 22:32:23 +0000
+++ bzrlib/check.py	2007-09-24 02:55:30 +0000
@@ -58,10 +58,12 @@
         self.repository.lock_read()
         self.progress = bzrlib.ui.ui_factory.nested_progress_bar()
         try:
-            self.progress.update('retrieving inventory', 0, 0)
+            self.progress.update('retrieving inventory', 0, 2)
             # do not put in init, as it should be done with progess,
             # and inside the lock.
             self.inventory_weave = self.repository.get_inventory_weave()
+            self.progress.update('checking revision graph', 1)
+            self.check_revision_graph()
             self.plan_revisions()
             revno = 0
             self.check_weaves()
@@ -75,6 +77,14 @@
             self.progress.finished()
             self.repository.unlock()
 
+    def check_revision_graph(self):
+        if not self.repository.revision_graph_can_have_wrong_parents():
+            # This check is not necessary.
+            self.revs_with_bad_parents_in_index = None
+            return
+        bad_revisions = self.repository._find_inconsistent_revision_parents()
+        self.revs_with_bad_parents_in_index = list(bad_revisions)
+
     def plan_revisions(self):
         repository = self.repository
         self.planned_revisions = set(repository.all_revision_ids())
@@ -113,6 +123,16 @@
                     note('      %s should be in the ancestry for:', link)
                     for linker in linkers:
                         note('       * %s', linker)
+        if self.revs_with_bad_parents_in_index:
+            note('%6d revisions have incorrect parents in the revision index',
+                 len(self.revs_with_bad_parents_in_index))
+            if verbose:
+                for item in self.revs_with_bad_parents_in_index:
+                    revision_id, index_parents, actual_parents = item
+                    note(
+                        '       %s has wrong parents in index: '
+                        '%r should be %r',
+                        revision_id, index_parents, actual_parents)
 
     def check_one_rev(self, rev_id):
         """Check one revision.

=== modified file 'bzrlib/progress.py'
--- bzrlib/progress.py	2007-07-11 06:47:30 +0000
+++ bzrlib/progress.py	2007-09-24 02:36:10 +0000
@@ -186,7 +186,7 @@
         self.MIN_PAUSE = 0.1 # seconds
         now = time.time()
         # starting now
-        self.start_time = now
+        self.start_time = None
         # next update should not throttle
         self.last_update = now - self.MIN_PAUSE - 1
 

=== modified file 'bzrlib/reconcile.py'
--- bzrlib/reconcile.py	2007-09-12 04:21:51 +0000
+++ bzrlib/reconcile.py	2007-09-24 03:45:21 +0000
@@ -20,8 +20,9 @@
 __all__ = ['reconcile', 'Reconciler', 'RepoReconciler', 'KnitReconciler']
 
 
+from bzrlib import errors
 from bzrlib import ui
-from bzrlib.trace import mutter
+from bzrlib.trace import mutter, note
 from bzrlib.tsort import TopoSorter
 
 
@@ -71,7 +72,13 @@
         repo_reconciler = self.repo.reconcile(thorough=True)
         self.inconsistent_parents = repo_reconciler.inconsistent_parents
         self.garbage_inventories = repo_reconciler.garbage_inventories
-        self.pb.note('Reconciliation complete.')
+        if repo_reconciler.aborted:
+            self.pb.note(
+                'Reconcile aborted: revision index has inconsistent parents.')
+            self.pb.note(
+                'Run "bzr check" for more details.')
+        else:
+            self.pb.note('Reconciliation complete.')
 
 
 class RepoReconciler(object):
@@ -89,6 +96,7 @@
         """
         self.garbage_inventories = 0
         self.inconsistent_parents = 0
+        self.aborted = False
         self.repo = repo
         self.thorough = thorough
 
@@ -275,7 +283,11 @@
     def _reconcile_steps(self):
         """Perform the steps to reconcile this repository."""
         if self.thorough:
-            self._load_indexes()
+            try:
+                self._load_indexes()
+            except errors.BzrCheckError:
+                self.aborted = True
+                return
             # knits never suffer this
             self._gc_inventory()
 
@@ -285,6 +297,7 @@
         self.pb.update('Reading indexes.', 0, 2)
         self.inventory = self.repo.get_inventory_weave()
         self.pb.update('Reading indexes.', 1, 2)
+        self.repo._check_for_inconsistent_revision_parents()
         self.revisions = self.repo._revision_store.get_revision_file(self.transaction)
         self.pb.update('Reading indexes.', 2, 2)
 

=== modified file 'bzrlib/remote.py'
--- bzrlib/remote.py	2007-08-22 05:28:32 +0000
+++ bzrlib/remote.py	2007-09-24 03:45:21 +0000
@@ -750,6 +750,19 @@
         self._ensure_real()
         return self._real_repository.has_signature_for_revision_id(revision_id)
 
+    def revision_graph_can_have_wrong_parents(self):
+        # The answer depends on the remote repo format.
+        self._ensure_real()
+        return self._real_repository.revision_graph_can_have_wrong_parents()
+
+    def _find_inconsistent_revision_parents(self):
+        self._ensure_real()
+        return self._real_repository._find_inconsistent_revision_parents()
+
+    def _check_for_inconsistent_revision_parents(self):
+        self._ensure_real()
+        return self._real_repository._check_for_inconsistent_revision_parents()
+
 
 class RemoteBranchLockableFiles(LockableFiles):
     """A 'LockableFiles' implementation that talks to a smart server.

=== modified file 'bzrlib/repofmt/knitrepo.py'
--- bzrlib/repofmt/knitrepo.py	2007-09-12 04:21:51 +0000
+++ bzrlib/repofmt/knitrepo.py	2007-09-24 02:55:30 +0000
@@ -224,6 +224,36 @@
     def _make_parents_provider(self):
         return _KnitParentsProvider(self._get_revision_vf())
 
+    def _find_inconsistent_revision_parents(self):
+        """Find revisions with different parent lists in the revision object
+        and in the index graph.
+
+        :returns: an iterator yielding tuples of (revison-id, parents-in-index,
+            parents-in-revision).
+        """
+        vf = self._get_revision_vf()
+        index_versions = vf.versions()
+        for index_version in index_versions:
+            parents_according_to_index = vf._index.get_parents_with_ghosts(
+                index_version)
+            revision = self._revision_store.get_revision(index_version,
+                self.get_transaction())
+            parents_according_to_revision = revision.parent_ids
+            if parents_according_to_index != parents_according_to_revision:
+                yield (index_version, parents_according_to_index,
+                    parents_according_to_revision)
+
+    def _check_for_inconsistent_revision_parents(self):
+        inconsistencies = list(self._find_inconsistent_revision_parents())
+        if inconsistencies:
+            raise errors.BzrCheckError(
+                "Revision knit has inconsistent parents.")
+
+    def revision_graph_can_have_wrong_parents(self):
+        # The revision.kndx could potentially claim a revision has a different
+        # parent to the revision text.
+        return True
+
 
 class KnitRepository3(KnitRepository):
 

=== modified file 'bzrlib/repofmt/weaverepo.py'
--- bzrlib/repofmt/weaverepo.py	2007-09-09 22:01:56 +0000
+++ bzrlib/repofmt/weaverepo.py	2007-09-24 02:55:30 +0000
@@ -105,6 +105,11 @@
         """Returns the policy for making working trees on new branches."""
         return True
 
+    def revision_graph_can_have_wrong_parents(self):
+        # XXX: This is an old format that we don't support full checking on, so
+        # just claim that checking for this inconsistency is not required.
+        return False
+
 
 class WeaveMetaDirRepository(MetaDirRepository):
     """A subclass of MetaDirRepository to set weave specific policy."""
@@ -121,6 +126,11 @@
         self.start_write_group()
         return result
 
+    def revision_graph_can_have_wrong_parents(self):
+        # XXX: This is an old format that we don't support full checking on, so
+        # just claim that checking for this inconsistency is not required.
+        return False
+
 
 class PreSplitOutRepositoryFormat(RepositoryFormat):
     """Base class for the pre split out repository formats."""
@@ -314,7 +324,6 @@
         """See RepositoryFormat._get_text_store()."""
         return self._get_versioned_file_store('weaves', transport, control_files)
 
-
 class RepositoryFormat7(MetaDirRepositoryFormat):
     """Bzr repository 7.
 

=== modified file 'bzrlib/repository.py'
--- bzrlib/repository.py	2007-09-12 21:27:42 +0000
+++ bzrlib/repository.py	2007-09-24 02:55:30 +0000
@@ -1203,7 +1203,18 @@
                     revision_id.decode('ascii')
                 except UnicodeDecodeError:
                     raise errors.NonAsciiRevisionId(method, self)
+    
+    def revision_graph_can_have_wrong_parents(self):
+        """Is it possible for this repository to have a revision graph with
+        incorrect parents?
 
+        If True, then this repository must also implement
+        _find_inconsistent_revision_parents so that check and reconcile can
+        check for inconsistencies before proceeding with other checks that may
+        depend on the revision index being consistent.
+        """
+        raise NotImplementedError(self.revision_graph_can_have_wrong_parents)
+        
 
 
 # remove these delegates a while after bzr 0.15

=== modified file 'bzrlib/tests/repository_implementations/__init__.py'
--- bzrlib/tests/repository_implementations/__init__.py	2007-08-17 05:16:14 +0000
+++ bzrlib/tests/repository_implementations/__init__.py	2007-09-14 04:17:57 +0000
@@ -99,6 +99,7 @@
     result = TestSuite()
     test_repository_implementations = [
         'bzrlib.tests.repository_implementations.test_break_lock',
+        'bzrlib.tests.repository_implementations.test_check',
         'bzrlib.tests.repository_implementations.test_commit_builder',
         'bzrlib.tests.repository_implementations.test_fetch',
         'bzrlib.tests.repository_implementations.test_fileid_involved',

=== modified file 'bzrlib/tests/repository_implementations/test_reconcile.py'
--- bzrlib/tests/repository_implementations/test_reconcile.py	2007-08-22 05:28:32 +0000
+++ bzrlib/tests/repository_implementations/test_reconcile.py	2007-09-24 03:45:21 +0000
@@ -14,19 +14,22 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
-"""Tests for reconiliation of repositories."""
+"""Tests for reconciliation of repositories."""
 
 
 import bzrlib
 import bzrlib.errors as errors
 from bzrlib.inventory import Inventory
 from bzrlib.reconcile import reconcile, Reconciler
+from bzrlib.repofmt.knitrepo import RepositoryFormatKnit
 from bzrlib.revision import Revision
-from bzrlib.tests import TestSkipped
+from bzrlib.tests import TestSkipped, TestNotApplicable
 from bzrlib.tests.repository_implementations.test_repository import TestCaseWithRepository
+from bzrlib.tests.repository_implementations.helpers import (
+    TestCaseWithBrokenRevisionIndex,
+    )
 from bzrlib.transport import get_transport
 from bzrlib.uncommit import uncommit
-from bzrlib.workingtree import WorkingTree
 
 
 class TestReconcile(TestCaseWithRepository):
@@ -374,3 +377,27 @@
         repo = d.open_repository()
         self.checkUnreconciled(d, repo.reconcile())
         self.checkUnreconciled(d, repo.reconcile(thorough=True))
+
+
+class TestBadRevisionParents(TestCaseWithBrokenRevisionIndex):
+
+    def test_aborts_if_bad_parents_in_index(self):
+        """Reconcile refuses to proceed if the revision index is wrong when
+        checked against the revision texts, so that it does not generate broken
+        data.
+
+        Ideally reconcile would fix this, but until we implement that we just
+        make sure we safely detect this problem.
+        """
+        repo = self.make_repo_with_extra_ghost_index()
+        reconciler = repo.reconcile(thorough=True)
+        self.assertTrue(reconciler.aborted,
+            "reconcile should have aborted due to bad parents.")
+
+    def test_does_not_abort_on_clean_repo(self):
+        repo = self.make_repository('.')
+        reconciler = repo.reconcile(thorough=True)
+        self.assertFalse(reconciler.aborted,
+            "reconcile should not have aborted on an unbroken repository.")
+
+

=== modified file 'bzrlib/tests/repository_implementations/test_repository.py'
--- bzrlib/tests/repository_implementations/test_repository.py	2007-08-21 03:40:50 +0000
+++ bzrlib/tests/repository_implementations/test_repository.py	2007-09-24 03:45:21 +0000
@@ -400,6 +400,22 @@
                           repository.iter_files_bytes(
                           [('file3-id', 'rev3', 'file1-notpresent')]))
 
+    def test_implements_revision_graph_can_have_wrong_parents(self):
+        """All repositories should implement
+        revision_graph_can_have_wrong_parents, so that check and reconcile can
+        work correctly.
+        """
+        repo = self.make_repository('.')
+        # This should work, not raise NotImplementedError:
+        result = repo.revision_graph_can_have_wrong_parents()
+        if result:
+            # If true, then this repo must also implement
+            # _find_inconsistent_revision_parents and
+            # _check_for_inconsistent_revision_parents.  So calling these
+            # should also not raise NotImplementedError.
+            list(repo._find_inconsistent_revision_parents())
+            repo._check_for_inconsistent_revision_parents()
+
 
 class TestRepositoryLocking(TestCaseWithRepository):
 

# Begin bundle
IyBCYXphYXIgcmV2aXNpb24gYnVuZGxlIHY0CiMKQlpoOTFBWSZTWYCUT2oAGgX/gGxWRAB7////
/////v////BgKL7nFNZ975wZ9vlUD5Pfe3u3aPPvjPeet4AeKoqQpSqqpUCRSgdsvvr42oKBMfb3
ysMrQo++8+99PpzpMD3ybZhT65cVh2cNjjtLS22ve96lfde7z48sXXDXbtF9DqlhJJAEGg0CehGm
E9JpqngmCZTymyR6bVNBgjQDaQShAAgFMTIRNNJ6EeUeoA0AADQZDQAASIIQk0CJ6p5NMmFP0UZD
QADRo0BoAAAAQpRASZo0k09EanqNNkeqPJNA9IADQDQAAABFEUxGmiJgaEwnqU/aqf5TEnoTahie
pGTymmmn6hGhoAwKkkAIAmiamI0yepkaKeamRMp6ZqnoaND1RtAACMyBxHHdQF4RWAEGXe2f2n/M
eBBEB0SbxTVA22ej3fkcm/HSj1cQ/VQSifZ39s8teuXF+x5jFe+j2cc/neSbS8Hhz7V+D/FXTe/9
X8rcDe62PFCxgi1eplY7v1/s472RSa8PRj5BRXoxJFivWp3t1j0jbJCHCfQO1l7DHnqFcPcsVNYT
KFuEQ5IEhzCqL3arup1OtpmfYY4gdqFhvfYb+lVgO/7AzXyaumUOsPmcfKKiZ3vbYsjTT8HZNfAM
ouDySo7WlxZVUtNQ3/Y/lvy8S2D3LHLQ69pC42VxoryrK+8Bw582HKvPRvKgknNJhyrqrP4duTsa
in+R3Swdx7RGKHAQNJSsh6v8fY+vy61bh6JliEyRMJCuNIT2J5wkKYtPRx49W3H4FmXaTsUSXkpf
AnJ3+/8PQoIgl1L5sgJmqqD2hBy5lDiFENuG4clW+g7iPUem55s36+3jliqoxkm5w7zD5Iz0bWZx
NRoxW20YvWZjFw87TqML7sb4y0vP7M0xPr6+rHfjifAkCbA9dEiFbxG6Isnnn0ySUli8iHMLcNA4
/rCLCjeL92XeqlZA+QQ8YhJVFCVUVVQ0gVVBQ1RSeDnzP8ivmk255y5aeX5yc9E606NvLNDZrxw2
vPMYxPFznHDCSSCh60RdE3V3iMRQ85CcczefKNbtHPEba2k4fJ5ZnXKSMQYupPPsjVYzg33gbDG5
b37SzBMW/bONsbWbZag02zdy9CzXKoGcy71ZddEcPiSlvSkYcPOqnCiDOIu+Ik0+T10nd0+5HvlH
eeO/UR7S8o9jP+TE7WgiqErvgfI80YS3BcN0LB+xWM+6QFoQxvilRDA/UJ8wozlHyA2IaoTQrUQI
+IicfJBVFKV0fJXM2BU6TKWkEFKs5rCC5hJNrPdxVP8FxathuG+SrcGjB5lgnAwg+aw0Xa0Mjlk5
P/ZY9CfzzzNNSYpFKQpE2G8pfZjiYokzJETEJFP+njgdFnbMZDlPdwjx+gnuxIfROMmMsFF3aV4I
sd5GG1Z2dXyFFMTBF2zXRR8FHUTlF5IOnKNI0PTZdjVUnZno9VVB8aM4cMWPTtkXhadHLOClD4Sl
CIJuTl7UqQ0uOcnYzRpswNjGNdxmGDHebIZd5U3MwfqcKPwRMhpCKjuYxrfTScUkvMe8SdkJhJ2T
JUdqfTuUVKaR1cgqIDUTC8WGlZzhUbN2dRhmGPon6ihAV627hw85adjsS2MjxJOftpQY3BCY4wkR
24xmqs27+ToSrrNzAQFZIHGHmxAUe0B+X6bFFCLY3ZWdZSZAtdjrKJQxfR9HMcKlFQJbAhQRRVFU
VQX165Le05y0qRpzaX1dHTTn9WnqX2rsqlmSpv0HSLtjkIruaehq1vyYXFSzmvcWyaRTBlZd2vaH
DxUgSXQgQxWBIiCtzGHeOaFBUFPAnfztps8vYeZcjN8EifKhHMJFCJ2UlsV4eJsKXOOM0jjWQq1J
yEkaaEqLdugmcUcfMc3Iz/Fy8z0MUW8/xap534I+Pd2JifP2m2ejfhvg49BnywS6SusorDMqOrlD
04dvW+6fS5iXwRScVy10Zvq1y83OHO9Xe3s1DJVkrlaCO4C58DfckdLAwyfBeI/vsZwEqs1SaZ+P
ijXKKTWnpuWlSI9NEn86iKoOI4kOkY5gZhOGEUYGYYGtoaFo97fk3GpDyK42y1lfPCaCuQmlrxEO
pa28Sw282mDeGL44sngKDrOzsh4VIdUdzP9SDDtDkVQSQlBQPqO1cD09nIOycjiUVbTfoYc9gUcO
dh7rnFdW82jTDkkyXtJ1YGYk94dFHx9BUGlsu6Bc5jqqDU7D4go8XY6cKX1ufhNc8fjwvU/F0c0Y
1V+lQR9dhAdI4dFY4sPDZGU2zC6u/bv6TH7TBqfE6ZskMOXhlBB+guVDoey/3ttd7mRkaHaJ6jKX
V81cviSpsESBT3RRwPNi8exjIhGB1HyJjyhppQqWWNpQkqSGMg/gIp+CutXf7PJhxnxERSJZWLvg
nyvgsvk8xOcKRxKFT6VWFFgiykOyHzD+CSnwfOUhZUPC8w3oXImrzRbF1MA8Y90arnCJrknDS4TR
rNg1D25epfRVNDD91eumUiRI1i6AwydRvAcMUU93yg5V9pwchF8UOboaVZBTBYbzh5IwrDMML2EO
2OxtQh9agOGdqdh8Q4Kpx013nRxdFIO9WjzLFIn3ee0M0Ef8ZVRcwEEElEBAtzAsAYIM7KyEqRBW
WN0LvJi8IPGKxoVz7EHWmjDPfITMszITMrMyPHHFi00/P8EwNExAiMOERdRh2/v+uuxV7nl9JJ63
tc6h+vXYCHo44rkJ70oaQO2TdHK6JAydmdPAei+zGPyMtVmiG10JflbJNfKXaRrF96+jFvSG5Q2V
JnURJZxMTc+i5MMFGcm0HK6zKzk0WVkKqrMhxYuDc0cHBohrV9CbPFdJutq4Za1gvkaNGjCyGixT
GR1YbpF+KGhi3KatVLr2TFist8P1YQcsWBzXBtw4NmXVpJN1KUXJvXsscHAvZrMMFYOW9aIG67cY
4SN6+5zZtTGUzzS9aJzZcNGGd18jFE2UGEk42SSbmmytdYTiM72TVvXm5s3P+zE9KGTgwTk33OvX
e4cpMX8/FybTb7OCV6+fxnih7jNPM1d8c/2xtvpEVTTsITowITUTR1HCd6okBRGeycRfdZkCYT9a
1JUuXESFVpEsIA82QacEpcEOw4/kAqzynmD7RHjDeCIk1KliAwpSoxMYnjsNZcJMnnjk2ehtsNmh
vCrYrcTgKbt9coZsCyK1zHEVvWo++Npl+3jG0t7ZiEyZqjBsZJJgvZC7i6Jyvb0jWEy6+nJO5v25
QywJTQphxWYwm5MRgopHVgYCzyBuZkdCZMdM0NTMpAws+GvMAbOrERA14IlCGn1KDjZTT2Ws6YqQ
3LGRmKSwOJHCFuAQZEEf4/x7iR3cjRzM3ZkpvXTq5PJc6qvXsLlmivF3L1MtyJkUiFyP/NQY7wxr
jXmnC6yUaTmXNAdiDzNR+bBivxyXdtdemGw0ZKa35JClIDTcbvzt7R0vbpG54+ey9WKX28HRwaoX
hW/vZqiTpYNoGoQUpEkON3WSlCpOpFzG6g86zIil9GksVYgkhzGB+pAQKnMk6DArYEREoDiIEC1o
AYgYnwZJSoizFS1psII7M803TZmoLE3CBtGyLoKV2JA6AiUkM5HG4lUTU0IQJMdy91aJDTJk35LM
XBjN71XbNXZc4r3sQfPAIGoueChfkOMC1LlxxsfTEDH/EPWez2hM8hA2ATp35Z57QNNGIijzoHCo
oU44UtlzlITM0s37Ql3oxarcVZvRsuwkZvQYyS5cGGnY8D37mCTzpfyccESVTU4PM5OamgoGKOOD
YstUlUGGLCpQW5sOLkbXFECT5lx5U8joYtcXO/PIp2syHWL0PMt3b3Jxzkb0z0resczIQVXRKjoE
xrHeBhDP3sGw4/rUORYqcgk4OzJ9/VZtzZqeDN11mrZvLlmKzRZ6UHBL2yPL4EgIDzOMe0OIdOGE
0hJ2p380JowlRGFwQ89Ei9C9pcm0i4vhNt0v2wyVpmNjvoQmIH6+ArMV4nQnc6FjkQLkMGH3LXOZ
M5VwXLmXOyaWyGLmhS2DSGM1k9i/lyc9GgpCuhwmPFYIENCmvUuXkw0iTwNFEXjYBdBwcrOu0J/v
/x+t707cWzZ8smzZ433MW7k8WDJ1fINzNe83j4NGbwbuxwcTZx0d659v70/hdBPnA3408MeAmg7d
UVTIePkulEKA9QbMr3CDBJswM10rvYLpGbqs9wu3UzzcMSh4bLrmPtBQ0mCyIiBSYnmgdhxnMiAm
GHgEOtDBGIk/RD51dyXYrYVSFQ0GCuJgJmYCw4UiNKcDIiKXMiuchQQt4niNI7kKDzBCBqNTIqEz
M5A6pPbI1SIYmcVNUDMqX6DYJimCZQ3KEyjEsiQZAeB6T1e3GOF4Ydhzl3UaHRB5jRtl4oujme57
kTiMSNbU1QVAKCo9EubHg9ELmczQubobq03vMRmd9aYOPFwbOezNZ7jVDx22Z6ujPgtlVmLKplju
eLxMjFspi4v3djzi4zctDYiMQLFRBPZ6cjUU2GDXU1DuHm5qZGbz9MyBqPKmhqUMjUmZEAkU41EB
8KNwbN0aeUVSDKsm2VXMsbxJ75u3gSEBUBwokrpa5uYJahtBUkScXJpfmVhum267m7/XwvN7IoV1
MEYMVNUcKch+b8Xk5A50RJFiZuKjCA8PW/nWOTDiT3j2Ko7cZ/BhAdQydQ4O494eb3U5NWbYhzi/
s4dtGjFi5XtGTJwWaYMytDw9MyUjziBgoZGo4qWFDQwKcHb3/80f29Ycfs659k6Zp3IVgxFdI6Mg
RZWe9g3onTpQsdrlKd5evvTePIFxmyayBFAidRSBNJJQtIIigPKlGEcWzL95McMRRNhTM6nIsSiX
dbBcqKLlcikKHqnl2bcZ3x8FTPBKZuLYoVEHE/3CZDfCIC69OmCp3RL4FHnngUFKUyLkJgMW5X9x
OeSncA6WJphcQeHexzoxlnOobaVBBtnPaec4kUtFsm+MviVbjVYTbdYRxOLRxvSQ773Mw8YxMMjc
sZ4CtO6p8JQibjGC4V3YcPLGO7UwPHvnhAdsUDHBUeYgOFeaFz4Pc877onM6uLVlg7OLBg7r2Bpj
wPNGTy3OTk1ZOzZ9Dh3AFoXqSRdY5WWThc02Naop4lOuh5G+hykkRA80hThEpCEAkDxCbssIPEoE
p75SeNBxAghrFwpKpOI1o8BFlDISPehhDqOWPvzoD+Mo+wfUS6QpiSkpq6RMwIi9pLu0bsZiZmGI
YzRMXDDDHMmSphpSCCoiw1MNDaY9RPkORo96fiKcqVSWiWJQS9ISUokgg7wG4mRdRD/pZOlHxP++
n0I0T+ft44pDyKhD2ngdx8xGTPmLsuAegZDm5gNAQMQfVAsoMQkMeH5CfYd3f+1LkXqrRUXwK6Jn
a17ib2XYR2QaWiQqiOF823zIHBmiHzbtIZHkjZYR32PRaR2IZGk7slRG2IyI4B4ZH82sfiXWYb9c
CKb6UX6aXDxzoYDWiXzRuIxiNpJQizoiSVbI+kaX9pGhaOQwotY7yOS7RPGV6qSxm4IidSRHGdsb
NxmJOMJ0I1luI1bZh8b3i+sHh0TImzQbnaZi3V9NwHuT45NUIfDw7eEryenATSJq60lNRwhKjEZ8
WviZbZjpzmswFNOGskrlQ/JDJLyzDilMBt2fZVRPjt4Je3p70P9BGRRFtGzEOES0uOKMLHrPdnb0
g+11D/IX0csJFul+sOROSgixZS1PFmXmeSkOsvl4YuCGHiHHETsek8Bw4pcgfZJVNpxW6UOPborC
nZHT2HjSjhEayUoOqub9FlHJ2IjMrFev6xHr0cKNEDkRI7714uHAx09URRNyQ8OX6xK2Hz7h/KQL
O3F8GOcYsIaJ4T+LpG76IkzjzR3HFD3GoZVsRtKhjzkwTXoIq2jNo+Qtu7M1yVyRPlR3glPGuTAQ
O6F6VwcgxGQwx69wuY09ntVFRUFRTXp2lsHHOLhsXpY4Ch6A2bj5DkHRybX3OhPDRBRTNRBOZnsG
Y+AbZu8DwpXO0qFbRsWJsvJEQDs3oQtNWT19dMhCIEhgEzl85RjAaNqXjF+CuXMr4Y/b+/9CnyJb
+ZpZ+/GTPB1hM6Bn9q9fSnITVI6nkKOLEiQxM9Zg83yWJhYYqRHH1ixQyND2H6xxUmQMjOhAuV9v
oRLGZY2JEj9AkR4UGXqTw9nisqVbuYuFnSHf34pxa8vWqwez072zY+UMzuf5lA9O6Euo4QORoMdD
jiJryG1kasbkAwCHn53fUgNNQO/3B9IOBUfZutFAZJfswazQofBIbB6UHKDrLnEwRCtpU57UxdcB
J+SedpoGXQkNKJJRIaUGWiQ0ohI0SGlAJHzF++QoDdZBlkOigpY0kOAT40u/YV0s8iD8JUCWdfRi
1JSnpTij90a8W89iVPRpNJJZa0/OuSNedOrJ6HG8phAMRlTSZAMCFYcmoN/pTZlCOzqAovCwDch9
DcW+hZ9Te+lTpTgUyimCnxs1z7ND2DzkGoootRAkUFSJQ6EzBUUkWhl+e9x92LZmved7ZPb6fwjk
3OTVd8Fyjm/J9/N52He7OjY+5DodV7f9PvweUgn3I2eLu5KcWBwnx1LfaLSPknzIfNOMcHlX5WJm
l0NSn2srFSUjGt2+VXpIjJLXKnspsqc1PF4LOp0fEQ7wp5EoB6M8GvWJ2FkekuZDy5OcfOP8JMa6
l4c4VzUu4qphwfh0XM+azFrC0mbE3P2/TH6r81OLV5ku9p/uo9z3KZYxCfEmEkr/uQTm5tu+m53l
Q4YJc9Czzb8llz0qXs3pZKlygyZzKo5i2BldVsGah5EMQi4XmWhr7so4PQOHITY2uzSD0W4eAD39
TpJ0JdhUKFNQQNg3MGoIvOPoTBRGqd5FyJgjMAsJJ+oaKUXIQ/Mh1WAGeIL9Vx/XbZzE+i45oZBp
hLUNq48hM7lQSe3GfDReg5NCJ0NQsxZFrKfM6PWxPeZPNp7BvexZdPh87s+cvdSxc+uZzFsZGg8e
PHGCcSpG5P4sTIe/E4EEcvaOvWQb8GDFGzc+t3Ne0VUJLWn7cnd/TVol6WwUlPEs3ZoYOz6U+X8r
U7nR3O/fTXoVEJi0TXS/cIUb5eKCJE5VG3WI5TyHHkhF3Ou9rYskk2ZvzMCLeaExdnr7/YlLz6Km
ynkWMfOhYROlKeAa5fmedDchl+l8Nl3KK7fMtkaKzqSij8+N6Nz6UGRiMrg7AhZ99fFbHjKwbUXS
94ugQx4JZ34IyVCT6SDRZtN8oMARRZQLEnleY88xE1sZF36bKBI/CPSQDNcdZJMsOrmWTQzGDiIb
2GQEwmxtNibFuLv7+k5knkOQQeePT7M1UzXunmNjg2OEvlnmPqfFgqVMzvPgNC+Src1MGMYHz8Oi
ty5NHnydyjzxBlf8q+iCPFMGprwcAvB0MmNC5EoPN/BdTc+kEvih64TsuYuy5G3oiaRUVFJ3Vrkj
4zMj1tCGE8ZF0k8OrwPS2ZNaTBrLPXGLe+Ripq7l7V2XvBd7YbHbgiS3rpVJKqfBU76FlWdXdiu3
7Wmj2s7nlmuXWVPQ+Z0dl8bk6TfoP4FnWPU9W53ujB95ybvEm6Kh6Grgc9+0+NWfEv9i95hODwZo
h+vjH1dO96u4EIOdfYc3djsTMSeMoXQmNKHwHy96bygNd0D4Xl76RwcTKoHAJUhHBC6QXUdVNJQT
WBDuPKYAJ5yUiApCGBCS6XObV+qwoZHE0121XpW6czc8BPFU8Z2x8kXJYB/NIbR3hKgnNTCZ9Jt0
rW0j11BszkOMngk7fJzoipBdJddci86jV5x3yuFtMdoCMr44e06sEfHJEAhASqpuRX2m/NjgiPGs
eIyrk8Roje9edMRWSQJiRIhCRJaRs0WO4KaBhZhFIzmy86iDzGPH2qYve8Wiy+nJ72z3FzRfebj7
W59/3/czTNlZoZrlUu4tV69i1cHq/ZP7e/w+9xO9R73Be9rexeBznFv1dHSz7mbJ9Ehi7oTes7Xx
xa4sezVowa72bg6SJ/mR/ER8zyI/ti0Sz4yGledAoHq++QLEd+RJCEZcau4IXDcvepFl/u4MW0kV
JgfBDwH6BPkfjiyiPN8/yj13rz00E59X7FSrfh+teOkPu/LjwSmBhkkzkZPqiSY0Y7wgsYANIXkM
XBB77JcZHAwyjypFpJUmeEJPe6wXoUO6SfEh7jXcQxCfFLEKUYPuQLvCEeBD1+2fB8d28FoIh4kR
EElea5EcU5gQrLud7F3x18kDL3oWj8Pz3/dCbrixN8QP3gUlLA4YGFDQRLCSgvQIUSW0k4C0jIJP
i6hg+SWWbupFxGZO+y7GHoBIflIWREhdCnQLiVsS9WEShCm2UXNRaRciWXaj8dY/9kLLRMTGJUMq
tUUyD4FBo+mhWlbOH05DaPml4N4C5lNp3TDiTMZgg9aZvPumGHJMTxnAA8UrEEwUxDEKkQKMySFN
1fTewNqUUEkMpXUhcjQ7EAGL0/XdV4CJ2hnmTcTRNDDUkBxPK6PtgeoSB7iOCi7yMF9qUBtQAiDB
qF1mYHdYRgKnN1cdTQcpbXfggALjfhA3JqUEO46zdoGFPpt4b0sRN37udN0hSqazmAMwPSCFUy+f
yAHqxG+jBL6X1MNbuzeKJ+mx5LpaQT9p5u888D7l/9WMfi2ke1Co3nm3x3qFvLyn53QS1JUsspQn
FSRo/RhhCf/8IszRCz5ZIwS8dks3z4Kl7n35nFUn6rznHfgi6qZG3n+uehuPzL3AxESSGK0KZbH0
PMoDuRXEOP3dA0yZhpefVoqkV9E7ytMAoBRRGnQMAHJ7DrtLCwelODQGghMqDPe/JmyIOZISL4UO
0tOJPYlq0hc69WzGvKFg5FvXBBkkxvvIAIYtg3QhXEdZ3rwYA79DhsbzbuAGwqR1kNHMYwjQ2M6a
pGhZUOBroGHemAZADqQPshfj8QBiCbE9eKG9WUNyyrhBxBN8J9n5P2Rf0ZDWZxGkS9UQuXqpZKQo
OIYRKwkBCrQ0hsSVAbK4XAGXmwk9I8hE8ngGkM5VopaqgpNXxXRCXx3TdJKUrGWJymEbCJn6uBaI
ToSYQpxxnPz2TQy33wA4JB4pMtQKiJRlpCCmgQhXVAusIgYOp+hLVE8RCHy7hUN45dZylBJN3omk
VJGKtCWEomYhQLjs1geP35fRjboYHFuib/Ey0Q79QaYK942q717Q5nAdrEpEDsPUmKyiO434B7KY
VElKVV6kN6pczdriLVBpUI4IVAuRP6FqQedcoWkK40QcSclwjcCnMvAbnmbsBI1Nhh4yLWfUSfGT
v1pMiWSGQ6S67cp7V0JlGzKIzTskuin1YogKt6IljArovyFxF+UeFDDpkURiObyH1z+pcoGmTIKu
YROiONCres+9zPmPAIbgg4KIc4XkoFSXQ0AMAt9jIDJMhA2pjrikMREYk509SULIjsmwD1KBjcxg
g8xzuYrndUUR/Qn1e9O1Bm0VIlVmlPSo7139TKghUWhEhMArtGvG+kYyOn7R3D6IcZopkYq1dlo8
UVGGkOzizcfivaEkpZPjPl+yP3eXUw0J/XwncMENi5T/b4XLqqHNhZhetEBHkpJfBXEkyPDJK8uM
K54vsRuA5ZFDTvN5WyYJ80fez+tekcDGeup+k6zeO9Dr/FUMFKXFw0UvyQvZ+Ji7Txnaj0D8xFqz
BFrsBcZx44g5iFDxIZhixYZkkCH1VQNeTBljHTUQvqKJBjMmzSklplRkW4aUsmEDREAwzmRGRGQF
RyeWemx1QrGhygfHnECJ6mGN8si9rlj+L3RaC87qWf9NOEh7YvwUtFrEboSVyjNUnL5/+VV3ywRw
SEokRS1OYBHJl0aAu4CfjPW0PNsVJFbTyI6E7E5HJ0Mqa35YZNFfAoczltBPOj0nukD1ryHFIglC
AYZ0wTAISYT1/fQ6rch8G01JEiTdKOSRIPr2e8HjzYmlx2Fp6Qt7pKC4n7FCpEwfRyaGWgoyMs+G
PjapahMx9U5WWkyu8lVOJjf4q9walaMYKaVMhgKGuhmsaIQ3X9g0NsZBtbYvZpEaJpoeQQ4D89bo
sT35iqUEhIBBqZJFpD9SRXVerY2VMwwRBNCTTOdwE2jUKeZi97YCkwvbNEJSlaBDSk0J1ZhzRdOm
ydovS1B7E8qjiLFeGqaHcShShTQUIxCSSCNUVSuiI5SJMiNOwbSDJ3TZ1lRiv992GaBgJLw601dT
o2Gi6IJsWRNTr1ERRXI6gQOgkG6Yjct5D7pvmT7XZBVHWHoOcmerOAMkeUytjSIRBELB68mFKLGd
LhBA8pigysMzqM281uNPVmHGSBu0bDzgh7Es1WibU2fZLPSWQywLFBJIQTseg6DUYpplmxmrFm/L
xqKjJFybSOJSylECutuCp5FKVloC0kMCi3CCCJEMOUBKTdDUMSsKzUT7TrC7GXYFKEJQnK5NVKRb
OYqmLms1mbZEaXYvEYdpPN9g+EINAsmIxNGJJROYl9DaK4h5iEFvoTSAoQLtc5iK4ZxI8doud23g
glQzRHUeZhrgCcE3JJSQREkSxLJKksdvb4V58NgO4OW/jB8yOCQ7AkopoV7SHjLpLJAGqgMPIgKP
aQoGF65soiNRzmDVKo/rGMhkS02yUtA+J2eYoOsQdhIjYjxkNgqpgFTWGZBXIl95tlEYR7tAfh7g
HgsCC9jyjo8NzwsNxvdpDIk0SGEfMXmff1kGGZxkDBAduLMlkAezGnMaeQNnL9fdpG1B8pyANDKC
36yyTWEEcUUNktS1QBuAOFPeFE0bJTQQoD+PTb6AH5/dzjg+hSaPhZ7Pa8+KF8kdSU/HuMePbAxo
qSeO9myCHz50mB44V7UhT7g2hCQ0/FIpeRpJXwEqe0Ov2jT6rjCRGykPkfWJ7Ovc+pjDZWJGIesl
2ylV4ytaRIVqCReQASAmKWsINpyD1ZhCtiQmEKO/O0HMfv1CHt5+Jxscn2cBOId5em38PC9PcPD1
RHoQ6oYsNse2ktroBkkXrn/i7kinChIQEontQA==


More information about the bazaar mailing list