/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 breezy/git/object_store.py

  • Committer: Jelmer Vernooij
  • Date: 2020-05-05 23:32:39 UTC
  • mto: (7490.7.21 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200505233239-kdmnmscn8eisltk6
Add a breezy.__main__ module.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2009 Canonical Ltd
 
1
# Copyright (C) 2009-2018 Jelmer Vernooij <jelmer@jelmer.uk>
 
2
# Copyright (C) 2012 Canonical Ltd
2
3
#
3
4
# This program is free software; you can redistribute it and/or modify
4
5
# it under the terms of the GNU General Public License as published by
12
13
#
13
14
# You should have received a copy of the GNU General Public License
14
15
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17
 
17
18
"""Map from Git sha's to Bazaar objects."""
18
19
 
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
 
20
from __future__ import absolute_import
30
21
 
31
22
from dulwich.objects import (
32
23
    Blob,
33
 
    )
34
 
 
35
 
 
36
 
class GitObjectConverter(object):
 
24
    Commit,
 
25
    Tree,
 
26
    sha_to_hex,
 
27
    ZERO_SHA,
 
28
    )
 
29
from dulwich.object_store import (
 
30
    BaseObjectStore,
 
31
    )
 
32
from dulwich.pack import (
 
33
    pack_objects_to_data,
 
34
    PackData,
 
35
    Pack,
 
36
    )
 
37
 
 
38
from .. import (
 
39
    errors,
 
40
    lru_cache,
 
41
    trace,
 
42
    osutils,
 
43
    ui,
 
44
    )
 
45
from ..lock import LogicalLockResult
 
46
from ..revision import (
 
47
    NULL_REVISION,
 
48
    )
 
49
from ..sixish import viewitems
 
50
from ..tree import InterTree
 
51
from ..bzr.testament import (
 
52
    StrictTestament3,
 
53
    )
 
54
 
 
55
from .cache import (
 
56
    from_repository as cache_from_repository,
 
57
    )
 
58
from .mapping import (
 
59
    default_mapping,
 
60
    entry_mode,
 
61
    extract_unusual_modes,
 
62
    mapping_registry,
 
63
    symlink_to_blob,
 
64
    )
 
65
from .unpeel_map import (
 
66
    UnpeelMap,
 
67
    )
 
68
 
 
69
import posixpath
 
70
import stat
 
71
 
 
72
 
 
73
BANNED_FILENAMES = ['.git']
 
74
 
 
75
 
 
76
def get_object_store(repo, mapping=None):
 
77
    git = getattr(repo, "_git", None)
 
78
    if git is not None:
 
79
        git.object_store.unlock = lambda: None
 
80
        git.object_store.lock_read = lambda: LogicalLockResult(lambda: None)
 
81
        git.object_store.lock_write = lambda: LogicalLockResult(lambda: None)
 
82
        return git.object_store
 
83
    return BazaarObjectStore(repo, mapping)
 
84
 
 
85
 
 
86
MAX_TREE_CACHE_SIZE = 50 * 1024 * 1024
 
87
 
 
88
 
 
89
class LRUTreeCache(object):
 
90
 
 
91
    def __init__(self, repository):
 
92
        def approx_tree_size(tree):
 
93
            # Very rough estimate, 250 per inventory entry
 
94
            try:
 
95
                inv = tree.root_inventory
 
96
            except AttributeError:
 
97
                inv = tree.inventory
 
98
            return len(inv) * 250
 
99
        self.repository = repository
 
100
        self._cache = lru_cache.LRUSizeCache(
 
101
            max_size=MAX_TREE_CACHE_SIZE, after_cleanup_size=None,
 
102
            compute_size=approx_tree_size)
 
103
 
 
104
    def revision_tree(self, revid):
 
105
        try:
 
106
            tree = self._cache[revid]
 
107
        except KeyError:
 
108
            tree = self.repository.revision_tree(revid)
 
109
            self.add(tree)
 
110
        return tree
 
111
 
 
112
    def iter_revision_trees(self, revids):
 
113
        trees = {}
 
114
        todo = []
 
115
        for revid in revids:
 
116
            try:
 
117
                tree = self._cache[revid]
 
118
            except KeyError:
 
119
                todo.append(revid)
 
120
            else:
 
121
                if tree.get_revision_id() != revid:
 
122
                    raise AssertionError(
 
123
                        "revision id did not match: %s != %s" % (
 
124
                            tree.get_revision_id(), revid))
 
125
                trees[revid] = tree
 
126
        for tree in self.repository.revision_trees(todo):
 
127
            trees[tree.get_revision_id()] = tree
 
128
            self.add(tree)
 
129
        return (trees[r] for r in revids)
 
130
 
 
131
    def revision_trees(self, revids):
 
132
        return list(self.iter_revision_trees(revids))
 
133
 
 
134
    def add(self, tree):
 
135
        self._cache[tree.get_revision_id()] = tree
 
136
 
 
137
 
 
138
def _find_missing_bzr_revids(graph, want, have):
 
139
    """Find the revisions that have to be pushed.
 
140
 
 
141
    :param get_parent_map: Function that returns the parents for a sequence
 
142
        of revisions.
 
143
    :param want: Revisions the target wants
 
144
    :param have: Revisions the target already has
 
145
    :return: Set of revisions to fetch
 
146
    """
 
147
    handled = set(have)
 
148
    todo = set()
 
149
    for rev in want:
 
150
        extra_todo = graph.find_unique_ancestors(rev, handled)
 
151
        todo.update(extra_todo)
 
152
        handled.update(extra_todo)
 
153
    if NULL_REVISION in todo:
 
154
        todo.remove(NULL_REVISION)
 
155
    return todo
 
156
 
 
157
 
 
158
def _check_expected_sha(expected_sha, object):
 
159
    """Check whether an object matches an expected SHA.
 
160
 
 
161
    :param expected_sha: None or expected SHA as either binary or as hex digest
 
162
    :param object: Object to verify
 
163
    """
 
164
    if expected_sha is None:
 
165
        return
 
166
    if len(expected_sha) == 40:
 
167
        if expected_sha != object.sha().hexdigest().encode('ascii'):
 
168
            raise AssertionError("Invalid sha for %r: %s" % (object,
 
169
                                                             expected_sha))
 
170
    elif len(expected_sha) == 20:
 
171
        if expected_sha != object.sha().digest():
 
172
            raise AssertionError("Invalid sha for %r: %s" % (
 
173
                object, sha_to_hex(expected_sha)))
 
174
    else:
 
175
        raise AssertionError("Unknown length %d for %r" % (len(expected_sha),
 
176
                                                           expected_sha))
 
177
 
 
178
 
 
179
def directory_to_tree(path, children, lookup_ie_sha1, unusual_modes,
 
180
                      empty_file_name, allow_empty=False):
 
181
    """Create a Git Tree object from a Bazaar directory.
 
182
 
 
183
    :param path: directory path
 
184
    :param children: Children inventory entries
 
185
    :param lookup_ie_sha1: Lookup the Git SHA1 for a inventory entry
 
186
    :param unusual_modes: Dictionary with unusual file modes by file ids
 
187
    :param empty_file_name: Name to use for dummy files in empty directories,
 
188
        None to ignore empty directories.
 
189
    """
 
190
    tree = Tree()
 
191
    for value in children:
 
192
        if value.name in BANNED_FILENAMES:
 
193
            continue
 
194
        child_path = osutils.pathjoin(path, value.name)
 
195
        try:
 
196
            mode = unusual_modes[child_path]
 
197
        except KeyError:
 
198
            mode = entry_mode(value)
 
199
        hexsha = lookup_ie_sha1(child_path, value)
 
200
        if hexsha is not None:
 
201
            tree.add(value.name.encode("utf-8"), mode, hexsha)
 
202
    if not allow_empty and len(tree) == 0:
 
203
        # Only the root can be an empty tree
 
204
        if empty_file_name is not None:
 
205
            tree.add(empty_file_name, stat.S_IFREG | 0o644, Blob().id)
 
206
        else:
 
207
            return None
 
208
    return tree
 
209
 
 
210
 
 
211
def _tree_to_objects(tree, parent_trees, idmap, unusual_modes,
 
212
                     dummy_file_name=None, add_cache_entry=None):
 
213
    """Iterate over the objects that were introduced in a revision.
 
214
 
 
215
    :param idmap: id map
 
216
    :param parent_trees: Parent revision trees
 
217
    :param unusual_modes: Unusual file modes dictionary
 
218
    :param dummy_file_name: File name to use for dummy files
 
219
        in empty directories. None to skip empty directories
 
220
    :return: Yields (path, object, ie) entries
 
221
    """
 
222
    dirty_dirs = set()
 
223
    new_blobs = []
 
224
    shamap = {}
 
225
    try:
 
226
        base_tree = parent_trees[0]
 
227
        other_parent_trees = parent_trees[1:]
 
228
    except IndexError:
 
229
        base_tree = tree._repository.revision_tree(NULL_REVISION)
 
230
        other_parent_trees = []
 
231
 
 
232
    def find_unchanged_parent_ie(path, kind, other, parent_trees):
 
233
        for ptree in parent_trees:
 
234
            intertree = InterTree.get(ptree, tree)
 
235
            ppath = intertree.find_source_path(path)
 
236
            if ppath is not None:
 
237
                pkind = ptree.kind(ppath)
 
238
                if kind == "file":
 
239
                    if (pkind == "file" and
 
240
                            ptree.get_file_sha1(ppath) == other):
 
241
                        return (
 
242
                            ptree.path2id(ppath), ptree.get_file_revision(ppath))
 
243
                if kind == "symlink":
 
244
                    if (pkind == "symlink" and
 
245
                            ptree.get_symlink_target(ppath) == other):
 
246
                        return (
 
247
                            ptree.path2id(ppath), ptree.get_file_revision(ppath))
 
248
        raise KeyError
 
249
 
 
250
    # Find all the changed blobs
 
251
    for change in tree.iter_changes(base_tree):
 
252
        if change.name[1] in BANNED_FILENAMES:
 
253
            continue
 
254
        if change.kind[1] == "file":
 
255
            sha1 = tree.get_file_sha1(change.path[1])
 
256
            blob_id = None
 
257
            try:
 
258
                (pfile_id, prevision) = find_unchanged_parent_ie(
 
259
                    change.path[1], change.kind[1], sha1, other_parent_trees)
 
260
            except KeyError:
 
261
                pass
 
262
            else:
 
263
                # It existed in one of the parents, with the same contents.
 
264
                # So no need to yield any new git objects.
 
265
                try:
 
266
                    blob_id = idmap.lookup_blob_id(
 
267
                        pfile_id, prevision)
 
268
                except KeyError:
 
269
                    if not change.changed_content:
 
270
                        # no-change merge ?
 
271
                        blob = Blob()
 
272
                        blob.data = tree.get_file_text(change.path[1])
 
273
                        blob_id = blob.id
 
274
            if blob_id is None:
 
275
                new_blobs.append((change.path[1], change.file_id))
 
276
            else:
 
277
                # TODO(jelmer): This code path does not have any test coverage.
 
278
                shamap[change.path[1]] = blob_id
 
279
                if add_cache_entry is not None:
 
280
                    add_cache_entry(
 
281
                        ("blob", blob_id),
 
282
                        (change.file_id, tree.get_file_revision(change.path[1])), change.path[1])
 
283
        elif change.kind[1] == "symlink":
 
284
            target = tree.get_symlink_target(change.path[1])
 
285
            blob = symlink_to_blob(target)
 
286
            shamap[change.path[1]] = blob.id
 
287
            if add_cache_entry is not None:
 
288
                add_cache_entry(
 
289
                    blob, (change.file_id, tree.get_file_revision(change.path[1])), change.path[1])
 
290
            try:
 
291
                find_unchanged_parent_ie(
 
292
                    change.path[1], change.kind[1], target, other_parent_trees)
 
293
            except KeyError:
 
294
                if change.changed_content:
 
295
                    yield (change.path[1], blob,
 
296
                           (change.file_id, tree.get_file_revision(change.path[1])))
 
297
        elif change.kind[1] is None:
 
298
            shamap[change.path[1]] = None
 
299
        elif change.kind[1] != 'directory':
 
300
            raise AssertionError(change.kind[1])
 
301
        for p in change.path:
 
302
            if p is None:
 
303
                continue
 
304
            dirty_dirs.add(osutils.dirname(p))
 
305
 
 
306
    # Fetch contents of the blobs that were changed
 
307
    for (path, file_id), chunks in tree.iter_files_bytes(
 
308
            [(path, (path, file_id)) for (path, file_id) in new_blobs]):
 
309
        obj = Blob()
 
310
        obj.chunked = chunks
 
311
        if add_cache_entry is not None:
 
312
            add_cache_entry(obj, (file_id, tree.get_file_revision(path)), path)
 
313
        yield path, obj, (file_id, tree.get_file_revision(path))
 
314
        shamap[path] = obj.id
 
315
 
 
316
    for path in unusual_modes:
 
317
        dirty_dirs.add(posixpath.dirname(path))
 
318
 
 
319
    for dir in list(dirty_dirs):
 
320
        for parent in osutils.parent_directories(dir):
 
321
            if parent in dirty_dirs:
 
322
                break
 
323
            dirty_dirs.add(parent)
 
324
 
 
325
    if dirty_dirs:
 
326
        dirty_dirs.add('')
 
327
 
 
328
    def ie_to_hexsha(path, ie):
 
329
        try:
 
330
            return shamap[path]
 
331
        except KeyError:
 
332
            pass
 
333
        # FIXME: Should be the same as in parent
 
334
        if ie.kind == "file":
 
335
            try:
 
336
                return idmap.lookup_blob_id(ie.file_id, ie.revision)
 
337
            except KeyError:
 
338
                # no-change merge ?
 
339
                blob = Blob()
 
340
                blob.data = tree.get_file_text(path)
 
341
                if add_cache_entry is not None:
 
342
                    add_cache_entry(blob, (ie.file_id, ie.revision), path)
 
343
                return blob.id
 
344
        elif ie.kind == "symlink":
 
345
            try:
 
346
                return idmap.lookup_blob_id(ie.file_id, ie.revision)
 
347
            except KeyError:
 
348
                # no-change merge ?
 
349
                target = tree.get_symlink_target(path)
 
350
                blob = symlink_to_blob(target)
 
351
                if add_cache_entry is not None:
 
352
                    add_cache_entry(blob, (ie.file_id, ie.revision), path)
 
353
                return blob.id
 
354
        elif ie.kind == "directory":
 
355
            # Not all cache backends store the tree information,
 
356
            # calculate again from scratch
 
357
            ret = directory_to_tree(
 
358
                path, ie.children.values(), ie_to_hexsha, unusual_modes,
 
359
                dummy_file_name, ie.parent_id is None)
 
360
            if ret is None:
 
361
                return ret
 
362
            return ret.id
 
363
        else:
 
364
            raise AssertionError
 
365
 
 
366
    for path in sorted(dirty_dirs, reverse=True):
 
367
        if not tree.has_filename(path):
 
368
            continue
 
369
 
 
370
        if tree.kind(path) != 'directory':
 
371
            continue
 
372
 
 
373
        obj = directory_to_tree(
 
374
            path, tree.iter_child_entries(path), ie_to_hexsha, unusual_modes,
 
375
            dummy_file_name, path == '')
 
376
 
 
377
        if obj is not None:
 
378
            file_id = tree.path2id(path)
 
379
            if add_cache_entry is not None:
 
380
                add_cache_entry(obj, (file_id, tree.get_revision_id()), path)
 
381
            yield path, obj, (file_id, tree.get_revision_id())
 
382
            shamap[path] = obj.id
 
383
 
 
384
 
 
385
class PackTupleIterable(object):
 
386
 
 
387
    def __init__(self, store):
 
388
        self.store = store
 
389
        self.store.lock_read()
 
390
        self.objects = {}
 
391
 
 
392
    def __del__(self):
 
393
        self.store.unlock()
 
394
 
 
395
    def add(self, sha, path):
 
396
        self.objects[sha] = path
 
397
 
 
398
    def __len__(self):
 
399
        return len(self.objects)
 
400
 
 
401
    def __iter__(self):
 
402
        return ((self.store[object_id], path) for (object_id, path) in
 
403
                viewitems(self.objects))
 
404
 
 
405
 
 
406
class BazaarObjectStore(BaseObjectStore):
 
407
    """A Git-style object store backed onto a Bazaar repository."""
37
408
 
38
409
    def __init__(self, repository, mapping=None):
39
410
        self.repository = repository
 
411
        self._map_updated = False
 
412
        self._locked = None
40
413
        if mapping is None:
41
 
            self.mapping = self.repository.get_mapping()
 
414
            self.mapping = default_mapping
42
415
        else:
43
416
            self.mapping = mapping
44
 
        self._idmap = GitShaMap(self.repository._transport)
45
 
 
46
 
    def _update_sha_map(self):
47
 
        all_revids = self.repository.all_revision_ids()
 
417
        self._cache = cache_from_repository(repository)
 
418
        self._content_cache_types = ("tree",)
 
419
        self.start_write_group = self._cache.idmap.start_write_group
 
420
        self.abort_write_group = self._cache.idmap.abort_write_group
 
421
        self.commit_write_group = self._cache.idmap.commit_write_group
 
422
        self.tree_cache = LRUTreeCache(self.repository)
 
423
        self.unpeel_map = UnpeelMap.from_repository(self.repository)
 
424
 
 
425
    def _missing_revisions(self, revisions):
 
426
        return self._cache.idmap.missing_revisions(revisions)
 
427
 
 
428
    def _update_sha_map(self, stop_revision=None):
 
429
        if not self.is_locked():
 
430
            raise errors.LockNotHeld(self)
 
431
        if self._map_updated:
 
432
            return
 
433
        if (stop_revision is not None and
 
434
                not self._missing_revisions([stop_revision])):
 
435
            return
48
436
        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()
 
437
        if stop_revision is None:
 
438
            all_revids = self.repository.all_revision_ids()
 
439
            missing_revids = self._missing_revisions(all_revids)
 
440
        else:
 
441
            heads = set([stop_revision])
 
442
            missing_revids = self._missing_revisions(heads)
 
443
            while heads:
 
444
                parents = graph.get_parent_map(heads)
 
445
                todo = set()
 
446
                for p in parents.values():
 
447
                    todo.update([x for x in p if x not in missing_revids])
 
448
                heads = self._missing_revisions(todo)
 
449
                missing_revids.update(heads)
 
450
        if NULL_REVISION in missing_revids:
 
451
            missing_revids.remove(NULL_REVISION)
 
452
        missing_revids = self.repository.has_revisions(missing_revids)
 
453
        if not missing_revids:
 
454
            if stop_revision is None:
 
455
                self._map_updated = True
 
456
            return
 
457
        self.start_write_group()
 
458
        try:
 
459
            with ui.ui_factory.nested_progress_bar() as pb:
 
460
                for i, revid in enumerate(graph.iter_topo_order(
 
461
                        missing_revids)):
 
462
                    trace.mutter('processing %r', revid)
 
463
                    pb.update("updating git map", i, len(missing_revids))
 
464
                    self._update_sha_map_revision(revid)
 
465
            if stop_revision is None:
 
466
                self._map_updated = True
 
467
        except BaseException:
 
468
            self.abort_write_group()
 
469
            raise
 
470
        else:
 
471
            self.commit_write_group()
 
472
 
 
473
    def __iter__(self):
 
474
        self._update_sha_map()
 
475
        return iter(self._cache.idmap.sha1s())
 
476
 
 
477
    def _reconstruct_commit(self, rev, tree_sha, lossy, verifiers):
 
478
        """Reconstruct a Commit object.
 
479
 
 
480
        :param rev: Revision object
 
481
        :param tree_sha: SHA1 of the root tree object
 
482
        :param lossy: Whether or not to roundtrip bzr metadata
 
483
        :param verifiers: Verifiers for the commits
 
484
        :return: Commit object
 
485
        """
 
486
        def parent_lookup(revid):
 
487
            try:
 
488
                return self._lookup_revision_sha1(revid)
 
489
            except errors.NoSuchRevision:
 
490
                return None
 
491
        return self.mapping.export_commit(rev, tree_sha, parent_lookup,
 
492
                                          lossy, verifiers)
 
493
 
 
494
    def _revision_to_objects(self, rev, tree, lossy, add_cache_entry=None):
 
495
        """Convert a revision to a set of git objects.
 
496
 
 
497
        :param rev: Bazaar revision object
 
498
        :param tree: Bazaar revision tree
 
499
        :param lossy: Whether to not roundtrip all Bazaar revision data
 
500
        """
 
501
        unusual_modes = extract_unusual_modes(rev)
 
502
        present_parents = self.repository.has_revisions(rev.parent_ids)
 
503
        parent_trees = self.tree_cache.revision_trees(
 
504
            [p for p in rev.parent_ids if p in present_parents])
 
505
        root_tree = None
 
506
        for path, obj, bzr_key_data in _tree_to_objects(
 
507
                tree, parent_trees, self._cache.idmap, unusual_modes,
 
508
                self.mapping.BZR_DUMMY_FILE, add_cache_entry):
 
509
            if path == "":
 
510
                root_tree = obj
 
511
                root_key_data = bzr_key_data
 
512
                # Don't yield just yet
 
513
            else:
 
514
                yield path, obj
 
515
        if root_tree is None:
 
516
            # Pointless commit - get the tree sha elsewhere
 
517
            if not rev.parent_ids:
 
518
                root_tree = Tree()
 
519
            else:
 
520
                base_sha1 = self._lookup_revision_sha1(rev.parent_ids[0])
 
521
                root_tree = self[self[base_sha1].tree]
 
522
            root_key_data = (tree.path2id(''), tree.get_revision_id())
 
523
        if add_cache_entry is not None:
 
524
            add_cache_entry(root_tree, root_key_data, "")
 
525
        yield "", root_tree
 
526
        if not lossy:
 
527
            testament3 = StrictTestament3(rev, tree)
 
528
            verifiers = {"testament3-sha1": testament3.as_sha1()}
 
529
        else:
 
530
            verifiers = {}
 
531
        commit_obj = self._reconstruct_commit(rev, root_tree.id,
 
532
                                              lossy=lossy, verifiers=verifiers)
 
533
        try:
 
534
            foreign_revid, mapping = mapping_registry.parse_revision_id(
 
535
                rev.revision_id)
 
536
        except errors.InvalidRevisionId:
 
537
            pass
 
538
        else:
 
539
            _check_expected_sha(foreign_revid, commit_obj)
 
540
        if add_cache_entry is not None:
 
541
            add_cache_entry(commit_obj, verifiers, None)
 
542
 
 
543
        yield None, commit_obj
 
544
 
 
545
    def _get_updater(self, rev):
 
546
        return self._cache.get_updater(rev)
59
547
 
60
548
    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)
 
549
        rev = self.repository.get_revision(revid)
 
550
        tree = self.tree_cache.revision_tree(rev.revision_id)
 
551
        updater = self._get_updater(rev)
 
552
        # FIXME JRV 2011-12-15: Shouldn't we try both values for lossy ?
 
553
        for path, obj in self._revision_to_objects(
 
554
                rev, tree, lossy=(not self.mapping.roundtripping),
 
555
                add_cache_entry=updater.add_object):
 
556
            if isinstance(obj, Commit):
 
557
                commit_obj = obj
 
558
        commit_obj = updater.finish()
 
559
        return commit_obj.id
 
560
 
 
561
    def _reconstruct_blobs(self, keys):
 
562
        """Return a Git Blob object from a fileid and revision stored in bzr.
 
563
 
 
564
        :param fileid: File id of the text
 
565
        :param revision: Revision of the text
 
566
        """
 
567
        stream = self.repository.iter_files_bytes(
 
568
            ((key[0], key[1], key) for key in keys))
 
569
        for (file_id, revision, expected_sha), chunks in stream:
 
570
            blob = Blob()
 
571
            blob.chunked = chunks
 
572
            if blob.id != expected_sha and blob.data == b"":
 
573
                # Perhaps it's a symlink ?
 
574
                tree = self.tree_cache.revision_tree(revision)
 
575
                path = tree.id2path(file_id)
 
576
                if tree.kind(path) == 'symlink':
 
577
                    blob = symlink_to_blob(tree.get_symlink_target(path))
 
578
            _check_expected_sha(expected_sha, blob)
 
579
            yield blob
 
580
 
 
581
    def _reconstruct_tree(self, fileid, revid, bzr_tree, unusual_modes,
 
582
                          expected_sha=None):
 
583
        """Return a Git Tree object from a file id and a revision stored in bzr.
 
584
 
 
585
        :param fileid: fileid in the tree.
 
586
        :param revision: Revision of the tree.
 
587
        """
 
588
        def get_ie_sha1(path, entry):
 
589
            if entry.kind == "directory":
 
590
                try:
 
591
                    return self._cache.idmap.lookup_tree_id(entry.file_id,
 
592
                                                            revid)
 
593
                except (NotImplementedError, KeyError):
 
594
                    obj = self._reconstruct_tree(
 
595
                        entry.file_id, revid, bzr_tree, unusual_modes)
 
596
                    if obj is None:
 
597
                        return None
 
598
                    else:
 
599
                        return obj.id
 
600
            elif entry.kind in ("file", "symlink"):
 
601
                try:
 
602
                    return self._cache.idmap.lookup_blob_id(entry.file_id,
 
603
                                                            entry.revision)
 
604
                except KeyError:
 
605
                    # no-change merge?
 
606
                    return next(self._reconstruct_blobs(
 
607
                        [(entry.file_id, entry.revision, None)])).id
 
608
            elif entry.kind == 'tree-reference':
 
609
                # FIXME: Make sure the file id is the root id
 
610
                return self._lookup_revision_sha1(entry.reference_revision)
 
611
            else:
 
612
                raise AssertionError("unknown entry kind '%s'" % entry.kind)
 
613
        path = bzr_tree.id2path(fileid)
 
614
        tree = directory_to_tree(
 
615
            path,
 
616
            bzr_tree.iter_child_entries(path),
 
617
            get_ie_sha1, unusual_modes, self.mapping.BZR_DUMMY_FILE,
 
618
            bzr_tree.path2id('') == fileid)
 
619
        if tree is not None:
 
620
            _check_expected_sha(expected_sha, tree)
 
621
        return tree
 
622
 
 
623
    def get_parents(self, sha):
 
624
        """Retrieve the parents of a Git commit by SHA1.
 
625
 
 
626
        :param sha: SHA1 of the commit
 
627
        :raises: KeyError, NotCommitError
 
628
        """
 
629
        return self[sha].parents
 
630
 
 
631
    def _lookup_revision_sha1(self, revid):
 
632
        """Return the SHA1 matching a Bazaar revision."""
 
633
        if revid == NULL_REVISION:
 
634
            return ZERO_SHA
 
635
        try:
 
636
            return self._cache.idmap.lookup_commit(revid)
 
637
        except KeyError:
 
638
            try:
 
639
                return mapping_registry.parse_revision_id(revid)[0]
 
640
            except errors.InvalidRevisionId:
 
641
                self._update_sha_map(revid)
 
642
                return self._cache.idmap.lookup_commit(revid)
 
643
 
 
644
    def get_raw(self, sha):
 
645
        """Get the raw representation of a Git object by SHA1.
 
646
 
 
647
        :param sha: SHA1 of the git object
 
648
        """
 
649
        if len(sha) == 20:
 
650
            sha = sha_to_hex(sha)
 
651
        obj = self[sha]
 
652
        return (obj.type, obj.as_raw_string())
 
653
 
 
654
    def __contains__(self, sha):
 
655
        # See if sha is in map
 
656
        try:
 
657
            for (type, type_data) in self.lookup_git_sha(sha):
 
658
                if type == "commit":
 
659
                    if self.repository.has_revision(type_data[0]):
 
660
                        return True
 
661
                elif type == "blob":
 
662
                    if type_data in self.repository.texts:
 
663
                        return True
 
664
                elif type == "tree":
 
665
                    if self.repository.has_revision(type_data[1]):
 
666
                        return True
 
667
                else:
 
668
                    raise AssertionError("Unknown object type '%s'" % type)
 
669
            else:
 
670
                return False
 
671
        except KeyError:
 
672
            return False
 
673
 
 
674
    def lock_read(self):
 
675
        self._locked = 'r'
 
676
        self._map_updated = False
 
677
        self.repository.lock_read()
 
678
        return LogicalLockResult(self.unlock)
 
679
 
 
680
    def lock_write(self):
 
681
        self._locked = 'r'
 
682
        self._map_updated = False
 
683
        self.repository.lock_write()
 
684
        return LogicalLockResult(self.unlock)
 
685
 
 
686
    def is_locked(self):
 
687
        return (self._locked is not None)
 
688
 
 
689
    def unlock(self):
 
690
        self._locked = None
 
691
        self._map_updated = False
 
692
        self.repository.unlock()
 
693
 
 
694
    def lookup_git_shas(self, shas):
 
695
        ret = {}
 
696
        for sha in shas:
 
697
            if sha == ZERO_SHA:
 
698
                ret[sha] = [("commit", (NULL_REVISION, None, {}))]
 
699
                continue
 
700
            try:
 
701
                ret[sha] = list(self._cache.idmap.lookup_git_sha(sha))
 
702
            except KeyError:
 
703
                # if not, see if there are any unconverted revisions and
 
704
                # add them to the map, search for sha in map again
 
705
                self._update_sha_map()
 
706
                try:
 
707
                    ret[sha] = list(self._cache.idmap.lookup_git_sha(sha))
 
708
                except KeyError:
 
709
                    pass
 
710
        return ret
 
711
 
 
712
    def lookup_git_sha(self, sha):
 
713
        return self.lookup_git_shas([sha])[sha]
87
714
 
88
715
    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)
97
 
        # convert object to git object
98
 
        if type == "commit":
99
 
            return self._get_commit(*type_data)
100
 
        elif type == "blob":
101
 
            return self._get_blob(*type_data)
102
 
        elif type == "tree":
103
 
            return self._get_tree(*type_data)
 
716
        for (kind, type_data) in self.lookup_git_sha(sha):
 
717
            # convert object to git object
 
718
            if kind == "commit":
 
719
                (revid, tree_sha, verifiers) = type_data
 
720
                try:
 
721
                    rev = self.repository.get_revision(revid)
 
722
                except errors.NoSuchRevision:
 
723
                    if revid == NULL_REVISION:
 
724
                        raise AssertionError(
 
725
                            "should not try to look up NULL_REVISION")
 
726
                    trace.mutter('entry for %s %s in shamap: %r, but not '
 
727
                                 'found in repository', kind, sha, type_data)
 
728
                    raise KeyError(sha)
 
729
                # FIXME: the type data should say whether conversion was
 
730
                # lossless
 
731
                commit = self._reconstruct_commit(
 
732
                    rev, tree_sha, lossy=(not self.mapping.roundtripping),
 
733
                    verifiers=verifiers)
 
734
                _check_expected_sha(sha, commit)
 
735
                return commit
 
736
            elif kind == "blob":
 
737
                (fileid, revision) = type_data
 
738
                blobs = self._reconstruct_blobs([(fileid, revision, sha)])
 
739
                return next(blobs)
 
740
            elif kind == "tree":
 
741
                (fileid, revid) = type_data
 
742
                try:
 
743
                    tree = self.tree_cache.revision_tree(revid)
 
744
                    rev = self.repository.get_revision(revid)
 
745
                except errors.NoSuchRevision:
 
746
                    trace.mutter(
 
747
                        'entry for %s %s in shamap: %r, but not found in '
 
748
                        'repository', kind, sha, type_data)
 
749
                    raise KeyError(sha)
 
750
                unusual_modes = extract_unusual_modes(rev)
 
751
                try:
 
752
                    return self._reconstruct_tree(
 
753
                        fileid, revid, tree, unusual_modes, expected_sha=sha)
 
754
                except errors.NoSuchRevision:
 
755
                    raise KeyError(sha)
 
756
            else:
 
757
                raise AssertionError("Unknown object type '%s'" % kind)
104
758
        else:
105
 
            raise AssertionError("Unknown object type '%s'" % type)
 
759
            raise KeyError(sha)
 
760
 
 
761
    def generate_lossy_pack_data(self, have, want, progress=None,
 
762
                                 get_tagged=None, ofs_delta=False):
 
763
        return pack_objects_to_data(
 
764
            self.generate_pack_contents(have, want, progress, get_tagged,
 
765
                                        lossy=True))
 
766
 
 
767
    def generate_pack_contents(self, have, want, progress=None,
 
768
                               ofs_delta=False, get_tagged=None, lossy=False):
 
769
        """Iterate over the contents of a pack file.
 
770
 
 
771
        :param have: List of SHA1s of objects that should not be sent
 
772
        :param want: List of SHA1s of objects that should be sent
 
773
        """
 
774
        processed = set()
 
775
        ret = self.lookup_git_shas(have + want)
 
776
        for commit_sha in have:
 
777
            commit_sha = self.unpeel_map.peel_tag(commit_sha, commit_sha)
 
778
            try:
 
779
                for (type, type_data) in ret[commit_sha]:
 
780
                    if type != "commit":
 
781
                        raise AssertionError("Type was %s, not commit" % type)
 
782
                    processed.add(type_data[0])
 
783
            except KeyError:
 
784
                trace.mutter("unable to find remote ref %s", commit_sha)
 
785
        pending = set()
 
786
        for commit_sha in want:
 
787
            if commit_sha in have:
 
788
                continue
 
789
            try:
 
790
                for (type, type_data) in ret[commit_sha]:
 
791
                    if type != "commit":
 
792
                        raise AssertionError("Type was %s, not commit" % type)
 
793
                    pending.add(type_data[0])
 
794
            except KeyError:
 
795
                pass
 
796
 
 
797
        graph = self.repository.get_graph()
 
798
        todo = _find_missing_bzr_revids(graph, pending, processed)
 
799
        ret = PackTupleIterable(self)
 
800
        with ui.ui_factory.nested_progress_bar() as pb:
 
801
            for i, revid in enumerate(graph.iter_topo_order(todo)):
 
802
                pb.update("generating git objects", i, len(todo))
 
803
                try:
 
804
                    rev = self.repository.get_revision(revid)
 
805
                except errors.NoSuchRevision:
 
806
                    continue
 
807
                tree = self.tree_cache.revision_tree(revid)
 
808
                for path, obj in self._revision_to_objects(
 
809
                        rev, tree, lossy=lossy):
 
810
                    ret.add(obj.id, path)
 
811
            return ret
 
812
 
 
813
    def add_thin_pack(self):
 
814
        import tempfile
 
815
        import os
 
816
        fd, path = tempfile.mkstemp(suffix=".pack")
 
817
        f = os.fdopen(fd, 'wb')
 
818
 
 
819
        def commit():
 
820
            from .fetch import import_git_objects
 
821
            os.fsync(fd)
 
822
            f.close()
 
823
            if os.path.getsize(path) == 0:
 
824
                return
 
825
            pd = PackData(path)
 
826
            pd.create_index_v2(path[:-5] + ".idx", self.object_store.get_raw)
 
827
 
 
828
            p = Pack(path[:-5])
 
829
            with self.repository.lock_write():
 
830
                self.repository.start_write_group()
 
831
                try:
 
832
                    import_git_objects(self.repository, self.mapping,
 
833
                                       p.iterobjects(get_raw=self.get_raw),
 
834
                                       self.object_store)
 
835
                except BaseException:
 
836
                    self.repository.abort_write_group()
 
837
                    raise
 
838
                else:
 
839
                    self.repository.commit_write_group()
 
840
        return f, commit
 
841
 
 
842
    # The pack isn't kept around anyway, so no point
 
843
    # in treating full packs different from thin packs
 
844
    add_pack = add_thin_pack