/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

Implement _bisect_recursive, which uses multiple bisect calls to
handle renames and finding entries in subdirs.
As is, this could be hooked into paths2ids() if the dirstate has not been loaded yet.
However, it doesn't quite provide enough, since the parsed entries would probably not
be saved. Further, the multiple bisect calls are less efficient then they could be,
because they do not remember the last bisect call.
We should explore switching to a caching structure, which maintains all records that
have been processed, in a structure that can be in-memory searched before going back
to disk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
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
 
16
 
 
17
"""Reconcilers are able to fix some potential data errors in a branch."""
 
18
 
 
19
 
 
20
__all__ = ['reconcile', 'Reconciler', 'RepoReconciler', 'KnitReconciler']
 
21
 
 
22
 
 
23
from bzrlib import ui
 
24
from bzrlib.trace import mutter
 
25
from bzrlib.tsort import TopoSorter
 
26
 
 
27
 
 
28
def reconcile(dir, other=None):
 
29
    """Reconcile the data in dir.
 
30
 
 
31
    Currently this is limited to a inventory 'reweave'.
 
32
 
 
33
    This is a convenience method, for using a Reconciler object.
 
34
 
 
35
    Directly using Reconciler is recommended for library users that
 
36
    desire fine grained control or analysis of the found issues.
 
37
 
 
38
    :param other: another bzrdir to reconcile against.
 
39
    """
 
40
    reconciler = Reconciler(dir, other=other)
 
41
    reconciler.reconcile()
 
42
 
 
43
 
 
44
class Reconciler(object):
 
45
    """Reconcilers are used to reconcile existing data."""
 
46
 
 
47
    def __init__(self, dir, other=None):
 
48
        """Create a Reconciler."""
 
49
        self.bzrdir = dir
 
50
 
 
51
    def reconcile(self):
 
52
        """Perform reconciliation.
 
53
        
 
54
        After reconciliation the following attributes document found issues:
 
55
        inconsistent_parents: The number of revisions in the repository whose
 
56
                              ancestry was being reported incorrectly.
 
57
        garbage_inventories: The number of inventory objects without revisions
 
58
                             that were garbage collected.
 
59
        """
 
60
        self.pb = ui.ui_factory.nested_progress_bar()
 
61
        try:
 
62
            self._reconcile()
 
63
        finally:
 
64
            self.pb.finished()
 
65
 
 
66
    def _reconcile(self):
 
67
        """Helper function for performing reconciliation."""
 
68
        self.repo = self.bzrdir.find_repository()
 
69
        self.pb.note('Reconciling repository %s',
 
70
                     self.repo.bzrdir.root_transport.base)
 
71
        repo_reconciler = self.repo.reconcile(thorough=True)
 
72
        self.inconsistent_parents = repo_reconciler.inconsistent_parents
 
73
        self.garbage_inventories = repo_reconciler.garbage_inventories
 
74
        self.pb.note('Reconciliation complete.')
 
75
 
 
76
 
 
77
class RepoReconciler(object):
 
78
    """Reconciler that reconciles a repository.
 
79
 
 
80
    Currently this consists of an inventory reweave with revision cross-checks.
 
81
    """
 
82
 
 
83
    def __init__(self, repo, other=None, thorough=False):
 
84
        """Construct a RepoReconciler.
 
85
 
 
86
        :param thorough: perform a thorough check which may take longer but
 
87
                         will correct non-data loss issues such as incorrect
 
88
                         cached data.
 
89
        """
 
90
        self.garbage_inventories = 0
 
91
        self.inconsistent_parents = 0
 
92
        self.repo = repo
 
93
        self.thorough = thorough
 
94
 
 
95
    def reconcile(self):
 
96
        """Perform reconciliation.
 
97
        
 
98
        After reconciliation the following attributes document found issues:
 
99
        inconsistent_parents: The number of revisions in the repository whose
 
100
                              ancestry was being reported incorrectly.
 
101
        garbage_inventories: The number of inventory objects without revisions
 
102
                             that were garbage collected.
 
103
        """
 
104
        self.repo.lock_write()
 
105
        try:
 
106
            self.pb = ui.ui_factory.nested_progress_bar()
 
107
            try:
 
108
                self._reconcile_steps()
 
109
            finally:
 
110
                self.pb.finished()
 
111
        finally:
 
112
            self.repo.unlock()
 
113
 
 
114
    def _reconcile_steps(self):
 
115
        """Perform the steps to reconcile this repository."""
 
116
        self._reweave_inventory()
 
117
 
 
118
    def _reweave_inventory(self):
 
119
        """Regenerate the inventory weave for the repository from scratch.
 
120
        
 
121
        This is a smart function: it will only do the reweave if doing it 
 
122
        will correct data issues. The self.thorough flag controls whether
 
123
        only data-loss causing issues (!self.thorough) or all issues
 
124
        (self.thorough) are treated as requiring the reweave.
 
125
        """
 
126
        # local because needing to know about WeaveFile is a wart we want to hide
 
127
        from bzrlib.weave import WeaveFile, Weave
 
128
        transaction = self.repo.get_transaction()
 
129
        self.pb.update('Reading inventory data.')
 
130
        self.inventory = self.repo.get_inventory_weave()
 
131
        # the total set of revisions to process
 
132
        self.pending = set([rev_id for rev_id in self.repo._revision_store.all_revision_ids(transaction)])
 
133
 
 
134
        # mapping from revision_id to parents
 
135
        self._rev_graph = {}
 
136
        # errors that we detect
 
137
        self.inconsistent_parents = 0
 
138
        # we need the revision id of each revision and its available parents list
 
139
        self._setup_steps(len(self.pending))
 
140
        for rev_id in self.pending:
 
141
            # put a revision into the graph.
 
142
            self._graph_revision(rev_id)
 
143
        self._check_garbage_inventories()
 
144
        # if there are no inconsistent_parents and 
 
145
        # (no garbage inventories or we are not doing a thorough check)
 
146
        if (not self.inconsistent_parents and 
 
147
            (not self.garbage_inventories or not self.thorough)):
 
148
            self.pb.note('Inventory ok.')
 
149
            return
 
150
        self.pb.update('Backing up inventory...', 0, 0)
 
151
        self.repo.control_weaves.copy(self.inventory, 'inventory.backup', self.repo.get_transaction())
 
152
        self.pb.note('Backup Inventory created.')
 
153
        # asking for '' should never return a non-empty weave
 
154
        new_inventory_vf = self.repo.control_weaves.get_empty('inventory.new',
 
155
            self.repo.get_transaction())
 
156
 
 
157
        # we have topological order of revisions and non ghost parents ready.
 
158
        self._setup_steps(len(self._rev_graph))
 
159
        for rev_id in TopoSorter(self._rev_graph.items()).iter_topo_order():
 
160
            parents = self._rev_graph[rev_id]
 
161
            # double check this really is in topological order.
 
162
            unavailable = [p for p in parents if p not in new_inventory_vf]
 
163
            assert len(unavailable) == 0
 
164
            # this entry has all the non ghost parents in the inventory
 
165
            # file already.
 
166
            self._reweave_step('adding inventories')
 
167
            if isinstance(new_inventory_vf, WeaveFile):
 
168
                # It's really a WeaveFile, but we call straight into the
 
169
                # Weave's add method to disable the auto-write-out behaviour.
 
170
                # This is done to avoid a revision_count * time-to-write additional overhead on 
 
171
                # reconcile.
 
172
                new_inventory_vf._check_write_ok()
 
173
                Weave._add_lines(new_inventory_vf, rev_id, parents, self.inventory.get_lines(rev_id),
 
174
                                 None)
 
175
            else:
 
176
                new_inventory_vf.add_lines(rev_id, parents, self.inventory.get_lines(rev_id))
 
177
 
 
178
        if isinstance(new_inventory_vf, WeaveFile):
 
179
            new_inventory_vf._save()
 
180
        # if this worked, the set of new_inventory_vf.names should equal
 
181
        # self.pending
 
182
        assert set(new_inventory_vf.versions()) == self.pending
 
183
        self.pb.update('Writing weave')
 
184
        self.repo.control_weaves.copy(new_inventory_vf, 'inventory', self.repo.get_transaction())
 
185
        self.repo.control_weaves.delete('inventory.new', self.repo.get_transaction())
 
186
        self.inventory = None
 
187
        self.pb.note('Inventory regenerated.')
 
188
 
 
189
    def _setup_steps(self, new_total):
 
190
        """Setup the markers we need to control the progress bar."""
 
191
        self.total = new_total
 
192
        self.count = 0
 
193
 
 
194
    def _graph_revision(self, rev_id):
 
195
        """Load a revision into the revision graph."""
 
196
        # pick a random revision
 
197
        # analyse revision id rev_id and put it in the stack.
 
198
        self._reweave_step('loading revisions')
 
199
        rev = self.repo.get_revision_reconcile(rev_id)
 
200
        assert rev.revision_id == rev_id
 
201
        parents = []
 
202
        for parent in rev.parent_ids:
 
203
            if self._parent_is_available(parent):
 
204
                parents.append(parent)
 
205
            else:
 
206
                mutter('found ghost %s', parent)
 
207
        self._rev_graph[rev_id] = parents   
 
208
        if self._parents_are_inconsistent(rev_id, parents):
 
209
            self.inconsistent_parents += 1
 
210
            mutter('Inconsistent inventory parents: id {%s} '
 
211
                   'inventory claims %r, '
 
212
                   'available parents are %r, '
 
213
                   'unavailable parents are %r',
 
214
                   rev_id, 
 
215
                   set(self.inventory.get_parents(rev_id)),
 
216
                   set(parents),
 
217
                   set(rev.parent_ids).difference(set(parents)))
 
218
 
 
219
    def _parents_are_inconsistent(self, rev_id, parents):
 
220
        """Return True if the parents list of rev_id does not match the weave.
 
221
 
 
222
        This detects inconsistencies based on the self.thorough value:
 
223
        if thorough is on, the first parent value is checked as well as ghost
 
224
        differences.
 
225
        Otherwise only the ghost differences are evaluated.
 
226
        """
 
227
        weave_parents = self.inventory.get_parents(rev_id)
 
228
        weave_missing_old_ghosts = set(weave_parents) != set(parents)
 
229
        first_parent_is_wrong = (
 
230
            len(weave_parents) and len(parents) and
 
231
            parents[0] != weave_parents[0])
 
232
        if self.thorough:
 
233
            return weave_missing_old_ghosts or first_parent_is_wrong
 
234
        else:
 
235
            return weave_missing_old_ghosts
 
236
 
 
237
    def _check_garbage_inventories(self):
 
238
        """Check for garbage inventories which we cannot trust
 
239
 
 
240
        We cant trust them because their pre-requisite file data may not
 
241
        be present - all we know is that their revision was not installed.
 
242
        """
 
243
        if not self.thorough:
 
244
            return
 
245
        inventories = set(self.inventory.versions())
 
246
        revisions = set(self._rev_graph.keys())
 
247
        garbage = inventories.difference(revisions)
 
248
        self.garbage_inventories = len(garbage)
 
249
        for revision_id in garbage:
 
250
            mutter('Garbage inventory {%s} found.', revision_id)
 
251
 
 
252
    def _parent_is_available(self, parent):
 
253
        """True if parent is a fully available revision
 
254
 
 
255
        A fully available revision has a inventory and a revision object in the
 
256
        repository.
 
257
        """
 
258
        return (parent in self._rev_graph or 
 
259
                (parent in self.inventory and self.repo.has_revision(parent)))
 
260
 
 
261
    def _reweave_step(self, message):
 
262
        """Mark a single step of regeneration complete."""
 
263
        self.pb.update(message, self.count, self.total)
 
264
        self.count += 1
 
265
 
 
266
 
 
267
class KnitReconciler(RepoReconciler):
 
268
    """Reconciler that reconciles a knit format repository.
 
269
 
 
270
    This will detect garbage inventories and remove them.
 
271
 
 
272
    Inconsistent parentage is checked for in the revision weave.
 
273
    """
 
274
 
 
275
    def _reconcile_steps(self):
 
276
        """Perform the steps to reconcile this repository."""
 
277
        if self.thorough:
 
278
            self._load_indexes()
 
279
            # knits never suffer this
 
280
            self._gc_inventory()
 
281
 
 
282
    def _load_indexes(self):
 
283
        """Load indexes for the reconciliation."""
 
284
        self.transaction = self.repo.get_transaction()
 
285
        self.pb.update('Reading indexes.', 0, 2)
 
286
        self.inventory = self.repo.get_inventory_weave()
 
287
        self.pb.update('Reading indexes.', 1, 2)
 
288
        self.revisions = self.repo._revision_store.get_revision_file(self.transaction)
 
289
        self.pb.update('Reading indexes.', 2, 2)
 
290
 
 
291
    def _gc_inventory(self):
 
292
        """Remove inventories that are not referenced from the revision store."""
 
293
        self.pb.update('Checking unused inventories.', 0, 1)
 
294
        self._check_garbage_inventories()
 
295
        self.pb.update('Checking unused inventories.', 1, 3)
 
296
        if not self.garbage_inventories:
 
297
            self.pb.note('Inventory ok.')
 
298
            return
 
299
        self.pb.update('Backing up inventory...', 0, 0)
 
300
        self.repo.control_weaves.copy(self.inventory, 'inventory.backup', self.transaction)
 
301
        self.pb.note('Backup Inventory created.')
 
302
        # asking for '' should never return a non-empty weave
 
303
        new_inventory_vf = self.repo.control_weaves.get_empty('inventory.new',
 
304
            self.transaction)
 
305
 
 
306
        # we have topological order of revisions and non ghost parents ready.
 
307
        self._setup_steps(len(self.revisions))
 
308
        for rev_id in TopoSorter(self.revisions.get_graph().items()).iter_topo_order():
 
309
            parents = self.revisions.get_parents(rev_id)
 
310
            # double check this really is in topological order.
 
311
            unavailable = [p for p in parents if p not in new_inventory_vf]
 
312
            assert len(unavailable) == 0
 
313
            # this entry has all the non ghost parents in the inventory
 
314
            # file already.
 
315
            self._reweave_step('adding inventories')
 
316
            # ugly but needed, weaves are just way tooooo slow else.
 
317
            new_inventory_vf.add_lines(rev_id, parents, self.inventory.get_lines(rev_id))
 
318
 
 
319
        # if this worked, the set of new_inventory_vf.names should equal
 
320
        # self.pending
 
321
        assert set(new_inventory_vf.versions()) == set(self.revisions.versions())
 
322
        self.pb.update('Writing weave')
 
323
        self.repo.control_weaves.copy(new_inventory_vf, 'inventory', self.transaction)
 
324
        self.repo.control_weaves.delete('inventory.new', self.transaction)
 
325
        self.inventory = None
 
326
        self.pb.note('Inventory regenerated.')
 
327
 
 
328
    def _check_garbage_inventories(self):
 
329
        """Check for garbage inventories which we cannot trust
 
330
 
 
331
        We cant trust them because their pre-requisite file data may not
 
332
        be present - all we know is that their revision was not installed.
 
333
        """
 
334
        inventories = set(self.inventory.versions())
 
335
        revisions = set(self.revisions.versions())
 
336
        garbage = inventories.difference(revisions)
 
337
        self.garbage_inventories = len(garbage)
 
338
        for revision_id in garbage:
 
339
            mutter('Garbage inventory {%s} found.', revision_id)