/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 fetch.py

UseĀ get_file_revision.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2008 Canonical Ltd
 
1
# Copyright (C) 2008-2010 Jelmer Vernooij <jelmer@samba.org>
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
from bzrlib import osutils
18
 
from bzrlib.errors import InvalidRevisionId
19
 
from bzrlib.inventory import Inventory
20
 
from bzrlib.repository import InterRepository
21
 
from bzrlib.trace import info
22
 
 
23
 
from bzrlib.plugins.git import git
24
 
from bzrlib.plugins.git.repository import LocalGitRepository, GitRepository, GitFormat
25
 
from bzrlib.plugins.git.remote import RemoteGitRepository
26
 
 
27
 
from dulwich.objects import Commit
28
 
 
29
 
from cStringIO import StringIO
30
 
 
31
 
 
32
 
class BzrFetchGraphWalker(object):
33
 
 
34
 
    def __init__(self, repository, mapping):
35
 
        self.repository = repository
36
 
        self.mapping = mapping
37
 
        self.done = set()
38
 
        self.heads = set(repository.all_revision_ids())
39
 
        self.parents = {}
40
 
 
41
 
    def ack(self, sha):
42
 
        revid = self.mapping.revision_id_foreign_to_bzr(sha)
43
 
        self.remove(revid)
44
 
 
45
 
    def remove(self, revid):
46
 
        self.done.add(revid)
47
 
        if ref in self.heads:
48
 
            self.heads.remove(revid)
49
 
        if revid in self.parents:
50
 
            for p in self.parents[revid]:
51
 
                self.remove(p)
52
 
 
53
 
    def next(self):
54
 
        while self.heads:
55
 
            ret = self.heads.pop()
56
 
            ps = self.repository.get_parent_map([ret])[ret]
57
 
            self.parents[ret] = ps
58
 
            self.heads.update([p for p in ps if not p in self.done])
59
 
            try:
60
 
                self.done.add(ret)
61
 
                return self.mapping.revision_id_bzr_to_foreign(ret)
62
 
            except InvalidRevisionId:
63
 
                pass
64
 
        return None
65
 
 
66
 
 
67
 
def import_git_blob(repo, mapping, path, blob):
 
17
from dulwich.objects import (
 
18
    Commit,
 
19
    Tag,
 
20
    Tree,
 
21
    S_ISGITLINK,
 
22
    ZERO_SHA,
 
23
    )
 
24
from dulwich.object_store import (
 
25
    tree_lookup_path,
 
26
    )
 
27
from itertools import (
 
28
    imap,
 
29
    )
 
30
import posixpath
 
31
import re
 
32
import stat
 
33
 
 
34
from bzrlib import (
 
35
    debug,
 
36
    osutils,
 
37
    trace,
 
38
    ui,
 
39
    )
 
40
from bzrlib.errors import (
 
41
    BzrError,
 
42
    NoSuchId,
 
43
    )
 
44
from bzrlib.inventory import (
 
45
    Inventory,
 
46
    InventoryDirectory,
 
47
    InventoryFile,
 
48
    InventoryLink,
 
49
    TreeReference,
 
50
    )
 
51
from bzrlib.repository import (
 
52
    InterRepository,
 
53
    )
 
54
from bzrlib.revision import (
 
55
    NULL_REVISION,
 
56
    )
 
57
try:
 
58
    from bzrlib.revisiontree import InventoryRevisionTree
 
59
except ImportError: # bzr < 2.4
 
60
    from bzrlib.revisiontree import RevisionTree as InventoryRevisionTree
 
61
from bzrlib.testament import (
 
62
    StrictTestament3,
 
63
    )
 
64
from bzrlib.tsort import (
 
65
    topo_sort,
 
66
    )
 
67
from bzrlib.versionedfile import (
 
68
    ChunkedContentFactory,
 
69
    )
 
70
 
 
71
from bzrlib.plugins.git.mapping import (
 
72
    DEFAULT_FILE_MODE,
 
73
    mode_is_executable,
 
74
    mode_kind,
 
75
    warn_unusual_mode,
 
76
    )
 
77
from bzrlib.plugins.git.object_store import (
 
78
    BazaarObjectStore,
 
79
    LRUTreeCache,
 
80
    _tree_to_objects,
 
81
    )
 
82
from bzrlib.plugins.git.refs import extract_tags
 
83
from bzrlib.plugins.git.remote import (
 
84
    RemoteGitRepository,
 
85
    )
 
86
from bzrlib.plugins.git.repository import (
 
87
    GitRepository,
 
88
    GitRepositoryFormat,
 
89
    LocalGitRepository,
 
90
    )
 
91
 
 
92
 
 
93
def import_git_blob(texts, mapping, path, name, (base_hexsha, hexsha), 
 
94
        base_inv, parent_id, revision_id,
 
95
        parent_invs, lookup_object, (base_mode, mode), store_updater,
 
96
        lookup_file_id):
68
97
    """Import a git blob object into a bzr repository.
69
98
 
70
 
    :param repo: bzr repository
 
99
    :param texts: VersionedFiles to add to
71
100
    :param path: Path in the tree
72
101
    :param blob: A git blob
73
 
    """
74
 
    file_id = mapping.generate_file_id(path)
75
 
    repo.texts.add_lines((file_id, blob.id),
76
 
        [], #FIXME 
77
 
        osutils.split_lines(blob.data))
78
 
    inv.add_path(path, "file", file_id)
79
 
 
80
 
 
81
 
def import_git_tree(repo, mapping, path, tree, inv, lookup_object):
 
102
    :return: Inventory delta for this file
 
103
    """
 
104
    if mapping.is_control_file(path):
 
105
        return []
 
106
    if base_hexsha == hexsha and base_mode == mode:
 
107
        # If nothing has changed since the base revision, we're done
 
108
        return []
 
109
    file_id = lookup_file_id(path)
 
110
    if stat.S_ISLNK(mode):
 
111
        cls = InventoryLink
 
112
    else:
 
113
        cls = InventoryFile
 
114
    ie = cls(file_id, name.decode("utf-8"), parent_id)
 
115
    if ie.kind == "file":
 
116
        ie.executable = mode_is_executable(mode)
 
117
    if base_hexsha == hexsha and mode_kind(base_mode) == mode_kind(mode):
 
118
        base_ie = base_inv[base_inv.path2id(path)]
 
119
        ie.text_size = base_ie.text_size
 
120
        ie.text_sha1 = base_ie.text_sha1
 
121
        if ie.kind == "symlink":
 
122
            ie.symlink_target = base_ie.symlink_target
 
123
        if ie.executable == base_ie.executable:
 
124
            ie.revision = base_ie.revision
 
125
        else:
 
126
            blob = lookup_object(hexsha)
 
127
    else:
 
128
        blob = lookup_object(hexsha)
 
129
        if ie.kind == "symlink":
 
130
            ie.revision = None
 
131
            ie.symlink_target = blob.data
 
132
        else:
 
133
            ie.text_size = sum(imap(len, blob.chunked))
 
134
            ie.text_sha1 = osutils.sha_strings(blob.chunked)
 
135
    # Check what revision we should store
 
136
    parent_keys = []
 
137
    for pinv in parent_invs:
 
138
        try:
 
139
            pie = pinv[file_id]
 
140
        except NoSuchId:
 
141
            continue
 
142
        if (pie.text_sha1 == ie.text_sha1 and
 
143
            pie.executable == ie.executable and
 
144
            pie.symlink_target == ie.symlink_target):
 
145
            # found a revision in one of the parents to use
 
146
            ie.revision = pie.revision
 
147
            break
 
148
        parent_key = (file_id, pie.revision)
 
149
        if not parent_key in parent_keys:
 
150
            parent_keys.append(parent_key)
 
151
    if ie.revision is None:
 
152
        # Need to store a new revision
 
153
        ie.revision = revision_id
 
154
        assert ie.revision is not None
 
155
        if ie.kind == 'symlink':
 
156
            chunks = []
 
157
        else: 
 
158
            chunks = blob.chunked
 
159
        texts.insert_record_stream([
 
160
            ChunkedContentFactory((file_id, ie.revision),
 
161
                tuple(parent_keys), ie.text_sha1, chunks)])
 
162
    invdelta = []
 
163
    if base_hexsha is not None:
 
164
        old_path = path.decode("utf-8") # Renames are not supported yet
 
165
        if stat.S_ISDIR(base_mode):
 
166
            invdelta.extend(remove_disappeared_children(base_inv, old_path,
 
167
                lookup_object(base_hexsha), [], lookup_object))
 
168
    else:
 
169
        old_path = None
 
170
    new_path = path.decode("utf-8")
 
171
    invdelta.append((old_path, new_path, file_id, ie))
 
172
    if base_hexsha != hexsha:
 
173
        store_updater.add_object(blob, ie, path)
 
174
    return invdelta
 
175
 
 
176
 
 
177
class SubmodulesRequireSubtrees(BzrError):
 
178
    _fmt = """The repository you are fetching from contains submodules. To continue, upgrade your Bazaar repository to a format that supports nested trees, such as 'development-subtree'."""
 
179
    internal = False
 
180
 
 
181
 
 
182
def import_git_submodule(texts, mapping, path, name, (base_hexsha, hexsha),
 
183
    base_inv, parent_id, revision_id, parent_invs, lookup_object,
 
184
    (base_mode, mode), store_updater, lookup_file_id):
 
185
    if base_hexsha == hexsha and base_mode == mode:
 
186
        return [], {}
 
187
    file_id = lookup_file_id(path)
 
188
    ie = TreeReference(file_id, name.decode("utf-8"), parent_id)
 
189
    ie.revision = revision_id
 
190
    if base_hexsha is None:
 
191
        oldpath = None
 
192
    else:
 
193
        oldpath = path
 
194
    ie.reference_revision = mapping.revision_id_foreign_to_bzr(hexsha)
 
195
    texts.insert_record_stream([
 
196
        ChunkedContentFactory((file_id, ie.revision), (), None, [])])
 
197
    invdelta = [(oldpath, path, file_id, ie)]
 
198
    return invdelta, {}
 
199
 
 
200
 
 
201
def remove_disappeared_children(base_inv, path, base_tree, existing_children,
 
202
        lookup_object):
 
203
    """Generate an inventory delta for removed children.
 
204
 
 
205
    :param base_inv: Base inventory against which to generate the 
 
206
        inventory delta.
 
207
    :param path: Path to process (unicode)
 
208
    :param base_tree: Git Tree base object
 
209
    :param existing_children: Children that still exist
 
210
    :param lookup_object: Lookup a git object by its SHA1
 
211
    :return: Inventory delta, as list
 
212
    """
 
213
    assert type(path) is unicode
 
214
    ret = []
 
215
    for name, mode, hexsha in base_tree.iteritems():
 
216
        if name in existing_children:
 
217
            continue
 
218
        c_path = posixpath.join(path, name.decode("utf-8"))
 
219
        file_id = base_inv.path2id(c_path)
 
220
        assert file_id is not None
 
221
        ret.append((c_path, None, file_id, None))
 
222
        if stat.S_ISDIR(mode):
 
223
            ret.extend(remove_disappeared_children(
 
224
                base_inv, c_path, lookup_object(hexsha), [], lookup_object))
 
225
    return ret
 
226
 
 
227
 
 
228
def import_git_tree(texts, mapping, path, name, (base_hexsha, hexsha),
 
229
        base_inv, parent_id, revision_id, parent_invs,
 
230
        lookup_object, (base_mode, mode), store_updater,
 
231
        lookup_file_id, allow_submodules=False):
82
232
    """Import a git tree object into a bzr repository.
83
233
 
84
 
    :param repo: A Bzr repository object
85
 
    :param path: Path in the tree
 
234
    :param texts: VersionedFiles object to add to
 
235
    :param path: Path in the tree (str)
 
236
    :param name: Name of the tree (str)
86
237
    :param tree: A git tree object
87
 
    :param inv: Inventory object
 
238
    :param base_inv: Base inventory against which to return inventory delta
 
239
    :return: Inventory delta for this subtree
88
240
    """
89
 
    file_id = mapping.generate_file_id(path)
90
 
    repo.texts.add_lines((file_id, tree.id),
91
 
        [], #FIXME 
92
 
        [])
93
 
    inv.add_path(path, "directory", file_id)
94
 
    for mode, name, hexsha in tree.entries():
95
 
        entry_kind = (mode & 0700000) / 0100000
96
 
        basename = name.decode("utf-8")
97
 
        if path == "":
98
 
            child_path = name
99
 
        else:
100
 
            child_path = urlutils.join(path, name)
101
 
        if entry_kind == 0:
102
 
            import_git_tree(repo, mapping, child_path, lookup_object, inv)
103
 
        elif entry_kind == 1:
104
 
            import_git_blob(repo, mapping, child_path, lookup_object, inv)
105
 
        else:
106
 
            raise AssertionError("Unknown blob kind, perms=%r." % (mode,))
107
 
 
108
 
 
109
 
def import_git_objects(repo, mapping, object_iter):
 
241
    assert type(path) is str
 
242
    assert type(name) is str
 
243
    if base_hexsha == hexsha and base_mode == mode:
 
244
        # If nothing has changed since the base revision, we're done
 
245
        return [], {}
 
246
    invdelta = []
 
247
    file_id = lookup_file_id(path)
 
248
    # We just have to hope this is indeed utf-8:
 
249
    ie = InventoryDirectory(file_id, name.decode("utf-8"), parent_id)
 
250
    tree = lookup_object(hexsha)
 
251
    if base_hexsha is None:
 
252
        base_tree = None
 
253
        old_path = None # Newly appeared here
 
254
    else:
 
255
        base_tree = lookup_object(base_hexsha)
 
256
        old_path = path.decode("utf-8") # Renames aren't supported yet
 
257
    new_path = path.decode("utf-8")
 
258
    if base_tree is None or type(base_tree) is not Tree:
 
259
        ie.revision = revision_id
 
260
        invdelta.append((old_path, new_path, ie.file_id, ie))
 
261
        texts.insert_record_stream([
 
262
            ChunkedContentFactory((ie.file_id, ie.revision), (), None, [])])
 
263
    # Remember for next time
 
264
    existing_children = set()
 
265
    child_modes = {}
 
266
    for name, child_mode, child_hexsha in tree.iteritems():
 
267
        existing_children.add(name)
 
268
        child_path = posixpath.join(path, name)
 
269
        if type(base_tree) is Tree:
 
270
            try:
 
271
                child_base_mode, child_base_hexsha = base_tree[name]
 
272
            except KeyError:
 
273
                child_base_hexsha = None
 
274
                child_base_mode = 0
 
275
        else:
 
276
            child_base_hexsha = None
 
277
            child_base_mode = 0
 
278
        if stat.S_ISDIR(child_mode):
 
279
            subinvdelta, grandchildmodes = import_git_tree(texts, mapping,
 
280
                child_path, name, (child_base_hexsha, child_hexsha), base_inv,
 
281
                file_id, revision_id, parent_invs, lookup_object,
 
282
                (child_base_mode, child_mode), store_updater, lookup_file_id,
 
283
                allow_submodules=allow_submodules)
 
284
        elif S_ISGITLINK(child_mode): # submodule
 
285
            if not allow_submodules:
 
286
                raise SubmodulesRequireSubtrees()
 
287
            subinvdelta, grandchildmodes = import_git_submodule(texts, mapping,
 
288
                child_path, name, (child_base_hexsha, child_hexsha), base_inv,
 
289
                file_id, revision_id, parent_invs, lookup_object,
 
290
                (child_base_mode, child_mode), store_updater, lookup_file_id)
 
291
        else:
 
292
            subinvdelta = import_git_blob(texts, mapping, child_path, name,
 
293
                (child_base_hexsha, child_hexsha), base_inv, file_id,
 
294
                revision_id, parent_invs, lookup_object,
 
295
                (child_base_mode, child_mode), store_updater, lookup_file_id)
 
296
            grandchildmodes = {}
 
297
        child_modes.update(grandchildmodes)
 
298
        invdelta.extend(subinvdelta)
 
299
        if child_mode not in (stat.S_IFDIR, DEFAULT_FILE_MODE,
 
300
                        stat.S_IFLNK, DEFAULT_FILE_MODE|0111):
 
301
            child_modes[child_path] = child_mode
 
302
    # Remove any children that have disappeared
 
303
    if base_tree is not None and type(base_tree) is Tree:
 
304
        invdelta.extend(remove_disappeared_children(base_inv, old_path,
 
305
            base_tree, existing_children, lookup_object))
 
306
    store_updater.add_object(tree, ie, path)
 
307
    return invdelta, child_modes
 
308
 
 
309
 
 
310
def verify_commit_reconstruction(target_git_object_retriever, lookup_object,
 
311
    o, rev, ret_tree, parent_trees, mapping, unusual_modes, verifiers):
 
312
    new_unusual_modes = mapping.export_unusual_file_modes(rev)
 
313
    if new_unusual_modes != unusual_modes:
 
314
        raise AssertionError("unusual modes don't match: %r != %r" % (
 
315
            unusual_modes, new_unusual_modes))
 
316
    # Verify that we can reconstruct the commit properly
 
317
    rec_o = target_git_object_retriever._reconstruct_commit(rev, o.tree, True,
 
318
        verifiers)
 
319
    if rec_o != o:
 
320
        raise AssertionError("Reconstructed commit differs: %r != %r" % (
 
321
            rec_o, o))
 
322
    diff = []
 
323
    new_objs = {}
 
324
    for path, obj, ie in _tree_to_objects(ret_tree, parent_trees,
 
325
        target_git_object_retriever._cache.idmap, unusual_modes, mapping.BZR_DUMMY_FILE):
 
326
        old_obj_id = tree_lookup_path(lookup_object, o.tree, path)[1]
 
327
        new_objs[path] = obj
 
328
        if obj.id != old_obj_id:
 
329
            diff.append((path, lookup_object(old_obj_id), obj))
 
330
    for (path, old_obj, new_obj) in diff:
 
331
        while (old_obj.type_name == "tree" and
 
332
               new_obj.type_name == "tree" and
 
333
               sorted(old_obj) == sorted(new_obj)):
 
334
            for name in old_obj:
 
335
                if old_obj[name][0] != new_obj[name][0]:
 
336
                    raise AssertionError("Modes for %s differ: %o != %o" %
 
337
                        (path, old_obj[name][0], new_obj[name][0]))
 
338
                if old_obj[name][1] != new_obj[name][1]:
 
339
                    # Found a differing child, delve deeper
 
340
                    path = posixpath.join(path, name)
 
341
                    old_obj = lookup_object(old_obj[name][1])
 
342
                    new_obj = new_objs[path]
 
343
                    break
 
344
        raise AssertionError("objects differ for %s: %r != %r" % (path,
 
345
            old_obj, new_obj))
 
346
 
 
347
 
 
348
def import_git_commit(repo, mapping, head, lookup_object,
 
349
                      target_git_object_retriever, trees_cache):
 
350
    o = lookup_object(head)
 
351
    # Note that this uses mapping.revision_id_foreign_to_bzr. If the parents
 
352
    # were bzr roundtripped revisions they would be specified in the
 
353
    # roundtrip data.
 
354
    rev, roundtrip_revid, verifiers = mapping.import_commit(
 
355
        o, mapping.revision_id_foreign_to_bzr)
 
356
    # We have to do this here, since we have to walk the tree and
 
357
    # we need to make sure to import the blobs / trees with the right
 
358
    # path; this may involve adding them more than once.
 
359
    parent_trees = trees_cache.revision_trees(rev.parent_ids)
 
360
    if parent_trees == []:
 
361
        base_inv = Inventory(root_id=None)
 
362
        base_tree = None
 
363
        base_mode = None
 
364
    else:
 
365
        base_inv = parent_trees[0].inventory
 
366
        base_tree = lookup_object(o.parents[0]).tree
 
367
        base_mode = stat.S_IFDIR
 
368
    store_updater = target_git_object_retriever._get_updater(rev)
 
369
    fileid_map = mapping.get_fileid_map(lookup_object, o.tree)
 
370
    inv_delta, unusual_modes = import_git_tree(repo.texts,
 
371
            mapping, "", "", (base_tree, o.tree), base_inv,
 
372
            None, rev.revision_id, [p.inventory for p in parent_trees],
 
373
            lookup_object, (base_mode, stat.S_IFDIR), store_updater,
 
374
            fileid_map.lookup_file_id,
 
375
            allow_submodules=getattr(repo._format, "supports_tree_reference", False))
 
376
    if unusual_modes != {}:
 
377
        for path, mode in unusual_modes.iteritems():
 
378
            warn_unusual_mode(rev.foreign_revid, path, mode)
 
379
        mapping.import_unusual_file_modes(rev, unusual_modes)
 
380
    try:
 
381
        basis_id = rev.parent_ids[0]
 
382
    except IndexError:
 
383
        basis_id = NULL_REVISION
 
384
        base_inv = None
 
385
    rev.inventory_sha1, inv = repo.add_inventory_by_delta(basis_id,
 
386
              inv_delta, rev.revision_id, rev.parent_ids, base_inv)
 
387
    # Check verifiers
 
388
    testament = StrictTestament3(rev, inv)
 
389
    if roundtrip_revid is not None:
 
390
        calculated_verifiers = { "testament3-sha1": testament.as_sha1() }
 
391
        original_revid = rev.revision_id
 
392
        rev.revision_id = roundtrip_revid
 
393
        if calculated_verifiers != verifiers:
 
394
            trace.mutter("Testament SHA1 %r for %r did not match %r.",
 
395
                         calculated_verifiers["testament3-sha1"],
 
396
                         rev.revision_id, verifiers["testament3-sha1"])
 
397
            rev.revision_id = original_revid
 
398
    else:
 
399
        calculated_verifiers = {}
 
400
    store_updater.add_object(o, calculated_verifiers, None)
 
401
    store_updater.finish()
 
402
    ret_tree = InventoryRevisionTree(repo, inv, rev.revision_id)
 
403
    trees_cache.add(ret_tree)
 
404
    repo.add_revision(rev.revision_id, rev)
 
405
    if "verify" in debug.debug_flags:
 
406
        verify_commit_reconstruction(target_git_object_retriever, 
 
407
            lookup_object, o, rev, ret_tree, parent_trees, mapping,
 
408
            unusual_modes, verifiers)
 
409
 
 
410
 
 
411
def import_git_objects(repo, mapping, object_iter,
 
412
    target_git_object_retriever, heads, pb=None, limit=None):
110
413
    """Import a set of git objects into a bzr repository.
111
414
 
112
 
    :param repo: Bazaar repository
 
415
    :param repo: Target Bazaar repository
113
416
    :param mapping: Mapping to use
114
417
    :param object_iter: Iterator over Git objects.
 
418
    :return: Tuple with pack hints and last imported revision id
115
419
    """
116
 
    # TODO: a more (memory-)efficient implementation of this
117
 
    objects = {}
118
 
    for o in object_iter:
119
 
        objects[o.id] = o
120
 
    root_trees = {}
 
420
    def lookup_object(sha):
 
421
        try:
 
422
            return object_iter[sha]
 
423
        except KeyError:
 
424
            return target_git_object_retriever[sha]
 
425
    graph = []
 
426
    checked = set()
 
427
    heads = list(set(heads))
 
428
    trees_cache = LRUTreeCache(repo)
121
429
    # Find and convert commit objects
122
 
    for o in objects.iterkeys():
 
430
    while heads:
 
431
        if pb is not None:
 
432
            pb.update("finding revisions to fetch", len(graph), None)
 
433
        head = heads.pop()
 
434
        assert isinstance(head, str)
 
435
        try:
 
436
            o = lookup_object(head)
 
437
        except KeyError:
 
438
            continue
123
439
        if isinstance(o, Commit):
124
 
            rev = mapping.import_commit(o)
125
 
            root_trees[rev] = objects[o.tree_sha]
 
440
            rev, roundtrip_revid, verifiers = mapping.import_commit(o,
 
441
                mapping.revision_id_foreign_to_bzr)
 
442
            if (repo.has_revision(rev.revision_id) or
 
443
                (roundtrip_revid and repo.has_revision(roundtrip_revid))):
 
444
                continue
 
445
            graph.append((o.id, o.parents))
 
446
            heads.extend([p for p in o.parents if p not in checked])
 
447
        elif isinstance(o, Tag):
 
448
            if o.object[1] not in checked:
 
449
                heads.append(o.object[1])
 
450
        else:
 
451
            trace.warning("Unable to import head object %r" % o)
 
452
        checked.add(o.id)
 
453
    del checked
 
454
    # Order the revisions
126
455
    # Create the inventory objects
127
 
    for rev, root_tree in root_trees.iteritems():
128
 
        # We have to do this here, since we have to walk the tree and 
129
 
        # we need to make sure to import the blobs / trees with the riht 
130
 
        # path; this may involve adding them more than once.
131
 
        inv = Inventory()
132
 
        def lookup_object(sha):
133
 
            if sha in objects:
134
 
                return objects[sha]
135
 
            return reconstruct_git_object(repo, mapping, sha)
136
 
        import_git_tree(repo, mapping, "", tree, inv, lookup_object)
137
 
        repo.add_revision(rev.revision_id, rev, inv)
138
 
 
139
 
 
140
 
def reconstruct_git_commit(repo, rev):
141
 
    raise NotImplementedError(self.reconstruct_git_commit)
142
 
 
143
 
 
144
 
def reconstruct_git_object(repo, mapping, sha):
145
 
    # Commit
146
 
    revid = mapping.revision_id_foreign_to_bzr(sha)
147
 
    try:
148
 
        rev = repo.get_revision(revid)
149
 
    except NoSuchRevision:
150
 
        pass
151
 
    else:
152
 
        return reconstruct_git_commit(rev)
153
 
 
154
 
    # TODO: Tree
155
 
    # TODO: Blob
156
 
    raise KeyError("No such object %s" % sha)
 
456
    batch_size = 1000
 
457
    revision_ids = topo_sort(graph)
 
458
    pack_hints = []
 
459
    if limit is not None:
 
460
        revision_ids = revision_ids[:limit]
 
461
    last_imported = None
 
462
    for offset in range(0, len(revision_ids), batch_size):
 
463
        target_git_object_retriever.start_write_group() 
 
464
        try:
 
465
            repo.start_write_group()
 
466
            try:
 
467
                for i, head in enumerate(
 
468
                    revision_ids[offset:offset+batch_size]):
 
469
                    if pb is not None:
 
470
                        pb.update("fetching revisions", offset+i,
 
471
                                  len(revision_ids))
 
472
                    import_git_commit(repo, mapping, head, lookup_object,
 
473
                        target_git_object_retriever, trees_cache)
 
474
                    last_imported = head
 
475
            except:
 
476
                repo.abort_write_group()
 
477
                raise
 
478
            else:
 
479
                hint = repo.commit_write_group()
 
480
                if hint is not None:
 
481
                    pack_hints.extend(hint)
 
482
        except:
 
483
            target_git_object_retriever.abort_write_group()
 
484
            raise
 
485
        else:
 
486
            target_git_object_retriever.commit_write_group()
 
487
    return pack_hints, last_imported
157
488
 
158
489
 
159
490
class InterGitRepository(InterRepository):
160
491
 
161
 
    _matching_repo_format = GitFormat()
 
492
    _matching_repo_format = GitRepositoryFormat()
 
493
 
 
494
    def _target_has_shas(self, shas):
 
495
        raise NotImplementedError(self._target_has_shas)
 
496
 
 
497
    def get_determine_wants_heads(self, wants, include_tags=False):
 
498
        wants = set(wants)
 
499
        def determine_wants(refs):
 
500
            potential = set(wants)
 
501
            if include_tags:
 
502
                potential.update([v[1] or v[0] for v in extract_tags(refs).itervalues()])
 
503
            return list(potential - self._target_has_shas(potential))
 
504
        return determine_wants
 
505
 
 
506
    def determine_wants_all(self, refs):
 
507
        potential = set([sha for (ref, sha) in refs.iteritems() if not ref.endswith("^{}")])
 
508
        return list(potential - self._target_has_shas(potential))
162
509
 
163
510
    @staticmethod
164
511
    def _get_repo_format_to_test():
168
515
        """See InterRepository.copy_content."""
169
516
        self.fetch(revision_id, pb, find_ghosts=False)
170
517
 
171
 
    def fetch(self, revision_id=None, pb=None, find_ghosts=False, 
172
 
              mapping=None):
 
518
 
 
519
class InterGitNonGitRepository(InterGitRepository):
 
520
    """Base InterRepository that copies revisions from a Git into a non-Git
 
521
    repository."""
 
522
 
 
523
    def _target_has_shas(self, shas):
 
524
        revids = [self.source.lookup_foreign_revision_id(sha) for sha in shas]
 
525
        return self.target.has_revisions(revids)
 
526
 
 
527
    def get_determine_wants_revids(self, revids, include_tags=False):
 
528
        wants = set()
 
529
        for revid in set(revids):
 
530
            git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
 
531
            wants.add(git_sha)
 
532
        return self.get_determine_wants_heads(wants, include_tags=include_tags)
 
533
 
 
534
    def fetch_objects(self, determine_wants, mapping, pb=None, limit=None):
 
535
        """Fetch objects from a remote server.
 
536
 
 
537
        :param determine_wants: determine_wants callback
 
538
        :param mapping: BzrGitMapping to use
 
539
        :param pb: Optional progress bar
 
540
        :param limit: Maximum number of commits to import.
 
541
        :return: Tuple with pack hint, last imported revision id and remote refs
 
542
        """
 
543
        raise NotImplementedError(self.fetch_objects)
 
544
 
 
545
    def fetch(self, revision_id=None, pb=None, find_ghosts=False,
 
546
              mapping=None, fetch_spec=None):
173
547
        if mapping is None:
174
548
            mapping = self.source.get_mapping()
 
549
        if revision_id is not None:
 
550
            interesting_heads = [revision_id]
 
551
        elif fetch_spec is not None:
 
552
            recipe = fetch_spec.get_recipe()
 
553
            if recipe[0] in ("search", "proxy-search"):
 
554
                interesting_heads = recipe[1]
 
555
            else:
 
556
                raise AssertionError("Unsupported search result type %s" % recipe[0])
 
557
        else:
 
558
            interesting_heads = None
 
559
 
 
560
        if interesting_heads is not None:
 
561
            determine_wants = self.get_determine_wants_revids(interesting_heads,
 
562
                include_tags=False)
 
563
        else:
 
564
            determine_wants = self.determine_wants_all
 
565
 
 
566
        (pack_hint, _, remote_refs) = self.fetch_objects(determine_wants,
 
567
            mapping, pb)
 
568
        if pack_hint is not None and self.target._format.pack_compresses:
 
569
            self.target.pack(hint=pack_hint)
 
570
        assert isinstance(remote_refs, dict)
 
571
        return remote_refs
 
572
 
 
573
 
 
574
_GIT_PROGRESS_RE = re.compile(r"(.*?): +(\d+)% \((\d+)/(\d+)\)")
 
575
def report_git_progress(pb, text):
 
576
    text = text.rstrip("\r\n")
 
577
    g = _GIT_PROGRESS_RE.match(text)
 
578
    if g is not None:
 
579
        (text, pct, current, total) = g.groups()
 
580
        pb.update(text, int(current), int(total))
 
581
    else:
 
582
        pb.update(text, 0, 0)
 
583
 
 
584
 
 
585
class DetermineWantsRecorder(object):
 
586
 
 
587
    def __init__(self, actual):
 
588
        self.actual = actual
 
589
        self.wants = []
 
590
        self.remote_refs = {}
 
591
 
 
592
    def __call__(self, refs):
 
593
        self.remote_refs = refs
 
594
        self.wants = self.actual(refs)
 
595
        return self.wants
 
596
 
 
597
 
 
598
class InterRemoteGitNonGitRepository(InterGitNonGitRepository):
 
599
    """InterRepository that copies revisions from a remote Git into a non-Git
 
600
    repository."""
 
601
 
 
602
    def get_target_heads(self):
 
603
        # FIXME: This should be more efficient
 
604
        all_revs = self.target.all_revision_ids()
 
605
        parent_map = self.target.get_parent_map(all_revs)
 
606
        all_parents = set()
 
607
        map(all_parents.update, parent_map.itervalues())
 
608
        return set(all_revs) - all_parents
 
609
 
 
610
    def fetch_objects(self, determine_wants, mapping, pb=None, limit=None):
 
611
        """See `InterGitNonGitRepository`."""
175
612
        def progress(text):
176
 
            if pb is not None:
177
 
                pb.note("git: %s" % text)
178
 
            else:
179
 
                info("git: %s" % text)
180
 
        def determine_wants(heads):
181
 
            if revision_id is None:
182
 
                ret = heads.values()
183
 
            else:
184
 
                ret = [mapping.revision_id_bzr_to_foreign(revision_id)]
185
 
            return [rev for rev in ret if not self.target.has_revision(mapping.revision_id_foreign_to_bzr(rev))]
186
 
        graph_walker = BzrFetchGraphWalker(self.target, mapping)
 
613
            report_git_progress(pb, text)
 
614
        store = BazaarObjectStore(self.target, mapping)
187
615
        self.target.lock_write()
188
616
        try:
189
 
            import_git_objects(self.target, mapping,
190
 
                self.source.fetch_objects(determine_wants, graph_walker, 
191
 
                    progress))
 
617
            heads = self.get_target_heads()
 
618
            graph_walker = store.get_graph_walker(
 
619
                    [store._lookup_revision_sha1(head) for head in heads])
 
620
            wants_recorder = DetermineWantsRecorder(determine_wants)
 
621
 
 
622
            create_pb = None
 
623
            if pb is None:
 
624
                create_pb = pb = ui.ui_factory.nested_progress_bar()
 
625
            try:
 
626
                objects_iter = self.source.fetch_objects(
 
627
                    wants_recorder, graph_walker, store.get_raw,
 
628
                    progress)
 
629
                trace.mutter("Importing %d new revisions", len(wants_recorder.wants))
 
630
                (pack_hint, last_rev) = import_git_objects(self.target, mapping,
 
631
                    objects_iter, store, wants_recorder.wants, pb, limit)
 
632
                return (pack_hint, last_rev, wants_recorder.remote_refs)
 
633
            finally:
 
634
                if create_pb:
 
635
                    create_pb.finished()
192
636
        finally:
193
637
            self.target.unlock()
194
638
 
195
639
    @staticmethod
196
640
    def is_compatible(source, target):
197
641
        """Be compatible with GitRepository."""
198
 
        # FIXME: Also check target uses VersionedFile
199
 
        return (isinstance(source, LocalGitRepository) and 
200
 
                target.supports_rich_root())
 
642
        return (isinstance(source, RemoteGitRepository) and
 
643
                target.supports_rich_root() and
 
644
                not isinstance(target, GitRepository) and
 
645
                target.texts is not None)
 
646
 
 
647
 
 
648
class InterLocalGitNonGitRepository(InterGitNonGitRepository):
 
649
    """InterRepository that copies revisions from a local Git into a non-Git
 
650
    repository."""
 
651
 
 
652
    def fetch_objects(self, determine_wants, mapping, pb=None, limit=None):
 
653
        """See `InterGitNonGitRepository`."""
 
654
        remote_refs = self.source._git.get_refs()
 
655
        wants = determine_wants(remote_refs)
 
656
        create_pb = None
 
657
        if pb is None:
 
658
            create_pb = pb = ui.ui_factory.nested_progress_bar()
 
659
        target_git_object_retriever = BazaarObjectStore(self.target, mapping)
 
660
        try:
 
661
            self.target.lock_write()
 
662
            try:
 
663
                (pack_hint, last_rev) = import_git_objects(self.target, mapping,
 
664
                    self.source._git.object_store,
 
665
                    target_git_object_retriever, wants, pb, limit)
 
666
                return (pack_hint, last_rev, remote_refs)
 
667
            finally:
 
668
                self.target.unlock()
 
669
        finally:
 
670
            if create_pb:
 
671
                create_pb.finished()
 
672
 
 
673
    @staticmethod
 
674
    def is_compatible(source, target):
 
675
        """Be compatible with GitRepository."""
 
676
        return (isinstance(source, LocalGitRepository) and
 
677
                target.supports_rich_root() and
 
678
                not isinstance(target, GitRepository) and
 
679
                target.texts is not None)
 
680
 
 
681
 
 
682
class InterGitGitRepository(InterGitRepository):
 
683
    """InterRepository that copies between Git repositories."""
 
684
 
 
685
    def fetch_objects(self, determine_wants, mapping, pb=None):
 
686
        def progress(text):
 
687
            trace.note("git: %s", text)
 
688
        graphwalker = self.target._git.get_graph_walker()
 
689
        if (isinstance(self.source, LocalGitRepository) and
 
690
            isinstance(self.target, LocalGitRepository)):
 
691
            refs = self.source._git.fetch(self.target._git, determine_wants,
 
692
                progress)
 
693
            return (None, None, refs)
 
694
        elif (isinstance(self.source, LocalGitRepository) and
 
695
              isinstance(self.target, RemoteGitRepository)):
 
696
            raise NotImplementedError
 
697
        elif (isinstance(self.source, RemoteGitRepository) and
 
698
              isinstance(self.target, LocalGitRepository)):
 
699
            f, commit = self.target._git.object_store.add_thin_pack()
 
700
            try:
 
701
                refs = self.source.bzrdir.root_transport.fetch_pack(
 
702
                    determine_wants, graphwalker, f.write, progress)
 
703
                commit()
 
704
                return (None, None, refs)
 
705
            except:
 
706
                f.close()
 
707
                raise
 
708
        else:
 
709
            raise AssertionError
 
710
 
 
711
    def _target_has_shas(self, shas):
 
712
        return set([sha for sha in shas if self.target._git.object_store])
 
713
 
 
714
    def fetch(self, revision_id=None, pb=None, find_ghosts=False,
 
715
              mapping=None, fetch_spec=None, branches=None):
 
716
        if mapping is None:
 
717
            mapping = self.source.get_mapping()
 
718
        r = self.target._git
 
719
        if revision_id is not None:
 
720
            args = [mapping.revision_id_bzr_to_foreign(revision_id)[0]]
 
721
        elif fetch_spec is not None:
 
722
            recipe = fetch_spec.get_recipe()
 
723
            if recipe[0] in ("search", "proxy-search"):
 
724
                heads = recipe[1]
 
725
            else:
 
726
                raise AssertionError("Unsupported search result type %s" % recipe[0])
 
727
            args = [mapping.revision_id_bzr_to_foreign(revid)[0] for revid in heads]
 
728
        if branches is not None:
 
729
            determine_wants = lambda x: [x[y] for y in branches if not x[y] in r.object_store and x[y] != ZERO_SHA]
 
730
        elif fetch_spec is None and revision_id is None:
 
731
            determine_wants = self.determine_wants_all
 
732
        else:
 
733
            determine_wants = lambda x: [y for y in args if not y in r.object_store and y != ZERO_SHA]
 
734
        wants_recorder = DetermineWantsRecorder(determine_wants)
 
735
        self.fetch_objects(wants_recorder, mapping)
 
736
        return wants_recorder.remote_refs
 
737
 
 
738
    @staticmethod
 
739
    def is_compatible(source, target):
 
740
        """Be compatible with GitRepository."""
 
741
        return (isinstance(source, GitRepository) and
 
742
                isinstance(target, GitRepository))
 
743
 
 
744
    def get_determine_wants_revids(self, revids, include_tags=False):
 
745
        wants = set()
 
746
        for revid in set(revids):
 
747
            git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
 
748
            wants.add(git_sha)
 
749
        return self.get_determine_wants_heads(wants, include_tags=include_tags)