/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 breezy/check.py

  • Committer: Jelmer Vernooij
  • Date: 2018-02-18 21:42:57 UTC
  • mto: This revision was merged to the branch mainline in revision 6859.
  • Revision ID: jelmer@jelmer.uk-20180218214257-jpevutp1wa30tz3v
Update TODO to reference Breezy, not Bazaar.

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
 
17
# TODO: Check ancestries are correct for every revision: includes
 
18
# every committed so far, and in a reasonable order.
 
19
 
 
20
# TODO: Also check non-mainline revisions mentioned as parents.
 
21
 
 
22
# TODO: Check for extra files in the control directory.
 
23
 
 
24
# TODO: Check revision, inventory and entry objects have all
 
25
# required fields.
 
26
 
 
27
# TODO: Get every revision in the revision-store even if they're not
 
28
# referenced by history and make sure they're all valid.
 
29
 
17
30
# TODO: Perhaps have a way to record errors other than by raising exceptions;
18
31
# would perhaps be enough to accumulate exception objects in a list without
19
32
# raising them.  If there's more than one exception it'd be good to see them
34
47
  indicating that the revision was found/not found.
35
48
"""
36
49
 
37
 
import contextlib
 
50
from __future__ import absolute_import
38
51
 
39
52
from . import (
40
53
    errors,
 
54
    ui,
41
55
    )
 
56
from .branch import Branch
42
57
from .controldir import ControlDir
 
58
from .revision import NULL_REVISION
 
59
from .sixish import (
 
60
    viewitems,
 
61
    )
43
62
from .trace import note
 
63
from .workingtree import WorkingTree
44
64
from .i18n import gettext
45
65
 
46
 
 
47
66
class Check(object):
48
67
    """Check a repository"""
49
68
 
54
73
        raise NotImplementedError(self.report_results)
55
74
 
56
75
 
57
 
def scan_branch(branch, needed_refs, exit_stack):
 
76
class VersionedFileCheck(Check):
 
77
    """Check a versioned file repository"""
 
78
 
 
79
    # The Check object interacts with InventoryEntry.check, etc.
 
80
 
 
81
    def __init__(self, repository, check_repo=True):
 
82
        self.repository = repository
 
83
        self.checked_rev_cnt = 0
 
84
        self.ghosts = set()
 
85
        self.missing_parent_links = {}
 
86
        self.missing_inventory_sha_cnt = 0
 
87
        self.missing_revision_cnt = 0
 
88
        self.checked_weaves = set()
 
89
        self.unreferenced_versions = set()
 
90
        self.inconsistent_parents = []
 
91
        self.rich_roots = repository.supports_rich_root()
 
92
        self.text_key_references = {}
 
93
        self.check_repo = check_repo
 
94
        self.other_results = []
 
95
        # Plain text lines to include in the report
 
96
        self._report_items = []
 
97
        # Keys we are looking for; may be large and need spilling to disk.
 
98
        # key->(type(revision/inventory/text/signature/map), sha1, first-referer)
 
99
        self.pending_keys = {}
 
100
        # Ancestors map for all of revisions being checked; while large helper
 
101
        # functions we call would create it anyway, so better to have once and
 
102
        # keep.
 
103
        self.ancestors = {}
 
104
 
 
105
    def check(self, callback_refs=None, check_repo=True):
 
106
        if callback_refs is None:
 
107
            callback_refs = {}
 
108
        self.repository.lock_read()
 
109
        self.progress = ui.ui_factory.nested_progress_bar()
 
110
        try:
 
111
            self.progress.update(gettext('check'), 0, 4)
 
112
            if self.check_repo:
 
113
                self.progress.update(gettext('checking revisions'), 0)
 
114
                self.check_revisions()
 
115
                self.progress.update(gettext('checking commit contents'), 1)
 
116
                self.repository._check_inventories(self)
 
117
                self.progress.update(gettext('checking file graphs'), 2)
 
118
                # check_weaves is done after the revision scan so that
 
119
                # revision index is known to be valid.
 
120
                self.check_weaves()
 
121
            self.progress.update(gettext('checking branches and trees'), 3)
 
122
            if callback_refs:
 
123
                repo = self.repository
 
124
                # calculate all refs, and callback the objects requesting them.
 
125
                refs = {}
 
126
                wanting_items = set()
 
127
                # Current crude version calculates everything and calls
 
128
                # everything at once. Doing a queue and popping as things are
 
129
                # satisfied would be cheaper on memory [but few people have
 
130
                # huge numbers of working trees today. TODO: fix before
 
131
                # landing].
 
132
                distances = set()
 
133
                existences = set()
 
134
                for ref, wantlist in viewitems(callback_refs):
 
135
                    wanting_items.update(wantlist)
 
136
                    kind, value = ref
 
137
                    if kind == 'trees':
 
138
                        refs[ref] = repo.revision_tree(value)
 
139
                    elif kind == 'lefthand-distance':
 
140
                        distances.add(value)
 
141
                    elif kind == 'revision-existence':
 
142
                        existences.add(value)
 
143
                    else:
 
144
                        raise AssertionError(
 
145
                            'unknown ref kind for ref %s' % ref)
 
146
                node_distances = repo.get_graph().find_lefthand_distances(distances)
 
147
                for key, distance in viewitems(node_distances):
 
148
                    refs[('lefthand-distance', key)] = distance
 
149
                    if key in existences and distance > 0:
 
150
                        refs[('revision-existence', key)] = True
 
151
                        existences.remove(key)
 
152
                parent_map = repo.get_graph().get_parent_map(existences)
 
153
                for key in parent_map:
 
154
                    refs[('revision-existence', key)] = True
 
155
                    existences.remove(key)
 
156
                for key in existences:
 
157
                    refs[('revision-existence', key)] = False
 
158
                for item in wanting_items:
 
159
                    if isinstance(item, WorkingTree):
 
160
                        item._check(refs)
 
161
                    if isinstance(item, Branch):
 
162
                        self.other_results.append(item.check(refs))
 
163
        finally:
 
164
            self.progress.finished()
 
165
            self.repository.unlock()
 
166
 
 
167
    def _check_revisions(self, revisions_iterator):
 
168
        """Check revision objects by decorating a generator.
 
169
 
 
170
        :param revisions_iterator: An iterator of(revid, Revision-or-None).
 
171
        :return: A generator of the contents of revisions_iterator.
 
172
        """
 
173
        self.planned_revisions = set()
 
174
        for revid, revision in revisions_iterator:
 
175
            yield revid, revision
 
176
            self._check_one_rev(revid, revision)
 
177
        # Flatten the revisions we found to guarantee consistent later
 
178
        # iteration.
 
179
        self.planned_revisions = list(self.planned_revisions)
 
180
        # TODO: extract digital signatures as items to callback on too.
 
181
 
 
182
    def check_revisions(self):
 
183
        """Scan revisions, checking data directly available as we go."""
 
184
        revision_iterator = self.repository.iter_revisions(
 
185
            self.repository.all_revision_ids())
 
186
        revision_iterator = self._check_revisions(revision_iterator)
 
187
        # We read the all revisions here:
 
188
        # - doing this allows later code to depend on the revision index.
 
189
        # - we can fill out existence flags at this point
 
190
        # - we can read the revision inventory sha at this point
 
191
        # - we can check properties and serialisers etc.
 
192
        if not self.repository._format.revision_graph_can_have_wrong_parents:
 
193
            # The check against the index isn't needed.
 
194
            self.revs_with_bad_parents_in_index = None
 
195
            for thing in revision_iterator:
 
196
                pass
 
197
        else:
 
198
            bad_revisions = self.repository._find_inconsistent_revision_parents(
 
199
                revision_iterator)
 
200
            self.revs_with_bad_parents_in_index = list(bad_revisions)
 
201
 
 
202
    def report_results(self, verbose):
 
203
        if self.check_repo:
 
204
            self._report_repo_results(verbose)
 
205
        for result in self.other_results:
 
206
            result.report_results(verbose)
 
207
 
 
208
    def _report_repo_results(self, verbose):
 
209
        note(gettext('checked repository {0} format {1}').format(
 
210
            self.repository.user_url,
 
211
            self.repository._format))
 
212
        note(gettext('%6d revisions'), self.checked_rev_cnt)
 
213
        note(gettext('%6d file-ids'), len(self.checked_weaves))
 
214
        if verbose:
 
215
            note(gettext('%6d unreferenced text versions'),
 
216
                len(self.unreferenced_versions))
 
217
        if verbose and len(self.unreferenced_versions):
 
218
                for file_id, revision_id in self.unreferenced_versions:
 
219
                    note(gettext('unreferenced version: {{{0}}} in {1}').format(revision_id,
 
220
                        file_id))
 
221
        if self.missing_inventory_sha_cnt:
 
222
            note(gettext('%6d revisions are missing inventory_sha1'),
 
223
                 self.missing_inventory_sha_cnt)
 
224
        if self.missing_revision_cnt:
 
225
            note(gettext('%6d revisions are mentioned but not present'),
 
226
                 self.missing_revision_cnt)
 
227
        if len(self.ghosts):
 
228
            note(gettext('%6d ghost revisions'), len(self.ghosts))
 
229
            if verbose:
 
230
                for ghost in self.ghosts:
 
231
                    note('      %s', ghost)
 
232
        if len(self.missing_parent_links):
 
233
            note(gettext('%6d revisions missing parents in ancestry'),
 
234
                 len(self.missing_parent_links))
 
235
            if verbose:
 
236
                for link, linkers in viewitems(self.missing_parent_links):
 
237
                    note(gettext('      %s should be in the ancestry for:'), link)
 
238
                    for linker in linkers:
 
239
                        note('       * %s', linker)
 
240
        if len(self.inconsistent_parents):
 
241
            note(gettext('%6d inconsistent parents'), len(self.inconsistent_parents))
 
242
            if verbose:
 
243
                for info in self.inconsistent_parents:
 
244
                    revision_id, file_id, found_parents, correct_parents = info
 
245
                    note(gettext('      * {0} version {1} has parents {2!r} '
 
246
                         'but should have {3!r}').format(
 
247
                         file_id, revision_id, found_parents,
 
248
                             correct_parents))
 
249
        if self.revs_with_bad_parents_in_index:
 
250
            note(gettext(
 
251
                 '%6d revisions have incorrect parents in the revision index'),
 
252
                 len(self.revs_with_bad_parents_in_index))
 
253
            if verbose:
 
254
                for item in self.revs_with_bad_parents_in_index:
 
255
                    revision_id, index_parents, actual_parents = item
 
256
                    note(gettext(
 
257
                        '       {0} has wrong parents in index: '
 
258
                        '{1!r} should be {2!r}').format(
 
259
                        revision_id, index_parents, actual_parents))
 
260
        for item in self._report_items:
 
261
            note(item)
 
262
 
 
263
    def _check_one_rev(self, rev_id, rev):
 
264
        """Cross-check one revision.
 
265
 
 
266
        :param rev_id: A revision id to check.
 
267
        :param rev: A revision or None to indicate a missing revision.
 
268
        """
 
269
        if rev.revision_id != rev_id:
 
270
            self._report_items.append(gettext(
 
271
                'Mismatched internal revid {{{0}}} and index revid {{{1}}}').format(
 
272
                rev.revision_id, rev_id))
 
273
            rev_id = rev.revision_id
 
274
        # Check this revision tree etc, and count as seen when we encounter a
 
275
        # reference to it.
 
276
        self.planned_revisions.add(rev_id)
 
277
        # It is not a ghost
 
278
        self.ghosts.discard(rev_id)
 
279
        # Count all parents as ghosts if we haven't seen them yet.
 
280
        for parent in rev.parent_ids:
 
281
            if not parent in self.planned_revisions:
 
282
                self.ghosts.add(parent)
 
283
        
 
284
        self.ancestors[rev_id] = tuple(rev.parent_ids) or (NULL_REVISION,)
 
285
        self.add_pending_item(rev_id, ('inventories', rev_id), 'inventory',
 
286
            rev.inventory_sha1)
 
287
        self.checked_rev_cnt += 1
 
288
 
 
289
    def add_pending_item(self, referer, key, kind, sha1):
 
290
        """Add a reference to a sha1 to be cross checked against a key.
 
291
 
 
292
        :param referer: The referer that expects key to have sha1.
 
293
        :param key: A storage key e.g. ('texts', 'foo@bar-20040504-1234')
 
294
        :param kind: revision/inventory/text/map/signature
 
295
        :param sha1: A hex sha1 or None if no sha1 is known.
 
296
        """
 
297
        existing = self.pending_keys.get(key)
 
298
        if existing:
 
299
            if sha1 != existing[1]:
 
300
                self._report_items.append(gettext('Multiple expected sha1s for {0}. {{{1}}}'
 
301
                    ' expects {{{2}}}, {{{3}}} expects {{{4}}}').format(
 
302
                    key, referer, sha1, existing[1], existing[0]))
 
303
        else:
 
304
            self.pending_keys[key] = (kind, sha1, referer)
 
305
 
 
306
    def check_weaves(self):
 
307
        """Check all the weaves we can get our hands on.
 
308
        """
 
309
        weave_ids = []
 
310
        storebar = ui.ui_factory.nested_progress_bar()
 
311
        try:
 
312
            self._check_weaves(storebar)
 
313
        finally:
 
314
            storebar.finished()
 
315
 
 
316
    def _check_weaves(self, storebar):
 
317
        storebar.update('text-index', 0, 2)
 
318
        if self.repository._format.fast_deltas:
 
319
            # We haven't considered every fileid instance so far.
 
320
            weave_checker = self.repository._get_versioned_file_checker(
 
321
                ancestors=self.ancestors)
 
322
        else:
 
323
            weave_checker = self.repository._get_versioned_file_checker(
 
324
                text_key_references=self.text_key_references,
 
325
                ancestors=self.ancestors)
 
326
        storebar.update('file-graph', 1)
 
327
        wrongs, unused_versions = weave_checker.check_file_version_parents(
 
328
            self.repository.texts)
 
329
        self.checked_weaves = weave_checker.file_ids
 
330
        for text_key, (stored_parents, correct_parents) in viewitems(wrongs):
 
331
            # XXX not ready for id join/split operations.
 
332
            weave_id = text_key[0]
 
333
            revision_id = text_key[-1]
 
334
            weave_parents = tuple([parent[-1] for parent in stored_parents])
 
335
            correct_parents = tuple([parent[-1] for parent in correct_parents])
 
336
            self.inconsistent_parents.append(
 
337
                (revision_id, weave_id, weave_parents, correct_parents))
 
338
        self.unreferenced_versions.update(unused_versions)
 
339
 
 
340
    def _add_entry_to_text_key_references(self, inv, entry):
 
341
        if not self.rich_roots and entry.name == '':
 
342
            return
 
343
        key = (entry.file_id, entry.revision)
 
344
        self.text_key_references.setdefault(key, False)
 
345
        if entry.revision == inv.revision_id:
 
346
            self.text_key_references[key] = True
 
347
 
 
348
 
 
349
def scan_branch(branch, needed_refs, to_unlock):
58
350
    """Scan a branch for refs.
59
351
 
60
352
    :param branch:  The branch to schedule for checking.
61
353
    :param needed_refs: Refs we are accumulating.
62
 
    :param exit_stack: The exit stack accumulating.
 
354
    :param to_unlock: The unlock list accumulating.
63
355
    """
64
356
    note(gettext("Checking branch at '%s'.") % (branch.base,))
65
 
    exit_stack.enter_context(branch.lock_read())
 
357
    branch.lock_read()
 
358
    to_unlock.append(branch)
66
359
    branch_refs = branch._get_check_refs()
67
360
    for ref in branch_refs:
68
361
        reflist = needed_refs.setdefault(ref, [])
69
362
        reflist.append(branch)
70
363
 
71
364
 
72
 
def scan_tree(base_tree, tree, needed_refs, exit_stack):
 
365
def scan_tree(base_tree, tree, needed_refs, to_unlock):
73
366
    """Scan a tree for refs.
74
367
 
75
368
    :param base_tree: The original tree check opened, used to detect duplicate
76
369
        tree checks.
77
370
    :param tree:  The tree to schedule for checking.
78
371
    :param needed_refs: Refs we are accumulating.
79
 
    :param exit_stack: The exit stack accumulating.
 
372
    :param to_unlock: The unlock list accumulating.
80
373
    """
81
374
    if base_tree is not None and tree.basedir == base_tree.basedir:
82
375
        return
83
376
    note(gettext("Checking working tree at '%s'.") % (tree.basedir,))
84
 
    exit_stack.enter_context(tree.lock_read())
 
377
    tree.lock_read()
 
378
    to_unlock.append(tree)
85
379
    tree_refs = tree._get_check_refs()
86
380
    for ref in tree_refs:
87
381
        reflist = needed_refs.setdefault(ref, [])
96
390
    """
97
391
    try:
98
392
        base_tree, branch, repo, relpath = \
99
 
            ControlDir.open_containing_tree_branch_or_repository(path)
 
393
                        ControlDir.open_containing_tree_branch_or_repository(path)
100
394
    except errors.NotBranchError:
101
395
        base_tree = branch = repo = None
102
396
 
103
 
    with contextlib.ExitStack() as exit_stack:
104
 
        needed_refs = {}
 
397
    to_unlock = []
 
398
    needed_refs= {}
 
399
    try:
105
400
        if base_tree is not None:
106
401
            # If the tree is a lightweight checkout we won't see it in
107
402
            # repo.find_branches - add now.
108
403
            if do_tree:
109
 
                scan_tree(None, base_tree, needed_refs, exit_stack)
 
404
                scan_tree(None, base_tree, needed_refs, to_unlock)
110
405
            branch = base_tree.branch
111
406
        if branch is not None:
112
407
            # We have a branch
114
409
                # The branch is in a shared repository
115
410
                repo = branch.repository
116
411
        if repo is not None:
117
 
            exit_stack.enter_context(repo.lock_read())
118
 
            branches = list(repo.find_branches(using=True))
 
412
            repo.lock_read()
 
413
            to_unlock.append(repo)
 
414
            branches = repo.find_branches(using=True)
119
415
            saw_tree = False
120
416
            if do_branch or do_tree:
121
417
                for branch in branches:
126
422
                        except (errors.NotLocalUrl, errors.NoWorkingTree):
127
423
                            pass
128
424
                        else:
129
 
                            scan_tree(base_tree, tree, needed_refs, exit_stack)
 
425
                            scan_tree(base_tree, tree, needed_refs, to_unlock)
130
426
                    if do_branch:
131
 
                        scan_branch(branch, needed_refs, exit_stack)
 
427
                        scan_branch(branch, needed_refs, to_unlock)
132
428
            if do_branch and not branches:
133
429
                note(gettext("No branch found at specified location."))
134
430
            if do_tree and base_tree is None and not saw_tree:
138
434
                    note(gettext("Checking repository at '%s'.")
139
435
                         % (repo.user_url,))
140
436
                result = repo.check(None, callback_refs=needed_refs,
141
 
                                    check_repo=do_repo)
 
437
                    check_repo=do_repo)
142
438
                result.report_results(verbose)
143
439
        else:
144
440
            if do_tree:
147
443
                note(gettext("No branch found at specified location."))
148
444
            if do_repo:
149
445
                note(gettext("No repository found at specified location."))
 
446
    finally:
 
447
        for thing in to_unlock:
 
448
            thing.unlock()