/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

More fixes for compatibility with bzr.dev testsuite.

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. "
 
179
            "To continue, upgrade your Bazaar repository to a format that "
 
180
            "supports nested trees, such as 'development-subtree'.")
 
181
    internal = False
 
182
 
 
183
 
 
184
def import_git_submodule(texts, mapping, path, name, (base_hexsha, hexsha),
 
185
    base_inv, parent_id, revision_id, parent_invs, lookup_object,
 
186
    (base_mode, mode), store_updater, lookup_file_id):
 
187
    """Import a git submodule."""
 
188
    if base_hexsha == hexsha and base_mode == mode:
 
189
        return [], {}
 
190
    file_id = lookup_file_id(path)
 
191
    ie = TreeReference(file_id, name.decode("utf-8"), parent_id)
 
192
    ie.revision = revision_id
 
193
    if base_hexsha is None:
 
194
        oldpath = None
 
195
    else:
 
196
        oldpath = path
 
197
    ie.reference_revision = mapping.revision_id_foreign_to_bzr(hexsha)
 
198
    texts.insert_record_stream([
 
199
        ChunkedContentFactory((file_id, ie.revision), (), None, [])])
 
200
    invdelta = [(oldpath, path, file_id, ie)]
 
201
    return invdelta, {}
 
202
 
 
203
 
 
204
def remove_disappeared_children(base_inv, path, base_tree, existing_children,
 
205
        lookup_object):
 
206
    """Generate an inventory delta for removed children.
 
207
 
 
208
    :param base_inv: Base inventory against which to generate the 
 
209
        inventory delta.
 
210
    :param path: Path to process (unicode)
 
211
    :param base_tree: Git Tree base object
 
212
    :param existing_children: Children that still exist
 
213
    :param lookup_object: Lookup a git object by its SHA1
 
214
    :return: Inventory delta, as list
 
215
    """
 
216
    assert type(path) is unicode
 
217
    ret = []
 
218
    for name, mode, hexsha in base_tree.iteritems():
 
219
        if name in existing_children:
 
220
            continue
 
221
        c_path = posixpath.join(path, name.decode("utf-8"))
 
222
        file_id = base_inv.path2id(c_path)
 
223
        assert file_id is not None
 
224
        ret.append((c_path, None, file_id, None))
 
225
        if stat.S_ISDIR(mode):
 
226
            ret.extend(remove_disappeared_children(
 
227
                base_inv, c_path, lookup_object(hexsha), [], lookup_object))
 
228
    return ret
 
229
 
 
230
 
 
231
def import_git_tree(texts, mapping, path, name, (base_hexsha, hexsha),
 
232
        base_inv, parent_id, revision_id, parent_invs,
 
233
        lookup_object, (base_mode, mode), store_updater,
 
234
        lookup_file_id, allow_submodules=False):
82
235
    """Import a git tree object into a bzr repository.
83
236
 
84
 
    :param repo: A Bzr repository object
85
 
    :param path: Path in the tree
 
237
    :param texts: VersionedFiles object to add to
 
238
    :param path: Path in the tree (str)
 
239
    :param name: Name of the tree (str)
86
240
    :param tree: A git tree object
87
 
    :param inv: Inventory object
 
241
    :param base_inv: Base inventory against which to return inventory delta
 
242
    :return: Inventory delta for this subtree
88
243
    """
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):
 
244
    assert type(path) is str
 
245
    assert type(name) is str
 
246
    if base_hexsha == hexsha and base_mode == mode:
 
247
        # If nothing has changed since the base revision, we're done
 
248
        return [], {}
 
249
    invdelta = []
 
250
    file_id = lookup_file_id(path)
 
251
    # We just have to hope this is indeed utf-8:
 
252
    ie = InventoryDirectory(file_id, name.decode("utf-8"), parent_id)
 
253
    tree = lookup_object(hexsha)
 
254
    if base_hexsha is None:
 
255
        base_tree = None
 
256
        old_path = None # Newly appeared here
 
257
    else:
 
258
        base_tree = lookup_object(base_hexsha)
 
259
        old_path = path.decode("utf-8") # Renames aren't supported yet
 
260
    new_path = path.decode("utf-8")
 
261
    if base_tree is None or type(base_tree) is not Tree:
 
262
        ie.revision = revision_id
 
263
        invdelta.append((old_path, new_path, ie.file_id, ie))
 
264
        texts.insert_record_stream([
 
265
            ChunkedContentFactory((ie.file_id, ie.revision), (), None, [])])
 
266
    # Remember for next time
 
267
    existing_children = set()
 
268
    child_modes = {}
 
269
    for name, child_mode, child_hexsha in tree.iteritems():
 
270
        existing_children.add(name)
 
271
        child_path = posixpath.join(path, name)
 
272
        if type(base_tree) is Tree:
 
273
            try:
 
274
                child_base_mode, child_base_hexsha = base_tree[name]
 
275
            except KeyError:
 
276
                child_base_hexsha = None
 
277
                child_base_mode = 0
 
278
        else:
 
279
            child_base_hexsha = None
 
280
            child_base_mode = 0
 
281
        if stat.S_ISDIR(child_mode):
 
282
            subinvdelta, grandchildmodes = import_git_tree(texts, mapping,
 
283
                child_path, name, (child_base_hexsha, child_hexsha), base_inv,
 
284
                file_id, revision_id, parent_invs, lookup_object,
 
285
                (child_base_mode, child_mode), store_updater, lookup_file_id,
 
286
                allow_submodules=allow_submodules)
 
287
        elif S_ISGITLINK(child_mode): # submodule
 
288
            if not allow_submodules:
 
289
                raise SubmodulesRequireSubtrees()
 
290
            subinvdelta, grandchildmodes = import_git_submodule(texts, mapping,
 
291
                child_path, name, (child_base_hexsha, child_hexsha), base_inv,
 
292
                file_id, revision_id, parent_invs, lookup_object,
 
293
                (child_base_mode, child_mode), store_updater, lookup_file_id)
 
294
        else:
 
295
            subinvdelta = import_git_blob(texts, mapping, child_path, name,
 
296
                (child_base_hexsha, child_hexsha), base_inv, file_id,
 
297
                revision_id, parent_invs, lookup_object,
 
298
                (child_base_mode, child_mode), store_updater, lookup_file_id)
 
299
            grandchildmodes = {}
 
300
        child_modes.update(grandchildmodes)
 
301
        invdelta.extend(subinvdelta)
 
302
        if child_mode not in (stat.S_IFDIR, DEFAULT_FILE_MODE,
 
303
                        stat.S_IFLNK, DEFAULT_FILE_MODE|0111):
 
304
            child_modes[child_path] = child_mode
 
305
    # Remove any children that have disappeared
 
306
    if base_tree is not None and type(base_tree) is Tree:
 
307
        invdelta.extend(remove_disappeared_children(base_inv, old_path,
 
308
            base_tree, existing_children, lookup_object))
 
309
    store_updater.add_object(tree, ie, path)
 
310
    return invdelta, child_modes
 
311
 
 
312
 
 
313
def verify_commit_reconstruction(target_git_object_retriever, lookup_object,
 
314
    o, rev, ret_tree, parent_trees, mapping, unusual_modes, verifiers):
 
315
    new_unusual_modes = mapping.export_unusual_file_modes(rev)
 
316
    if new_unusual_modes != unusual_modes:
 
317
        raise AssertionError("unusual modes don't match: %r != %r" % (
 
318
            unusual_modes, new_unusual_modes))
 
319
    # Verify that we can reconstruct the commit properly
 
320
    rec_o = target_git_object_retriever._reconstruct_commit(rev, o.tree, True,
 
321
        verifiers)
 
322
    if rec_o != o:
 
323
        raise AssertionError("Reconstructed commit differs: %r != %r" % (
 
324
            rec_o, o))
 
325
    diff = []
 
326
    new_objs = {}
 
327
    for path, obj, ie in _tree_to_objects(ret_tree, parent_trees,
 
328
        target_git_object_retriever._cache.idmap, unusual_modes,
 
329
        mapping.BZR_DUMMY_FILE):
 
330
        old_obj_id = tree_lookup_path(lookup_object, o.tree, path)[1]
 
331
        new_objs[path] = obj
 
332
        if obj.id != old_obj_id:
 
333
            diff.append((path, lookup_object(old_obj_id), obj))
 
334
    for (path, old_obj, new_obj) in diff:
 
335
        while (old_obj.type_name == "tree" and
 
336
               new_obj.type_name == "tree" and
 
337
               sorted(old_obj) == sorted(new_obj)):
 
338
            for name in old_obj:
 
339
                if old_obj[name][0] != new_obj[name][0]:
 
340
                    raise AssertionError("Modes for %s differ: %o != %o" %
 
341
                        (path, old_obj[name][0], new_obj[name][0]))
 
342
                if old_obj[name][1] != new_obj[name][1]:
 
343
                    # Found a differing child, delve deeper
 
344
                    path = posixpath.join(path, name)
 
345
                    old_obj = lookup_object(old_obj[name][1])
 
346
                    new_obj = new_objs[path]
 
347
                    break
 
348
        raise AssertionError("objects differ for %s: %r != %r" % (path,
 
349
            old_obj, new_obj))
 
350
 
 
351
 
 
352
def import_git_commit(repo, mapping, head, lookup_object,
 
353
                      target_git_object_retriever, trees_cache):
 
354
    o = lookup_object(head)
 
355
    # Note that this uses mapping.revision_id_foreign_to_bzr. If the parents
 
356
    # were bzr roundtripped revisions they would be specified in the
 
357
    # roundtrip data.
 
358
    rev, roundtrip_revid, verifiers = mapping.import_commit(
 
359
        o, mapping.revision_id_foreign_to_bzr)
 
360
    # We have to do this here, since we have to walk the tree and
 
361
    # we need to make sure to import the blobs / trees with the right
 
362
    # path; this may involve adding them more than once.
 
363
    parent_trees = trees_cache.revision_trees(rev.parent_ids)
 
364
    if parent_trees == []:
 
365
        base_inv = Inventory(root_id=None)
 
366
        base_tree = None
 
367
        base_mode = None
 
368
    else:
 
369
        base_inv = parent_trees[0].inventory
 
370
        base_tree = lookup_object(o.parents[0]).tree
 
371
        base_mode = stat.S_IFDIR
 
372
    store_updater = target_git_object_retriever._get_updater(rev)
 
373
    fileid_map = mapping.get_fileid_map(lookup_object, o.tree)
 
374
    inv_delta, unusual_modes = import_git_tree(repo.texts,
 
375
            mapping, "", "", (base_tree, o.tree), base_inv,
 
376
            None, rev.revision_id, [p.inventory for p in parent_trees],
 
377
            lookup_object, (base_mode, stat.S_IFDIR), store_updater,
 
378
            fileid_map.lookup_file_id,
 
379
            allow_submodules=getattr(repo._format, "supports_tree_reference",
 
380
                False))
 
381
    if unusual_modes != {}:
 
382
        for path, mode in unusual_modes.iteritems():
 
383
            warn_unusual_mode(rev.foreign_revid, path, mode)
 
384
        mapping.import_unusual_file_modes(rev, unusual_modes)
 
385
    try:
 
386
        basis_id = rev.parent_ids[0]
 
387
    except IndexError:
 
388
        basis_id = NULL_REVISION
 
389
        base_inv = None
 
390
    rev.inventory_sha1, inv = repo.add_inventory_by_delta(basis_id,
 
391
              inv_delta, rev.revision_id, rev.parent_ids, base_inv)
 
392
    ret_tree = InventoryRevisionTree(repo, inv, rev.revision_id)
 
393
    if roundtrip_revid is not None:
 
394
        original_revid = rev.revision_id
 
395
        rev.revision_id = roundtrip_revid
 
396
        # Check verifiers
 
397
        if getattr(StrictTestament3, "from_revision_tree", None):
 
398
            testament = StrictTestament3(rev, ret_tree)
 
399
        else: # bzr < 2.4
 
400
            testament = StrictTestament3(rev, inv)
 
401
        calculated_verifiers = { "testament3-sha1": testament.as_sha1() }
 
402
        if calculated_verifiers != verifiers:
 
403
            trace.mutter("Testament SHA1 %r for %r did not match %r.",
 
404
                         calculated_verifiers["testament3-sha1"],
 
405
                         rev.revision_id, verifiers["testament3-sha1"])
 
406
            rev.revision_id = original_revid
 
407
    else:
 
408
        calculated_verifiers = {}
 
409
    store_updater.add_object(o, calculated_verifiers, None)
 
410
    store_updater.finish()
 
411
    trees_cache.add(ret_tree)
 
412
    repo.add_revision(rev.revision_id, rev)
 
413
    if "verify" in debug.debug_flags:
 
414
        verify_commit_reconstruction(target_git_object_retriever, 
 
415
            lookup_object, o, rev, ret_tree, parent_trees, mapping,
 
416
            unusual_modes, verifiers)
 
417
 
 
418
 
 
419
def import_git_objects(repo, mapping, object_iter,
 
420
    target_git_object_retriever, heads, pb=None, limit=None):
110
421
    """Import a set of git objects into a bzr repository.
111
422
 
112
 
    :param repo: Bazaar repository
 
423
    :param repo: Target Bazaar repository
113
424
    :param mapping: Mapping to use
114
425
    :param object_iter: Iterator over Git objects.
 
426
    :return: Tuple with pack hints and last imported revision id
115
427
    """
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 = {}
 
428
    def lookup_object(sha):
 
429
        try:
 
430
            return object_iter[sha]
 
431
        except KeyError:
 
432
            return target_git_object_retriever[sha]
 
433
    graph = []
 
434
    checked = set()
 
435
    heads = list(set(heads))
 
436
    trees_cache = LRUTreeCache(repo)
121
437
    # Find and convert commit objects
122
 
    for o in objects.iterkeys():
 
438
    while heads:
 
439
        if pb is not None:
 
440
            pb.update("finding revisions to fetch", len(graph), None)
 
441
        head = heads.pop()
 
442
        assert isinstance(head, str)
 
443
        try:
 
444
            o = lookup_object(head)
 
445
        except KeyError:
 
446
            continue
123
447
        if isinstance(o, Commit):
124
 
            rev = mapping.import_commit(o)
125
 
            root_trees[rev] = objects[o.tree_sha]
 
448
            rev, roundtrip_revid, verifiers = mapping.import_commit(o,
 
449
                mapping.revision_id_foreign_to_bzr)
 
450
            if (repo.has_revision(rev.revision_id) or
 
451
                (roundtrip_revid and repo.has_revision(roundtrip_revid))):
 
452
                continue
 
453
            graph.append((o.id, o.parents))
 
454
            heads.extend([p for p in o.parents if p not in checked])
 
455
        elif isinstance(o, Tag):
 
456
            if o.object[1] not in checked:
 
457
                heads.append(o.object[1])
 
458
        else:
 
459
            trace.warning("Unable to import head object %r" % o)
 
460
        checked.add(o.id)
 
461
    del checked
 
462
    # Order the revisions
126
463
    # 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)
 
464
    batch_size = 1000
 
465
    revision_ids = topo_sort(graph)
 
466
    pack_hints = []
 
467
    if limit is not None:
 
468
        revision_ids = revision_ids[:limit]
 
469
    last_imported = None
 
470
    for offset in range(0, len(revision_ids), batch_size):
 
471
        target_git_object_retriever.start_write_group() 
 
472
        try:
 
473
            repo.start_write_group()
 
474
            try:
 
475
                for i, head in enumerate(
 
476
                    revision_ids[offset:offset+batch_size]):
 
477
                    if pb is not None:
 
478
                        pb.update("fetching revisions", offset+i,
 
479
                                  len(revision_ids))
 
480
                    import_git_commit(repo, mapping, head, lookup_object,
 
481
                        target_git_object_retriever, trees_cache)
 
482
                    last_imported = head
 
483
            except:
 
484
                repo.abort_write_group()
 
485
                raise
 
486
            else:
 
487
                hint = repo.commit_write_group()
 
488
                if hint is not None:
 
489
                    pack_hints.extend(hint)
 
490
        except:
 
491
            target_git_object_retriever.abort_write_group()
 
492
            raise
 
493
        else:
 
494
            target_git_object_retriever.commit_write_group()
 
495
    return pack_hints, last_imported
157
496
 
158
497
 
159
498
class InterGitRepository(InterRepository):
160
499
 
161
 
    _matching_repo_format = GitFormat()
 
500
    _matching_repo_format = GitRepositoryFormat()
 
501
 
 
502
    def _target_has_shas(self, shas):
 
503
        raise NotImplementedError(self._target_has_shas)
 
504
 
 
505
    def get_determine_wants_heads(self, wants, include_tags=False):
 
506
        wants = set(wants)
 
507
        def determine_wants(refs):
 
508
            potential = set(wants)
 
509
            if include_tags:
 
510
                potential.update(
 
511
                    [v[1] or v[0] for v in extract_tags(refs).itervalues()])
 
512
            return list(potential - self._target_has_shas(potential))
 
513
        return determine_wants
 
514
 
 
515
    def determine_wants_all(self, refs):
 
516
        potential = set([sha for (ref, sha) in refs.iteritems() if not
 
517
            ref.endswith("^{}")])
 
518
        return list(potential - self._target_has_shas(potential))
162
519
 
163
520
    @staticmethod
164
521
    def _get_repo_format_to_test():
168
525
        """See InterRepository.copy_content."""
169
526
        self.fetch(revision_id, pb, find_ghosts=False)
170
527
 
171
 
    def fetch(self, revision_id=None, pb=None, find_ghosts=False, 
172
 
              mapping=None):
173
 
        if mapping is None:
174
 
            mapping = self.source.get_mapping()
175
 
        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)
187
 
        self.target.lock_write()
188
 
        try:
189
 
            import_git_objects(self.target, mapping,
190
 
                self.source.fetch_objects(determine_wants, graph_walker, 
191
 
                    progress))
192
 
        finally:
193
 
            self.target.unlock()
194
 
 
195
 
    @staticmethod
196
 
    def is_compatible(source, target):
197
 
        """Be compatible with GitRepository."""
198
 
        # FIXME: Also check target uses VersionedFile
199
 
        return (isinstance(source, LocalGitRepository) and 
200
 
                target.supports_rich_root())
 
528
 
 
529
class InterGitNonGitRepository(InterGitRepository):
 
530
    """Base InterRepository that copies revisions from a Git into a non-Git
 
531
    repository."""
 
532
 
 
533
    def _target_has_shas(self, shas):
 
534
        revids = [self.source.lookup_foreign_revision_id(sha) for sha in shas]
 
535
        return self.target.has_revisions(revids)
 
536
 
 
537
    def get_determine_wants_revids(self, revids, include_tags=False):
 
538
        wants = set()
 
539
        for revid in set(revids):
 
540
            git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
 
541
            wants.add(git_sha)
 
542
        return self.get_determine_wants_heads(wants,
 
543
            include_tags=include_tags)
 
544
 
 
545
    def fetch_objects(self, determine_wants, mapping, pb=None, limit=None):
 
546
        """Fetch objects from a remote server.
 
547
 
 
548
        :param determine_wants: determine_wants callback
 
549
        :param mapping: BzrGitMapping to use
 
550
        :param pb: Optional progress bar
 
551
        :param limit: Maximum number of commits to import.
 
552
        :return: Tuple with pack hint, last imported revision id and remote refs
 
553
        """
 
554
        raise NotImplementedError(self.fetch_objects)
 
555
 
 
556
    def fetch(self, revision_id=None, pb=None, find_ghosts=False,
 
557
              mapping=None, fetch_spec=None):
 
558
        if mapping is None:
 
559
            mapping = self.source.get_mapping()
 
560
        if revision_id is not None:
 
561
            interesting_heads = [revision_id]
 
562
        elif fetch_spec is not None:
 
563
            recipe = fetch_spec.get_recipe()
 
564
            if recipe[0] in ("search", "proxy-search"):
 
565
                interesting_heads = recipe[1]
 
566
            else:
 
567
                raise AssertionError("Unsupported search result type %s" %
 
568
                        recipe[0])
 
569
        else:
 
570
            interesting_heads = None
 
571
 
 
572
        if interesting_heads is not None:
 
573
            determine_wants = self.get_determine_wants_revids(
 
574
                interesting_heads, include_tags=False)
 
575
        else:
 
576
            determine_wants = self.determine_wants_all
 
577
 
 
578
        (pack_hint, _, remote_refs) = self.fetch_objects(determine_wants,
 
579
            mapping, pb)
 
580
        if pack_hint is not None and self.target._format.pack_compresses:
 
581
            self.target.pack(hint=pack_hint)
 
582
        assert isinstance(remote_refs, dict)
 
583
        return remote_refs
 
584
 
 
585
 
 
586
_GIT_PROGRESS_RE = re.compile(r"(.*?): +(\d+)% \((\d+)/(\d+)\)")
 
587
def report_git_progress(pb, text):
 
588
    text = text.rstrip("\r\n")
 
589
    g = _GIT_PROGRESS_RE.match(text)
 
590
    if g is not None:
 
591
        (text, pct, current, total) = g.groups()
 
592
        pb.update(text, int(current), int(total))
 
593
    else:
 
594
        pb.update(text, 0, 0)
 
595
 
 
596
 
 
597
class DetermineWantsRecorder(object):
 
598
 
 
599
    def __init__(self, actual):
 
600
        self.actual = actual
 
601
        self.wants = []
 
602
        self.remote_refs = {}
 
603
 
 
604
    def __call__(self, refs):
 
605
        self.remote_refs = refs
 
606
        self.wants = self.actual(refs)
 
607
        return self.wants
 
608
 
 
609
 
 
610
class InterRemoteGitNonGitRepository(InterGitNonGitRepository):
 
611
    """InterRepository that copies revisions from a remote Git into a non-Git
 
612
    repository."""
 
613
 
 
614
    def get_target_heads(self):
 
615
        # FIXME: This should be more efficient
 
616
        all_revs = self.target.all_revision_ids()
 
617
        parent_map = self.target.get_parent_map(all_revs)
 
618
        all_parents = set()
 
619
        map(all_parents.update, parent_map.itervalues())
 
620
        return set(all_revs) - all_parents
 
621
 
 
622
    def fetch_objects(self, determine_wants, mapping, pb=None, limit=None):
 
623
        """See `InterGitNonGitRepository`."""
 
624
        def progress(text):
 
625
            report_git_progress(pb, text)
 
626
        store = BazaarObjectStore(self.target, mapping)
 
627
        store.lock_write()
 
628
        try:
 
629
            heads = self.get_target_heads()
 
630
            graph_walker = store.get_graph_walker(
 
631
                    [store._lookup_revision_sha1(head) for head in heads])
 
632
            wants_recorder = DetermineWantsRecorder(determine_wants)
 
633
 
 
634
            create_pb = None
 
635
            if pb is None:
 
636
                create_pb = pb = ui.ui_factory.nested_progress_bar()
 
637
            try:
 
638
                objects_iter = self.source.fetch_objects(
 
639
                    wants_recorder, graph_walker, store.get_raw,
 
640
                    progress)
 
641
                trace.mutter("Importing %d new revisions",
 
642
                             len(wants_recorder.wants))
 
643
                (pack_hint, last_rev) = import_git_objects(self.target,
 
644
                    mapping, objects_iter, store, wants_recorder.wants, pb,
 
645
                    limit)
 
646
                return (pack_hint, last_rev, wants_recorder.remote_refs)
 
647
            finally:
 
648
                if create_pb:
 
649
                    create_pb.finished()
 
650
        finally:
 
651
            store.unlock()
 
652
 
 
653
    @staticmethod
 
654
    def is_compatible(source, target):
 
655
        """Be compatible with GitRepository."""
 
656
        if not isinstance(source, RemoteGitRepository):
 
657
            return False
 
658
        if not target.supports_rich_root():
 
659
            return False
 
660
        if isinstance(target, GitRepository):
 
661
            return False
 
662
        if not getattr(target._format, "supports_full_versioned_files", True):
 
663
            return False
 
664
        return True
 
665
 
 
666
 
 
667
class InterLocalGitNonGitRepository(InterGitNonGitRepository):
 
668
    """InterRepository that copies revisions from a local Git into a non-Git
 
669
    repository."""
 
670
 
 
671
    def fetch_objects(self, determine_wants, mapping, pb=None, limit=None):
 
672
        """See `InterGitNonGitRepository`."""
 
673
        remote_refs = self.source._git.get_refs()
 
674
        wants = determine_wants(remote_refs)
 
675
        create_pb = None
 
676
        if pb is None:
 
677
            create_pb = pb = ui.ui_factory.nested_progress_bar()
 
678
        target_git_object_retriever = BazaarObjectStore(self.target, mapping)
 
679
        try:
 
680
            target_git_object_retriever.lock_write()
 
681
            try:
 
682
                (pack_hint, last_rev) = import_git_objects(self.target,
 
683
                    mapping, self.source._git.object_store,
 
684
                    target_git_object_retriever, wants, pb, limit)
 
685
                return (pack_hint, last_rev, remote_refs)
 
686
            finally:
 
687
                target_git_object_retriever.unlock()
 
688
        finally:
 
689
            if create_pb:
 
690
                create_pb.finished()
 
691
 
 
692
    @staticmethod
 
693
    def is_compatible(source, target):
 
694
        """Be compatible with GitRepository."""
 
695
        if not isinstance(source, LocalGitRepository):
 
696
            return False
 
697
        if not target.supports_rich_root():
 
698
            return False
 
699
        if isinstance(target, GitRepository):
 
700
            return False
 
701
        if not getattr(target._format, "supports_full_versioned_files", True):
 
702
            return False
 
703
        return True
 
704
 
 
705
 
 
706
class InterGitGitRepository(InterGitRepository):
 
707
    """InterRepository that copies between Git repositories."""
 
708
 
 
709
    def fetch_objects(self, determine_wants, mapping, pb=None):
 
710
        def progress(text):
 
711
            trace.note("git: %s", text)
 
712
        graphwalker = self.target._git.get_graph_walker()
 
713
        if (isinstance(self.source, LocalGitRepository) and
 
714
            isinstance(self.target, LocalGitRepository)):
 
715
            refs = self.source._git.fetch(self.target._git, determine_wants,
 
716
                progress)
 
717
            return (None, None, refs)
 
718
        elif (isinstance(self.source, LocalGitRepository) and
 
719
              isinstance(self.target, RemoteGitRepository)):
 
720
            raise NotImplementedError
 
721
        elif (isinstance(self.source, RemoteGitRepository) and
 
722
              isinstance(self.target, LocalGitRepository)):
 
723
            f, commit = self.target._git.object_store.add_thin_pack()
 
724
            try:
 
725
                refs = self.source.bzrdir.root_transport.fetch_pack(
 
726
                    determine_wants, graphwalker, f.write, progress)
 
727
                commit()
 
728
                return (None, None, refs)
 
729
            except:
 
730
                f.close()
 
731
                raise
 
732
        else:
 
733
            raise AssertionError
 
734
 
 
735
    def _target_has_shas(self, shas):
 
736
        return set([sha for sha in shas if self.target._git.object_store])
 
737
 
 
738
    def fetch(self, revision_id=None, pb=None, find_ghosts=False,
 
739
              mapping=None, fetch_spec=None, branches=None):
 
740
        if mapping is None:
 
741
            mapping = self.source.get_mapping()
 
742
        r = self.target._git
 
743
        if revision_id is not None:
 
744
            args = [mapping.revision_id_bzr_to_foreign(revision_id)[0]]
 
745
        elif fetch_spec is not None:
 
746
            recipe = fetch_spec.get_recipe()
 
747
            if recipe[0] in ("search", "proxy-search"):
 
748
                heads = recipe[1]
 
749
            else:
 
750
                raise AssertionError(
 
751
                    "Unsupported search result type %s" % recipe[0])
 
752
            args = [mapping.revision_id_bzr_to_foreign(revid)[0] for revid in
 
753
                    heads]
 
754
        if branches is not None:
 
755
            determine_wants = lambda x: [x[y] for y in branches if not x[y] in r.object_store and x[y] != ZERO_SHA]
 
756
        elif fetch_spec is None and revision_id is None:
 
757
            determine_wants = self.determine_wants_all
 
758
        else:
 
759
            determine_wants = lambda x: [y for y in args if not y in r.object_store and y != ZERO_SHA]
 
760
        wants_recorder = DetermineWantsRecorder(determine_wants)
 
761
        self.fetch_objects(wants_recorder, mapping)
 
762
        return wants_recorder.remote_refs
 
763
 
 
764
    @staticmethod
 
765
    def is_compatible(source, target):
 
766
        """Be compatible with GitRepository."""
 
767
        return (isinstance(source, GitRepository) and
 
768
                isinstance(target, GitRepository))
 
769
 
 
770
    def get_determine_wants_revids(self, revids, include_tags=False):
 
771
        wants = set()
 
772
        for revid in set(revids):
 
773
            git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
 
774
            wants.add(git_sha)
 
775
        return self.get_determine_wants_heads(wants,
 
776
            include_tags=include_tags)