/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

Update to newer version of Dulwich, saner branch names.

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
                for (pinv, pinvshamap) in zip(parent_invs, parent_invshamaps):
 
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] = pinvshamap.lookup_blob(
 
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
 
159
                new_trees[urlutils.dirname(path)] = ie.parent_id
 
160
            shamap[ie.file_id] = blob.id
 
161
        elif ie.kind == "directory":
 
162
            for (pinv, pinvshamap) in zip(parent_invs, parent_invshamaps):
 
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] = pinvshamap.lookup_tree(
 
172
                                ie.file_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, fid), chunks in iter_files_bytes(
 
183
        [(ie.file_id, ie.revision, (path, ie.file_id))
 
184
            for (path, ie) in new_blobs]):
 
185
        obj = Blob()
 
186
        obj.data = "".join(chunks)
 
187
        yield path, obj
 
188
        shamap[fid] = 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
 
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._idmap = idmap_from_repository(repository)
 
234
        self.start_write_group = self._idmap.start_write_group
 
235
        self.abort_write_group = self._idmap.abort_write_group
 
236
        self.commit_write_group = self._idmap.commit_write_group
 
237
        self.parent_invs_cache = LRUInventoryCache(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._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._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._idmap.sha1s())
 
277
 
 
278
    def _revision_to_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, inv):
 
288
        unusual_modes = extract_unusual_modes(rev)
 
289
        present_parents = self.repository.has_revisions(rev.parent_ids)
 
290
        has_ghost_parents = (len(rev.parent_ids) < len(present_parents))
 
291
        parent_invs = self.parent_invs_cache.get_inventories(
 
292
            [p for p in rev.parent_ids if p in present_parents])
 
293
        parent_invshamaps = [self._idmap.get_inventory_sha_map(r) for r in rev.parent_ids if r in present_parents]
 
294
        tree_sha = None
 
295
        for path, obj in _inventory_to_objects(inv, parent_invs,
 
296
                parent_invshamaps, unusual_modes,
 
297
                self.repository.iter_files_bytes, has_ghost_parents):
 
298
            yield path, obj
 
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
                tree_sha = parent_invshamaps[0][inv.root.file_id]
 
306
        commit_obj = self._revision_to_commit(rev, tree_sha)
 
307
        try:
 
308
            foreign_revid, mapping = mapping_registry.parse_revision_id(rev.revision_id)
 
309
        except errors.InvalidRevisionId:
 
310
            pass
 
311
        else:
 
312
            _check_expected_sha(foreign_revid, commit_obj)
 
313
        yield None, commit_obj
59
314
 
60
315
    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))
 
316
        rev = self.repository.get_revision(revid)
 
317
        inv = self.parent_invs_cache.get_inventory(rev.revision_id)
 
318
        commit_obj = None
 
319
        entries = []
 
320
        for path, obj in self._revision_to_objects(rev, inv):
 
321
            if obj.type_name == "commit":
 
322
                commit_obj = obj
 
323
            elif obj.type_name in ("blob", "tree"):
 
324
                file_id = inv.path2id(path)
 
325
                ie = inv[file_id]
 
326
                if obj.type_name == "blob":
 
327
                    revision = ie.revision
 
328
                else:
 
329
                    revision = revid
 
330
                entries.append((file_id, obj.type_name, obj.id, revision))
69
331
            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")
 
332
                raise AssertionError
 
333
        self._idmap.add_entries(revid, rev.parent_ids, commit_obj.id, 
 
334
            commit_obj.tree, entries)
 
335
        return commit_obj.id
 
336
 
 
337
    def _get_blob(self, fileid, revision, expected_sha):
 
338
        """Return a Git Blob object from a fileid and revision stored in bzr.
 
339
 
 
340
        :param fileid: File id of the text
 
341
        :param revision: Revision of the text
 
342
        """
77
343
        blob = Blob()
78
 
        blob._text = text
 
344
        chunks = self.repository.iter_files_bytes([(fileid, revision, None)]).next()[1]
 
345
        blob.data = "".join(chunks)
 
346
        if blob.id != expected_sha:
 
347
            # Perhaps it's a symlink ?
 
348
            inv = self.parent_invs_cache.get_inventory(revision)
 
349
            entry = inv[fileid]
 
350
            assert entry.kind == 'symlink'
 
351
            blob = symlink_to_blob(entry)
 
352
        _check_expected_sha(expected_sha, blob)
79
353
        return blob
80
354
 
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 
 
355
    def _get_tree(self, fileid, revid, inv, unusual_modes, expected_sha=None):
 
356
        """Return a Git Tree object from a file id and a revision stored in bzr.
 
357
 
 
358
        :param fileid: fileid in the tree.
 
359
        :param revision: Revision of the tree.
 
360
        """
 
361
        invshamap = self._idmap.get_inventory_sha_map(inv.revision_id)
 
362
        def get_ie_sha1(entry):
 
363
            if entry.kind == "directory":
 
364
                try:
 
365
                    return invshamap.lookup_tree(entry.file_id)
 
366
                except (NotImplementedError, KeyError):
 
367
                    obj = self._get_tree(entry.file_id, revid, inv,
 
368
                        unusual_modes)
 
369
                    if obj is None:
 
370
                        return None
 
371
                    else:
 
372
                        return obj.id
 
373
            elif entry.kind in ("file", "symlink"):
 
374
                return invshamap.lookup_blob(entry.file_id, entry.revision)
 
375
            else:
 
376
                raise AssertionError("unknown entry kind '%s'" % entry.kind)
 
377
        tree = directory_to_tree(inv[fileid], get_ie_sha1, unusual_modes)
 
378
        _check_expected_sha(expected_sha, tree)
 
379
        return tree
 
380
 
 
381
    def get_parents(self, sha):
 
382
        """Retrieve the parents of a Git commit by SHA1.
 
383
 
 
384
        :param sha: SHA1 of the commit
 
385
        :raises: KeyError, NotCommitError
 
386
        """
 
387
        return self[sha].parents
 
388
 
 
389
    def _lookup_revision_sha1(self, revid):
 
390
        """Return the SHA1 matching a Bazaar revision."""
 
391
        if revid == NULL_REVISION:
 
392
            return "0" * 40
 
393
        try:
 
394
            return self._idmap.lookup_commit(revid)
 
395
        except KeyError:
 
396
            try:
 
397
                return mapping_registry.parse_revision_id(revid)[0]
 
398
            except errors.InvalidRevisionId:
 
399
                self._update_sha_map(revid)
 
400
                return self._idmap.lookup_commit(revid)
 
401
 
 
402
    def get_raw(self, sha):
 
403
        """Get the raw representation of a Git object by SHA1.
 
404
 
 
405
        :param sha: SHA1 of the git object
 
406
        """
 
407
        obj = self[sha]
 
408
        return (obj.type, obj.as_raw_string())
 
409
 
 
410
    def __contains__(self, sha):
 
411
        # See if sha is in map
 
412
        try:
 
413
            (type, type_data) = self._lookup_git_sha(sha)
 
414
            if type == "commit":
 
415
                return self.repository.has_revision(type_data[0])
 
416
            elif type == "blob":
 
417
                return self.repository.texts.has_version(type_data)
 
418
            elif type == "tree":
 
419
                return self.repository.has_revision(type_data[1])
 
420
            else:
 
421
                raise AssertionError("Unknown object type '%s'" % type)
 
422
        except KeyError:
 
423
            return False
 
424
 
 
425
    def _lookup_git_sha(self, sha):
 
426
        # See if sha is in map
 
427
        try:
 
428
            return self._idmap.lookup_git_sha(sha)
 
429
        except KeyError:
 
430
            # if not, see if there are any unconverted revisions and add them
94
431
            # to the map, search for sha in map again
95
432
            self._update_sha_map()
96
 
            (type, type_data) = self._idmap.lookup_git_sha(sha)
 
433
            return self._idmap.lookup_git_sha(sha)
 
434
 
 
435
    def __getitem__(self, sha):
 
436
        (type, type_data) = self._lookup_git_sha(sha)
97
437
        # convert object to git object
98
438
        if type == "commit":
99
 
            return self._get_commit(*type_data)
 
439
            (revid, tree_sha) = type_data
 
440
            try:
 
441
                rev = self.repository.get_revision(revid)
 
442
            except errors.NoSuchRevision:
 
443
                trace.mutter('entry for %s %s in shamap: %r, but not found in repository', type, sha, type_data)
 
444
                raise KeyError(sha)
 
445
            commit = self._revision_to_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._get_blob(fileid, revision, expected_sha=sha)
102
451
        elif type == "tree":
103
 
            return self._get_tree(*type_data)
 
452
            (fileid, revid) = type_data
 
453
            try:
 
454
                inv = self.parent_invs_cache.get_inventory(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._get_tree(fileid, revid, inv,
 
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
                inv = self.parent_invs_cache.get_inventory(revid)
 
509
                for path, obj in self._revision_to_objects(rev, inv):
 
510
                    ret.append((obj, path))
 
511
        finally:
 
512
            pb.finished()
 
513
        return ret