/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

merge server fixes

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