/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/reconcile.py

  • Committer: Robert Collins
  • Date: 2010-05-05 00:05:29 UTC
  • mto: This revision was merged to the branch mainline in revision 5206.
  • Revision ID: robertc@robertcollins.net-20100505000529-ltmllyms5watqj5u
Make 'pydoc bzrlib.tests.build_tree_shape' useful.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# (C) 2005, 2006 Canonical Limited.
 
1
# Copyright (C) 2006-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
"""Reconcilers are able to fix some potential data errors in a branch."""
18
18
 
19
19
 
20
 
__all__ = ['reconcile', 'Reconciler', 'RepoReconciler']
21
 
 
22
 
 
23
 
import bzrlib.branch
24
 
import bzrlib.errors as errors
25
 
import bzrlib.progress
 
20
__all__ = [
 
21
    'KnitReconciler',
 
22
    'PackReconciler',
 
23
    'reconcile',
 
24
    'Reconciler',
 
25
    'RepoReconciler',
 
26
    ]
 
27
 
 
28
 
 
29
from bzrlib import (
 
30
    cleanup,
 
31
    errors,
 
32
    ui,
 
33
    )
26
34
from bzrlib.trace import mutter
27
 
from bzrlib.tsort import TopoSorter
28
 
import bzrlib.ui as ui
29
 
 
30
 
 
31
 
def reconcile(dir):
 
35
from bzrlib.tsort import topo_sort
 
36
from bzrlib.versionedfile import AdapterFactory, FulltextContentFactory
 
37
 
 
38
 
 
39
def reconcile(dir, other=None):
32
40
    """Reconcile the data in dir.
33
41
 
34
42
    Currently this is limited to a inventory 'reweave'.
37
45
 
38
46
    Directly using Reconciler is recommended for library users that
39
47
    desire fine grained control or analysis of the found issues.
 
48
 
 
49
    :param other: another bzrdir to reconcile against.
40
50
    """
41
 
    reconciler = Reconciler(dir)
 
51
    reconciler = Reconciler(dir, other=other)
42
52
    reconciler.reconcile()
43
53
 
44
54
 
45
55
class Reconciler(object):
46
56
    """Reconcilers are used to reconcile existing data."""
47
57
 
48
 
    def __init__(self, dir):
 
58
    def __init__(self, dir, other=None):
 
59
        """Create a Reconciler."""
49
60
        self.bzrdir = dir
50
61
 
51
62
    def reconcile(self):
52
63
        """Perform reconciliation.
53
 
        
 
64
 
54
65
        After reconciliation the following attributes document found issues:
55
66
        inconsistent_parents: The number of revisions in the repository whose
56
67
                              ancestry was being reported incorrectly.
57
68
        garbage_inventories: The number of inventory objects without revisions
58
69
                             that were garbage collected.
 
70
        fixed_branch_history: None if there was no branch, False if the branch
 
71
                              history was correct, True if the branch history
 
72
                              needed to be re-normalized.
59
73
        """
60
74
        self.pb = ui.ui_factory.nested_progress_bar()
61
75
        try:
65
79
 
66
80
    def _reconcile(self):
67
81
        """Helper function for performing reconciliation."""
 
82
        self._reconcile_branch()
 
83
        self._reconcile_repository()
 
84
 
 
85
    def _reconcile_branch(self):
 
86
        try:
 
87
            self.branch = self.bzrdir.open_branch()
 
88
        except errors.NotBranchError:
 
89
            # Nothing to check here
 
90
            self.fixed_branch_history = None
 
91
            return
 
92
        ui.ui_factory.note('Reconciling branch %s' % self.branch.base)
 
93
        branch_reconciler = self.branch.reconcile(thorough=True)
 
94
        self.fixed_branch_history = branch_reconciler.fixed_history
 
95
 
 
96
    def _reconcile_repository(self):
68
97
        self.repo = self.bzrdir.find_repository()
69
 
        self.pb.note('Reconciling repository %s',
70
 
                     self.repo.bzrdir.root_transport.base)
71
 
        repo_reconciler = RepoReconciler(self.repo)
72
 
        repo_reconciler.reconcile()
 
98
        ui.ui_factory.note('Reconciling repository %s' %
 
99
            self.repo.user_url)
 
100
        self.pb.update("Reconciling repository", 0, 1)
 
101
        repo_reconciler = self.repo.reconcile(thorough=True)
73
102
        self.inconsistent_parents = repo_reconciler.inconsistent_parents
74
103
        self.garbage_inventories = repo_reconciler.garbage_inventories
75
 
        self.pb.note('Reconciliation complete.')
 
104
        if repo_reconciler.aborted:
 
105
            ui.ui_factory.note(
 
106
                'Reconcile aborted: revision index has inconsistent parents.')
 
107
            ui.ui_factory.note(
 
108
                'Run "bzr check" for more details.')
 
109
        else:
 
110
            ui.ui_factory.note('Reconciliation complete.')
 
111
 
 
112
 
 
113
class BranchReconciler(object):
 
114
    """Reconciler that works on a branch."""
 
115
 
 
116
    def __init__(self, a_branch, thorough=False):
 
117
        self.fixed_history = None
 
118
        self.thorough = thorough
 
119
        self.branch = a_branch
 
120
 
 
121
    def reconcile(self):
 
122
        operation = cleanup.OperationWithCleanups(self._reconcile)
 
123
        self.add_cleanup = operation.add_cleanup
 
124
        operation.run_simple()
 
125
 
 
126
    def _reconcile(self):
 
127
        self.branch.lock_write()
 
128
        self.add_cleanup(self.branch.unlock)
 
129
        self.pb = ui.ui_factory.nested_progress_bar()
 
130
        self.add_cleanup(self.pb.finished)
 
131
        self._reconcile_steps()
 
132
 
 
133
    def _reconcile_steps(self):
 
134
        self._reconcile_revision_history()
 
135
 
 
136
    def _reconcile_revision_history(self):
 
137
        repo = self.branch.repository
 
138
        last_revno, last_revision_id = self.branch.last_revision_info()
 
139
        real_history = []
 
140
        try:
 
141
            for revid in repo.iter_reverse_revision_history(
 
142
                    last_revision_id):
 
143
                real_history.append(revid)
 
144
        except errors.RevisionNotPresent:
 
145
            pass # Hit a ghost left hand parent
 
146
        real_history.reverse()
 
147
        if last_revno != len(real_history):
 
148
            self.fixed_history = True
 
149
            # Technically for Branch5 formats, it is more efficient to use
 
150
            # set_revision_history, as this will regenerate it again.
 
151
            # Not really worth a whole BranchReconciler class just for this,
 
152
            # though.
 
153
            ui.ui_factory.note('Fixing last revision info %s => %s' % (
 
154
                 last_revno, len(real_history)))
 
155
            self.branch.set_last_revision_info(len(real_history),
 
156
                                               last_revision_id)
 
157
        else:
 
158
            self.fixed_history = False
 
159
            ui.ui_factory.note('revision_history ok.')
76
160
 
77
161
 
78
162
class RepoReconciler(object):
79
163
    """Reconciler that reconciles a repository.
80
164
 
 
165
    The goal of repository reconciliation is to make any derived data
 
166
    consistent with the core data committed by a user. This can involve
 
167
    reindexing, or removing unreferenced data if that can interfere with
 
168
    queries in a given repository.
 
169
 
81
170
    Currently this consists of an inventory reweave with revision cross-checks.
82
171
    """
83
172
 
84
 
    def __init__(self, repo):
 
173
    def __init__(self, repo, other=None, thorough=False):
 
174
        """Construct a RepoReconciler.
 
175
 
 
176
        :param thorough: perform a thorough check which may take longer but
 
177
                         will correct non-data loss issues such as incorrect
 
178
                         cached data.
 
179
        """
 
180
        self.garbage_inventories = 0
 
181
        self.inconsistent_parents = 0
 
182
        self.aborted = False
85
183
        self.repo = repo
 
184
        self.thorough = thorough
86
185
 
87
186
    def reconcile(self):
88
187
        """Perform reconciliation.
89
 
        
 
188
 
90
189
        After reconciliation the following attributes document found issues:
91
190
        inconsistent_parents: The number of revisions in the repository whose
92
191
                              ancestry was being reported incorrectly.
93
192
        garbage_inventories: The number of inventory objects without revisions
94
193
                             that were garbage collected.
95
194
        """
 
195
        operation = cleanup.OperationWithCleanups(self._reconcile)
 
196
        self.add_cleanup = operation.add_cleanup
 
197
        operation.run_simple()
 
198
 
 
199
    def _reconcile(self):
96
200
        self.repo.lock_write()
97
 
        try:
98
 
            self.pb = ui.ui_factory.nested_progress_bar()
99
 
            try:
100
 
                self._reconcile_steps()
101
 
            finally:
102
 
                self.pb.finished()
103
 
        finally:
104
 
            self.repo.unlock()
 
201
        self.add_cleanup(self.repo.unlock)
 
202
        self.pb = ui.ui_factory.nested_progress_bar()
 
203
        self.add_cleanup(self.pb.finished)
 
204
        self._reconcile_steps()
105
205
 
106
206
    def _reconcile_steps(self):
107
207
        """Perform the steps to reconcile this repository."""
108
208
        self._reweave_inventory()
109
209
 
110
210
    def _reweave_inventory(self):
111
 
        """Regenerate the inventory weave for the repository from scratch."""
112
 
        # local because its really a wart we want to hide
 
211
        """Regenerate the inventory weave for the repository from scratch.
 
212
 
 
213
        This is a smart function: it will only do the reweave if doing it
 
214
        will correct data issues. The self.thorough flag controls whether
 
215
        only data-loss causing issues (!self.thorough) or all issues
 
216
        (self.thorough) are treated as requiring the reweave.
 
217
        """
 
218
        # local because needing to know about WeaveFile is a wart we want to hide
113
219
        from bzrlib.weave import WeaveFile, Weave
114
220
        transaction = self.repo.get_transaction()
115
 
        self.pb.update('Reading inventory data.')
116
 
        self.inventory = self.repo.get_inventory_weave()
 
221
        self.pb.update('Reading inventory data')
 
222
        self.inventory = self.repo.inventories
 
223
        self.revisions = self.repo.revisions
117
224
        # the total set of revisions to process
118
 
        self.pending = set([rev_id for rev_id in self.repo._revision_store.all_revision_ids(transaction)])
 
225
        self.pending = set([key[-1] for key in self.revisions.keys()])
119
226
 
120
227
        # mapping from revision_id to parents
121
228
        self._rev_graph = {}
127
234
            # put a revision into the graph.
128
235
            self._graph_revision(rev_id)
129
236
        self._check_garbage_inventories()
130
 
        if not self.inconsistent_parents and not self.garbage_inventories:
131
 
            self.pb.note('Inventory ok.')
 
237
        # if there are no inconsistent_parents and
 
238
        # (no garbage inventories or we are not doing a thorough check)
 
239
        if (not self.inconsistent_parents and
 
240
            (not self.garbage_inventories or not self.thorough)):
 
241
            ui.ui_factory.note('Inventory ok.')
132
242
            return
133
 
        self.pb.update('Backing up inventory...', 0, 0)
134
 
        self.repo.control_weaves.copy(self.inventory, 'inventory.backup', self.repo.get_transaction())
135
 
        self.pb.note('Backup Inventory created.')
136
 
        # asking for '' should never return a non-empty weave
137
 
        new_inventory_vf = self.repo.control_weaves.get_empty('inventory.new',
138
 
            self.repo.get_transaction())
 
243
        self.pb.update('Backing up inventory', 0, 0)
 
244
        self.repo._backup_inventory()
 
245
        ui.ui_factory.note('Backup inventory created.')
 
246
        new_inventories = self.repo._temp_inventories()
139
247
 
140
248
        # we have topological order of revisions and non ghost parents ready.
141
249
        self._setup_steps(len(self._rev_graph))
142
 
        for rev_id in TopoSorter(self._rev_graph.items()).iter_topo_order():
143
 
            parents = self._rev_graph[rev_id]
144
 
            # double check this really is in topological order.
145
 
            unavailable = [p for p in parents if p not in new_inventory_vf]
146
 
            assert len(unavailable) == 0
147
 
            # this entry has all the non ghost parents in the inventory
148
 
            # file already.
149
 
            self._reweave_step('adding inventories')
150
 
            if isinstance(new_inventory_vf, WeaveFile):
151
 
                # It's really a WeaveFile, but we call straight into the
152
 
                # Weave's add method to disable the auto-write-out behaviour.
153
 
                # This is done to avoid a revision_count * time-to-write additional overhead on 
154
 
                # reconcile.
155
 
                new_inventory_vf._check_write_ok()
156
 
                Weave._add_lines(new_inventory_vf, rev_id, parents, self.inventory.get_lines(rev_id),
157
 
                                 None)
158
 
            else:
159
 
                new_inventory_vf.add_lines(rev_id, parents, self.inventory.get_lines(rev_id))
160
 
 
161
 
        if isinstance(new_inventory_vf, WeaveFile):
162
 
            new_inventory_vf._save()
163
 
        # if this worked, the set of new_inventory_vf.names should equal
 
250
        revision_keys = [(rev_id,) for rev_id in topo_sort(self._rev_graph)]
 
251
        stream = self._change_inv_parents(
 
252
            self.inventory.get_record_stream(revision_keys, 'unordered', True),
 
253
            self._new_inv_parents,
 
254
            set(revision_keys))
 
255
        new_inventories.insert_record_stream(stream)
 
256
        # if this worked, the set of new_inventories.keys should equal
164
257
        # self.pending
165
 
        assert set(new_inventory_vf.versions()) == self.pending
 
258
        if not (set(new_inventories.keys()) ==
 
259
            set([(revid,) for revid in self.pending])):
 
260
            raise AssertionError()
166
261
        self.pb.update('Writing weave')
167
 
        self.repo.control_weaves.copy(new_inventory_vf, 'inventory', self.repo.get_transaction())
168
 
        self.repo.control_weaves.delete('inventory.new', self.repo.get_transaction())
 
262
        self.repo._activate_new_inventory()
169
263
        self.inventory = None
170
 
        self.pb.note('Inventory regenerated.')
 
264
        ui.ui_factory.note('Inventory regenerated.')
 
265
 
 
266
    def _new_inv_parents(self, revision_key):
 
267
        """Lookup ghost-filtered parents for revision_key."""
 
268
        # Use the filtered ghostless parents list:
 
269
        return tuple([(revid,) for revid in self._rev_graph[revision_key[-1]]])
 
270
 
 
271
    def _change_inv_parents(self, stream, get_parents, all_revision_keys):
 
272
        """Adapt a record stream to reconcile the parents."""
 
273
        for record in stream:
 
274
            wanted_parents = get_parents(record.key)
 
275
            if wanted_parents and wanted_parents[0] not in all_revision_keys:
 
276
                # The check for the left most parent only handles knit
 
277
                # compressors, but this code only applies to knit and weave
 
278
                # repositories anyway.
 
279
                bytes = record.get_bytes_as('fulltext')
 
280
                yield FulltextContentFactory(record.key, wanted_parents, record.sha1, bytes)
 
281
            else:
 
282
                adapted_record = AdapterFactory(record.key, wanted_parents, record)
 
283
                yield adapted_record
 
284
            self._reweave_step('adding inventories')
171
285
 
172
286
    def _setup_steps(self, new_total):
173
287
        """Setup the markers we need to control the progress bar."""
180
294
        # analyse revision id rev_id and put it in the stack.
181
295
        self._reweave_step('loading revisions')
182
296
        rev = self.repo.get_revision_reconcile(rev_id)
183
 
        assert rev.revision_id == rev_id
184
297
        parents = []
185
298
        for parent in rev.parent_ids:
186
299
            if self._parent_is_available(parent):
187
300
                parents.append(parent)
188
301
            else:
189
302
                mutter('found ghost %s', parent)
190
 
        self._rev_graph[rev_id] = parents   
191
 
        if set(self.inventory.get_parents(rev_id)) != set(parents):
192
 
            self.inconsistent_parents += 1
193
 
            mutter('Inconsistent inventory parents: id {%s} '
194
 
                   'inventory claims %r, '
195
 
                   'available parents are %r, '
196
 
                   'unavailable parents are %r',
197
 
                   rev_id, 
198
 
                   set(self.inventory.get_parents(rev_id)),
199
 
                   set(parents),
200
 
                   set(rev.parent_ids).difference(set(parents)))
 
303
        self._rev_graph[rev_id] = parents
201
304
 
202
305
    def _check_garbage_inventories(self):
203
306
        """Check for garbage inventories which we cannot trust
205
308
        We cant trust them because their pre-requisite file data may not
206
309
        be present - all we know is that their revision was not installed.
207
310
        """
208
 
        inventories = set(self.inventory.versions())
209
 
        revisions = set(self._rev_graph.keys())
 
311
        if not self.thorough:
 
312
            return
 
313
        inventories = set(self.inventory.keys())
 
314
        revisions = set(self.revisions.keys())
210
315
        garbage = inventories.difference(revisions)
211
316
        self.garbage_inventories = len(garbage)
212
 
        for revision_id in garbage:
213
 
            mutter('Garbage inventory {%s} found.', revision_id)
 
317
        for revision_key in garbage:
 
318
            mutter('Garbage inventory {%s} found.', revision_key[-1])
214
319
 
215
320
    def _parent_is_available(self, parent):
216
321
        """True if parent is a fully available revision
218
323
        A fully available revision has a inventory and a revision object in the
219
324
        repository.
220
325
        """
221
 
        return (parent in self._rev_graph or 
222
 
                (parent in self.inventory and self.repo.has_revision(parent)))
 
326
        if parent in self._rev_graph:
 
327
            return True
 
328
        inv_present = (1 == len(self.inventory.get_parent_map([(parent,)])))
 
329
        return (inv_present and self.repo.has_revision(parent))
223
330
 
224
331
    def _reweave_step(self, message):
225
332
        """Mark a single step of regeneration complete."""
230
337
class KnitReconciler(RepoReconciler):
231
338
    """Reconciler that reconciles a knit format repository.
232
339
 
233
 
    This will detect garbage inventories and remove them.
234
 
 
235
 
    Inconsistent parentage is checked for in the revision weave.
 
340
    This will detect garbage inventories and remove them in thorough mode.
236
341
    """
237
342
 
238
343
    def _reconcile_steps(self):
239
344
        """Perform the steps to reconcile this repository."""
240
 
        self._load_indexes()
241
 
        # knits never suffer this
242
 
        self.inconsistent_parents = 0
243
 
        self._gc_inventory()
 
345
        if self.thorough:
 
346
            try:
 
347
                self._load_indexes()
 
348
            except errors.BzrCheckError:
 
349
                self.aborted = True
 
350
                return
 
351
            # knits never suffer this
 
352
            self._gc_inventory()
 
353
            self._fix_text_parents()
244
354
 
245
355
    def _load_indexes(self):
246
356
        """Load indexes for the reconciliation."""
247
357
        self.transaction = self.repo.get_transaction()
248
 
        self.pb.update('Reading indexes.', 0, 2)
249
 
        self.inventory = self.repo.get_inventory_weave()
250
 
        self.pb.update('Reading indexes.', 1, 2)
251
 
        self.revisions = self.repo._revision_store.get_revision_file(self.transaction)
252
 
        self.pb.update('Reading indexes.', 2, 2)
 
358
        self.pb.update('Reading indexes', 0, 2)
 
359
        self.inventory = self.repo.inventories
 
360
        self.pb.update('Reading indexes', 1, 2)
 
361
        self.repo._check_for_inconsistent_revision_parents()
 
362
        self.revisions = self.repo.revisions
 
363
        self.pb.update('Reading indexes', 2, 2)
253
364
 
254
365
    def _gc_inventory(self):
255
366
        """Remove inventories that are not referenced from the revision store."""
256
 
        self.pb.update('Checking unused inventories.', 0, 1)
 
367
        self.pb.update('Checking unused inventories', 0, 1)
257
368
        self._check_garbage_inventories()
258
 
        self.pb.update('Checking unused inventories.', 1, 3)
 
369
        self.pb.update('Checking unused inventories', 1, 3)
259
370
        if not self.garbage_inventories:
260
 
            self.pb.note('Inventory ok.')
 
371
            ui.ui_factory.note('Inventory ok.')
261
372
            return
262
 
        self.pb.update('Backing up inventory...', 0, 0)
263
 
        self.repo.control_weaves.copy(self.inventory, 'inventory.backup', self.transaction)
264
 
        self.pb.note('Backup Inventory created.')
 
373
        self.pb.update('Backing up inventory', 0, 0)
 
374
        self.repo._backup_inventory()
 
375
        ui.ui_factory.note('Backup Inventory created')
265
376
        # asking for '' should never return a non-empty weave
266
 
        new_inventory_vf = self.repo.control_weaves.get_empty('inventory.new',
267
 
            self.transaction)
268
 
 
 
377
        new_inventories = self.repo._temp_inventories()
269
378
        # we have topological order of revisions and non ghost parents ready.
270
 
        self._setup_steps(len(self.revisions))
271
 
        for rev_id in TopoSorter(self.revisions.get_graph().items()).iter_topo_order():
272
 
            parents = self.revisions.get_parents(rev_id)
273
 
            # double check this really is in topological order.
274
 
            unavailable = [p for p in parents if p not in new_inventory_vf]
275
 
            assert len(unavailable) == 0
276
 
            # this entry has all the non ghost parents in the inventory
277
 
            # file already.
278
 
            self._reweave_step('adding inventories')
279
 
            # ugly but needed, weaves are just way tooooo slow else.
280
 
            new_inventory_vf.add_lines(rev_id, parents, self.inventory.get_lines(rev_id))
281
 
 
 
379
        graph = self.revisions.get_parent_map(self.revisions.keys())
 
380
        revision_keys = topo_sort(graph)
 
381
        revision_ids = [key[-1] for key in revision_keys]
 
382
        self._setup_steps(len(revision_keys))
 
383
        stream = self._change_inv_parents(
 
384
            self.inventory.get_record_stream(revision_keys, 'unordered', True),
 
385
            graph.__getitem__,
 
386
            set(revision_keys))
 
387
        new_inventories.insert_record_stream(stream)
282
388
        # if this worked, the set of new_inventory_vf.names should equal
283
 
        # self.pending
284
 
        assert set(new_inventory_vf.versions()) == set(self.revisions.versions())
 
389
        # the revisionds list
 
390
        if not(set(new_inventories.keys()) == set(revision_keys)):
 
391
            raise AssertionError()
285
392
        self.pb.update('Writing weave')
286
 
        self.repo.control_weaves.copy(new_inventory_vf, 'inventory', self.transaction)
287
 
        self.repo.control_weaves.delete('inventory.new', self.transaction)
 
393
        self.repo._activate_new_inventory()
288
394
        self.inventory = None
289
 
        self.pb.note('Inventory regenerated.')
290
 
 
291
 
    def _reinsert_revisions(self):
292
 
        """Correct the revision history for revisions in the revision knit."""
293
 
        # the total set of revisions to process
294
 
        self.pending = set(self.revisions.versions())
295
 
 
296
 
        # mapping from revision_id to parents
297
 
        self._rev_graph = {}
298
 
        # errors that we detect
299
 
        self.inconsistent_parents = 0
300
 
        # we need the revision id of each revision and its available parents list
301
 
        self._setup_steps(len(self.pending))
302
 
        for rev_id in self.pending:
303
 
            # put a revision into the graph.
304
 
            self._graph_revision(rev_id)
305
 
 
306
 
        if not self.inconsistent_parents:
307
 
            self.pb.note('Revision history accurate.')
 
395
        ui.ui_factory.note('Inventory regenerated.')
 
396
 
 
397
    def _fix_text_parents(self):
 
398
        """Fix bad versionedfile parent entries.
 
399
 
 
400
        It is possible for the parents entry in a versionedfile entry to be
 
401
        inconsistent with the values in the revision and inventory.
 
402
 
 
403
        This method finds entries with such inconsistencies, corrects their
 
404
        parent lists, and replaces the versionedfile with a corrected version.
 
405
        """
 
406
        transaction = self.repo.get_transaction()
 
407
        versions = [key[-1] for key in self.revisions.keys()]
 
408
        mutter('Prepopulating revision text cache with %d revisions',
 
409
                len(versions))
 
410
        vf_checker = self.repo._get_versioned_file_checker()
 
411
        bad_parents, unused_versions = vf_checker.check_file_version_parents(
 
412
            self.repo.texts, self.pb)
 
413
        text_index = vf_checker.text_index
 
414
        per_id_bad_parents = {}
 
415
        for key in unused_versions:
 
416
            # Ensure that every file with unused versions gets rewritten.
 
417
            # NB: This is really not needed, reconcile != pack.
 
418
            per_id_bad_parents[key[0]] = {}
 
419
        # Generate per-knit/weave data.
 
420
        for key, details in bad_parents.iteritems():
 
421
            file_id = key[0]
 
422
            rev_id = key[1]
 
423
            knit_parents = tuple([parent[-1] for parent in details[0]])
 
424
            correct_parents = tuple([parent[-1] for parent in details[1]])
 
425
            file_details = per_id_bad_parents.setdefault(file_id, {})
 
426
            file_details[rev_id] = (knit_parents, correct_parents)
 
427
        file_id_versions = {}
 
428
        for text_key in text_index:
 
429
            versions_list = file_id_versions.setdefault(text_key[0], [])
 
430
            versions_list.append(text_key[1])
 
431
        # Do the reconcile of individual weaves.
 
432
        for num, file_id in enumerate(per_id_bad_parents):
 
433
            self.pb.update('Fixing text parents', num,
 
434
                           len(per_id_bad_parents))
 
435
            versions_with_bad_parents = per_id_bad_parents[file_id]
 
436
            id_unused_versions = set(key[-1] for key in unused_versions
 
437
                if key[0] == file_id)
 
438
            if file_id in file_id_versions:
 
439
                file_versions = file_id_versions[file_id]
 
440
            else:
 
441
                # This id was present in the disk store but is not referenced
 
442
                # by any revision at all.
 
443
                file_versions = []
 
444
            self._fix_text_parent(file_id, versions_with_bad_parents,
 
445
                 id_unused_versions, file_versions)
 
446
 
 
447
    def _fix_text_parent(self, file_id, versions_with_bad_parents,
 
448
            unused_versions, all_versions):
 
449
        """Fix bad versionedfile entries in a single versioned file."""
 
450
        mutter('fixing text parent: %r (%d versions)', file_id,
 
451
                len(versions_with_bad_parents))
 
452
        mutter('(%d are unused)', len(unused_versions))
 
453
        new_file_id = 'temp:%s' % file_id
 
454
        new_parents = {}
 
455
        needed_keys = set()
 
456
        for version in all_versions:
 
457
            if version in unused_versions:
 
458
                continue
 
459
            elif version in versions_with_bad_parents:
 
460
                parents = versions_with_bad_parents[version][1]
 
461
            else:
 
462
                pmap = self.repo.texts.get_parent_map([(file_id, version)])
 
463
                parents = [key[-1] for key in pmap[(file_id, version)]]
 
464
            new_parents[(new_file_id, version)] = [
 
465
                (new_file_id, parent) for parent in parents]
 
466
            needed_keys.add((file_id, version))
 
467
        def fix_parents(stream):
 
468
            for record in stream:
 
469
                bytes = record.get_bytes_as('fulltext')
 
470
                new_key = (new_file_id, record.key[-1])
 
471
                parents = new_parents[new_key]
 
472
                yield FulltextContentFactory(new_key, parents, record.sha1, bytes)
 
473
        stream = self.repo.texts.get_record_stream(needed_keys, 'topological', True)
 
474
        self.repo._remove_file_id(new_file_id)
 
475
        self.repo.texts.insert_record_stream(fix_parents(stream))
 
476
        self.repo._remove_file_id(file_id)
 
477
        if len(new_parents):
 
478
            self.repo._move_file_id(new_file_id, file_id)
 
479
 
 
480
 
 
481
class PackReconciler(RepoReconciler):
 
482
    """Reconciler that reconciles a pack based repository.
 
483
 
 
484
    Garbage inventories do not affect ancestry queries, and removal is
 
485
    considerably more expensive as there is no separate versioned file for
 
486
    them, so they are not cleaned. In short it is currently a no-op.
 
487
 
 
488
    In future this may be a good place to hook in annotation cache checking,
 
489
    index recreation etc.
 
490
    """
 
491
 
 
492
    # XXX: The index corruption that _fix_text_parents performs is needed for
 
493
    # packs, but not yet implemented. The basic approach is to:
 
494
    #  - lock the names list
 
495
    #  - perform a customised pack() that regenerates data as needed
 
496
    #  - unlock the names list
 
497
    # https://bugs.edge.launchpad.net/bzr/+bug/154173
 
498
 
 
499
    def _reconcile_steps(self):
 
500
        """Perform the steps to reconcile this repository."""
 
501
        if not self.thorough:
308
502
            return
309
 
        self._setup_steps(len(self._rev_graph))
310
 
        for rev_id, parents in self._rev_graph.items():
311
 
            if parents != self.revisions.get_parents(rev_id):
312
 
                self.revisions.fix_parents(rev_id, parents)
313
 
            self._reweave_step('Fixing parents')
314
 
        self.pb.note('Ancestry corrected.')
315
 
 
316
 
    def _graph_revision(self, rev_id):
317
 
        """Load a revision into the revision graph."""
318
 
        # pick a random revision
319
 
        # analyse revision id rev_id and put it in the stack.
320
 
        self._reweave_step('loading revisions')
321
 
        rev = self.repo._revision_store.get_revision(rev_id, self.transaction)
322
 
        assert rev.revision_id == rev_id
323
 
        parents = []
324
 
        for parent in rev.parent_ids:
325
 
            if self.revisions.has_version(parent):
326
 
                parents.append(parent)
327
 
            else:
328
 
                mutter('found ghost %s', parent)
329
 
        self._rev_graph[rev_id] = parents   
330
 
        if set(self.inventory.get_parents(rev_id)) != set(parents):
331
 
            self.inconsistent_parents += 1
332
 
            mutter('Inconsistent inventory parents: id {%s} '
333
 
                   'inventory claims %r, '
334
 
                   'available parents are %r, '
335
 
                   'unavailable parents are %r',
336
 
                   rev_id, 
337
 
                   set(self.inventory.get_parents(rev_id)),
338
 
                   set(parents),
339
 
                   set(rev.parent_ids).difference(set(parents)))
340
 
 
341
 
    def _check_garbage_inventories(self):
342
 
        """Check for garbage inventories which we cannot trust
343
 
 
344
 
        We cant trust them because their pre-requisite file data may not
345
 
        be present - all we know is that their revision was not installed.
 
503
        collection = self.repo._pack_collection
 
504
        collection.ensure_loaded()
 
505
        collection.lock_names()
 
506
        self.add_cleanup(collection._unlock_names)
 
507
        packs = collection.all_packs()
 
508
        all_revisions = self.repo.all_revision_ids()
 
509
        total_inventories = len(list(
 
510
            collection.inventory_index.combined_index.iter_all_entries()))
 
511
        if len(all_revisions):
 
512
            new_pack =  self.repo._reconcile_pack(collection, packs,
 
513
                ".reconcile", all_revisions, self.pb)
 
514
            if new_pack is not None:
 
515
                self._discard_and_save(packs)
 
516
        else:
 
517
            # only make a new pack when there is data to copy.
 
518
            self._discard_and_save(packs)
 
519
        self.garbage_inventories = total_inventories - len(list(
 
520
            collection.inventory_index.combined_index.iter_all_entries()))
 
521
 
 
522
    def _discard_and_save(self, packs):
 
523
        """Discard some packs from the repository.
 
524
 
 
525
        This removes them from the memory index, saves the in-memory index
 
526
        which makes the newly reconciled pack visible and hides the packs to be
 
527
        discarded, and finally renames the packs being discarded into the
 
528
        obsolete packs directory.
 
529
 
 
530
        :param packs: The packs to discard.
346
531
        """
347
 
        inventories = set(self.inventory.versions())
348
 
        revisions = set(self.revisions.versions())
349
 
        garbage = inventories.difference(revisions)
350
 
        self.garbage_inventories = len(garbage)
351
 
        for revision_id in garbage:
352
 
            mutter('Garbage inventory {%s} found.', revision_id)
 
532
        for pack in packs:
 
533
            self.repo._pack_collection._remove_pack_from_memory(pack)
 
534
        self.repo._pack_collection._save_pack_names()
 
535
        self.repo._pack_collection._obsolete_packs(packs)