/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to object_store.py

More work on roundtrip push support.

Show diffs side-by-side

added added

removed removed

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