/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

Avoid invoking git directly.

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.cache import (
 
47
    from_repository as cache_from_repository,
 
48
    )
 
49
 
 
50
import posixpath
 
51
import stat
 
52
 
 
53
 
 
54
def get_object_store(repo, mapping=None):
 
55
    git = getattr(repo, "_git", None)
 
56
    if git is not None:
 
57
        return git.object_store
 
58
    return BazaarObjectStore(repo, mapping)
 
59
 
 
60
 
 
61
MAX_TREE_CACHE_SIZE = 50 * 1024 * 1024
 
62
 
 
63
 
 
64
class LRUTreeCache(object):
 
65
 
 
66
    def __init__(self, repository):
 
67
        def approx_tree_size(tree):
 
68
            # Very rough estimate, 1k per inventory entry
 
69
            return len(tree.inventory) * 1024
 
70
        self.repository = repository
 
71
        self._cache = lru_cache.LRUSizeCache(max_size=MAX_TREE_CACHE_SIZE,
 
72
            after_cleanup_size=None, compute_size=approx_tree_size)
 
73
 
 
74
    def revision_tree(self, revid):
 
75
        try:
 
76
            tree = self._cache[revid]
 
77
        except KeyError:
 
78
            tree = self.repository.revision_tree(revid)
 
79
            self.add(tree)
 
80
        assert tree.get_revision_id() == tree.inventory.revision_id
 
81
        return tree
 
82
 
 
83
    def iter_revision_trees(self, revids):
 
84
        trees = {}
 
85
        todo = []
 
86
        for revid in revids:
 
87
            try:
 
88
                tree = self._cache[revid]
 
89
            except KeyError:
 
90
                todo.append(revid)
 
91
            else:
 
92
                assert tree.get_revision_id() == revid
 
93
                assert tree.inventory.revision_id == revid
 
94
                trees[revid] = tree
 
95
        for tree in self.repository.revision_trees(todo):
 
96
            trees[tree.get_revision_id()] = tree
 
97
            self.add(tree)
 
98
        return (trees[r] for r in revids)
 
99
 
 
100
    def revision_trees(self, revids):
 
101
        return list(self.iter_revision_trees(revids))
 
102
 
 
103
    def add(self, tree):
 
104
        self._cache.add(tree.get_revision_id(), tree)
 
105
 
 
106
 
 
107
def _find_missing_bzr_revids(get_parent_map, want, have):
 
108
    """Find the revisions that have to be pushed.
 
109
 
 
110
    :param get_parent_map: Function that returns the parents for a sequence
 
111
        of revisions.
 
112
    :param want: Revisions the target wants
 
113
    :param have: Revisions the target already has
 
114
    :return: Set of revisions to fetch
 
115
    """
 
116
    pending = want - have
 
117
    processed = set()
 
118
    todo = set()
 
119
    while pending:
 
120
        processed.update(pending)
 
121
        next_map = get_parent_map(pending)
 
122
        next_pending = set()
 
123
        for item in next_map.iteritems():
 
124
            if item[0] in have:
 
125
                continue
 
126
            todo.add(item[0])
 
127
            next_pending.update(p for p in item[1] if p not in processed)
 
128
        pending = next_pending
 
129
    if NULL_REVISION in todo:
 
130
        todo.remove(NULL_REVISION)
 
131
    return todo
 
132
 
 
133
 
 
134
def _check_expected_sha(expected_sha, object):
 
135
    """Check whether an object matches an expected SHA.
 
136
 
 
137
    :param expected_sha: None or expected SHA as either binary or as hex digest
 
138
    :param object: Object to verify
 
139
    """
 
140
    if expected_sha is None:
 
141
        return
 
142
    if len(expected_sha) == 40:
 
143
        if expected_sha != object.sha().hexdigest():
 
144
            raise AssertionError("Invalid sha for %r: %s" % (object,
 
145
                expected_sha))
 
146
    elif len(expected_sha) == 20:
 
147
        if expected_sha != object.sha().digest():
 
148
            raise AssertionError("Invalid sha for %r: %s" % (object,
 
149
                sha_to_hex(expected_sha)))
 
150
    else:
 
151
        raise AssertionError("Unknown length %d for %r" % (len(expected_sha),
 
152
            expected_sha))
 
153
 
 
154
 
 
155
def _tree_to_objects(tree, parent_trees, idmap, unusual_modes,
 
156
                     dummy_file_name=None):
 
157
    """Iterate over the objects that were introduced in a revision.
 
158
 
 
159
    :param idmap: id map
 
160
    :param parent_trees: Parent revision trees
 
161
    :param unusual_modes: Unusual file modes dictionary
 
162
    :param dummy_file_name: File name to use for dummy files
 
163
        in empty directories. None to skip empty directories
 
164
    :return: Yields (path, object, ie) entries
 
165
    """
 
166
    new_trees = {}
 
167
    new_blobs = []
 
168
    shamap = {}
 
169
    try:
 
170
        base_tree = parent_trees[0]
 
171
        other_parent_trees = parent_trees[1:]
 
172
    except IndexError:
 
173
        base_tree = tree._repository.revision_tree(NULL_REVISION)
 
174
        other_parent_trees = []
 
175
    def find_unchanged_parent_ie(ie, parent_trees):
 
176
        assert ie.kind in ("symlink", "file")
 
177
        for ptree in parent_trees:
 
178
            try:
 
179
                pie = ptree.inventory[ie.file_id]
 
180
            except errors.NoSuchId:
 
181
                pass
 
182
            else:
 
183
                if (pie.text_sha1 == ie.text_sha1 and 
 
184
                    pie.kind == ie.kind and
 
185
                    pie.symlink_target == ie.symlink_target):
 
186
                    return pie
 
187
        raise KeyError
 
188
 
 
189
    # Find all the changed blobs
 
190
    for (file_id, path, changed_content, versioned, parent, name, kind,
 
191
         executable) in tree.iter_changes(base_tree):
 
192
        if kind[1] == "file":
 
193
            ie = tree.inventory[file_id]
 
194
            if changed_content:
 
195
                try:
 
196
                    pie = find_unchanged_parent_ie(ie, other_parent_trees)
 
197
                except KeyError:
 
198
                    pass
 
199
                else:
 
200
                    try:
 
201
                        shamap[ie.file_id] = idmap.lookup_blob_id(
 
202
                            pie.file_id, pie.revision)
 
203
                    except KeyError:
 
204
                        # no-change merge ?
 
205
                        blob = Blob()
 
206
                        blob.data = tree.get_file_text(ie.file_id)
 
207
                        shamap[ie.file_id] = blob.id
 
208
            if not file_id in shamap:
 
209
                new_blobs.append((path[1], ie))
 
210
            new_trees[posixpath.dirname(path[1])] = parent[1]
 
211
        elif kind[1] == "symlink":
 
212
            ie = tree.inventory[file_id]
 
213
            if changed_content:
 
214
                blob = symlink_to_blob(ie)
 
215
                shamap[file_id] = blob.id
 
216
                try:
 
217
                    find_unchanged_parent_ie(ie, other_parent_trees)
 
218
                except KeyError:
 
219
                    yield path[1], blob, ie
 
220
            new_trees[posixpath.dirname(path[1])] = parent[1]
 
221
        elif kind[1] not in (None, "directory"):
 
222
            raise AssertionError(kind[1])
 
223
        if path[0] is not None and parent[0] in tree.inventory and tree.inventory[parent[0]].kind == "directory":
 
224
            # Removal
 
225
            new_trees[posixpath.dirname(path[0])] = parent[0]
 
226
    
 
227
    # Fetch contents of the blobs that were changed
 
228
    for (path, ie), chunks in tree.iter_files_bytes(
 
229
        [(ie.file_id, (path, ie)) for (path, ie) in new_blobs]):
 
230
        obj = Blob()
 
231
        obj.chunked = chunks
 
232
        yield path, obj, ie
 
233
        shamap[ie.file_id] = obj.id
 
234
 
 
235
    for path in unusual_modes:
 
236
        parent_path = posixpath.dirname(path)
 
237
        new_trees[parent_path] = tree.path2id(parent_path)
 
238
 
 
239
    trees = {}
 
240
    while new_trees:
 
241
        items = new_trees.items()
 
242
        new_trees = {}
 
243
        for path, file_id in items:
 
244
            parent_id = tree.inventory[file_id].parent_id
 
245
            if parent_id is not None:
 
246
                parent_path = urlutils.dirname(path)
 
247
                new_trees[parent_path] = parent_id
 
248
            trees[path] = file_id
 
249
 
 
250
    def ie_to_hexsha(ie):
 
251
        try:
 
252
            return shamap[ie.file_id]
 
253
        except KeyError:
 
254
            # FIXME: Should be the same as in parent
 
255
            if ie.kind in ("file", "symlink"):
 
256
                try:
 
257
                    return idmap.lookup_blob_id(ie.file_id, ie.revision)
 
258
                except KeyError:
 
259
                    # no-change merge ?
 
260
                    blob = Blob()
 
261
                    blob.data = tree.get_file_text(ie.file_id)
 
262
                    return blob.id
 
263
            elif ie.kind == "directory":
 
264
                # Not all cache backends store the tree information, 
 
265
                # calculate again from scratch
 
266
                ret = directory_to_tree(ie, ie_to_hexsha, unusual_modes,
 
267
                    dummy_file_name)
 
268
                if ret is None:
 
269
                    return ret
 
270
                return ret.id
 
271
            else:
 
272
                raise AssertionError
 
273
 
 
274
    for path in sorted(trees.keys(), reverse=True):
 
275
        ie = tree.inventory[trees[path]]
 
276
        assert ie.kind == "directory"
 
277
        obj = directory_to_tree(ie, ie_to_hexsha, unusual_modes,
 
278
            dummy_file_name)
 
279
        if obj is not None:
 
280
            yield path, obj, ie
 
281
            shamap[ie.file_id] = obj.id
 
282
 
 
283
 
 
284
class BazaarObjectStore(BaseObjectStore):
 
285
    """A Git-style object store backed onto a Bazaar repository."""
37
286
 
38
287
    def __init__(self, repository, mapping=None):
39
288
        self.repository = repository
40
289
        if mapping is None:
41
 
            self.mapping = self.repository.get_mapping()
 
290
            self.mapping = default_mapping
42
291
        else:
43
292
            self.mapping = mapping
44
 
        self._idmap = GitShaMap(self.repository._transport)
 
293
        self._cache = cache_from_repository(repository)
 
294
        self._content_cache_types = ("tree")
 
295
        self.start_write_group = self._cache.idmap.start_write_group
 
296
        self.abort_write_group = self._cache.idmap.abort_write_group
 
297
        self.commit_write_group = self._cache.idmap.commit_write_group
 
298
        self.tree_cache = LRUTreeCache(self.repository)
45
299
 
46
 
    def _update_sha_map(self):
47
 
        all_revids = self.repository.all_revision_ids()
 
300
    def _update_sha_map(self, stop_revision=None):
48
301
        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()
 
302
        if stop_revision is None:
 
303
            heads = graph.heads(self.repository.all_revision_ids())
 
304
        else:
 
305
            heads = set([stop_revision])
 
306
        missing_revids = self._cache.idmap.missing_revisions(heads)
 
307
        while heads:
 
308
            parents = graph.get_parent_map(heads)
 
309
            todo = set()
 
310
            for p in parents.values():
 
311
                todo.update([x for x in p if x not in missing_revids])
 
312
            heads = self._cache.idmap.missing_revisions(todo)
 
313
            missing_revids.update(heads)
 
314
        if NULL_REVISION in missing_revids:
 
315
            missing_revids.remove(NULL_REVISION)
 
316
        missing_revids = self.repository.has_revisions(missing_revids)
 
317
        if not missing_revids:
 
318
            return
 
319
        self.start_write_group()
 
320
        try:
 
321
            pb = ui.ui_factory.nested_progress_bar()
 
322
            try:
 
323
                for i, revid in enumerate(graph.iter_topo_order(missing_revids)):
 
324
                    trace.mutter('processing %r', revid)
 
325
                    pb.update("updating git map", i, len(missing_revids))
 
326
                    self._update_sha_map_revision(revid)
 
327
            finally:
 
328
                pb.finished()
 
329
        except:
 
330
            self.abort_write_group()
 
331
            raise
 
332
        else:
 
333
            self.commit_write_group()
 
334
 
 
335
    def __iter__(self):
 
336
        self._update_sha_map()
 
337
        return iter(self._cache.idmap.sha1s())
 
338
 
 
339
    def _reconstruct_commit(self, rev, tree_sha, roundtrip):
 
340
        def parent_lookup(revid):
 
341
            try:
 
342
                return self._lookup_revision_sha1(revid)
 
343
            except errors.NoSuchRevision:
 
344
                return None
 
345
        return self.mapping.export_commit(rev, tree_sha, parent_lookup,
 
346
            roundtrip)
 
347
 
 
348
    def _create_fileid_map_blob(self, inv):
 
349
        # FIXME: This can probably be a lot more efficient, 
 
350
        # not all files necessarily have to be processed.
 
351
        file_ids = {}
 
352
        for (path, ie) in inv.iter_entries():
 
353
            if self.mapping.generate_file_id(path) != ie.file_id:
 
354
                file_ids[path] = ie.file_id
 
355
        return self.mapping.export_fileid_map(file_ids)
 
356
 
 
357
    def _revision_to_objects(self, rev, tree, roundtrip):
 
358
        """Convert a revision to a set of git objects.
 
359
 
 
360
        :param rev: Bazaar revision object
 
361
        :param tree: Bazaar revision tree
 
362
        :param roundtrip: Whether to roundtrip all Bazaar revision data
 
363
        """
 
364
        unusual_modes = extract_unusual_modes(rev)
 
365
        present_parents = self.repository.has_revisions(rev.parent_ids)
 
366
        parent_trees = self.tree_cache.revision_trees(
 
367
            [p for p in rev.parent_ids if p in present_parents])
 
368
        root_tree = None
 
369
        for path, obj, ie in _tree_to_objects(tree, parent_trees,
 
370
                self._cache.idmap, unusual_modes, self.mapping.BZR_DUMMY_FILE):
 
371
            if path == "":
 
372
                root_tree = obj
 
373
                root_ie = ie
 
374
                # Don't yield just yet
 
375
            else:
 
376
                yield path, obj, ie
 
377
        if root_tree is None:
 
378
            # Pointless commit - get the tree sha elsewhere
 
379
            if not rev.parent_ids:
 
380
                root_tree = Tree()
 
381
            else:
 
382
                base_sha1 = self._lookup_revision_sha1(rev.parent_ids[0])
 
383
                root_tree = self[self[base_sha1].tree]
 
384
            root_ie = tree.inventory.root
 
385
        if roundtrip and self.mapping.BZR_FILE_IDS_FILE is not None:
 
386
            b = self._create_fileid_map_blob(tree.inventory)
 
387
            if b is not None:
 
388
                root_tree[self.mapping.BZR_FILE_IDS_FILE] = ((stat.S_IFREG | 0644), b.id)
 
389
                yield self.mapping.BZR_FILE_IDS_FILE, b, None
 
390
        yield "", root_tree, root_ie
 
391
        commit_obj = self._reconstruct_commit(rev, root_tree.id,
 
392
            roundtrip=roundtrip)
 
393
        try:
 
394
            foreign_revid, mapping = mapping_registry.parse_revision_id(
 
395
                rev.revision_id)
 
396
        except errors.InvalidRevisionId:
 
397
            pass
 
398
        else:
 
399
            _check_expected_sha(foreign_revid, commit_obj)
 
400
        yield None, commit_obj, None
 
401
 
 
402
    def _get_updater(self, rev):
 
403
        return self._cache.get_updater(rev)
59
404
 
60
405
    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)
 
406
        rev = self.repository.get_revision(revid)
 
407
        tree = self.tree_cache.revision_tree(rev.revision_id)
 
408
        updater = self._get_updater(rev)
 
409
        for path, obj, ie in self._revision_to_objects(rev, tree,
 
410
            roundtrip=True):
 
411
            updater.add_object(obj, ie, path)
 
412
        commit_obj = updater.finish()
 
413
        return commit_obj.id
 
414
 
 
415
    def _reconstruct_blobs(self, keys):
 
416
        """Return a Git Blob object from a fileid and revision stored in bzr.
 
417
 
 
418
        :param fileid: File id of the text
 
419
        :param revision: Revision of the text
 
420
        """
 
421
        stream = self.repository.iter_files_bytes(
 
422
            ((key[0], key[1], key) for key in keys))
 
423
        for (fileid, revision, expected_sha), chunks in stream:
 
424
            blob = Blob()
 
425
            blob.chunked = chunks
 
426
            if blob.id != expected_sha and blob.data == "":
 
427
                # Perhaps it's a symlink ?
 
428
                tree = self.tree_cache.revision_tree(revision)
 
429
                entry = tree.inventory[fileid]
 
430
                if entry.kind == 'symlink':
 
431
                    blob = symlink_to_blob(entry)
 
432
            _check_expected_sha(expected_sha, blob)
 
433
            yield blob
 
434
 
 
435
    def _reconstruct_tree(self, fileid, revid, inv, unusual_modes,
 
436
        expected_sha=None):
 
437
        """Return a Git Tree object from a file id and a revision stored in bzr.
 
438
 
 
439
        :param fileid: fileid in the tree.
 
440
        :param revision: Revision of the tree.
 
441
        """
 
442
        def get_ie_sha1(entry):
 
443
            if entry.kind == "directory":
 
444
                try:
 
445
                    return self._cache.idmap.lookup_tree_id(entry.file_id,
 
446
                        revid)
 
447
                except (NotImplementedError, KeyError):
 
448
                    obj = self._reconstruct_tree(entry.file_id, revid, inv,
 
449
                        unusual_modes)
 
450
                    if obj is None:
 
451
                        return None
 
452
                    else:
 
453
                        return obj.id
 
454
            elif entry.kind in ("file", "symlink"):
 
455
                try:
 
456
                    return self._cache.idmap.lookup_blob_id(entry.file_id,
 
457
                        entry.revision)
 
458
                except KeyError:
 
459
                    # no-change merge?
 
460
                    return self._reconstruct_blobs(
 
461
                        [(entry.file_id, entry.revision, None)]).next().id
 
462
            else:
 
463
                raise AssertionError("unknown entry kind '%s'" % entry.kind)
 
464
        tree = directory_to_tree(inv[fileid], get_ie_sha1, unusual_modes,
 
465
            self.mapping.BZR_DUMMY_FILE)
 
466
        if (inv.root.file_id == fileid and
 
467
            self.mapping.BZR_FILE_IDS_FILE is not None):
 
468
            b = self._create_fileid_map_blob(inv)
 
469
            # If this is the root tree, add the file ids
 
470
            tree[self.mapping.BZR_FILE_IDS_FILE] = ((stat.S_IFREG | 0644), b.id)
 
471
        _check_expected_sha(expected_sha, tree)
 
472
        return tree
 
473
 
 
474
    def get_parents(self, sha):
 
475
        """Retrieve the parents of a Git commit by SHA1.
 
476
 
 
477
        :param sha: SHA1 of the commit
 
478
        :raises: KeyError, NotCommitError
 
479
        """
 
480
        return self[sha].parents
 
481
 
 
482
    def _lookup_revision_sha1(self, revid):
 
483
        """Return the SHA1 matching a Bazaar revision."""
 
484
        from dulwich.protocol import ZERO_SHA
 
485
        if revid == NULL_REVISION:
 
486
            return ZERO_SHA
 
487
        try:
 
488
            return self._cache.idmap.lookup_commit(revid)
 
489
        except KeyError:
 
490
            try:
 
491
                return mapping_registry.parse_revision_id(revid)[0]
 
492
            except errors.InvalidRevisionId:
 
493
                self.repository.lock_read()
 
494
                try:
 
495
                    self._update_sha_map(revid)
 
496
                finally:
 
497
                    self.repository.unlock()
 
498
                return self._cache.idmap.lookup_commit(revid)
 
499
 
 
500
    def get_raw(self, sha):
 
501
        """Get the raw representation of a Git object by SHA1.
 
502
 
 
503
        :param sha: SHA1 of the git object
 
504
        """
 
505
        obj = self[sha]
 
506
        return (obj.type, obj.as_raw_string())
 
507
 
 
508
    def __contains__(self, sha):
 
509
        # See if sha is in map
 
510
        try:
 
511
            (type, type_data) = self.lookup_git_sha(sha)
 
512
            if type == "commit":
 
513
                return self.repository.has_revision(type_data[0])
 
514
            elif type == "blob":
 
515
                return self.repository.texts.has_key(type_data)
 
516
            elif type == "tree":
 
517
                return self.repository.has_revision(type_data[1])
 
518
            else:
 
519
                raise AssertionError("Unknown object type '%s'" % type)
 
520
        except KeyError:
 
521
            return False
 
522
 
 
523
    def lookup_git_shas(self, shas, update_map=True):
 
524
        from dulwich.protocol import ZERO_SHA
 
525
        ret = {}
 
526
        for sha in shas:
 
527
            if sha == ZERO_SHA:
 
528
                ret[sha] = ("commit", (NULL_REVISION, None))
 
529
                continue
 
530
            try:
 
531
                ret[sha] = self._cache.idmap.lookup_git_sha(sha)
 
532
            except KeyError:
 
533
                if update_map:
 
534
                    # if not, see if there are any unconverted revisions and add
 
535
                    # them to the map, search for sha in map again
 
536
                    self._update_sha_map()
 
537
                    update_map = False
 
538
                    try:
 
539
                        ret[sha] = self._cache.idmap.lookup_git_sha(sha)
 
540
                    except KeyError:
 
541
                        pass
 
542
        return ret
 
543
 
 
544
    def lookup_git_sha(self, sha, update_map=True):
 
545
        return self.lookup_git_shas([sha], update_map=update_map)[sha]
87
546
 
88
547
    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 
94
 
            # to the map, search for sha in map again
95
 
            self._update_sha_map()
96
 
            (type, type_data) = self._idmap.lookup_git_sha(sha)
 
548
        if self._cache.content_cache is not None:
 
549
            try:
 
550
                return self._cache.content_cache[sha]
 
551
            except KeyError:
 
552
                pass
 
553
        (type, type_data) = self.lookup_git_sha(sha)
97
554
        # convert object to git object
98
555
        if type == "commit":
99
 
            return self._get_commit(*type_data)
 
556
            (revid, tree_sha) = type_data
 
557
            try:
 
558
                rev = self.repository.get_revision(revid)
 
559
            except errors.NoSuchRevision:
 
560
                trace.mutter('entry for %s %s in shamap: %r, but not found in '
 
561
                             'repository', type, sha, type_data)
 
562
                raise KeyError(sha)
 
563
            commit = self._reconstruct_commit(rev, tree_sha, roundtrip=True)
 
564
            _check_expected_sha(sha, commit)
 
565
            return commit
100
566
        elif type == "blob":
101
 
            return self._get_blob(*type_data)
 
567
            (fileid, revision) = type_data
 
568
            return self._reconstruct_blobs([(fileid, revision, sha)]).next()
102
569
        elif type == "tree":
103
 
            return self._get_tree(*type_data)
 
570
            (fileid, revid) = type_data
 
571
            try:
 
572
                tree = self.tree_cache.revision_tree(revid)
 
573
                rev = self.repository.get_revision(revid)
 
574
            except errors.NoSuchRevision:
 
575
                trace.mutter('entry for %s %s in shamap: %r, but not found in repository', type, sha, type_data)
 
576
                raise KeyError(sha)
 
577
            unusual_modes = extract_unusual_modes(rev)
 
578
            try:
 
579
                return self._reconstruct_tree(fileid, revid, tree.inventory,
 
580
                    unusual_modes, expected_sha=sha)
 
581
            except errors.NoSuchRevision:
 
582
                raise KeyError(sha)
104
583
        else:
105
584
            raise AssertionError("Unknown object type '%s'" % type)
 
585
 
 
586
    def generate_lossy_pack_contents(self, have, want, progress=None,
 
587
            get_tagged=None):
 
588
        return self.generate_pack_contents(have, want, progress, get_tagged,
 
589
            lossy=True)
 
590
 
 
591
    def generate_pack_contents(self, have, want, progress=None,
 
592
            get_tagged=None, lossy=False):
 
593
        """Iterate over the contents of a pack file.
 
594
 
 
595
        :param have: List of SHA1s of objects that should not be sent
 
596
        :param want: List of SHA1s of objects that should be sent
 
597
        """
 
598
        processed = set()
 
599
        ret = self.lookup_git_shas(have + want)
 
600
        for commit_sha in have:
 
601
            try:
 
602
                (type, (revid, tree_sha)) = ret[commit_sha]
 
603
            except KeyError:
 
604
                pass
 
605
            else:
 
606
                assert type == "commit"
 
607
                processed.add(revid)
 
608
        pending = set()
 
609
        for commit_sha in want:
 
610
            if commit_sha in have:
 
611
                continue
 
612
            try:
 
613
                (type, (revid, tree_sha)) = ret[commit_sha]
 
614
            except KeyError:
 
615
                pass
 
616
            else:
 
617
                assert type == "commit"
 
618
                pending.add(revid)
 
619
 
 
620
        todo = _find_missing_bzr_revids(self.repository.get_parent_map, 
 
621
                                        pending, processed)
 
622
        trace.mutter('sending revisions %r', todo)
 
623
        ret = []
 
624
        pb = ui.ui_factory.nested_progress_bar()
 
625
        try:
 
626
            for i, revid in enumerate(todo):
 
627
                pb.update("generating git objects", i, len(todo))
 
628
                rev = self.repository.get_revision(revid)
 
629
                tree = self.tree_cache.revision_tree(revid)
 
630
                for path, obj, ie in self._revision_to_objects(rev, tree,
 
631
                    roundtrip=not lossy):
 
632
                    ret.append((obj, path))
 
633
        finally:
 
634
            pb.finished()
 
635
        return ret
 
636
 
 
637
    def add_thin_pack(self):
 
638
        import tempfile
 
639
        import os
 
640
        fd, path = tempfile.mkstemp(suffix=".pack")
 
641
        f = os.fdopen(fd, 'wb')
 
642
        def commit():
 
643
            from dulwich.pack import PackData, Pack
 
644
            from bzrlib.plugins.git.fetch import import_git_objects
 
645
            os.fsync(fd)
 
646
            f.close()
 
647
            if os.path.getsize(path) == 0:
 
648
                return
 
649
            pd = PackData(path)
 
650
            pd.create_index_v2(path[:-5]+".idx", self.object_store.get_raw)
 
651
 
 
652
            p = Pack(path[:-5])
 
653
            self.repository.lock_write()
 
654
            try:
 
655
                self.repository.start_write_group()
 
656
                try:
 
657
                    import_git_objects(self.repository, self.mapping, 
 
658
                        p.iterobjects(get_raw=self.get_raw),
 
659
                        self.object_store)
 
660
                except:
 
661
                    self.repository.abort_write_group()
 
662
                    raise
 
663
                else:
 
664
                    self.repository.commit_write_group()
 
665
            finally:
 
666
                self.repository.unlock()
 
667
        return f, commit
 
668
 
 
669
    # The pack isn't kept around anyway, so no point 
 
670
    # in treating full packs different from thin packs
 
671
    add_pack = add_thin_pack