1
# Copyright (C) 2008 Canonical Ltd
 
 
3
# This program is free software; you can redistribute it and/or modify
 
 
4
# it under the terms of the GNU General Public License as published by
 
 
5
# the Free Software Foundation; either version 2 of the License, or
 
 
6
# (at your option) any later version.
 
 
8
# This program is distributed in the hope that it will be useful,
 
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
 
11
# GNU General Public License for more details.
 
 
13
# You should have received a copy of the GNU General Public License
 
 
14
# along with this program; if not, write to the Free Software
 
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
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
 
 
24
from bzrlib.plugins.git.repository import (
 
 
29
from bzrlib.plugins.git.remote import RemoteGitRepository
 
 
32
from dulwich.client import SimpleFetchGraphWalker
 
 
33
from dulwich.objects import Commit
 
 
35
from cStringIO import StringIO
 
 
38
class BzrFetchGraphWalker(object):
 
 
40
    def __init__(self, repository, mapping):
 
 
41
        self.repository = repository
 
 
42
        self.mapping = mapping
 
 
44
        self.heads = set(repository.all_revision_ids())
 
 
48
        revid = self.mapping.revision_id_foreign_to_bzr(sha)
 
 
51
    def remove(self, revid):
 
 
53
        if revid in self.heads:
 
 
54
            self.heads.remove(revid)
 
 
55
        if revid in self.parents:
 
 
56
            for p in self.parents[revid]:
 
 
61
            ret = self.heads.pop()
 
 
62
            ps = self.repository.get_parent_map([ret])[ret]
 
 
63
            self.parents[ret] = ps
 
 
64
            self.heads.update([p for p in ps if not p in self.done])
 
 
67
                return self.mapping.revision_id_bzr_to_foreign(ret)
 
 
68
            except InvalidRevisionId:
 
 
73
def import_git_blob(repo, mapping, path, blob, inv, parent_invs, executable):
 
 
74
    """Import a git blob object into a bzr repository.
 
 
76
    :param repo: bzr repository
 
 
77
    :param path: Path in the tree
 
 
78
    :param blob: A git blob
 
 
80
    file_id = mapping.generate_file_id(path)
 
 
81
    text_revision = inv.revision_id
 
 
82
    repo.texts.add_lines((file_id, text_revision),
 
 
83
        [(file_id, p[file_id].revision) for p in parent_invs if file_id in p],
 
 
84
        osutils.split_lines(blob.data))
 
 
85
    ie = inv.add_path(path, "file", file_id)
 
 
86
    ie.revision = text_revision
 
 
87
    ie.text_size = len(blob.data)
 
 
88
    ie.text_sha1 = osutils.sha_string(blob.data)
 
 
89
    ie.executable = executable
 
 
92
def import_git_tree(repo, mapping, path, tree, inv, parent_invs, lookup_object):
 
 
93
    """Import a git tree object into a bzr repository.
 
 
95
    :param repo: A Bzr repository object
 
 
96
    :param path: Path in the tree
 
 
97
    :param tree: A git tree object
 
 
98
    :param inv: Inventory object
 
 
100
    file_id = mapping.generate_file_id(path)
 
 
101
    text_revision = inv.revision_id
 
 
102
    repo.texts.add_lines((file_id, text_revision),
 
 
103
        [(file_id, p[file_id].revision) for p in parent_invs if file_id in p],
 
 
105
    ie = inv.add_path(path, "directory", file_id)
 
 
106
    ie.revision = text_revision
 
 
107
    for mode, name, hexsha in tree.entries():
 
 
108
        entry_kind = (mode & 0700000) / 0100000
 
 
109
        basename = name.decode("utf-8")
 
 
113
            child_path = urlutils.join(path, name)
 
 
115
            tree = lookup_object(hexsha)
 
 
116
            import_git_tree(repo, mapping, child_path, tree, inv, parent_invs, lookup_object)
 
 
117
        elif entry_kind == 1:
 
 
118
            blob = lookup_object(hexsha)
 
 
119
            fs_mode = mode & 0777
 
 
120
            import_git_blob(repo, mapping, child_path, blob, inv, parent_invs, bool(fs_mode & 0111))
 
 
122
            raise AssertionError("Unknown blob kind, perms=%r." % (mode,))
 
 
125
def import_git_objects(repo, mapping, num_objects, object_iter, pb=None):
 
 
126
    """Import a set of git objects into a bzr repository.
 
 
128
    :param repo: Bazaar repository
 
 
129
    :param mapping: Mapping to use
 
 
130
    :param num_objects: Number of objects.
 
 
131
    :param object_iter: Iterator over Git objects.
 
 
133
    # TODO: a more (memory-)efficient implementation of this
 
 
135
    for i, (o, _) in enumerate(object_iter):
 
 
137
            pb.update("fetching objects", i, num_objects) 
 
 
142
    # Find and convert commit objects
 
 
143
    for o in objects.itervalues():
 
 
144
        if isinstance(o, Commit):
 
 
145
            rev = mapping.import_commit(o)
 
 
146
            root_trees[rev.revision_id] = objects[o.tree]
 
 
147
            revisions[rev.revision_id] = rev
 
 
148
            graph.append((rev.revision_id, rev.parent_ids))
 
 
149
    # Order the revisions
 
 
150
    # Create the inventory objects
 
 
151
    for i, revid in enumerate(topo_sort(graph)):
 
 
153
            pb.update("fetching revisions", i, len(graph))
 
 
154
        root_tree = root_trees[revid]
 
 
155
        rev = revisions[revid]
 
 
156
        # We have to do this here, since we have to walk the tree and 
 
 
157
        # we need to make sure to import the blobs / trees with the riht 
 
 
158
        # path; this may involve adding them more than once.
 
 
160
        inv.revision_id = rev.revision_id
 
 
161
        def lookup_object(sha):
 
 
164
            return reconstruct_git_object(repo, mapping, sha)
 
 
165
        parent_invs = [repo.get_inventory(r) for r in rev.parent_ids]
 
 
166
        import_git_tree(repo, mapping, "", root_tree, inv, parent_invs, 
 
 
168
        repo.add_revision(rev.revision_id, rev, inv)
 
 
171
def reconstruct_git_commit(repo, rev):
 
 
172
    raise NotImplementedError(self.reconstruct_git_commit)
 
 
175
def reconstruct_git_object(repo, mapping, sha):
 
 
177
    revid = mapping.revision_id_foreign_to_bzr(sha)
 
 
179
        rev = repo.get_revision(revid)
 
 
180
    except NoSuchRevision:
 
 
183
        return reconstruct_git_commit(rev)
 
 
187
    raise KeyError("No such object %s" % sha)
 
 
190
class InterGitNonGitRepository(InterRepository):
 
 
192
    _matching_repo_format = GitFormat()
 
 
195
    def _get_repo_format_to_test():
 
 
198
    def copy_content(self, revision_id=None, pb=None):
 
 
199
        """See InterRepository.copy_content."""
 
 
200
        self.fetch(revision_id, pb, find_ghosts=False)
 
 
202
    def fetch(self, revision_id=None, pb=None, find_ghosts=False, 
 
 
205
            mapping = self.source.get_mapping()
 
 
207
            pb.update("git: %s" % text.rstrip("\r\n"), 0, 0)
 
 
208
        def determine_wants(heads):
 
 
209
            if revision_id is None:
 
 
212
                ret = [mapping.revision_id_bzr_to_foreign(revision_id)]
 
 
213
            return [rev for rev in ret if not self.target.has_revision(mapping.revision_id_foreign_to_bzr(rev))]
 
 
214
        graph_walker = BzrFetchGraphWalker(self.target, mapping)
 
 
217
            create_pb = pb = ui.ui_factory.nested_progress_bar()
 
 
219
            self.target.lock_write()
 
 
221
                self.target.start_write_group()
 
 
223
                    (num_objects, objects_iter) = \
 
 
224
                            self.source.fetch_objects(determine_wants, 
 
 
225
                                graph_walker, progress)
 
 
226
                    import_git_objects(self.target, mapping, num_objects, 
 
 
229
                    self.target.commit_write_group()
 
 
237
    def is_compatible(source, target):
 
 
238
        """Be compatible with GitRepository."""
 
 
239
        # FIXME: Also check target uses VersionedFile
 
 
240
        return (isinstance(source, GitRepository) and 
 
 
241
                target.supports_rich_root() and
 
 
242
                not isinstance(target, GitRepository))
 
 
245
class InterGitRepository(InterRepository):
 
 
247
    _matching_repo_format = GitFormat()
 
 
250
    def _get_repo_format_to_test():
 
 
253
    def copy_content(self, revision_id=None, pb=None):
 
 
254
        """See InterRepository.copy_content."""
 
 
255
        self.fetch(revision_id, pb, find_ghosts=False)
 
 
257
    def fetch(self, revision_id=None, pb=None, find_ghosts=False, 
 
 
260
            mapping = self.source.get_mapping()
 
 
262
            info("git: %s", text)
 
 
264
        if revision_id is None:
 
 
265
            determine_wants = lambda x: [y for y in x.values() if not y in r.object_store]
 
 
267
            args = [mapping.revision_id_bzr_to_foreign(revision_id)]
 
 
268
            determine_wants = lambda x: [y for y in args if not y in r.object_store]
 
 
270
        graphwalker = SimpleFetchGraphWalker(r.heads().values(), r.get_parents)
 
 
271
        f, commit = r.object_store.add_pack()
 
 
273
            self.source._git.fetch_pack(path, determine_wants, graphwalker, f.write, progress)
 
 
281
    def is_compatible(source, target):
 
 
282
        """Be compatible with GitRepository."""
 
 
283
        return (isinstance(source, GitRepository) and 
 
 
284
                isinstance(target, GitRepository))