/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 object_store.py

Add trivial object store tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2009 Canonical Ltd
 
1
# Copyright (C) 2009 Jelmer Vernooij <jelmer@samba.org>
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
16
16
 
17
17
"""Map from Git sha's to Bazaar objects."""
18
18
 
19
 
import bzrlib
20
 
 
21
 
from bzrlib import ui
22
 
 
23
 
from bzrlib.errors import NoSuchRevision
24
 
 
25
 
from bzrlib.plugins.git.mapping import (
26
 
    inventory_to_tree_and_blobs,
27
 
    revision_to_commit,
28
 
    )
29
 
from bzrlib.plugins.git.shamap import GitShaMap
30
 
 
31
19
from dulwich.objects import (
32
20
    Blob,
33
 
    )
34
 
 
35
 
 
36
 
class GitObjectConverter(object):
 
21
    Tree,
 
22
    sha_to_hex,
 
23
    )
 
24
from dulwich.object_store import (
 
25
    BaseObjectStore,
 
26
    )
 
27
 
 
28
from bzrlib import (
 
29
    errors,
 
30
    lru_cache,
 
31
    trace,
 
32
    ui,
 
33
    urlutils,
 
34
    )
 
35
from bzrlib.revision import (
 
36
    NULL_REVISION,
 
37
    )
 
38
 
 
39
from bzrlib.plugins.git.mapping import (
 
40
    default_mapping,
 
41
    directory_to_tree,
 
42
    extract_unusual_modes,
 
43
    mapping_registry,
 
44
    symlink_to_blob,
 
45
    )
 
46
from bzrlib.plugins.git.shamap import (
 
47
    from_repository as idmap_from_repository,
 
48
    )
 
49
 
 
50
 
 
51
def get_object_store(repo, mapping=None):
 
52
    git = getattr(repo, "_git", None)
 
53
    if git is not None:
 
54
        return git.object_store
 
55
    return BazaarObjectStore(repo, mapping)
 
56
 
 
57
 
 
58
MAX_INV_CACHE_SIZE = 50 * 1024 * 1024
 
59
 
 
60
 
 
61
class LRUInventoryCache(object):
 
62
 
 
63
    def __init__(self, repository):
 
64
        def approx_inv_size(inv):
 
65
            # Very rough estimate, 1k per inventory entry
 
66
            return len(inv) * 1024
 
67
        self.repository = repository
 
68
        self._cache = lru_cache.LRUSizeCache(max_size=MAX_INV_CACHE_SIZE,
 
69
            after_cleanup_size=None, compute_size=approx_inv_size)
 
70
 
 
71
    def get_inventory(self, revid):            
 
72
        try:
 
73
            return self._cache[revid] 
 
74
        except KeyError:
 
75
            inv = self.repository.get_inventory(revid)
 
76
            self._cache.add(revid, inv)
 
77
            return inv
 
78
 
 
79
    def iter_inventories(self, revids):
 
80
        invs = dict([(k, self._cache.get(k)) for k in revids]) 
 
81
        for inv in self.repository.iter_inventories(
 
82
                [r for r, v in invs.iteritems() if v is None]):
 
83
            invs[inv.revision_id] = inv
 
84
            self._cache.add(inv.revision_id, inv)
 
85
        return (invs[r] for r in revids)
 
86
 
 
87
    def get_inventories(self, revids):
 
88
        return list(self.iter_inventories(revids))
 
89
 
 
90
    def add(self, revid, inv):
 
91
        self._cache.add(revid, inv)
 
92
 
 
93
 
 
94
def _check_expected_sha(expected_sha, object):
 
95
    """Check whether an object matches an expected SHA.
 
96
 
 
97
    :param expected_sha: None or expected SHA as either binary or as hex digest
 
98
    :param object: Object to verify
 
99
    """
 
100
    if expected_sha is None:
 
101
        return
 
102
    if len(expected_sha) == 40:
 
103
        if expected_sha != object.sha().hexdigest():
 
104
            raise AssertionError("Invalid sha for %r: %s" % (object,
 
105
                expected_sha))
 
106
    elif len(expected_sha) == 20:
 
107
        if expected_sha != object.sha().digest():
 
108
            raise AssertionError("Invalid sha for %r: %s" % (object,
 
109
                sha_to_hex(expected_sha)))
 
110
    else:
 
111
        raise AssertionError("Unknown length %d for %r" % (len(expected_sha),
 
112
            expected_sha))
 
113
 
 
114
 
 
115
def _inventory_to_objects(inv, parent_invs, parent_invshamaps,
 
116
        unusual_modes, iter_files_bytes, has_ghost_parents):
 
117
    """Iterate over the objects that were introduced in a revision.
 
118
 
 
119
    :param inv: Inventory to process
 
120
    :param parent_invs: parent inventory SHA maps
 
121
    :param parent_invshamaps: parent inventory SHA Map
 
122
    :param unusual_modes: Unusual file modes
 
123
    :param iter_files_bytes: Repository.iter_files_bytes-like callback
 
124
    :return: Yields (path, object) entries
 
125
    """
 
126
    new_trees = {}
 
127
    new_blobs = []
 
128
    shamap = {}
 
129
    for path, ie in inv.entries():
 
130
        if ie.kind == "file":
 
131
            if ie.revision == inv.revision_id:
 
132
                new_blobs.append((path, ie))
 
133
                new_trees[urlutils.dirname(path)] = ie.parent_id
 
134
            elif has_ghost_parents:
 
135
                for (pinv, pinvshamap) in zip(parent_invs, parent_invshamaps):
 
136
                    try:
 
137
                        pie = pinv[ie.file_id]
 
138
                    except errors.NoSuchId:
 
139
                        pass
 
140
                    else:
 
141
                        if (pie.kind == ie.kind and
 
142
                            pie.text_sha1 == ie.text_sha1):
 
143
                            shamap[ie.file_id] = pinvshamap.lookup_blob(
 
144
                                pie.file_id, pie.revision)
 
145
                            break
 
146
                else:
 
147
                    new_blobs.append((path, ie))
 
148
                    new_trees[urlutils.dirname(path)] = ie.parent_id
 
149
        elif ie.kind == "symlink":
 
150
            blob = symlink_to_blob(ie)
 
151
            for pinv in parent_invs:
 
152
                try:
 
153
                    pie = pinv[ie.file_id]
 
154
                except errors.NoSuchId:
 
155
                    pass
 
156
                else:
 
157
                    if (ie.kind == pie.kind and
 
158
                        ie.symlink_target == pie.symlink_target):
 
159
                        break
 
160
            else:
 
161
                yield path, blob
 
162
            shamap[ie.file_id] = blob.id
 
163
            new_trees[urlutils.dirname(path)] = ie.parent_id
 
164
        elif ie.kind == "directory":
 
165
            for (pinv, pinvshamap) in zip(parent_invs, parent_invshamaps):
 
166
                try:
 
167
                    pie = pinv[ie.file_id]
 
168
                except errors.NoSuchId:
 
169
                    pass
 
170
                else:
 
171
                    if (pie.kind == ie.kind and 
 
172
                        pie.children.keys() == ie.children.keys()):
 
173
                        try:
 
174
                            shamap[ie.file_id] = pinvshamap.lookup_tree(
 
175
                                ie.file_id)
 
176
                        except NotImplementedError:
 
177
                            pass
 
178
                        else:
 
179
                            break
 
180
            else:
 
181
                new_trees[path] = ie.file_id
 
182
        else:
 
183
            raise AssertionError(ie.kind)
 
184
    
 
185
    for (path, fid), chunks in iter_files_bytes(
 
186
        [(ie.file_id, ie.revision, (path, ie.file_id))
 
187
            for (path, ie) in new_blobs]):
 
188
        obj = Blob()
 
189
        obj.data = "".join(chunks)
 
190
        yield path, obj
 
191
        shamap[fid] = obj.id
 
192
 
 
193
    assert all([ie.file_id in shamap for (path, ie) in new_blobs])
 
194
 
 
195
    for fid in unusual_modes:
 
196
        new_trees[inv.id2path(fid)] = inv[fid].parent_id
 
197
    
 
198
    trees = {}
 
199
    while new_trees:
 
200
        items = new_trees.items()
 
201
        new_trees = {}
 
202
        for path, file_id in items:
 
203
            parent_id = inv[file_id].parent_id
 
204
            if parent_id is not None:
 
205
                parent_path = urlutils.dirname(path)
 
206
                new_trees[parent_path] = parent_id
 
207
            trees[path] = file_id
 
208
 
 
209
    for path in sorted(trees.keys(), reverse=True):
 
210
        ie = inv[trees[path]]
 
211
        assert ie.kind == "directory"
 
212
        obj = directory_to_tree(ie, 
 
213
                lambda ie: shamap[ie.file_id], unusual_modes)
 
214
        if obj is not None:
 
215
            shamap[ie.file_id] = obj.id
 
216
            yield path, obj
 
217
 
 
218
 
 
219
class BazaarObjectStore(BaseObjectStore):
 
220
    """A Git-style object store backed onto a Bazaar repository."""
37
221
 
38
222
    def __init__(self, repository, mapping=None):
39
223
        self.repository = repository
40
224
        if mapping is None:
41
 
            self.mapping = self.repository.get_mapping()
 
225
            self.mapping = default_mapping
42
226
        else:
43
227
            self.mapping = mapping
44
 
        self._idmap = GitShaMap(self.repository._transport)
 
228
        self._idmap = idmap_from_repository(repository)
 
229
        self.start_write_group = self._idmap.start_write_group
 
230
        self.abort_write_group = self._idmap.abort_write_group
 
231
        self.commit_write_group = self._idmap.commit_write_group
 
232
        self.parent_invs_cache = LRUInventoryCache(self.repository)
45
233
 
46
 
    def _update_sha_map(self):
47
 
        all_revids = self.repository.all_revision_ids()
 
234
    def _update_sha_map(self, stop_revision=None):
48
235
        graph = self.repository.get_graph()
49
 
        present_revids = set(self._idmap.revids())
50
 
        pb = ui.ui_factory.nested_progress_bar()
51
 
        try:
52
 
            for i, revid in enumerate(graph.iter_topo_order(all_revids)):
53
 
                if revid in present_revids:
54
 
                    continue
55
 
                pb.update("updating git map", i, len(all_revids))
56
 
                self._update_sha_map_revision(revid)
57
 
        finally:
58
 
            pb.finished()
 
236
        if stop_revision is None:
 
237
            heads = graph.heads(self.repository.all_revision_ids())
 
238
        else:
 
239
            heads = set([stop_revision])
 
240
        missing_revids = self._idmap.missing_revisions(heads)
 
241
        while heads:
 
242
            parents = graph.get_parent_map(heads)
 
243
            todo = set()
 
244
            for p in parents.values():
 
245
                todo.update([x for x in p if x not in missing_revids])
 
246
            heads = self._idmap.missing_revisions(todo)
 
247
            missing_revids.update(heads)
 
248
        if NULL_REVISION in missing_revids:
 
249
            missing_revids.remove(NULL_REVISION)
 
250
        missing_revids = self.repository.has_revisions(missing_revids)
 
251
        if not missing_revids:
 
252
            return
 
253
        self.start_write_group()
 
254
        try:
 
255
            pb = ui.ui_factory.nested_progress_bar()
 
256
            try:
 
257
                for i, revid in enumerate(graph.iter_topo_order(missing_revids)):
 
258
                    trace.mutter('processing %r', revid)
 
259
                    pb.update("updating git map", i, len(missing_revids))
 
260
                    self._update_sha_map_revision(revid)
 
261
            finally:
 
262
                pb.finished()
 
263
        except:
 
264
            self.abort_write_group()
 
265
            raise
 
266
        else:
 
267
            self.commit_write_group()
 
268
 
 
269
    def __iter__(self):
 
270
        self._update_sha_map()
 
271
        return iter(self._idmap.sha1s())
 
272
 
 
273
    def _revision_to_commit(self, rev, tree_sha):
 
274
        def parent_lookup(revid):
 
275
            try:
 
276
                return self._lookup_revision_sha1(revid)
 
277
            except errors.NoSuchRevision:
 
278
                trace.warning("Ignoring ghost parent %s", revid)
 
279
                return None
 
280
        return self.mapping.export_commit(rev, tree_sha, parent_lookup)
 
281
 
 
282
    def _revision_to_objects(self, rev, inv):
 
283
        unusual_modes = extract_unusual_modes(rev)
 
284
        present_parents = self.repository.has_revisions(rev.parent_ids)
 
285
        has_ghost_parents = (len(rev.parent_ids) < len(present_parents))
 
286
        parent_invs = self.parent_invs_cache.get_inventories(
 
287
            [p for p in rev.parent_ids if p in present_parents])
 
288
        parent_invshamaps = [self._idmap.get_inventory_sha_map(r) for r in rev.parent_ids if r in present_parents]
 
289
        tree_sha = None
 
290
        for path, obj in _inventory_to_objects(inv, parent_invs,
 
291
                parent_invshamaps, unusual_modes,
 
292
                self.repository.iter_files_bytes, has_ghost_parents):
 
293
            yield path, obj
 
294
            if path == "":
 
295
                tree_sha = obj.id
 
296
        if tree_sha is None:
 
297
            if not rev.parent_ids:
 
298
                tree_sha = Tree().id
 
299
            else:
 
300
                tree_sha = parent_invshamaps[0][inv.root.file_id]
 
301
        commit_obj = self._revision_to_commit(rev, tree_sha)
 
302
        try:
 
303
            foreign_revid, mapping = mapping_registry.parse_revision_id(rev.revision_id)
 
304
        except errors.InvalidRevisionId:
 
305
            pass
 
306
        else:
 
307
            _check_expected_sha(foreign_revid, commit_obj)
 
308
        yield None, commit_obj
59
309
 
60
310
    def _update_sha_map_revision(self, revid):
61
 
        inv = self.repository.get_inventory(revid)
62
 
        objects = inventory_to_tree_and_blobs(self.repository, self.mapping, revid)
63
 
        for sha, o, path in objects:
64
 
            if path == "":
65
 
                tree_sha = sha
66
 
            ie = inv[inv.path2id(path)]
67
 
            if ie.kind in ("file", "symlink"):
68
 
                self._idmap.add_entry(sha, "blob", (ie.file_id, ie.revision))
 
311
        rev = self.repository.get_revision(revid)
 
312
        inv = self.parent_invs_cache.get_inventory(rev.revision_id)
 
313
        commit_obj = None
 
314
        entries = []
 
315
        for path, obj in self._revision_to_objects(rev, inv):
 
316
            if obj._type == "commit":
 
317
                commit_obj = obj
 
318
            elif obj._type in ("blob", "tree"):
 
319
                file_id = inv.path2id(path)
 
320
                ie = inv[file_id]
 
321
                if obj._type == "blob":
 
322
                    revision = ie.revision
 
323
                else:
 
324
                    revision = revid
 
325
                entries.append((file_id, obj._type, obj.id, revision))
69
326
            else:
70
 
                self._idmap.add_entry(sha, "tree", (ie.file_id, ie.revision))
71
 
        rev = self.repository.get_revision(revid)
72
 
        commit_obj = revision_to_commit(rev, tree_sha, self._idmap._parent_lookup)
73
 
        self._idmap.add_entry(commit_obj.sha().hexdigest(), "commit", (revid, tree_sha))
74
 
 
75
 
    def _get_blob(self, fileid, revision):
76
 
        text = self.repository.texts.get_record_stream([(fileid, revision)], "unordered", True).next().get_bytes_as("fulltext")
 
327
                raise AssertionError
 
328
        self._idmap.add_entries(revid, rev.parent_ids, commit_obj.id, 
 
329
            commit_obj.tree, entries)
 
330
        return commit_obj.id
 
331
 
 
332
    def _get_blob(self, fileid, revision, expected_sha):
 
333
        """Return a Git Blob object from a fileid and revision stored in bzr.
 
334
 
 
335
        :param fileid: File id of the text
 
336
        :param revision: Revision of the text
 
337
        """
77
338
        blob = Blob()
78
 
        blob._text = text
 
339
        chunks = self.repository.iter_files_bytes([(fileid, revision, None)]).next()[1]
 
340
        blob.data = "".join(chunks)
 
341
        if blob.id != expected_sha:
 
342
            # Perhaps it's a symlink ?
 
343
            inv = self.parent_invs_cache.get_inventory(revision)
 
344
            entry = inv[fileid]
 
345
            assert entry.kind == 'symlink'
 
346
            blob = symlink_to_blob(entry)
 
347
        _check_expected_sha(expected_sha, blob)
79
348
        return blob
80
349
 
81
 
    def _get_tree(self, fileid, revid):
82
 
        raise NotImplementedError(self._get_tree)
83
 
 
84
 
    def _get_commit(self, revid, tree_sha):
85
 
        rev = self.repository.get_revision(revid)
86
 
        return revision_to_commit(rev, tree_sha, self._idmap._parent_lookup)
87
 
 
88
 
    def __getitem__(self, sha):
89
 
        # See if sha is in map
90
 
        try:
91
 
            (type, type_data) = self._idmap.lookup_git_sha(sha)
92
 
        except KeyError:
93
 
            # if not, see if there are any unconverted revisions and add them 
 
350
    def _get_tree(self, fileid, revid, inv, unusual_modes, expected_sha=None):
 
351
        """Return a Git Tree object from a file id and a revision stored in bzr.
 
352
 
 
353
        :param fileid: fileid in the tree.
 
354
        :param revision: Revision of the tree.
 
355
        """
 
356
        invshamap = self._idmap.get_inventory_sha_map(inv.revision_id)
 
357
        def get_ie_sha1(entry):
 
358
            if entry.kind == "directory":
 
359
                return invshamap.lookup_tree(entry.file_id)
 
360
            elif entry.kind in ("file", "symlink"):
 
361
                return invshamap.lookup_blob(entry.file_id, entry.revision)
 
362
            else:
 
363
                raise AssertionError("unknown entry kind '%s'" % entry.kind)
 
364
        tree = directory_to_tree(inv[fileid], get_ie_sha1, unusual_modes)
 
365
        _check_expected_sha(expected_sha, tree)
 
366
        return tree
 
367
 
 
368
    def get_parents(self, sha):
 
369
        """Retrieve the parents of a Git commit by SHA1.
 
370
 
 
371
        :param sha: SHA1 of the commit
 
372
        :raises: KeyError, NotCommitError
 
373
        """
 
374
        return self[sha].parents
 
375
 
 
376
    def _lookup_revision_sha1(self, revid):
 
377
        """Return the SHA1 matching a Bazaar revision."""
 
378
        if revid == NULL_REVISION:
 
379
            return "0" * 40
 
380
        try:
 
381
            return self._idmap.lookup_commit(revid)
 
382
        except KeyError:
 
383
            try:
 
384
                return mapping_registry.parse_revision_id(revid)[0]
 
385
            except errors.InvalidRevisionId:
 
386
                self._update_sha_map(revid)
 
387
                return self._idmap.lookup_commit(revid)
 
388
 
 
389
    def get_raw(self, sha):
 
390
        """Get the raw representation of a Git object by SHA1.
 
391
 
 
392
        :param sha: SHA1 of the git object
 
393
        """
 
394
        obj = self[sha]
 
395
        return (obj.type, obj.as_raw_string())
 
396
 
 
397
    def __contains__(self, sha):
 
398
        # See if sha is in map
 
399
        try:
 
400
            (type, type_data) = self._lookup_git_sha(sha)
 
401
            if type == "commit":
 
402
                return self.repository.has_revision(type_data[0])
 
403
            elif type == "blob":
 
404
                return self.repository.texts.has_version(type_data)
 
405
            elif type == "tree":
 
406
                return self.repository.has_revision(type_data[1])
 
407
            else:
 
408
                raise AssertionError("Unknown object type '%s'" % type)
 
409
        except KeyError:
 
410
            return False
 
411
 
 
412
    def _lookup_git_sha(self, sha):
 
413
        # See if sha is in map
 
414
        try:
 
415
            return self._idmap.lookup_git_sha(sha)
 
416
        except KeyError:
 
417
            # if not, see if there are any unconverted revisions and add them
94
418
            # to the map, search for sha in map again
95
419
            self._update_sha_map()
96
 
            (type, type_data) = self._idmap.lookup_git_sha(sha)
 
420
            return self._idmap.lookup_git_sha(sha)
 
421
 
 
422
    def __getitem__(self, sha):
 
423
        (type, type_data) = self._lookup_git_sha(sha)
97
424
        # convert object to git object
98
425
        if type == "commit":
99
 
            return self._get_commit(*type_data)
 
426
            (revid, tree_sha) = type_data
 
427
            try:
 
428
                rev = self.repository.get_revision(revid)
 
429
            except errors.NoSuchRevision:
 
430
                trace.mutter('entry for %s %s in shamap: %r, but not found in repository', type, sha, type_data)
 
431
                raise KeyError(sha)
 
432
            commit = self._revision_to_commit(rev, tree_sha)
 
433
            _check_expected_sha(sha, commit)
 
434
            return commit
100
435
        elif type == "blob":
101
 
            return self._get_blob(*type_data)
 
436
            (fileid, revision) = type_data
 
437
            return self._get_blob(fileid, revision, expected_sha=sha)
102
438
        elif type == "tree":
103
 
            return self._get_tree(*type_data)
 
439
            (fileid, revid) = type_data
 
440
            try:
 
441
                inv = self.parent_invs_cache.get_inventory(revid)
 
442
                rev = self.repository.get_revision(revid)
 
443
            except errors.NoSuchRevision:
 
444
                trace.mutter('entry for %s %s in shamap: %r, but not found in repository', type, sha, type_data)
 
445
                raise KeyError(sha)
 
446
            unusual_modes = extract_unusual_modes(rev)
 
447
            try:
 
448
                return self._get_tree(fileid, revid, inv,
 
449
                    unusual_modes, expected_sha=sha)
 
450
            except errors.NoSuchRevision:
 
451
                raise KeyError(sha)
104
452
        else:
105
453
            raise AssertionError("Unknown object type '%s'" % type)
 
454
 
 
455
    def generate_pack_contents(self, have, want):
 
456
        """Iterate over the contents of a pack file.
 
457
 
 
458
        :param have: List of SHA1s of objects that should not be sent
 
459
        :param want: List of SHA1s of objects that should be sent
 
460
        """
 
461
        processed = set()
 
462
        for commit_sha in have:
 
463
            try:
 
464
                (type, (revid, tree_sha)) = self._lookup_git_sha(commit_sha)
 
465
            except KeyError:
 
466
                pass
 
467
            else:
 
468
                assert type == "commit"
 
469
                processed.add(revid)
 
470
        pending = set()
 
471
        for commit_sha in want:
 
472
            if commit_sha in have:
 
473
                continue
 
474
            (type, (revid, tree_sha)) = self._lookup_git_sha(commit_sha)
 
475
            assert type == "commit"
 
476
            pending.add(revid)
 
477
        todo = set()
 
478
        while pending:
 
479
            processed.update(pending)
 
480
            next_map = self.repository.get_parent_map(pending)
 
481
            next_pending = set()
 
482
            for item in next_map.iteritems():
 
483
                todo.add(item[0])
 
484
                next_pending.update(p for p in item[1] if p not in processed)
 
485
            pending = next_pending
 
486
        if NULL_REVISION in todo:
 
487
            todo.remove(NULL_REVISION)
 
488
        trace.mutter('sending revisions %r', todo)
 
489
        ret = []
 
490
        pb = ui.ui_factory.nested_progress_bar()
 
491
        try:
 
492
            for i, revid in enumerate(todo):
 
493
                pb.update("generating git objects", i, len(todo))
 
494
                rev = self.repository.get_revision(revid)
 
495
                inv = self.parent_invs_cache.get_inventory(revid)
 
496
                for path, obj in self._revision_to_objects(rev, inv):
 
497
                    ret.append((obj, path))
 
498
        finally:
 
499
            pb.finished()
 
500
        return ret