/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

Default to non-bare repositories when initializing a control directory.

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