/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

Split out _inventory_to_objects into a function.

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):
 
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 in ("file", "symlink"):
 
131
            for (pinv, pinvshamap) in zip(parent_invs, parent_invshamaps):
 
132
                try:
 
133
                    pie = pinv[ie.file_id]
 
134
                except errors.NoSuchId:
 
135
                    pass
 
136
                else:
 
137
                    if (pie.kind == ie.kind and
 
138
                        pie.text_sha1 == ie.text_sha1):
 
139
                        shamap[ie.file_id] = pinvshamap.lookup_blob(
 
140
                            pie.file_id, pie.revision)
 
141
                        break
 
142
            else:
 
143
                if ie.kind == "file":
 
144
                    new_blobs.append((path, ie))
 
145
                else:
 
146
                    blob = symlink_to_blob(ie)
 
147
                    yield path, blob
 
148
                    shamap[ie.file_id] = blob.id
 
149
                new_trees[urlutils.dirname(path)] = ie.parent_id
 
150
        elif ie.kind == "directory":
 
151
            for (pinv, pinvshamap) in zip(parent_invs, parent_invshamaps):
 
152
                try:
 
153
                    pie = pinv[ie.file_id]
 
154
                except errors.NoSuchId:
 
155
                    pass
 
156
                else:
 
157
                    if (pie.kind == ie.kind and 
 
158
                        pie.children.keys() == ie.children.keys()):
 
159
                        try:
 
160
                            shamap[ie.file_id] = pinvshamap.lookup_tree(
 
161
                                ie.file_id)
 
162
                        except NotImplementedError:
 
163
                            pass
 
164
                        else:
 
165
                            break
 
166
            else:
 
167
                new_trees[path] = ie.file_id
 
168
        else:
 
169
            raise AssertionError(ie.kind)
 
170
    
 
171
    for (path, fid), chunks in iter_files_bytes(
 
172
        [(ie.file_id, ie.revision, (path, ie.file_id)) for (path, ie) in new_blobs]):
 
173
        obj = Blob()
 
174
        obj.data = "".join(chunks)
 
175
        yield path, obj
 
176
        shamap[fid] = obj.id
 
177
 
 
178
    for fid in unusual_modes:
 
179
        new_trees[inv.id2path(fid)] = inv[fid].parent_id
 
180
    
 
181
    trees = {}
 
182
    while new_trees:
 
183
        items = new_trees.items()
 
184
        new_trees = {}
 
185
        for path, file_id in items:
 
186
            parent_id = inv[file_id].parent_id
 
187
            if parent_id is not None:
 
188
                parent_path = urlutils.dirname(path)
 
189
                new_trees[parent_path] = parent_id
 
190
            trees[path] = file_id
 
191
 
 
192
    for path in sorted(trees.keys(), reverse=True):
 
193
        ie = inv[trees[path]]
 
194
        assert ie.kind == "directory"
 
195
        obj = directory_to_tree(ie, 
 
196
                lambda ie: shamap[ie.file_id], unusual_modes)
 
197
        if obj is not None:
 
198
            shamap[ie.file_id] = obj.id
 
199
            yield path, obj
 
200
 
 
201
 
 
202
class BazaarObjectStore(BaseObjectStore):
 
203
    """A Git-style object store backed onto a Bazaar repository."""
37
204
 
38
205
    def __init__(self, repository, mapping=None):
39
206
        self.repository = repository
40
207
        if mapping is None:
41
 
            self.mapping = self.repository.get_mapping()
 
208
            self.mapping = default_mapping
42
209
        else:
43
210
            self.mapping = mapping
44
 
        self._idmap = GitShaMap(self.repository._transport)
 
211
        self._idmap = idmap_from_repository(repository)
 
212
        self.start_write_group = self._idmap.start_write_group
 
213
        self.abort_write_group = self._idmap.abort_write_group
 
214
        self.commit_write_group = self._idmap.commit_write_group
 
215
        self.parent_invs_cache = LRUInventoryCache(self.repository)
45
216
 
46
 
    def _update_sha_map(self):
47
 
        all_revids = self.repository.all_revision_ids()
 
217
    def _update_sha_map(self, stop_revision=None):
48
218
        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()
 
219
        if stop_revision is None:
 
220
            heads = graph.heads(self.repository.all_revision_ids())
 
221
        else:
 
222
            heads = set([stop_revision])
 
223
        missing_revids = self._idmap.missing_revisions(heads)
 
224
        while heads:
 
225
            parents = graph.get_parent_map(heads)
 
226
            todo = set()
 
227
            for p in parents.values():
 
228
                todo.update([x for x in p if x not in missing_revids])
 
229
            heads = self._idmap.missing_revisions(todo)
 
230
            missing_revids.update(heads)
 
231
        if NULL_REVISION in missing_revids:
 
232
            missing_revids.remove(NULL_REVISION)
 
233
        missing_revids = self.repository.has_revisions(missing_revids)
 
234
        if not missing_revids:
 
235
            return
 
236
        self.start_write_group()
 
237
        try:
 
238
            pb = ui.ui_factory.nested_progress_bar()
 
239
            try:
 
240
                for i, revid in enumerate(graph.iter_topo_order(missing_revids)):
 
241
                    trace.mutter('processing %r', revid)
 
242
                    pb.update("updating git map", i, len(missing_revids))
 
243
                    self._update_sha_map_revision(revid)
 
244
            finally:
 
245
                pb.finished()
 
246
        except:
 
247
            self.abort_write_group()
 
248
            raise
 
249
        else:
 
250
            self.commit_write_group()
 
251
 
 
252
    def __iter__(self):
 
253
        self._update_sha_map()
 
254
        return iter(self._idmap.sha1s())
 
255
 
 
256
    def _revision_to_commit(self, rev, tree_sha):
 
257
        def parent_lookup(revid):
 
258
            try:
 
259
                return self._lookup_revision_sha1(revid)
 
260
            except errors.NoSuchRevision:
 
261
                trace.warning("Ignoring ghost parent %s", revid)
 
262
                return None
 
263
        return self.mapping.export_commit(rev, tree_sha, parent_lookup)
 
264
 
 
265
    def _revision_to_objects(self, rev, inv):
 
266
        unusual_modes = extract_unusual_modes(rev)
 
267
        present_parents = self.repository.has_revisions(rev.parent_ids)
 
268
        parent_invs = self.parent_invs_cache.get_inventories(
 
269
            [p for p in rev.parent_ids if p in present_parents])
 
270
        parent_invshamaps = [self._idmap.get_inventory_sha_map(r) for r in rev.parent_ids if r in present_parents]
 
271
        tree_sha = None
 
272
        for path, obj in _inventory_to_objects(inv, parent_invs,
 
273
                parent_invshamaps, unusual_modes,
 
274
                self.repository.iter_files_bytes):
 
275
            yield path, obj
 
276
            if path == "":
 
277
                tree_sha = obj.id
 
278
        if tree_sha is None:
 
279
            if not rev.parent_ids:
 
280
                tree_sha = Tree().id
 
281
            else:
 
282
                tree_sha = parent_invshamaps[0][inv.root.file_id]
 
283
        commit_obj = self._revision_to_commit(rev, tree_sha)
 
284
        try:
 
285
            foreign_revid, mapping = mapping_registry.parse_revision_id(rev.revision_id)
 
286
        except errors.InvalidRevisionId:
 
287
            pass
 
288
        else:
 
289
            _check_expected_sha(foreign_revid, commit_obj)
 
290
        yield None, commit_obj
59
291
 
60
292
    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))
 
293
        rev = self.repository.get_revision(revid)
 
294
        inv = self.parent_invs_cache.get_inventory(rev.revision_id)
 
295
        commit_obj = None
 
296
        entries = []
 
297
        for path, obj in self._revision_to_objects(rev, inv):
 
298
            if obj._type == "commit":
 
299
                commit_obj = obj
 
300
            elif obj._type in ("blob", "tree"):
 
301
                file_id = inv.path2id(path)
 
302
                ie = inv[file_id]
 
303
                if obj._type == "blob":
 
304
                    revision = ie.revision
 
305
                else:
 
306
                    revision = revid
 
307
                entries.append((file_id, obj._type, obj.id, revision))
69
308
            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")
 
309
                raise AssertionError
 
310
        self._idmap.add_entries(revid, rev.parent_ids, commit_obj.id, 
 
311
            commit_obj.tree, entries)
 
312
        return commit_obj.id
 
313
 
 
314
    def _get_blob(self, fileid, revision, expected_sha):
 
315
        """Return a Git Blob object from a fileid and revision stored in bzr.
 
316
 
 
317
        :param fileid: File id of the text
 
318
        :param revision: Revision of the text
 
319
        """
77
320
        blob = Blob()
78
 
        blob._text = text
 
321
        chunks = self.repository.iter_files_bytes([(fileid, revision, None)]).next()[1]
 
322
        blob.data = "".join(chunks)
 
323
        if blob.id != expected_sha:
 
324
            # Perhaps it's a symlink ?
 
325
            inv = self.parent_invs_cache.get_inventory(revision)
 
326
            entry = inv[fileid]
 
327
            assert entry.kind == 'symlink'
 
328
            blob = symlink_to_blob(entry)
 
329
        _check_expected_sha(expected_sha, blob)
79
330
        return blob
80
331
 
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 
 
332
    def _get_tree(self, fileid, revid, inv, unusual_modes, expected_sha=None):
 
333
        """Return a Git Tree object from a file id and a revision stored in bzr.
 
334
 
 
335
        :param fileid: fileid in the tree.
 
336
        :param revision: Revision of the tree.
 
337
        """
 
338
        invshamap = self._idmap.get_inventory_sha_map(inv.revision_id)
 
339
        def get_ie_sha1(entry):
 
340
            if entry.kind == "directory":
 
341
                return invshamap.lookup_tree(entry.file_id)
 
342
            elif entry.kind in ("file", "symlink"):
 
343
                return invshamap.lookup_blob(entry.file_id, entry.revision)
 
344
            else:
 
345
                raise AssertionError("unknown entry kind '%s'" % entry.kind)
 
346
        tree = directory_to_tree(inv[fileid], get_ie_sha1, unusual_modes)
 
347
        _check_expected_sha(expected_sha, tree)
 
348
        return tree
 
349
 
 
350
    def get_parents(self, sha):
 
351
        """Retrieve the parents of a Git commit by SHA1.
 
352
 
 
353
        :param sha: SHA1 of the commit
 
354
        :raises: KeyError, NotCommitError
 
355
        """
 
356
        return self[sha].parents
 
357
 
 
358
    def _lookup_revision_sha1(self, revid):
 
359
        """Return the SHA1 matching a Bazaar revision."""
 
360
        if revid == NULL_REVISION:
 
361
            return "0" * 40
 
362
        try:
 
363
            return self._idmap.lookup_commit(revid)
 
364
        except KeyError:
 
365
            try:
 
366
                return mapping_registry.parse_revision_id(revid)[0]
 
367
            except errors.InvalidRevisionId:
 
368
                self._update_sha_map(revid)
 
369
                return self._idmap.lookup_commit(revid)
 
370
 
 
371
    def get_raw(self, sha):
 
372
        """Get the raw representation of a Git object by SHA1.
 
373
 
 
374
        :param sha: SHA1 of the git object
 
375
        """
 
376
        obj = self[sha]
 
377
        return (obj.type, obj.as_raw_string())
 
378
 
 
379
    def __contains__(self, sha):
 
380
        # See if sha is in map
 
381
        try:
 
382
            (type, type_data) = self._lookup_git_sha(sha)
 
383
            if type == "commit":
 
384
                return self.repository.has_revision(type_data[0])
 
385
            elif type == "blob":
 
386
                return self.repository.texts.has_version(type_data)
 
387
            elif type == "tree":
 
388
                return self.repository.has_revision(type_data[1])
 
389
            else:
 
390
                raise AssertionError("Unknown object type '%s'" % type)
 
391
        except KeyError:
 
392
            return False
 
393
 
 
394
    def _lookup_git_sha(self, sha):
 
395
        # See if sha is in map
 
396
        try:
 
397
            return self._idmap.lookup_git_sha(sha)
 
398
        except KeyError:
 
399
            # if not, see if there are any unconverted revisions and add them
94
400
            # to the map, search for sha in map again
95
401
            self._update_sha_map()
96
 
            (type, type_data) = self._idmap.lookup_git_sha(sha)
 
402
            return self._idmap.lookup_git_sha(sha)
 
403
 
 
404
    def __getitem__(self, sha):
 
405
        (type, type_data) = self._lookup_git_sha(sha)
97
406
        # convert object to git object
98
407
        if type == "commit":
99
 
            return self._get_commit(*type_data)
 
408
            (revid, tree_sha) = type_data
 
409
            try:
 
410
                rev = self.repository.get_revision(revid)
 
411
            except errors.NoSuchRevision:
 
412
                trace.mutter('entry for %s %s in shamap: %r, but not found in repository', type, sha, type_data)
 
413
                raise KeyError(sha)
 
414
            commit = self._revision_to_commit(rev, tree_sha)
 
415
            _check_expected_sha(sha, commit)
 
416
            return commit
100
417
        elif type == "blob":
101
 
            return self._get_blob(*type_data)
 
418
            (fileid, revision) = type_data
 
419
            return self._get_blob(fileid, revision, expected_sha=sha)
102
420
        elif type == "tree":
103
 
            return self._get_tree(*type_data)
 
421
            (fileid, revid) = type_data
 
422
            try:
 
423
                inv = self.parent_invs_cache.get_inventory(revid)
 
424
                rev = self.repository.get_revision(revid)
 
425
            except errors.NoSuchRevision:
 
426
                trace.mutter('entry for %s %s in shamap: %r, but not found in repository', type, sha, type_data)
 
427
                raise KeyError(sha)
 
428
            unusual_modes = extract_unusual_modes(rev)
 
429
            try:
 
430
                return self._get_tree(fileid, revid, inv,
 
431
                    unusual_modes, expected_sha=sha)
 
432
            except errors.NoSuchRevision:
 
433
                raise KeyError(sha)
104
434
        else:
105
435
            raise AssertionError("Unknown object type '%s'" % type)
 
436
 
 
437
    def generate_pack_contents(self, have, want):
 
438
        """Iterate over the contents of a pack file.
 
439
 
 
440
        :param have: List of SHA1s of objects that should not be sent
 
441
        :param want: List of SHA1s of objects that should be sent
 
442
        """
 
443
        processed = set()
 
444
        for commit_sha in have:
 
445
            try:
 
446
                (type, (revid, tree_sha)) = self._lookup_git_sha(commit_sha)
 
447
            except KeyError:
 
448
                pass
 
449
            else:
 
450
                assert type == "commit"
 
451
                processed.add(revid)
 
452
        pending = set()
 
453
        for commit_sha in want:
 
454
            if commit_sha in have:
 
455
                continue
 
456
            (type, (revid, tree_sha)) = self._lookup_git_sha(commit_sha)
 
457
            assert type == "commit"
 
458
            pending.add(revid)
 
459
        todo = set()
 
460
        while pending:
 
461
            processed.update(pending)
 
462
            next_map = self.repository.get_parent_map(pending)
 
463
            next_pending = set()
 
464
            for item in next_map.iteritems():
 
465
                todo.add(item[0])
 
466
                next_pending.update(p for p in item[1] if p not in processed)
 
467
            pending = next_pending
 
468
        if NULL_REVISION in todo:
 
469
            todo.remove(NULL_REVISION)
 
470
        trace.mutter('sending revisions %r', todo)
 
471
        ret = []
 
472
        pb = ui.ui_factory.nested_progress_bar()
 
473
        try:
 
474
            for i, revid in enumerate(todo):
 
475
                pb.update("generating git objects", i, len(todo))
 
476
                rev = self.repository.get_revision(revid)
 
477
                inv = self.parent_invs_cache.get_inventory(revid)
 
478
                for path, obj in self._revision_to_objects(rev, inv):
 
479
                    ret.append((obj, path))
 
480
        finally:
 
481
            pb.finished()
 
482
        return ret