/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 test fixes.

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
    tree_supplement = 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
            tree_supplement.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 verifiers:
 
398
            if getattr(StrictTestament3, "from_revision_tree", None):
 
399
                testament = StrictTestament3(rev, ret_tree)
 
400
            else: # bzr < 2.4
 
401
                testament = StrictTestament3(rev, inv)
 
402
            calculated_verifiers = { "testament3-sha1": testament.as_sha1() }
 
403
            if calculated_verifiers != verifiers:
 
404
                trace.mutter("Testament SHA1 %r for %r did not match %r.",
 
405
                             calculated_verifiers["testament3-sha1"],
 
406
                             rev.revision_id, verifiers["testament3-sha1"])
 
407
                rev.revision_id = original_revid
 
408
        else:
 
409
            calculated_verifiers = {}
 
410
    else:
 
411
        calculated_verifiers = {}
 
412
    store_updater.add_object(o, calculated_verifiers, None)
 
413
    store_updater.finish()
 
414
    trees_cache.add(ret_tree)
 
415
    repo.add_revision(rev.revision_id, rev)
 
416
    if "verify" in debug.debug_flags:
 
417
        verify_commit_reconstruction(target_git_object_retriever, 
 
418
            lookup_object, o, rev, ret_tree, parent_trees, mapping,
 
419
            unusual_modes, verifiers)
 
420
 
 
421
 
 
422
def import_git_objects(repo, mapping, object_iter,
 
423
    target_git_object_retriever, heads, pb=None, limit=None):
110
424
    """Import a set of git objects into a bzr repository.
111
425
 
112
 
    :param repo: Bazaar repository
 
426
    :param repo: Target Bazaar repository
113
427
    :param mapping: Mapping to use
114
428
    :param object_iter: Iterator over Git objects.
 
429
    :return: Tuple with pack hints and last imported revision id
115
430
    """
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 = {}
 
431
    def lookup_object(sha):
 
432
        try:
 
433
            return object_iter[sha]
 
434
        except KeyError:
 
435
            return target_git_object_retriever[sha]
 
436
    graph = []
 
437
    checked = set()
 
438
    heads = list(set(heads))
 
439
    trees_cache = LRUTreeCache(repo)
121
440
    # Find and convert commit objects
122
 
    for o in objects.iterkeys():
 
441
    while heads:
 
442
        if pb is not None:
 
443
            pb.update("finding revisions to fetch", len(graph), None)
 
444
        head = heads.pop()
 
445
        assert isinstance(head, str)
 
446
        try:
 
447
            o = lookup_object(head)
 
448
        except KeyError:
 
449
            continue
123
450
        if isinstance(o, Commit):
124
 
            rev = mapping.import_commit(o)
125
 
            root_trees[rev] = objects[o.tree_sha]
 
451
            rev, roundtrip_revid, verifiers = mapping.import_commit(o,
 
452
                mapping.revision_id_foreign_to_bzr)
 
453
            if (repo.has_revision(rev.revision_id) or
 
454
                (roundtrip_revid and repo.has_revision(roundtrip_revid))):
 
455
                continue
 
456
            graph.append((o.id, o.parents))
 
457
            heads.extend([p for p in o.parents if p not in checked])
 
458
        elif isinstance(o, Tag):
 
459
            if o.object[1] not in checked:
 
460
                heads.append(o.object[1])
 
461
        else:
 
462
            trace.warning("Unable to import head object %r" % o)
 
463
        checked.add(o.id)
 
464
    del checked
 
465
    # Order the revisions
126
466
    # 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)
 
467
    batch_size = 1000
 
468
    revision_ids = topo_sort(graph)
 
469
    pack_hints = []
 
470
    if limit is not None:
 
471
        revision_ids = revision_ids[:limit]
 
472
    last_imported = None
 
473
    for offset in range(0, len(revision_ids), batch_size):
 
474
        target_git_object_retriever.start_write_group() 
 
475
        try:
 
476
            repo.start_write_group()
 
477
            try:
 
478
                for i, head in enumerate(
 
479
                    revision_ids[offset:offset+batch_size]):
 
480
                    if pb is not None:
 
481
                        pb.update("fetching revisions", offset+i,
 
482
                                  len(revision_ids))
 
483
                    import_git_commit(repo, mapping, head, lookup_object,
 
484
                        target_git_object_retriever, trees_cache)
 
485
                    last_imported = head
 
486
            except:
 
487
                repo.abort_write_group()
 
488
                raise
 
489
            else:
 
490
                hint = repo.commit_write_group()
 
491
                if hint is not None:
 
492
                    pack_hints.extend(hint)
 
493
        except:
 
494
            target_git_object_retriever.abort_write_group()
 
495
            raise
 
496
        else:
 
497
            target_git_object_retriever.commit_write_group()
 
498
    return pack_hints, last_imported
157
499
 
158
500
 
159
501
class InterGitRepository(InterRepository):
160
502
 
161
 
    _matching_repo_format = GitFormat()
 
503
    _matching_repo_format = GitRepositoryFormat()
 
504
 
 
505
    def _target_has_shas(self, shas):
 
506
        raise NotImplementedError(self._target_has_shas)
 
507
 
 
508
    def get_determine_wants_heads(self, wants, include_tags=False):
 
509
        wants = set(wants)
 
510
        def determine_wants(refs):
 
511
            potential = set(wants)
 
512
            if include_tags:
 
513
                potential.update(
 
514
                    [v[1] or v[0] for v in extract_tags(refs).itervalues()])
 
515
            return list(potential - self._target_has_shas(potential))
 
516
        return determine_wants
 
517
 
 
518
    def determine_wants_all(self, refs):
 
519
        potential = set([sha for (ref, sha) in refs.iteritems() if not
 
520
            ref.endswith("^{}")])
 
521
        return list(potential - self._target_has_shas(potential))
162
522
 
163
523
    @staticmethod
164
524
    def _get_repo_format_to_test():
168
528
        """See InterRepository.copy_content."""
169
529
        self.fetch(revision_id, pb, find_ghosts=False)
170
530
 
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())
 
531
 
 
532
class InterGitNonGitRepository(InterGitRepository):
 
533
    """Base InterRepository that copies revisions from a Git into a non-Git
 
534
    repository."""
 
535
 
 
536
    def _target_has_shas(self, shas):
 
537
        revids = [self.source.lookup_foreign_revision_id(sha) for sha in shas]
 
538
        return self.target.has_revisions(revids)
 
539
 
 
540
    def get_determine_wants_revids(self, revids, include_tags=False):
 
541
        wants = set()
 
542
        for revid in set(revids):
 
543
            git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
 
544
            wants.add(git_sha)
 
545
        return self.get_determine_wants_heads(wants,
 
546
            include_tags=include_tags)
 
547
 
 
548
    def fetch_objects(self, determine_wants, mapping, pb=None, limit=None):
 
549
        """Fetch objects from a remote server.
 
550
 
 
551
        :param determine_wants: determine_wants callback
 
552
        :param mapping: BzrGitMapping to use
 
553
        :param pb: Optional progress bar
 
554
        :param limit: Maximum number of commits to import.
 
555
        :return: Tuple with pack hint, last imported revision id and remote refs
 
556
        """
 
557
        raise NotImplementedError(self.fetch_objects)
 
558
 
 
559
    def fetch(self, revision_id=None, pb=None, find_ghosts=False,
 
560
              mapping=None, fetch_spec=None):
 
561
        if mapping is None:
 
562
            mapping = self.source.get_mapping()
 
563
        if revision_id is not None:
 
564
            interesting_heads = [revision_id]
 
565
        elif fetch_spec is not None:
 
566
            recipe = fetch_spec.get_recipe()
 
567
            if recipe[0] in ("search", "proxy-search"):
 
568
                interesting_heads = recipe[1]
 
569
            else:
 
570
                raise AssertionError("Unsupported search result type %s" %
 
571
                        recipe[0])
 
572
        else:
 
573
            interesting_heads = None
 
574
 
 
575
        if interesting_heads is not None:
 
576
            determine_wants = self.get_determine_wants_revids(
 
577
                interesting_heads, include_tags=False)
 
578
        else:
 
579
            determine_wants = self.determine_wants_all
 
580
 
 
581
        (pack_hint, _, remote_refs) = self.fetch_objects(determine_wants,
 
582
            mapping, pb)
 
583
        if pack_hint is not None and self.target._format.pack_compresses:
 
584
            self.target.pack(hint=pack_hint)
 
585
        assert isinstance(remote_refs, dict)
 
586
        return remote_refs
 
587
 
 
588
 
 
589
_GIT_PROGRESS_RE = re.compile(r"(.*?): +(\d+)% \((\d+)/(\d+)\)")
 
590
def report_git_progress(pb, text):
 
591
    text = text.rstrip("\r\n")
 
592
    g = _GIT_PROGRESS_RE.match(text)
 
593
    if g is not None:
 
594
        (text, pct, current, total) = g.groups()
 
595
        pb.update(text, int(current), int(total))
 
596
    else:
 
597
        pb.update(text, 0, 0)
 
598
 
 
599
 
 
600
class DetermineWantsRecorder(object):
 
601
 
 
602
    def __init__(self, actual):
 
603
        self.actual = actual
 
604
        self.wants = []
 
605
        self.remote_refs = {}
 
606
 
 
607
    def __call__(self, refs):
 
608
        self.remote_refs = refs
 
609
        self.wants = self.actual(refs)
 
610
        return self.wants
 
611
 
 
612
 
 
613
class InterRemoteGitNonGitRepository(InterGitNonGitRepository):
 
614
    """InterRepository that copies revisions from a remote Git into a non-Git
 
615
    repository."""
 
616
 
 
617
    def get_target_heads(self):
 
618
        # FIXME: This should be more efficient
 
619
        all_revs = self.target.all_revision_ids()
 
620
        parent_map = self.target.get_parent_map(all_revs)
 
621
        all_parents = set()
 
622
        map(all_parents.update, parent_map.itervalues())
 
623
        return set(all_revs) - all_parents
 
624
 
 
625
    def fetch_objects(self, determine_wants, mapping, pb=None, limit=None):
 
626
        """See `InterGitNonGitRepository`."""
 
627
        def progress(text):
 
628
            report_git_progress(pb, text)
 
629
        store = BazaarObjectStore(self.target, mapping)
 
630
        store.lock_write()
 
631
        try:
 
632
            heads = self.get_target_heads()
 
633
            graph_walker = store.get_graph_walker(
 
634
                    [store._lookup_revision_sha1(head) for head in heads])
 
635
            wants_recorder = DetermineWantsRecorder(determine_wants)
 
636
 
 
637
            create_pb = None
 
638
            if pb is None:
 
639
                create_pb = pb = ui.ui_factory.nested_progress_bar()
 
640
            try:
 
641
                objects_iter = self.source.fetch_objects(
 
642
                    wants_recorder, graph_walker, store.get_raw,
 
643
                    progress)
 
644
                trace.mutter("Importing %d new revisions",
 
645
                             len(wants_recorder.wants))
 
646
                (pack_hint, last_rev) = import_git_objects(self.target,
 
647
                    mapping, objects_iter, store, wants_recorder.wants, pb,
 
648
                    limit)
 
649
                return (pack_hint, last_rev, wants_recorder.remote_refs)
 
650
            finally:
 
651
                if create_pb:
 
652
                    create_pb.finished()
 
653
        finally:
 
654
            store.unlock()
 
655
 
 
656
    @staticmethod
 
657
    def is_compatible(source, target):
 
658
        """Be compatible with GitRepository."""
 
659
        if not isinstance(source, RemoteGitRepository):
 
660
            return False
 
661
        if not target.supports_rich_root():
 
662
            return False
 
663
        if isinstance(target, GitRepository):
 
664
            return False
 
665
        if not getattr(target._format, "supports_full_versioned_files", True):
 
666
            return False
 
667
        return True
 
668
 
 
669
 
 
670
class InterLocalGitNonGitRepository(InterGitNonGitRepository):
 
671
    """InterRepository that copies revisions from a local Git into a non-Git
 
672
    repository."""
 
673
 
 
674
    def fetch_objects(self, determine_wants, mapping, pb=None, limit=None):
 
675
        """See `InterGitNonGitRepository`."""
 
676
        remote_refs = self.source._git.get_refs()
 
677
        wants = determine_wants(remote_refs)
 
678
        create_pb = None
 
679
        if pb is None:
 
680
            create_pb = pb = ui.ui_factory.nested_progress_bar()
 
681
        target_git_object_retriever = BazaarObjectStore(self.target, mapping)
 
682
        try:
 
683
            target_git_object_retriever.lock_write()
 
684
            try:
 
685
                (pack_hint, last_rev) = import_git_objects(self.target,
 
686
                    mapping, self.source._git.object_store,
 
687
                    target_git_object_retriever, wants, pb, limit)
 
688
                return (pack_hint, last_rev, remote_refs)
 
689
            finally:
 
690
                target_git_object_retriever.unlock()
 
691
        finally:
 
692
            if create_pb:
 
693
                create_pb.finished()
 
694
 
 
695
    @staticmethod
 
696
    def is_compatible(source, target):
 
697
        """Be compatible with GitRepository."""
 
698
        if not isinstance(source, LocalGitRepository):
 
699
            return False
 
700
        if not target.supports_rich_root():
 
701
            return False
 
702
        if isinstance(target, GitRepository):
 
703
            return False
 
704
        if not getattr(target._format, "supports_full_versioned_files", True):
 
705
            return False
 
706
        return True
 
707
 
 
708
 
 
709
class InterGitGitRepository(InterGitRepository):
 
710
    """InterRepository that copies between Git repositories."""
 
711
 
 
712
    def fetch_objects(self, determine_wants, mapping, pb=None):
 
713
        def progress(text):
 
714
            trace.note("git: %s", text)
 
715
        graphwalker = self.target._git.get_graph_walker()
 
716
        if (isinstance(self.source, LocalGitRepository) and
 
717
            isinstance(self.target, LocalGitRepository)):
 
718
            refs = self.source._git.fetch(self.target._git, determine_wants,
 
719
                progress)
 
720
            return (None, None, refs)
 
721
        elif (isinstance(self.source, LocalGitRepository) and
 
722
              isinstance(self.target, RemoteGitRepository)):
 
723
            raise NotImplementedError
 
724
        elif (isinstance(self.source, RemoteGitRepository) and
 
725
              isinstance(self.target, LocalGitRepository)):
 
726
            f, commit = self.target._git.object_store.add_thin_pack()
 
727
            try:
 
728
                refs = self.source.bzrdir.root_transport.fetch_pack(
 
729
                    determine_wants, graphwalker, f.write, progress)
 
730
                commit()
 
731
                return (None, None, refs)
 
732
            except:
 
733
                f.close()
 
734
                raise
 
735
        else:
 
736
            raise AssertionError
 
737
 
 
738
    def _target_has_shas(self, shas):
 
739
        return set([sha for sha in shas if self.target._git.object_store])
 
740
 
 
741
    def fetch(self, revision_id=None, pb=None, find_ghosts=False,
 
742
              mapping=None, fetch_spec=None, branches=None):
 
743
        if mapping is None:
 
744
            mapping = self.source.get_mapping()
 
745
        r = self.target._git
 
746
        if revision_id is not None:
 
747
            args = [self.source.lookup_bzr_revision_id(revision_id)[0]]
 
748
        elif fetch_spec is not None:
 
749
            recipe = fetch_spec.get_recipe()
 
750
            if recipe[0] in ("search", "proxy-search"):
 
751
                heads = recipe[1]
 
752
            else:
 
753
                raise AssertionError(
 
754
                    "Unsupported search result type %s" % recipe[0])
 
755
            args = [mapping.revision_id_bzr_to_foreign(revid)[0] for revid in
 
756
                    heads]
 
757
        if branches is not None:
 
758
            determine_wants = lambda x: [x[y] for y in branches if not x[y] in r.object_store and x[y] != ZERO_SHA]
 
759
        elif fetch_spec is None and revision_id is None:
 
760
            determine_wants = self.determine_wants_all
 
761
        else:
 
762
            determine_wants = lambda x: [y for y in args if not y in r.object_store and y != ZERO_SHA]
 
763
        wants_recorder = DetermineWantsRecorder(determine_wants)
 
764
        self.fetch_objects(wants_recorder, mapping)
 
765
        return wants_recorder.remote_refs
 
766
 
 
767
    @staticmethod
 
768
    def is_compatible(source, target):
 
769
        """Be compatible with GitRepository."""
 
770
        return (isinstance(source, GitRepository) and
 
771
                isinstance(target, GitRepository))
 
772
 
 
773
    def get_determine_wants_revids(self, revids, include_tags=False):
 
774
        wants = set()
 
775
        for revid in set(revids):
 
776
            git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
 
777
            wants.add(git_sha)
 
778
        return self.get_determine_wants_heads(wants,
 
779
            include_tags=include_tags)