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):
 
 
39
    """GraphWalker implementation that uses a Bazaar repository."""
 
 
41
    def __init__(self, repository, mapping):
 
 
42
        self.repository = repository
 
 
43
        self.mapping = mapping
 
 
45
        self.heads = set(repository.all_revision_ids())
 
 
49
        return iter(self.next, None)
 
 
52
        revid = self.mapping.revision_id_foreign_to_bzr(sha)
 
 
55
    def remove(self, revid):
 
 
57
        if revid in self.heads:
 
 
58
            self.heads.remove(revid)
 
 
59
        if revid in self.parents:
 
 
60
            for p in self.parents[revid]:
 
 
65
            ret = self.heads.pop()
 
 
66
            ps = self.repository.get_parent_map([ret])[ret]
 
 
67
            self.parents[ret] = ps
 
 
68
            self.heads.update([p for p in ps if not p in self.done])
 
 
71
                return self.mapping.revision_id_bzr_to_foreign(ret)[0]
 
 
72
            except InvalidRevisionId:
 
 
77
def import_git_blob(repo, mapping, path, blob, inv, parent_invs, executable):
 
 
78
    """Import a git blob object into a bzr repository.
 
 
80
    :param repo: bzr repository
 
 
81
    :param path: Path in the tree
 
 
82
    :param blob: A git blob
 
 
84
    file_id = mapping.generate_file_id(path)
 
 
85
    text_revision = inv.revision_id
 
 
86
    repo.texts.add_lines((file_id, text_revision),
 
 
87
        [(file_id, p[file_id].revision) for p in parent_invs if file_id in p],
 
 
88
        osutils.split_lines(blob.data))
 
 
89
    ie = inv.add_path(path, "file", file_id)
 
 
90
    ie.revision = text_revision
 
 
91
    ie.text_size = len(blob.data)
 
 
92
    ie.text_sha1 = osutils.sha_string(blob.data)
 
 
93
    ie.executable = executable
 
 
96
def import_git_tree(repo, mapping, path, tree, inv, parent_invs, lookup_object):
 
 
97
    """Import a git tree object into a bzr repository.
 
 
99
    :param repo: A Bzr repository object
 
 
100
    :param path: Path in the tree
 
 
101
    :param tree: A git tree object
 
 
102
    :param inv: Inventory object
 
 
104
    file_id = mapping.generate_file_id(path)
 
 
105
    text_revision = inv.revision_id
 
 
106
    repo.texts.add_lines((file_id, text_revision),
 
 
107
        [(file_id, p[file_id].revision) for p in parent_invs if file_id in p],
 
 
109
    ie = inv.add_path(path, "directory", file_id)
 
 
110
    ie.revision = text_revision
 
 
111
    for mode, name, hexsha in tree.entries():
 
 
112
        entry_kind = (mode & 0700000) / 0100000
 
 
113
        basename = name.decode("utf-8")
 
 
117
            child_path = urlutils.join(path, name)
 
 
119
            tree = lookup_object(hexsha)
 
 
120
            import_git_tree(repo, mapping, child_path, tree, inv, parent_invs, lookup_object)
 
 
121
        elif entry_kind == 1:
 
 
122
            blob = lookup_object(hexsha)
 
 
123
            fs_mode = mode & 0777
 
 
124
            import_git_blob(repo, mapping, child_path, blob, inv, parent_invs, bool(fs_mode & 0111))
 
 
126
            raise AssertionError("Unknown blob kind, perms=%r." % (mode,))
 
 
129
def import_git_objects(repo, mapping, num_objects, object_iter, pb=None):
 
 
130
    """Import a set of git objects into a bzr repository.
 
 
132
    :param repo: Bazaar repository
 
 
133
    :param mapping: Mapping to use
 
 
134
    :param num_objects: Number of objects.
 
 
135
    :param object_iter: Iterator over Git objects.
 
 
137
    # TODO: a more (memory-)efficient implementation of this
 
 
139
    for i, (o, _) in enumerate(object_iter):
 
 
141
            pb.update("fetching objects", i, num_objects) 
 
 
146
    # Find and convert commit objects
 
 
147
    for o in objects.itervalues():
 
 
148
        if isinstance(o, Commit):
 
 
149
            rev = mapping.import_commit(o)
 
 
150
            root_trees[rev.revision_id] = objects[o.tree]
 
 
151
            revisions[rev.revision_id] = rev
 
 
152
            graph.append((rev.revision_id, rev.parent_ids))
 
 
153
    # Order the revisions
 
 
154
    # Create the inventory objects
 
 
155
    for i, revid in enumerate(topo_sort(graph)):
 
 
157
            pb.update("fetching revisions", i, len(graph))
 
 
158
        root_tree = root_trees[revid]
 
 
159
        rev = revisions[revid]
 
 
160
        # We have to do this here, since we have to walk the tree and 
 
 
161
        # we need to make sure to import the blobs / trees with the riht 
 
 
162
        # path; this may involve adding them more than once.
 
 
164
        inv.revision_id = rev.revision_id
 
 
165
        def lookup_object(sha):
 
 
168
            return reconstruct_git_object(repo, mapping, sha)
 
 
169
        parent_invs = [repo.get_inventory(r) for r in rev.parent_ids]
 
 
170
        import_git_tree(repo, mapping, "", root_tree, inv, parent_invs, 
 
 
172
        repo.add_revision(rev.revision_id, rev, inv)
 
 
175
def reconstruct_git_commit(repo, rev):
 
 
176
    raise NotImplementedError(self.reconstruct_git_commit)
 
 
179
def reconstruct_git_object(repo, mapping, sha):
 
 
181
    revid = mapping.revision_id_foreign_to_bzr(sha)
 
 
183
        rev = repo.get_revision(revid)
 
 
184
    except NoSuchRevision:
 
 
187
        return reconstruct_git_commit(rev)
 
 
191
    raise KeyError("No such object %s" % sha)
 
 
194
class InterGitNonGitRepository(InterRepository):
 
 
196
    _matching_repo_format = GitFormat()
 
 
199
    def _get_repo_format_to_test():
 
 
202
    def copy_content(self, revision_id=None, pb=None):
 
 
203
        """See InterRepository.copy_content."""
 
 
204
        self.fetch(revision_id, pb, find_ghosts=False)
 
 
206
    def fetch(self, revision_id=None, pb=None, find_ghosts=False, 
 
 
209
            mapping = self.source.get_mapping()
 
 
211
            pb.update("git: %s" % text.rstrip("\r\n"), 0, 0)
 
 
212
        def determine_wants(heads):
 
 
213
            if revision_id is None:
 
 
216
                ret = [mapping.revision_id_bzr_to_foreign(revision_id)[0]]
 
 
217
            return [rev for rev in ret if not self.target.has_revision(mapping.revision_id_foreign_to_bzr(rev))]
 
 
218
        graph_walker = BzrFetchGraphWalker(self.target, mapping)
 
 
221
            create_pb = pb = ui.ui_factory.nested_progress_bar()
 
 
223
            self.target.lock_write()
 
 
225
                self.target.start_write_group()
 
 
227
                    (num_objects, objects_iter) = \
 
 
228
                            self.source.fetch_objects(determine_wants, 
 
 
229
                                graph_walker, progress)
 
 
230
                    import_git_objects(self.target, mapping, num_objects, 
 
 
233
                    self.target.commit_write_group()
 
 
241
    def is_compatible(source, target):
 
 
242
        """Be compatible with GitRepository."""
 
 
243
        # FIXME: Also check target uses VersionedFile
 
 
244
        return (isinstance(source, GitRepository) and 
 
 
245
                target.supports_rich_root() and
 
 
246
                not isinstance(target, GitRepository))
 
 
249
class InterGitRepository(InterRepository):
 
 
251
    _matching_repo_format = GitFormat()
 
 
254
    def _get_repo_format_to_test():
 
 
257
    def copy_content(self, revision_id=None, pb=None):
 
 
258
        """See InterRepository.copy_content."""
 
 
259
        self.fetch(revision_id, pb, find_ghosts=False)
 
 
261
    def fetch(self, revision_id=None, pb=None, find_ghosts=False, 
 
 
264
            mapping = self.source.get_mapping()
 
 
266
            info("git: %s", text)
 
 
268
        if revision_id is None:
 
 
269
            determine_wants = lambda x: [y for y in x.values() if not y in r.object_store]
 
 
271
            args = [mapping.revision_id_bzr_to_foreign(revision_id)[0]]
 
 
272
            determine_wants = lambda x: [y for y in args if not y in r.object_store]
 
 
274
        graphwalker = SimpleFetchGraphWalker(r.heads().values(), r.get_parents)
 
 
275
        f, commit = r.object_store.add_pack()
 
 
277
            self.source._git.fetch_pack(path, determine_wants, graphwalker, f.write, progress)
 
 
285
    def is_compatible(source, target):
 
 
286
        """Be compatible with GitRepository."""
 
 
287
        return (isinstance(source, GitRepository) and 
 
 
288
                isinstance(target, GitRepository))