/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 bzr 2.3 compatibility.

Show diffs side-by-side

added added

removed removed

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