/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-06 02:13:25 UTC
  • mfrom: (7490.7.21 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200506021325-awbmmqu1zyorz7sj
Merge 3.1 branch.

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