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 import git
25
from bzrlib.plugins.git.repository import LocalGitRepository, GitRepository, GitFormat
26
from bzrlib.plugins.git.remote import RemoteGitRepository
28
from dulwich.objects import Commit
30
from cStringIO import StringIO
33
class BzrFetchGraphWalker(object):
35
def __init__(self, repository, mapping):
36
self.repository = repository
37
self.mapping = mapping
39
self.heads = set(repository.all_revision_ids())
43
revid = self.mapping.revision_id_foreign_to_bzr(sha)
46
def remove(self, revid):
49
self.heads.remove(revid)
50
if revid in self.parents:
51
for p in self.parents[revid]:
56
ret = self.heads.pop()
57
ps = self.repository.get_parent_map([ret])[ret]
58
self.parents[ret] = ps
59
self.heads.update([p for p in ps if not p in self.done])
62
return self.mapping.revision_id_bzr_to_foreign(ret)
63
except InvalidRevisionId:
68
def import_git_blob(repo, mapping, path, blob, inv, parent_invs, executable):
69
"""Import a git blob object into a bzr repository.
71
:param repo: bzr repository
72
:param path: Path in the tree
73
:param blob: A git blob
75
file_id = mapping.generate_file_id(path)
76
text_revision = inv.revision_id
77
repo.texts.add_lines((file_id, text_revision),
78
[(file_id, p[file_id].revision) for p in parent_invs if file_id in p],
79
osutils.split_lines(blob.data))
80
ie = inv.add_path(path, "file", file_id)
81
ie.revision = text_revision
82
ie.text_size = len(blob.data)
83
ie.text_sha1 = osutils.sha_string(blob.data)
84
ie.executable = executable
87
def import_git_tree(repo, mapping, path, tree, inv, parent_invs, lookup_object):
88
"""Import a git tree object into a bzr repository.
90
:param repo: A Bzr repository object
91
:param path: Path in the tree
92
:param tree: A git tree object
93
:param inv: Inventory object
95
file_id = mapping.generate_file_id(path)
96
text_revision = inv.revision_id
97
repo.texts.add_lines((file_id, text_revision),
98
[(file_id, p[file_id].revision) for p in parent_invs if file_id in p],
100
ie = inv.add_path(path, "directory", file_id)
101
ie.revision = text_revision
102
for mode, name, hexsha in tree.entries():
103
entry_kind = (mode & 0700000) / 0100000
104
basename = name.decode("utf-8")
108
child_path = urlutils.join(path, name)
110
tree = lookup_object(hexsha)
111
import_git_tree(repo, mapping, child_path, tree, inv, parent_invs, lookup_object)
112
elif entry_kind == 1:
113
blob = lookup_object(hexsha)
114
fs_mode = mode & 0777
115
import_git_blob(repo, mapping, child_path, blob, inv, parent_invs, bool(fs_mode & 0111))
117
raise AssertionError("Unknown blob kind, perms=%r." % (mode,))
120
def import_git_objects(repo, mapping, object_iter, pb=None):
121
"""Import a set of git objects into a bzr repository.
123
:param repo: Bazaar repository
124
:param mapping: Mapping to use
125
:param object_iter: Iterator over Git objects.
127
# TODO: a more (memory-)efficient implementation of this
129
for i, o in enumerate(object_iter):
131
pb.update("fetching objects", i)
136
# Find and convert commit objects
137
for o in objects.itervalues():
138
if isinstance(o, Commit):
139
rev = mapping.import_commit(o)
140
root_trees[rev.revision_id] = objects[o.tree]
141
revisions[rev.revision_id] = rev
142
graph.append((rev.revision_id, rev.parent_ids))
143
# Order the revisions
144
# Create the inventory objects
145
for i, revid in enumerate(topo_sort(graph)):
147
pb.update("fetching revisions", i, len(graph))
148
root_tree = root_trees[revid]
149
rev = revisions[revid]
150
# We have to do this here, since we have to walk the tree and
151
# we need to make sure to import the blobs / trees with the riht
152
# path; this may involve adding them more than once.
154
inv.revision_id = rev.revision_id
155
def lookup_object(sha):
158
return reconstruct_git_object(repo, mapping, sha)
159
parent_invs = [repo.get_inventory(r) for r in rev.parent_ids]
160
import_git_tree(repo, mapping, "", root_tree, inv, parent_invs, lookup_object)
161
repo.add_revision(rev.revision_id, rev, inv)
164
def reconstruct_git_commit(repo, rev):
165
raise NotImplementedError(self.reconstruct_git_commit)
168
def reconstruct_git_object(repo, mapping, sha):
170
revid = mapping.revision_id_foreign_to_bzr(sha)
172
rev = repo.get_revision(revid)
173
except NoSuchRevision:
176
return reconstruct_git_commit(rev)
180
raise KeyError("No such object %s" % sha)
183
class InterGitRepository(InterRepository):
185
_matching_repo_format = GitFormat()
188
def _get_repo_format_to_test():
191
def copy_content(self, revision_id=None, pb=None):
192
"""See InterRepository.copy_content."""
193
self.fetch(revision_id, pb, find_ghosts=False)
195
def fetch(self, revision_id=None, pb=None, find_ghosts=False,
198
mapping = self.source.get_mapping()
201
pb.note("git: %s" % text)
203
info("git: %s" % text)
204
def determine_wants(heads):
205
if revision_id is None:
208
ret = [mapping.revision_id_bzr_to_foreign(revision_id)]
209
return [rev for rev in ret if not self.target.has_revision(mapping.revision_id_foreign_to_bzr(rev))]
210
graph_walker = BzrFetchGraphWalker(self.target, mapping)
211
self.target.lock_write()
213
self.target.start_write_group()
215
import_git_objects(self.target, mapping,
216
iter(self.source.fetch_objects(determine_wants, graph_walker,
219
self.target.commit_write_group()
224
def is_compatible(source, target):
225
"""Be compatible with GitRepository."""
226
# FIXME: Also check target uses VersionedFile
227
return (isinstance(source, LocalGitRepository) and
228
target.supports_rich_root())