/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

Fix more tests.

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