/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-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

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