/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

Provide right infrastructure for foreign repository tests from bzrlib.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2008 Canonical Ltd
 
1
# Copyright (C) 2008 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, ui, urlutils
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
 
from bzrlib.tsort import topo_sort
23
 
 
24
 
from bzrlib.plugins.git import git
 
17
from dulwich.objects import (
 
18
    Commit,
 
19
    Tag,
 
20
    S_ISGITLINK,
 
21
    )
 
22
from dulwich.object_store import (
 
23
    tree_lookup_path,
 
24
    )
 
25
import re
 
26
import stat
 
27
 
 
28
from bzrlib import (
 
29
    debug,
 
30
    osutils,
 
31
    trace,
 
32
    ui,
 
33
    urlutils,
 
34
    )
 
35
from bzrlib.errors import (
 
36
    BzrError,
 
37
    NoSuchId,
 
38
    )
 
39
from bzrlib.inventory import (
 
40
    Inventory,
 
41
    InventoryDirectory,
 
42
    InventoryFile,
 
43
    InventoryLink,
 
44
    )
 
45
from bzrlib.lru_cache import (
 
46
    LRUCache,
 
47
    )
 
48
from bzrlib.repository import (
 
49
    InterRepository,
 
50
    )
 
51
from bzrlib.revision import (
 
52
    NULL_REVISION,
 
53
    )
 
54
from bzrlib.tsort import (
 
55
    topo_sort,
 
56
    )
 
57
from bzrlib.versionedfile import (
 
58
    FulltextContentFactory,
 
59
    )
 
60
 
 
61
from bzrlib.plugins.git.mapping import (
 
62
    DEFAULT_FILE_MODE,
 
63
    inventory_to_tree_and_blobs,
 
64
    mode_is_executable,
 
65
    squash_revision,
 
66
    warn_unusual_mode,
 
67
    )
 
68
from bzrlib.plugins.git.object_store import (
 
69
    BazaarObjectStore,
 
70
    )
 
71
from bzrlib.plugins.git.remote import (
 
72
    RemoteGitRepository,
 
73
    )
25
74
from bzrlib.plugins.git.repository import (
26
 
        LocalGitRepository, 
27
 
        GitRepository, 
28
 
        GitFormat,
29
 
        )
30
 
from bzrlib.plugins.git.remote import RemoteGitRepository
31
 
 
32
 
from dulwich.objects import Commit
33
 
 
34
 
from cStringIO import StringIO
35
 
 
36
 
 
37
 
class BzrFetchGraphWalker(object):
38
 
 
39
 
    def __init__(self, repository, mapping):
40
 
        self.repository = repository
41
 
        self.mapping = mapping
42
 
        self.done = set()
43
 
        self.heads = set(repository.all_revision_ids())
44
 
        self.parents = {}
45
 
 
46
 
    def ack(self, sha):
47
 
        revid = self.mapping.revision_id_foreign_to_bzr(sha)
48
 
        self.remove(revid)
49
 
 
50
 
    def remove(self, revid):
51
 
        self.done.add(revid)
52
 
        if ref in self.heads:
53
 
            self.heads.remove(revid)
54
 
        if revid in self.parents:
55
 
            for p in self.parents[revid]:
56
 
                self.remove(p)
57
 
 
58
 
    def next(self):
59
 
        while self.heads:
60
 
            ret = self.heads.pop()
61
 
            ps = self.repository.get_parent_map([ret])[ret]
62
 
            self.parents[ret] = ps
63
 
            self.heads.update([p for p in ps if not p in self.done])
64
 
            try:
65
 
                self.done.add(ret)
66
 
                return self.mapping.revision_id_bzr_to_foreign(ret)
67
 
            except InvalidRevisionId:
68
 
                pass
69
 
        return None
70
 
 
71
 
 
72
 
def import_git_blob(repo, mapping, path, blob, inv, parent_invs, executable):
 
75
    GitRepository, 
 
76
    GitRepositoryFormat,
 
77
    LocalGitRepository,
 
78
    )
 
79
 
 
80
 
 
81
def import_git_blob(texts, mapping, path, hexsha, base_inv, base_ie, parent_id, 
 
82
    revision_id, parent_invs, shagitmap, lookup_object, executable, symlink):
73
83
    """Import a git blob object into a bzr repository.
74
84
 
75
 
    :param repo: bzr repository
 
85
    :param texts: VersionedFiles to add to
76
86
    :param path: Path in the tree
77
87
    :param blob: A git blob
 
88
    :return: Inventory delta for this file
78
89
    """
79
90
    file_id = mapping.generate_file_id(path)
80
 
    text_revision = inv.revision_id
81
 
    repo.texts.add_lines((file_id, text_revision),
82
 
        [(file_id, p[file_id].revision) for p in parent_invs if file_id in p],
83
 
        osutils.split_lines(blob.data))
84
 
    ie = inv.add_path(path, "file", file_id)
85
 
    ie.revision = text_revision
86
 
    ie.text_size = len(blob.data)
87
 
    ie.text_sha1 = osutils.sha_string(blob.data)
 
91
    if symlink:
 
92
        cls = InventoryLink
 
93
    else:
 
94
        cls = InventoryFile
 
95
    # We just have to hope this is indeed utf-8:
 
96
    ie = cls(file_id, urlutils.basename(path).decode("utf-8"), parent_id)
88
97
    ie.executable = executable
89
 
 
90
 
 
91
 
def import_git_tree(repo, mapping, path, tree, inv, parent_invs, lookup_object):
 
98
    # See if this has changed at all
 
99
    if base_ie is None:
 
100
        base_sha = None
 
101
    else:
 
102
        try:
 
103
            base_sha = shagitmap.lookup_blob(file_id, base_ie.revision)
 
104
        except KeyError:
 
105
            base_sha = None
 
106
        else:
 
107
            if (base_sha == hexsha and base_ie.executable == ie.executable
 
108
                and base_ie.kind == ie.kind):
 
109
                # If nothing has changed since the base revision, we're done
 
110
                return [], []
 
111
    if base_sha == hexsha and base_ie.kind == ie.kind:
 
112
        ie.text_size = base_ie.text_size
 
113
        ie.text_sha1 = base_ie.text_sha1
 
114
        ie.symlink_target = base_ie.symlink_target
 
115
        if ie.executable == base_ie.executable:
 
116
            ie.revision = base_ie.revision
 
117
        else:
 
118
            blob = lookup_object(hexsha)
 
119
    else:
 
120
        blob = lookup_object(hexsha)
 
121
        if ie.kind == "symlink":
 
122
            ie.revision = None
 
123
            ie.symlink_target = blob.data
 
124
            ie.text_size = None
 
125
            ie.text_sha1 = None
 
126
        else:
 
127
            ie.text_size = len(blob.data)
 
128
            ie.text_sha1 = osutils.sha_string(blob.data)
 
129
    # Check what revision we should store
 
130
    parent_keys = []
 
131
    for pinv in parent_invs:
 
132
        if pinv.revision_id == base_inv.revision_id:
 
133
            pie = base_ie
 
134
            if pie is None:
 
135
                continue
 
136
        else:
 
137
            try:
 
138
                pie = pinv[file_id]
 
139
            except NoSuchId:
 
140
                continue
 
141
        if pie.text_sha1 == ie.text_sha1 and pie.executable == ie.executable and pie.symlink_target == ie.symlink_target:
 
142
            # found a revision in one of the parents to use
 
143
            ie.revision = pie.revision
 
144
            break
 
145
        parent_keys.append((file_id, pie.revision))
 
146
    if ie.revision is None:
 
147
        # Need to store a new revision
 
148
        ie.revision = revision_id
 
149
        assert file_id is not None
 
150
        assert ie.revision is not None
 
151
        texts.insert_record_stream([FulltextContentFactory((file_id, ie.revision), tuple(parent_keys), ie.text_sha1, blob.data)])
 
152
        shamap = [(hexsha, "blob", (ie.file_id, ie.revision))]
 
153
    else:
 
154
        shamap = []
 
155
    invdelta = []
 
156
    if base_ie is not None: 
 
157
        old_path = base_inv.id2path(file_id)
 
158
        if base_ie.kind == "directory":
 
159
            invdelta.extend(remove_disappeared_children(old_path, base_ie.children, []))
 
160
    else:
 
161
        old_path = None
 
162
    invdelta.append((old_path, path, file_id, ie))
 
163
    return (invdelta, shamap)
 
164
 
 
165
 
 
166
class SubmodulesNotSupported(BzrError):
 
167
 
 
168
    _fmt = """Submodules can not yet be imported (requires nested tree support in Bazaar)."""
 
169
    internal = False
 
170
 
 
171
 
 
172
def import_git_submodule(texts, mapping, path, hexsha, base_inv, base_ie, 
 
173
    parent_id, revision_id, parent_invs, shagitmap, lookup_object):
 
174
    raise SubmodulesNotSupported()
 
175
 
 
176
 
 
177
def remove_disappeared_children(path, base_children, existing_children):
 
178
    ret = []
 
179
    deletable = [(osutils.pathjoin(path, k), v) for k,v in base_children.iteritems() if k not in existing_children]
 
180
    while deletable:
 
181
        (path, ie) = deletable.pop()
 
182
        ret.append((path, None, ie.file_id, None))
 
183
        if ie.kind == "directory":
 
184
            for name, child_ie in ie.children.iteritems():
 
185
                deletable.append((osutils.pathjoin(path, name), child_ie))
 
186
    return ret
 
187
 
 
188
 
 
189
def import_git_tree(texts, mapping, path, hexsha, base_inv, base_ie, parent_id, 
 
190
    revision_id, parent_invs, shagitmap, lookup_object):
92
191
    """Import a git tree object into a bzr repository.
93
192
 
94
 
    :param repo: A Bzr repository object
 
193
    :param texts: VersionedFiles object to add to
95
194
    :param path: Path in the tree
96
195
    :param tree: A git tree object
97
 
    :param inv: Inventory object
 
196
    :param base_inv: Base inventory against which to return inventory delta
 
197
    :return: Inventory delta for this subtree
98
198
    """
 
199
    invdelta = []
99
200
    file_id = mapping.generate_file_id(path)
100
 
    text_revision = inv.revision_id
101
 
    repo.texts.add_lines((file_id, text_revision),
102
 
        [(file_id, p[file_id].revision) for p in parent_invs if file_id in p],
103
 
        [])
104
 
    ie = inv.add_path(path, "directory", file_id)
105
 
    ie.revision = text_revision
106
 
    for mode, name, hexsha in tree.entries():
107
 
        entry_kind = (mode & 0700000) / 0100000
 
201
    # We just have to hope this is indeed utf-8:
 
202
    ie = InventoryDirectory(file_id, urlutils.basename(path.decode("utf-8")), 
 
203
        parent_id)
 
204
    if base_ie is None:
 
205
        # Newly appeared here
 
206
        ie.revision = revision_id
 
207
        texts.insert_record_stream([FulltextContentFactory((file_id, ie.revision), (), None, "")])
 
208
        invdelta.append((None, path, file_id, ie))
 
209
    else:
 
210
        # See if this has changed at all
 
211
        try:
 
212
            base_sha = shagitmap.lookup_tree(file_id, base_inv.revision_id)
 
213
        except KeyError:
 
214
            pass
 
215
        else:
 
216
            if base_sha == hexsha:
 
217
                # If nothing has changed since the base revision, we're done
 
218
                return [], {}, []
 
219
        if base_ie.kind != "directory":
 
220
            ie.revision = revision_id
 
221
            texts.insert_record_stream([FulltextContentFactory((ie.file_id, ie.revision), (), None, "")])
 
222
            invdelta.append((base_inv.id2path(ie.file_id), path, ie.file_id, ie))
 
223
    if base_ie is not None and base_ie.kind == "directory":
 
224
        base_children = base_ie.children
 
225
    else:
 
226
        base_children = {}
 
227
    # Remember for next time
 
228
    existing_children = set()
 
229
    child_modes = {}
 
230
    shamap = []
 
231
    tree = lookup_object(hexsha)
 
232
    for mode, name, child_hexsha in tree.entries():
108
233
        basename = name.decode("utf-8")
109
 
        if path == "":
110
 
            child_path = name
111
 
        else:
112
 
            child_path = urlutils.join(path, name)
113
 
        if entry_kind == 0:
114
 
            tree = lookup_object(hexsha)
115
 
            import_git_tree(repo, mapping, child_path, tree, inv, parent_invs, lookup_object)
116
 
        elif entry_kind == 1:
117
 
            blob = lookup_object(hexsha)
118
 
            fs_mode = mode & 0777
119
 
            import_git_blob(repo, mapping, child_path, blob, inv, parent_invs, bool(fs_mode & 0111))
120
 
        else:
121
 
            raise AssertionError("Unknown blob kind, perms=%r." % (mode,))
122
 
 
123
 
 
124
 
def import_git_objects(repo, mapping, object_iter, pb=None):
 
234
        existing_children.add(basename)
 
235
        child_path = osutils.pathjoin(path, name)
 
236
        if stat.S_ISDIR(mode):
 
237
            subinvdelta, grandchildmodes, subshamap = import_git_tree(
 
238
                    texts, mapping, child_path, child_hexsha, base_inv, 
 
239
                    base_children.get(basename), file_id, revision_id, parent_invs, shagitmap,
 
240
                    lookup_object)
 
241
            invdelta.extend(subinvdelta)
 
242
            child_modes.update(grandchildmodes)
 
243
            shamap.extend(subshamap)
 
244
        elif S_ISGITLINK(mode): # submodule
 
245
            subinvdelta, grandchildmodes, subshamap = import_git_submodule(
 
246
                    texts, mapping, child_path, child_hexsha, base_inv, base_children.get(basename),
 
247
                    file_id, revision_id, parent_invs, shagitmap, lookup_object)
 
248
            invdelta.extend(subinvdelta)
 
249
            child_modes.update(grandchildmodes)
 
250
            shamap.extend(subshamap)
 
251
        else:
 
252
            subinvdelta, subshamap = import_git_blob(texts, mapping, 
 
253
                    child_path, child_hexsha, base_inv, base_children.get(basename), file_id,
 
254
                    revision_id, parent_invs, shagitmap, lookup_object, 
 
255
                    mode_is_executable(mode), stat.S_ISLNK(mode))
 
256
            invdelta.extend(subinvdelta)
 
257
            shamap.extend(subshamap)
 
258
        if mode not in (stat.S_IFDIR, DEFAULT_FILE_MODE,
 
259
                        stat.S_IFLNK, DEFAULT_FILE_MODE|0111):
 
260
            child_modes[child_path] = mode
 
261
    # Remove any children that have disappeared
 
262
    if base_ie is not None and base_ie.kind == "directory":
 
263
        invdelta.extend(remove_disappeared_children(base_inv.id2path(file_id), 
 
264
            base_children, existing_children))
 
265
    shamap.append((hexsha, "tree", (file_id, revision_id)))
 
266
    return invdelta, child_modes, shamap
 
267
 
 
268
 
 
269
def import_git_objects(repo, mapping, object_iter, target_git_object_retriever, 
 
270
        heads, pb=None):
125
271
    """Import a set of git objects into a bzr repository.
126
272
 
127
 
    :param repo: Bazaar repository
 
273
    :param repo: Target Bazaar repository
128
274
    :param mapping: Mapping to use
129
275
    :param object_iter: Iterator over Git objects.
130
276
    """
 
277
    def lookup_object(sha):
 
278
        try:
 
279
            return object_iter[sha]
 
280
        except KeyError:
 
281
            return target_git_object_retriever[sha]
131
282
    # TODO: a more (memory-)efficient implementation of this
132
 
    objects = {}
133
 
    for i, o in enumerate(object_iter):
134
 
        if pb is not None:
135
 
            pb.update("fetching objects", i) 
136
 
        objects[o.id] = o
137
283
    graph = []
138
284
    root_trees = {}
139
285
    revisions = {}
 
286
    checked = set()
 
287
    heads = list(heads)
 
288
    parent_invs_cache = LRUCache(50)
140
289
    # Find and convert commit objects
141
 
    for o in objects.itervalues():
 
290
    while heads:
 
291
        if pb is not None:
 
292
            pb.update("finding revisions to fetch", len(graph), None)
 
293
        head = heads.pop()
 
294
        assert isinstance(head, str)
 
295
        try:
 
296
            o = lookup_object(head)
 
297
        except KeyError:
 
298
            trace.mutter('missing head %s', head)
 
299
            continue
142
300
        if isinstance(o, Commit):
143
301
            rev = mapping.import_commit(o)
144
 
            root_trees[rev.revision_id] = objects[o.tree]
 
302
            if repo.has_revision(rev.revision_id):
 
303
                continue
 
304
            squash_revision(repo, rev)
 
305
            root_trees[rev.revision_id] = o.tree
145
306
            revisions[rev.revision_id] = rev
146
307
            graph.append((rev.revision_id, rev.parent_ids))
 
308
            target_git_object_retriever._idmap.add_entry(o.id, "commit", 
 
309
                    (rev.revision_id, o.tree))
 
310
            heads.extend([p for p in o.parents if p not in checked])
 
311
        elif isinstance(o, Tag):
 
312
            heads.append(o.object[1])
 
313
        else:
 
314
            trace.warning("Unable to import head object %r" % o)
 
315
        checked.add(head)
147
316
    # Order the revisions
148
317
    # Create the inventory objects
149
318
    for i, revid in enumerate(topo_sort(graph)):
150
319
        if pb is not None:
151
320
            pb.update("fetching revisions", i, len(graph))
152
 
        root_tree = root_trees[revid]
153
321
        rev = revisions[revid]
154
322
        # We have to do this here, since we have to walk the tree and 
155
 
        # we need to make sure to import the blobs / trees with the riht 
 
323
        # we need to make sure to import the blobs / trees with the right 
156
324
        # path; this may involve adding them more than once.
157
 
        inv = Inventory()
158
 
        inv.revision_id = rev.revision_id
159
 
        def lookup_object(sha):
160
 
            if sha in objects:
161
 
                return objects[sha]
162
 
            return reconstruct_git_object(repo, mapping, sha)
163
 
        parent_invs = [repo.get_inventory(r) for r in rev.parent_ids]
164
 
        import_git_tree(repo, mapping, "", root_tree, inv, parent_invs, lookup_object)
165
 
        repo.add_revision(rev.revision_id, rev, inv)
166
 
 
167
 
 
168
 
def reconstruct_git_commit(repo, rev):
169
 
    raise NotImplementedError(self.reconstruct_git_commit)
170
 
 
171
 
 
172
 
def reconstruct_git_object(repo, mapping, sha):
173
 
    # Commit
174
 
    revid = mapping.revision_id_foreign_to_bzr(sha)
175
 
    try:
176
 
        rev = repo.get_revision(revid)
177
 
    except NoSuchRevision:
178
 
        pass
179
 
    else:
180
 
        return reconstruct_git_commit(rev)
181
 
 
182
 
    # TODO: Tree
183
 
    # TODO: Blob
184
 
    raise KeyError("No such object %s" % sha)
 
325
        parent_invs = []
 
326
        for parent_id in rev.parent_ids:
 
327
            try:
 
328
                parent_invs.append(parent_invs_cache[parent_id])
 
329
            except KeyError:
 
330
                parent_inv = repo.get_inventory(parent_id)
 
331
                parent_invs.append(parent_inv)
 
332
                parent_invs_cache[parent_id] = parent_inv
 
333
        if parent_invs == []:
 
334
            base_inv = Inventory(root_id=None)
 
335
            base_ie = None
 
336
        else:
 
337
            base_inv = parent_invs[0]
 
338
            base_ie = base_inv.root
 
339
        inv_delta, unusual_modes, shamap = import_git_tree(repo.texts, 
 
340
                mapping, "", root_trees[revid], base_inv, base_ie, None, revid, 
 
341
                parent_invs, target_git_object_retriever._idmap, lookup_object)
 
342
        target_git_object_retriever._idmap.add_entries(shamap)
 
343
        if unusual_modes != {}:
 
344
            for path, mode in unusual_modes.iteritems():
 
345
                warn_unusual_mode(rev.foreign_revid, path, mode)
 
346
            mapping.import_unusual_file_modes(rev, unusual_modes)
 
347
        try:
 
348
            basis_id = rev.parent_ids[0]
 
349
        except IndexError:
 
350
            basis_id = NULL_REVISION
 
351
            base_inv = None
 
352
        rev.inventory_sha1, inv = repo.add_inventory_by_delta(basis_id,
 
353
                  inv_delta, rev.revision_id, rev.parent_ids,
 
354
                  base_inv)
 
355
        parent_invs_cache[rev.revision_id] = inv
 
356
        repo.add_revision(rev.revision_id, rev)
 
357
        if "verify" in debug.debug_flags:
 
358
            new_unusual_modes = mapping.export_unusual_file_modes(rev)
 
359
            if new_unusual_modes != unusual_modes:
 
360
                raise AssertionError("unusual modes don't match: %r != %r" % (unusual_modes, new_unusual_modes))
 
361
            objs = inventory_to_tree_and_blobs(inv, repo.texts, mapping, unusual_modes)
 
362
            for sha1, newobj, path in objs:
 
363
                assert path is not None
 
364
                oldobj = tree_lookup_path(lookup_object, root_trees[revid], path)
 
365
                if oldobj != newobj:
 
366
                    raise AssertionError("%r != %r in %s" % (oldobj, newobj, path))
 
367
 
 
368
    target_git_object_retriever._idmap.commit()
185
369
 
186
370
 
187
371
class InterGitRepository(InterRepository):
188
372
 
189
 
    _matching_repo_format = GitFormat()
 
373
    _matching_repo_format = GitRepositoryFormat()
190
374
 
191
375
    @staticmethod
192
376
    def _get_repo_format_to_test():
196
380
        """See InterRepository.copy_content."""
197
381
        self.fetch(revision_id, pb, find_ghosts=False)
198
382
 
199
 
    def fetch(self, revision_id=None, pb=None, find_ghosts=False, 
200
 
              mapping=None):
 
383
    def fetch(self, revision_id=None, pb=None, find_ghosts=False, mapping=None,
 
384
            fetch_spec=None):
 
385
        self.fetch_refs(revision_id=revision_id, pb=pb, find_ghosts=find_ghosts,
 
386
                mapping=mapping, fetch_spec=fetch_spec)
 
387
 
 
388
 
 
389
class InterGitNonGitRepository(InterGitRepository):
 
390
    """Base InterRepository that copies revisions from a Git into a non-Git 
 
391
    repository."""
 
392
 
 
393
    def fetch_refs(self, revision_id=None, pb=None, find_ghosts=False, 
 
394
              mapping=None, fetch_spec=None):
201
395
        if mapping is None:
202
396
            mapping = self.source.get_mapping()
203
 
        def progress(text):
204
 
            pb.note("git: %s", text)
205
 
        def determine_wants(heads):
206
 
            if revision_id is None:
207
 
                ret = heads.values()
 
397
        if revision_id is not None:
 
398
            interesting_heads = [revision_id]
 
399
        elif fetch_spec is not None:
 
400
            interesting_heads = fetch_spec.heads
 
401
        else:
 
402
            interesting_heads = None
 
403
        self._refs = {}
 
404
        def determine_wants(refs):
 
405
            self._refs = refs
 
406
            if interesting_heads is None:
 
407
                ret = [sha for (ref, sha) in refs.iteritems() if not ref.endswith("^{}")]
208
408
            else:
209
 
                ret = [mapping.revision_id_bzr_to_foreign(revision_id)]
 
409
                ret = [mapping.revision_id_bzr_to_foreign(revid)[0] for revid in interesting_heads if revid not in (None, NULL_REVISION)]
210
410
            return [rev for rev in ret if not self.target.has_revision(mapping.revision_id_foreign_to_bzr(rev))]
211
 
        graph_walker = BzrFetchGraphWalker(self.target, mapping)
 
411
        pack_hint = self.fetch_objects(determine_wants, mapping, pb)
 
412
        if pack_hint is not None and self.target._format.pack_compresses:
 
413
            self.target.pack(hint=pack_hint)
 
414
        if interesting_heads is not None:
 
415
            present_interesting_heads = self.target.has_revisions(interesting_heads)
 
416
            missing_interesting_heads = set(interesting_heads) - present_interesting_heads
 
417
            if missing_interesting_heads:
 
418
                raise AssertionError("Missing interesting heads: %r" % missing_interesting_heads)
 
419
        return self._refs
 
420
 
 
421
 
 
422
_GIT_PROGRESS_RE = re.compile(r"(.*?): +(\d+)% \((\d+)/(\d+)\)")
 
423
def report_git_progress(pb, text):
 
424
    text = text.rstrip("\r\n")
 
425
    g = _GIT_PROGRESS_RE.match(text)
 
426
    if g is not None:
 
427
        (text, pct, current, total) = g.groups()
 
428
        pb.update(text, int(current), int(total))
 
429
    else:
 
430
        pb.update(text, 0, 0)
 
431
 
 
432
 
 
433
class InterRemoteGitNonGitRepository(InterGitNonGitRepository):
 
434
    """InterRepository that copies revisions from a remote Git into a non-Git 
 
435
    repository."""
 
436
 
 
437
    def get_target_heads(self):
 
438
        # FIXME: This should be more efficient
 
439
        all_revs = self.target.all_revision_ids()
 
440
        parent_map = self.target.get_parent_map(all_revs)
 
441
        all_parents = set()
 
442
        map(all_parents.update, parent_map.itervalues())
 
443
        return set(all_revs) - all_parents
 
444
 
 
445
    def fetch_objects(self, determine_wants, mapping, pb=None):
 
446
        def progress(text):
 
447
            report_git_progress(pb, text)
 
448
        store = BazaarObjectStore(self.target, mapping)
 
449
        self.target.lock_write()
 
450
        try:
 
451
            heads = self.get_target_heads()
 
452
            graph_walker = store.get_graph_walker(
 
453
                    [store._lookup_revision_sha1(head) for head in heads])
 
454
            recorded_wants = []
 
455
 
 
456
            def record_determine_wants(heads):
 
457
                wants = determine_wants(heads)
 
458
                recorded_wants.extend(wants)
 
459
                return wants
 
460
        
 
461
            create_pb = None
 
462
            if pb is None:
 
463
                create_pb = pb = ui.ui_factory.nested_progress_bar()
 
464
            try:
 
465
                self.target.start_write_group()
 
466
                try:
 
467
                    objects_iter = self.source.fetch_objects(
 
468
                                record_determine_wants, graph_walker, 
 
469
                                store.get_raw, progress)
 
470
                    import_git_objects(self.target, mapping, objects_iter, 
 
471
                            store, recorded_wants, pb)
 
472
                finally:
 
473
                    pack_hint = self.target.commit_write_group()
 
474
                return pack_hint
 
475
            finally:
 
476
                if create_pb:
 
477
                    create_pb.finished()
 
478
        finally:
 
479
            self.target.unlock()
 
480
 
 
481
    @staticmethod
 
482
    def is_compatible(source, target):
 
483
        """Be compatible with GitRepository."""
 
484
        # FIXME: Also check target uses VersionedFile
 
485
        return (isinstance(source, RemoteGitRepository) and 
 
486
                target.supports_rich_root() and
 
487
                not isinstance(target, GitRepository))
 
488
 
 
489
 
 
490
class InterLocalGitNonGitRepository(InterGitNonGitRepository):
 
491
    """InterRepository that copies revisions from a local Git into a non-Git 
 
492
    repository."""
 
493
 
 
494
    def fetch_objects(self, determine_wants, mapping, pb=None):
 
495
        wants = determine_wants(self.source._git.get_refs())
212
496
        create_pb = None
213
497
        if pb is None:
214
498
            create_pb = pb = ui.ui_factory.nested_progress_bar()
 
499
        target_git_object_retriever = BazaarObjectStore(self.target, mapping)
215
500
        try:
216
501
            self.target.lock_write()
217
502
            try:
218
503
                self.target.start_write_group()
219
504
                try:
220
 
                    import_git_objects(self.target, mapping,
221
 
                        iter(self.source.fetch_objects(determine_wants, graph_walker, 
222
 
                            progress)), pb)
 
505
                    import_git_objects(self.target, mapping, 
 
506
                            self.source._git.object_store, 
 
507
                            target_git_object_retriever, wants, pb)
223
508
                finally:
224
 
                    self.target.commit_write_group()
 
509
                    pack_hint = self.target.commit_write_group()
 
510
                return pack_hint
225
511
            finally:
226
512
                self.target.unlock()
227
513
        finally:
232
518
    def is_compatible(source, target):
233
519
        """Be compatible with GitRepository."""
234
520
        # FIXME: Also check target uses VersionedFile
 
521
        return (isinstance(source, LocalGitRepository) and 
 
522
                target.supports_rich_root() and
 
523
                not isinstance(target, GitRepository))
 
524
 
 
525
 
 
526
class InterGitGitRepository(InterGitRepository):
 
527
    """InterRepository that copies between Git repositories."""
 
528
 
 
529
    def fetch_objects(self, determine_wants, mapping, pb=None):
 
530
        def progress(text):
 
531
            trace.note("git: %s", text)
 
532
        graphwalker = self.target._git.get_graph_walker()
 
533
        if isinstance(self.source, LocalGitRepository) and isinstance(self.target, LocalGitRepository):
 
534
            return self.source._git.fetch(self.target._git, determine_wants, 
 
535
                progress)
 
536
        elif isinstance(self.source, LocalGitRepository) and isinstance(self.target, RemoteGitRepository):
 
537
            raise NotImplementedError
 
538
        elif isinstance(self.source, RemoteGitRepository) and isinstance(self.target, LocalGitRepository):
 
539
            f, commit = self.target._git.object_store.add_thin_pack()
 
540
            try:
 
541
                refs = self.source._git.fetch_pack(determine_wants, graphwalker,
 
542
                                                   f.write, progress)
 
543
                commit()
 
544
                return refs
 
545
            except:
 
546
                f.close()
 
547
                raise
 
548
        else:
 
549
            raise AssertionError
 
550
 
 
551
    def fetch_refs(self, revision_id=None, pb=None, find_ghosts=False, 
 
552
              mapping=None, fetch_spec=None, branches=None):
 
553
        if mapping is None:
 
554
            mapping = self.source.get_mapping()
 
555
        r = self.target._git
 
556
        if revision_id is not None:
 
557
            args = [mapping.revision_id_bzr_to_foreign(revision_id)[0]]
 
558
        elif fetch_spec is not None:
 
559
            args = [mapping.revision_id_bzr_to_foreign(revid)[0] for revid in fetch_spec.heads]
 
560
        if branches is not None:
 
561
            determine_wants = lambda x: [x[y] for y in branches if not x[y] in r.object_store]
 
562
        elif fetch_spec is None and revision_id is None:
 
563
            determine_wants = r.object_store.determine_wants_all
 
564
        else:
 
565
            determine_wants = lambda x: [y for y in args if not y in r.object_store]
 
566
        return self.fetch_objects(determine_wants, mapping)
 
567
 
 
568
 
 
569
    @staticmethod
 
570
    def is_compatible(source, target):
 
571
        """Be compatible with GitRepository."""
235
572
        return (isinstance(source, GitRepository) and 
236
 
                target.supports_rich_root())
 
573
                isinstance(target, GitRepository))