/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 formatting, remove catch-all for exceptions when opening local repositories.

Show diffs side-by-side

added added

removed removed

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