/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

More docstrings, prefer migrating git.db to migrating git.tdb.

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 cache_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_TREE_CACHE_SIZE = 50 * 1024 * 1024
 
59
 
 
60
 
 
61
class LRUTreeCache(object):
 
62
 
 
63
    def __init__(self, repository):
 
64
        def approx_tree_size(tree):
 
65
            # Very rough estimate, 1k per inventory entry
 
66
            return len(tree.inventory) * 1024
 
67
        self.repository = repository
 
68
        self._cache = lru_cache.LRUSizeCache(max_size=MAX_TREE_CACHE_SIZE,
 
69
            after_cleanup_size=None, compute_size=approx_tree_size)
 
70
 
 
71
    def revision_tree(self, revid):            
 
72
        try:
 
73
            return self._cache[revid] 
 
74
        except KeyError:
 
75
            tree = self.repository.revision_tree(revid)
 
76
            self.add(tree)
 
77
            return tree
 
78
 
 
79
    def iter_revision_trees(self, revids):
 
80
        trees = dict([(k, self._cache.get(k)) for k in revids]) 
 
81
        for tree in self.repository.revision_trees(
 
82
                [r for r, v in trees.iteritems() if v is None]):
 
83
            trees[tree.get_revision_id()] = tree
 
84
            self.add(tree)
 
85
        return (trees[r] for r in revids)
 
86
 
 
87
    def revision_trees(self, revids):
 
88
        return list(self.iter_revision_trees(revids))
 
89
 
 
90
    def add(self, tree):
 
91
        self._cache.add(tree.get_revision_id(), tree)
 
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 _tree_to_objects(tree, parent_trees, idmap, unusual_modes):
 
116
    """Iterate over the objects that were introduced in a revision.
 
117
 
 
118
    :param idmap: id map
 
119
    :param unusual_modes: Unusual file modes
 
120
    :return: Yields (path, object, ie) entries
 
121
    """
 
122
    new_trees = {}
 
123
    new_blobs = []
 
124
    shamap = {}
 
125
    try:
 
126
        base_tree = parent_trees[0]
 
127
        other_parent_trees = parent_trees[1:]
 
128
    except IndexError:
 
129
        base_tree = tree._repository.revision_tree(NULL_REVISION)
 
130
        other_parent_trees = []
 
131
    for (file_id, path, changed_content, versioned, parent, name, kind,
 
132
         executable) in tree.iter_changes(base_tree):
 
133
        if kind[1] == "file":
 
134
            ie = tree.inventory[file_id]
 
135
            if changed_content:
 
136
                # Content changed, find the correct revision id to use
 
137
                for ptree in other_parent_trees:
 
138
                    try:
 
139
                        pie = ptree.inventory[file_id]
 
140
                    except errors.NoSuchId:
 
141
                        pass
 
142
                    else:
 
143
                        if (pie.text_sha1 == ie.text_sha1 and 
 
144
                            pie.kind == ie.kind):
 
145
                            shamap[ie.file_id] = idmap.lookup_blob_id(
 
146
                                pie.file_id, pie.revision)
 
147
                            break
 
148
            if not file_id in shamap:
 
149
                new_blobs.append((path[1], ie))
 
150
            new_trees[urlutils.dirname(path[1])] = parent[1]
 
151
        elif kind[1] == "symlink":
 
152
            ie = tree.inventory[file_id]
 
153
            if changed_content:
 
154
                blob = symlink_to_blob(ie)
 
155
                for ptree in other_parent_trees:
 
156
                    try:
 
157
                        pie = ptree.inventory[file_id]
 
158
                    except errors.NoSuchId:
 
159
                        pass
 
160
                    else:
 
161
                        if (ie.kind == pie.kind and
 
162
                            ie.symlink_target == pie.symlink_target):
 
163
                            break
 
164
            else:
 
165
                yield path[1], blob, ie
 
166
            shamap[file_id] = blob.id
 
167
            new_trees[urlutils.dirname(path[1])] = parent[1]
 
168
        elif kind[1] not in (None, "directory"):
 
169
            raise AssertionError(kind[1])
 
170
        if path[0] is not None:
 
171
            new_trees[urlutils.dirname(path[0])] = parent[0]
 
172
    
 
173
    for (path, ie), chunks in tree.iter_files_bytes(
 
174
        [(ie.file_id, (path, ie)) for (path, ie) in new_blobs]):
 
175
        obj = Blob()
 
176
        obj.chunked = chunks
 
177
        yield path, obj, ie
 
178
        shamap[ie.file_id] = obj.id
 
179
 
 
180
    for fid in unusual_modes:
 
181
        new_trees[tree.id2path(fid)] = tree.inventory[fid].parent_id
 
182
    
 
183
    trees = {}
 
184
    while new_trees:
 
185
        items = new_trees.items()
 
186
        new_trees = {}
 
187
        for path, file_id in items:
 
188
            try:
 
189
                parent_id = tree.inventory[file_id].parent_id
 
190
            except errors.NoSuchId:
 
191
                # Directory was removed recursively perhaps ?
 
192
                continue
 
193
            if parent_id is not None:
 
194
                parent_path = urlutils.dirname(path)
 
195
                new_trees[parent_path] = parent_id
 
196
            trees[path] = file_id
 
197
 
 
198
    def ie_to_hexsha(ie):
 
199
        try:
 
200
            return shamap[ie.file_id]
 
201
        except KeyError:
 
202
            if ie.kind in ("file", "symlink"):
 
203
                return idmap.lookup_blob_id(ie.file_id, ie.revision)
 
204
            elif ie.kind == "directory":
 
205
                # Not all cache backends store the tree information, 
 
206
                # calculate again from scratch
 
207
                ret = directory_to_tree(ie, ie_to_hexsha, unusual_modes)
 
208
                if ret is None:
 
209
                    return ret
 
210
                return ret.id
 
211
            else:
 
212
                raise AssertionError
 
213
 
 
214
    for path in sorted(trees.keys(), reverse=True):
 
215
        ie = tree.inventory[trees[path]]
 
216
        assert ie.kind == "directory"
 
217
        obj = directory_to_tree(ie, ie_to_hexsha, unusual_modes)
 
218
        if obj is not None:
 
219
            yield path, obj, ie
 
220
            shamap[ie.file_id] = obj.id
 
221
 
 
222
 
 
223
class BazaarObjectStore(BaseObjectStore):
 
224
    """A Git-style object store backed onto a Bazaar repository."""
37
225
 
38
226
    def __init__(self, repository, mapping=None):
39
227
        self.repository = repository
40
228
        if mapping is None:
41
 
            self.mapping = self.repository.get_mapping()
 
229
            self.mapping = default_mapping
42
230
        else:
43
231
            self.mapping = mapping
44
 
        self._idmap = GitShaMap(self.repository._transport)
 
232
        self._cache = cache_from_repository(repository)
 
233
        self._content_cache_types = ("tree")
 
234
        self.start_write_group = self._cache.idmap.start_write_group
 
235
        self.abort_write_group = self._cache.idmap.abort_write_group
 
236
        self.commit_write_group = self._cache.idmap.commit_write_group
 
237
        self.tree_cache = LRUTreeCache(self.repository)
45
238
 
46
 
    def _update_sha_map(self):
47
 
        all_revids = self.repository.all_revision_ids()
 
239
    def _update_sha_map(self, stop_revision=None):
48
240
        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()
 
241
        if stop_revision is None:
 
242
            heads = graph.heads(self.repository.all_revision_ids())
 
243
        else:
 
244
            heads = set([stop_revision])
 
245
        missing_revids = self._cache.idmap.missing_revisions(heads)
 
246
        while heads:
 
247
            parents = graph.get_parent_map(heads)
 
248
            todo = set()
 
249
            for p in parents.values():
 
250
                todo.update([x for x in p if x not in missing_revids])
 
251
            heads = self._cache.idmap.missing_revisions(todo)
 
252
            missing_revids.update(heads)
 
253
        if NULL_REVISION in missing_revids:
 
254
            missing_revids.remove(NULL_REVISION)
 
255
        missing_revids = self.repository.has_revisions(missing_revids)
 
256
        if not missing_revids:
 
257
            return
 
258
        self.start_write_group()
 
259
        try:
 
260
            pb = ui.ui_factory.nested_progress_bar()
 
261
            try:
 
262
                for i, revid in enumerate(graph.iter_topo_order(missing_revids)):
 
263
                    trace.mutter('processing %r', revid)
 
264
                    pb.update("updating git map", i, len(missing_revids))
 
265
                    self._update_sha_map_revision(revid)
 
266
            finally:
 
267
                pb.finished()
 
268
        except:
 
269
            self.abort_write_group()
 
270
            raise
 
271
        else:
 
272
            self.commit_write_group()
 
273
 
 
274
    def __iter__(self):
 
275
        self._update_sha_map()
 
276
        return iter(self._cache.idmap.sha1s())
 
277
 
 
278
    def _reconstruct_commit(self, rev, tree_sha):
 
279
        def parent_lookup(revid):
 
280
            try:
 
281
                return self._lookup_revision_sha1(revid)
 
282
            except errors.NoSuchRevision:
 
283
                trace.warning("Ignoring ghost parent %s", revid)
 
284
                return None
 
285
        return self.mapping.export_commit(rev, tree_sha, parent_lookup)
 
286
 
 
287
    def _revision_to_objects(self, rev, tree):
 
288
        unusual_modes = extract_unusual_modes(rev)
 
289
        present_parents = self.repository.has_revisions(rev.parent_ids)
 
290
        parent_trees = self.tree_cache.revision_trees(
 
291
            [p for p in rev.parent_ids if p in present_parents])
 
292
        tree_sha = None
 
293
        for path, obj, ie in _tree_to_objects(tree, parent_trees,
 
294
                self._cache.idmap, unusual_modes):
 
295
            yield path, obj, ie
 
296
            if path == "":
 
297
                tree_sha = obj.id
 
298
        if tree_sha is None:
 
299
            # Pointless commit - get the tree sha elsewhere
 
300
            if not rev.parent_ids:
 
301
                tree_sha = Tree().id
 
302
            else:
 
303
                base_sha1 = self._lookup_revision_sha1(rev.parent_ids[0])
 
304
                tree_sha = self[base_sha1].tree
 
305
        commit_obj = self._reconstruct_commit(rev, tree_sha)
 
306
        try:
 
307
            foreign_revid, mapping = mapping_registry.parse_revision_id(
 
308
                rev.revision_id)
 
309
        except errors.InvalidRevisionId:
 
310
            pass
 
311
        else:
 
312
            _check_expected_sha(foreign_revid, commit_obj)
 
313
        yield None, commit_obj, None
 
314
 
 
315
    def _get_updater(self, rev):
 
316
        return self._cache.get_updater(rev)
59
317
 
60
318
    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))
69
 
            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")
77
 
        blob = Blob()
78
 
        blob._text = text
79
 
        return blob
80
 
 
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 
 
319
        rev = self.repository.get_revision(revid)
 
320
        tree = self.tree_cache.revision_tree(rev.revision_id)
 
321
        updater = self._get_updater(rev)
 
322
        for path, obj, ie in self._revision_to_objects(rev, tree):
 
323
            updater.add_object(obj, ie)
 
324
        commit_obj = updater.finish()
 
325
        return commit_obj.id
 
326
 
 
327
    def _reconstruct_blobs(self, keys):
 
328
        """Return a Git Blob object from a fileid and revision stored in bzr.
 
329
 
 
330
        :param fileid: File id of the text
 
331
        :param revision: Revision of the text
 
332
        """
 
333
        stream = self.repository.iter_files_bytes(
 
334
            ((key[0], key[1], key) for key in keys))
 
335
        for (fileid, revision, expected_sha), chunks in stream:
 
336
            blob = Blob()
 
337
            blob.chunked = chunks
 
338
            if blob.id != expected_sha:
 
339
                # Perhaps it's a symlink ?
 
340
                tree = self.tree_cache.revision_tree(revision)
 
341
                entry = tree.inventory[fileid]
 
342
                assert entry.kind == 'symlink'
 
343
                blob = symlink_to_blob(entry)
 
344
            _check_expected_sha(expected_sha, blob)
 
345
            yield blob
 
346
 
 
347
    def _reconstruct_tree(self, fileid, revid, inv, unusual_modes,
 
348
        expected_sha=None):
 
349
        """Return a Git Tree object from a file id and a revision stored in bzr.
 
350
 
 
351
        :param fileid: fileid in the tree.
 
352
        :param revision: Revision of the tree.
 
353
        """
 
354
        def get_ie_sha1(entry):
 
355
            if entry.kind == "directory":
 
356
                try:
 
357
                    return self._cache.idmap.lookup_tree_id(entry.file_id,
 
358
                        revid)
 
359
                except (NotImplementedError, KeyError):
 
360
                    obj = self._reconstruct_tree(entry.file_id, revid, inv,
 
361
                        unusual_modes)
 
362
                    if obj is None:
 
363
                        return None
 
364
                    else:
 
365
                        return obj.id
 
366
            elif entry.kind in ("file", "symlink"):
 
367
                return self._cache.idmap.lookup_blob_id(entry.file_id,
 
368
                    entry.revision)
 
369
            else:
 
370
                raise AssertionError("unknown entry kind '%s'" % entry.kind)
 
371
        tree = directory_to_tree(inv[fileid], get_ie_sha1, unusual_modes)
 
372
        _check_expected_sha(expected_sha, tree)
 
373
        return tree
 
374
 
 
375
    def get_parents(self, sha):
 
376
        """Retrieve the parents of a Git commit by SHA1.
 
377
 
 
378
        :param sha: SHA1 of the commit
 
379
        :raises: KeyError, NotCommitError
 
380
        """
 
381
        return self[sha].parents
 
382
 
 
383
    def _lookup_revision_sha1(self, revid):
 
384
        """Return the SHA1 matching a Bazaar revision."""
 
385
        if revid == NULL_REVISION:
 
386
            return "0" * 40
 
387
        try:
 
388
            return self._cache.idmap.lookup_commit(revid)
 
389
        except KeyError:
 
390
            try:
 
391
                return mapping_registry.parse_revision_id(revid)[0]
 
392
            except errors.InvalidRevisionId:
 
393
                self._update_sha_map(revid)
 
394
                return self._cache.idmap.lookup_commit(revid)
 
395
 
 
396
    def get_raw(self, sha):
 
397
        """Get the raw representation of a Git object by SHA1.
 
398
 
 
399
        :param sha: SHA1 of the git object
 
400
        """
 
401
        obj = self[sha]
 
402
        return (obj.type, obj.as_raw_string())
 
403
 
 
404
    def __contains__(self, sha):
 
405
        # See if sha is in map
 
406
        try:
 
407
            (type, type_data) = self._lookup_git_sha(sha)
 
408
            if type == "commit":
 
409
                return self.repository.has_revision(type_data[0])
 
410
            elif type == "blob":
 
411
                return self.repository.texts.has_version(type_data)
 
412
            elif type == "tree":
 
413
                return self.repository.has_revision(type_data[1])
 
414
            else:
 
415
                raise AssertionError("Unknown object type '%s'" % type)
 
416
        except KeyError:
 
417
            return False
 
418
 
 
419
    def _lookup_git_sha(self, sha):
 
420
        # See if sha is in map
 
421
        try:
 
422
            return self._cache.idmap.lookup_git_sha(sha)
 
423
        except KeyError:
 
424
            # if not, see if there are any unconverted revisions and add them
94
425
            # to the map, search for sha in map again
95
426
            self._update_sha_map()
96
 
            (type, type_data) = self._idmap.lookup_git_sha(sha)
 
427
            return self._cache.idmap.lookup_git_sha(sha)
 
428
 
 
429
    def __getitem__(self, sha):
 
430
        if self._cache.content_cache is not None:
 
431
            try:
 
432
                return self._cache.content_cache[sha]
 
433
            except KeyError:
 
434
                pass
 
435
        (type, type_data) = self._lookup_git_sha(sha)
97
436
        # convert object to git object
98
437
        if type == "commit":
99
 
            return self._get_commit(*type_data)
 
438
            (revid, tree_sha) = type_data
 
439
            try:
 
440
                rev = self.repository.get_revision(revid)
 
441
            except errors.NoSuchRevision:
 
442
                trace.mutter('entry for %s %s in shamap: %r, but not found in '
 
443
                             'repository', type, sha, type_data)
 
444
                raise KeyError(sha)
 
445
            commit = self._reconstruct_commit(rev, tree_sha)
 
446
            _check_expected_sha(sha, commit)
 
447
            return commit
100
448
        elif type == "blob":
101
 
            return self._get_blob(*type_data)
 
449
            (fileid, revision) = type_data
 
450
            return self._reconstruct_blobs([(fileid, revision, sha)]).next()
102
451
        elif type == "tree":
103
 
            return self._get_tree(*type_data)
 
452
            (fileid, revid) = type_data
 
453
            try:
 
454
                tree = self.tree_cache.revision_tree(revid)
 
455
                rev = self.repository.get_revision(revid)
 
456
            except errors.NoSuchRevision:
 
457
                trace.mutter('entry for %s %s in shamap: %r, but not found in repository', type, sha, type_data)
 
458
                raise KeyError(sha)
 
459
            unusual_modes = extract_unusual_modes(rev)
 
460
            try:
 
461
                return self._reconstruct_tree(fileid, revid, tree.inventory,
 
462
                    unusual_modes, expected_sha=sha)
 
463
            except errors.NoSuchRevision:
 
464
                raise KeyError(sha)
104
465
        else:
105
466
            raise AssertionError("Unknown object type '%s'" % type)
 
467
 
 
468
    def generate_pack_contents(self, have, want):
 
469
        """Iterate over the contents of a pack file.
 
470
 
 
471
        :param have: List of SHA1s of objects that should not be sent
 
472
        :param want: List of SHA1s of objects that should be sent
 
473
        """
 
474
        processed = set()
 
475
        for commit_sha in have:
 
476
            try:
 
477
                (type, (revid, tree_sha)) = self._lookup_git_sha(commit_sha)
 
478
            except KeyError:
 
479
                pass
 
480
            else:
 
481
                assert type == "commit"
 
482
                processed.add(revid)
 
483
        pending = set()
 
484
        for commit_sha in want:
 
485
            if commit_sha in have:
 
486
                continue
 
487
            (type, (revid, tree_sha)) = self._lookup_git_sha(commit_sha)
 
488
            assert type == "commit"
 
489
            pending.add(revid)
 
490
        todo = set()
 
491
        while pending:
 
492
            processed.update(pending)
 
493
            next_map = self.repository.get_parent_map(pending)
 
494
            next_pending = set()
 
495
            for item in next_map.iteritems():
 
496
                todo.add(item[0])
 
497
                next_pending.update(p for p in item[1] if p not in processed)
 
498
            pending = next_pending
 
499
        if NULL_REVISION in todo:
 
500
            todo.remove(NULL_REVISION)
 
501
        trace.mutter('sending revisions %r', todo)
 
502
        ret = []
 
503
        pb = ui.ui_factory.nested_progress_bar()
 
504
        try:
 
505
            for i, revid in enumerate(todo):
 
506
                pb.update("generating git objects", i, len(todo))
 
507
                rev = self.repository.get_revision(revid)
 
508
                tree = self.tree_cache.revision_tree(revid)
 
509
                for path, obj, ie in self._revision_to_objects(rev, tree):
 
510
                    ret.append((obj, path))
 
511
        finally:
 
512
            pb.finished()
 
513
        return ret