1
# Copyright (C) 2009-2018 Jelmer Vernooij <jelmer@jelmer.uk>
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""InterRepository operations."""
19
from __future__ import absolute_import
21
from io import BytesIO
23
from dulwich.errors import (
26
from dulwich.object_store import (
27
ObjectStoreGraphWalker,
29
from dulwich.protocol import (
33
from dulwich.walk import Walker
35
from ...errors import (
37
FetchLimitUnsupported,
40
NoRoundtrippingSupport,
43
from ...repository import (
46
from ...revision import (
59
DetermineWantsRecorder,
61
from .mapping import (
64
from .object_store import (
69
MissingObjectsIterator,
75
from .repository import (
83
from .unpeel_map import (
88
class InterToGitRepository(InterRepository):
89
"""InterRepository that copies into a Git repository."""
91
_matching_repo_format = GitRepositoryFormat()
93
def __init__(self, source, target):
94
super(InterToGitRepository, self).__init__(source, target)
95
self.mapping = self.target.get_mapping()
96
self.source_store = get_object_store(self.source, self.mapping)
99
def _get_repo_format_to_test():
102
def copy_content(self, revision_id=None, pb=None):
103
"""See InterRepository.copy_content."""
104
self.fetch(revision_id, pb, find_ghosts=False)
106
def fetch_refs(self, update_refs, lossy, overwrite=False):
107
"""Fetch possibly roundtripped revisions into the target repository
110
:param update_refs: Generate refs to fetch. Receives dictionary
111
with old refs (git shas), returns dictionary of new names to
113
:param lossy: Whether to roundtrip
114
:return: old refs, new refs
116
raise NotImplementedError(self.fetch_refs)
118
def search_missing_revision_ids(self,
119
find_ghosts=True, revision_ids=None, if_present_ids=None,
121
if limit is not None:
122
raise FetchLimitUnsupported(self)
126
todo.extend(revision_ids)
128
todo.extend(revision_ids)
129
with self.source_store.lock_read():
130
for revid in revision_ids:
131
if revid == NULL_REVISION:
133
git_sha = self.source_store._lookup_revision_sha1(revid)
134
git_shas.append(git_sha)
135
walker = Walker(self.source_store,
136
include=git_shas, exclude=[
137
sha for sha in self.target.controldir.get_refs_container().as_dict().values()
139
missing_revids = set()
141
for (kind, type_data) in self.source_store.lookup_git_sha(entry.commit.id):
143
missing_revids.add(type_data[0])
144
return self.source.revision_ids_to_search_result(missing_revids)
146
def _warn_slow(self):
148
'Pushing from a Bazaar to a Git repository. '
149
'For better performance, push into a Bazaar repository.')
152
class InterToLocalGitRepository(InterToGitRepository):
153
"""InterBranch implementation between a Bazaar and a Git repository."""
155
def __init__(self, source, target):
156
super(InterToLocalGitRepository, self).__init__(source, target)
157
self.target_store = self.target.controldir._git.object_store
158
self.target_refs = self.target.controldir._git.refs
160
def _commit_needs_fetching(self, sha_id):
162
return (sha_id not in self.target_store)
163
except NoSuchRevision:
167
def _revision_needs_fetching(self, sha_id, revid):
168
if revid == NULL_REVISION:
172
sha_id = self.source_store._lookup_revision_sha1(revid)
175
return self._commit_needs_fetching(sha_id)
177
def missing_revisions(self, stop_revisions):
178
"""Find the revisions that are missing from the target repository.
180
:param stop_revisions: Revisions to check for (tuples with
182
:return: sequence of missing revisions, in topological order
183
:raise: NoSuchRevision if the stop_revisions are not present in
188
for (sha1, revid) in stop_revisions:
189
if sha1 is not None and revid is not None:
190
revid_sha_map[revid] = sha1
191
stop_revids.append(revid)
192
elif sha1 is not None:
193
if self._commit_needs_fetching(sha1):
194
for (kind, (revid, tree_sha, verifiers)) in self.source_store.lookup_git_sha(sha1):
195
revid_sha_map[revid] = sha1
196
stop_revids.append(revid)
200
stop_revids.append(revid)
202
graph = self.source.get_graph()
203
pb = ui.ui_factory.nested_progress_bar()
207
for revid in stop_revids:
208
sha1 = revid_sha_map.get(revid)
209
if (not revid in missing and
210
self._revision_needs_fetching(sha1, revid)):
212
new_stop_revids.append(revid)
214
parent_map = graph.get_parent_map(new_stop_revids)
215
for parent_revids in parent_map.itervalues():
216
stop_revids.update(parent_revids)
217
pb.update("determining revisions to fetch", len(missing))
220
return graph.iter_topo_order(missing)
222
def _get_target_bzr_refs(self):
223
"""Return a dictionary with references.
225
:return: Dictionary with reference names as keys and tuples
226
with Git SHA, Bazaar revid as values.
230
for k in self.target._git.refs.allkeys():
232
v = self.target._git.refs[k]
237
for (kind, type_data) in self.source_store.lookup_git_sha(v):
238
if kind == "commit" and self.source.has_revision(type_data[0]):
245
bzr_refs[k] = (v, revid)
248
def fetch_refs(self, update_refs, lossy, overwrite=False):
250
with self.source_store.lock_read():
251
old_refs = self._get_target_bzr_refs()
252
new_refs = update_refs(old_refs)
253
revidmap = self.fetch_objects(
254
[(git_sha, bzr_revid) for (git_sha, bzr_revid) in new_refs.values() if git_sha is None or not git_sha.startswith('ref:')], lossy=lossy)
255
for name, (gitid, revid) in new_refs.iteritems():
258
gitid = revidmap[revid][0]
260
gitid = self.source_store._lookup_revision_sha1(revid)
261
if len(gitid) != 40 and not gitid.startswith('ref: '):
262
raise AssertionError("invalid ref contents: %r" % gitid)
263
self.target_refs[name] = gitid
264
return revidmap, old_refs, new_refs
266
def fetch_objects(self, revs, lossy, limit=None):
267
if not lossy and not self.mapping.roundtripping:
268
for git_sha, bzr_revid in revs:
269
if bzr_revid is not None and needs_roundtripping(self.source, bzr_revid):
270
raise NoPushSupport(self.source, self.target, self.mapping,
272
with self.source_store.lock_read():
273
todo = list(self.missing_revisions(revs))[:limit]
275
pb = ui.ui_factory.nested_progress_bar()
277
object_generator = MissingObjectsIterator(
278
self.source_store, self.source, pb)
279
for (old_revid, git_sha) in object_generator.import_revisions(
282
new_revid = self.mapping.revision_id_foreign_to_bzr(git_sha)
284
new_revid = old_revid
286
self.mapping.revision_id_bzr_to_foreign(old_revid)
287
except InvalidRevisionId:
288
refname = self.mapping.revid_as_refname(old_revid)
289
self.target_refs[refname] = git_sha
290
revidmap[old_revid] = (git_sha, new_revid)
291
self.target_store.add_objects(object_generator)
296
def fetch(self, revision_id=None, pb=None, find_ghosts=False,
297
fetch_spec=None, mapped_refs=None):
298
if mapped_refs is not None:
299
stop_revisions = mapped_refs
300
elif revision_id is not None:
301
stop_revisions = [(None, revision_id)]
302
elif fetch_spec is not None:
303
recipe = fetch_spec.get_recipe()
304
if recipe[0] in ("search", "proxy-search"):
305
stop_revisions = [(None, revid) for revid in recipe[1]]
307
raise AssertionError("Unsupported search result type %s" % recipe[0])
309
stop_revisions = [(None, revid) for revid in self.source.all_revision_ids()]
312
self.fetch_objects(stop_revisions, lossy=False)
313
except NoPushSupport:
314
raise NoRoundtrippingSupport(self.source, self.target)
317
def is_compatible(source, target):
318
"""Be compatible with GitRepository."""
319
return (not isinstance(source, GitRepository) and
320
isinstance(target, LocalGitRepository))
323
class InterToRemoteGitRepository(InterToGitRepository):
325
def fetch_refs(self, update_refs, lossy, overwrite=False):
326
"""Import the gist of the ancestry of a particular revision."""
327
if not lossy and not self.mapping.roundtripping:
328
raise NoPushSupport(self.source, self.target, self.mapping)
329
unpeel_map = UnpeelMap.from_repository(self.source)
331
def determine_wants(old_refs):
333
self.old_refs = dict([(k, (v, None)) for (k, v) in old_refs.iteritems()])
334
self.new_refs = update_refs(self.old_refs)
335
for name, (gitid, revid) in self.new_refs.iteritems():
337
git_sha = self.source_store._lookup_revision_sha1(revid)
338
gitid = unpeel_map.re_unpeel_tag(git_sha, old_refs.get(name))
340
if remote_divergence(old_refs.get(name), gitid, self.source_store):
341
raise DivergedBranches(self.source, self.target)
345
with self.source_store.lock_read():
346
new_refs = self.target.send_pack(determine_wants,
347
self.source_store.generate_lossy_pack_data)
349
return revidmap, self.old_refs, self.new_refs
352
def is_compatible(source, target):
353
"""Be compatible with GitRepository."""
354
return (not isinstance(source, GitRepository) and
355
isinstance(target, RemoteGitRepository))
358
class InterFromGitRepository(InterRepository):
360
_matching_repo_format = GitRepositoryFormat()
362
def _target_has_shas(self, shas):
363
raise NotImplementedError(self._target_has_shas)
365
def get_determine_wants_heads(self, wants, include_tags=False):
367
def determine_wants(refs):
368
potential = set(wants)
370
for k, unpeeled in refs.iteritems():
371
if k.endswith("^{}"):
375
if unpeeled == ZERO_SHA:
377
potential.add(unpeeled)
378
return list(potential - self._target_has_shas(potential))
379
return determine_wants
381
def determine_wants_all(self, refs):
382
raise NotImplementedError(self.determine_wants_all)
385
def _get_repo_format_to_test():
388
def copy_content(self, revision_id=None):
389
"""See InterRepository.copy_content."""
390
self.fetch(revision_id, find_ghosts=False)
392
def search_missing_revision_ids(self,
393
find_ghosts=True, revision_ids=None, if_present_ids=None,
395
if limit is not None:
396
raise FetchLimitUnsupported(self)
400
todo.extend(revision_ids)
402
todo.extend(revision_ids)
403
with self.lock_read():
404
for revid in revision_ids:
405
if revid == NULL_REVISION:
407
git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
408
git_shas.append(git_sha)
409
walker = Walker(self.source._git.object_store,
410
include=git_shas, exclude=[
411
sha for sha in self.target.controldir.get_refs_container().as_dict().values()
413
missing_revids = set()
415
missing_revids.add(self.source.lookup_foreign_revision_id(entry.commit.id))
416
return self.source.revision_ids_to_search_result(missing_revids)
419
class InterGitNonGitRepository(InterFromGitRepository):
420
"""Base InterRepository that copies revisions from a Git into a non-Git
423
def _target_has_shas(self, shas):
427
revid = self.source.lookup_foreign_revision_id(sha)
428
except NotCommitError:
429
# Commit is definitely not present
433
return set([revids[r] for r in self.target.has_revisions(revids)])
435
def determine_wants_all(self, refs):
437
for k, v in refs.iteritems():
438
# For non-git target repositories, only worry about peeled
441
potential.add(self.source.controldir.get_peeled(k) or v)
442
return list(potential - self._target_has_shas(potential))
444
def get_determine_wants_heads(self, wants, include_tags=False):
446
def determine_wants(refs):
447
potential = set(wants)
449
for k, unpeeled in refs.iteritems():
452
if unpeeled == ZERO_SHA:
454
potential.add(self.source.controldir.get_peeled(k) or unpeeled)
455
return list(potential - self._target_has_shas(potential))
456
return determine_wants
458
def _warn_slow(self):
460
'Fetching from Git to Bazaar repository. '
461
'For better performance, fetch into a Git repository.')
463
def fetch_objects(self, determine_wants, mapping, limit=None, lossy=False):
464
"""Fetch objects from a remote server.
466
:param determine_wants: determine_wants callback
467
:param mapping: BzrGitMapping to use
468
:param limit: Maximum number of commits to import.
469
:return: Tuple with pack hint, last imported revision id and remote refs
471
raise NotImplementedError(self.fetch_objects)
473
def get_determine_wants_revids(self, revids, include_tags=False):
475
for revid in set(revids):
476
if self.target.has_revision(revid):
478
git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
480
return self.get_determine_wants_heads(wants, include_tags=include_tags)
482
def fetch(self, revision_id=None, find_ghosts=False,
483
mapping=None, fetch_spec=None, include_tags=False):
485
mapping = self.source.get_mapping()
486
if revision_id is not None:
487
interesting_heads = [revision_id]
488
elif fetch_spec is not None:
489
recipe = fetch_spec.get_recipe()
490
if recipe[0] in ("search", "proxy-search"):
491
interesting_heads = recipe[1]
493
raise AssertionError("Unsupported search result type %s" %
496
interesting_heads = None
498
if interesting_heads is not None:
499
determine_wants = self.get_determine_wants_revids(
500
interesting_heads, include_tags=include_tags)
502
determine_wants = self.determine_wants_all
504
(pack_hint, _, remote_refs) = self.fetch_objects(determine_wants,
506
if pack_hint is not None and self.target._format.pack_compresses:
507
self.target.pack(hint=pack_hint)
511
class InterRemoteGitNonGitRepository(InterGitNonGitRepository):
512
"""InterRepository that copies revisions from a remote Git into a non-Git
515
def get_target_heads(self):
516
# FIXME: This should be more efficient
517
all_revs = self.target.all_revision_ids()
518
parent_map = self.target.get_parent_map(all_revs)
520
map(all_parents.update, parent_map.itervalues())
521
return set(all_revs) - all_parents
523
def fetch_objects(self, determine_wants, mapping, limit=None, lossy=False):
524
"""See `InterGitNonGitRepository`."""
526
store = get_object_store(self.target, mapping)
527
with store.lock_write():
528
heads = self.get_target_heads()
529
graph_walker = ObjectStoreGraphWalker(
530
[store._lookup_revision_sha1(head) for head in heads],
531
lambda sha: store[sha].parents)
532
wants_recorder = DetermineWantsRecorder(determine_wants)
534
pb = ui.ui_factory.nested_progress_bar()
536
objects_iter = self.source.fetch_objects(
537
wants_recorder, graph_walker, store.get_raw)
538
trace.mutter("Importing %d new revisions",
539
len(wants_recorder.wants))
540
(pack_hint, last_rev) = import_git_objects(self.target,
541
mapping, objects_iter, store, wants_recorder.wants, pb,
543
return (pack_hint, last_rev, wants_recorder.remote_refs)
548
def is_compatible(source, target):
549
"""Be compatible with GitRepository."""
550
if not isinstance(source, RemoteGitRepository):
552
if not target.supports_rich_root():
554
if isinstance(target, GitRepository):
556
if not getattr(target._format, "supports_full_versioned_files", True):
561
class InterLocalGitNonGitRepository(InterGitNonGitRepository):
562
"""InterRepository that copies revisions from a local Git into a non-Git
565
def fetch_objects(self, determine_wants, mapping, limit=None, lossy=False):
566
"""See `InterGitNonGitRepository`."""
568
remote_refs = self.source.controldir.get_refs_container().as_dict()
569
wants = determine_wants(remote_refs)
571
pb = ui.ui_factory.nested_progress_bar()
572
target_git_object_retriever = get_object_store(self.target, mapping)
574
target_git_object_retriever.lock_write()
576
(pack_hint, last_rev) = import_git_objects(self.target,
577
mapping, self.source._git.object_store,
578
target_git_object_retriever, wants, pb, limit)
579
return (pack_hint, last_rev, remote_refs)
581
target_git_object_retriever.unlock()
586
def is_compatible(source, target):
587
"""Be compatible with GitRepository."""
588
if not isinstance(source, LocalGitRepository):
590
if not target.supports_rich_root():
592
if isinstance(target, GitRepository):
594
if not getattr(target._format, "supports_full_versioned_files", True):
599
class InterGitGitRepository(InterFromGitRepository):
600
"""InterRepository that copies between Git repositories."""
602
def fetch_refs(self, update_refs, lossy, overwrite=False):
604
raise LossyPushToSameVCS(self.source, self.target)
605
old_refs = self.target.controldir.get_refs_container()
607
def determine_wants(heads):
608
old_refs = dict([(k, (v, None)) for (k, v) in heads.as_dict().iteritems()])
609
new_refs = update_refs(old_refs)
610
ref_changes.update(new_refs)
611
return [sha1 for (sha1, bzr_revid) in new_refs.itervalues()]
612
self.fetch_objects(determine_wants, lossy=lossy)
613
for k, (git_sha, bzr_revid) in ref_changes.iteritems():
614
self.target._git.refs[k] = git_sha
615
new_refs = self.target.controldir.get_refs_container()
616
return None, old_refs, new_refs
618
def fetch_objects(self, determine_wants, mapping=None, limit=None, lossy=False):
619
raise NotImplementedError(self.fetch_objects)
621
def _target_has_shas(self, shas):
622
return set([sha for sha in shas if sha in self.target._git.object_store])
624
def fetch(self, revision_id=None, find_ghosts=False,
625
mapping=None, fetch_spec=None, branches=None, limit=None, include_tags=False):
627
mapping = self.source.get_mapping()
628
if revision_id is not None:
630
elif fetch_spec is not None:
631
recipe = fetch_spec.get_recipe()
632
if recipe[0] in ("search", "proxy-search"):
635
raise AssertionError(
636
"Unsupported search result type %s" % recipe[0])
638
if branches is not None:
639
def determine_wants(refs):
641
for name, value in refs.iteritems():
642
if value == ZERO_SHA:
645
if name in branches or (include_tags and is_tag(name)):
648
elif fetch_spec is None and revision_id is None:
649
determine_wants = self.determine_wants_all
651
determine_wants = self.get_determine_wants_revids(args, include_tags=include_tags)
652
wants_recorder = DetermineWantsRecorder(determine_wants)
653
self.fetch_objects(wants_recorder, mapping, limit=limit)
654
return wants_recorder.remote_refs
656
def get_determine_wants_revids(self, revids, include_tags=False):
658
for revid in set(revids):
659
if revid == NULL_REVISION:
661
git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
663
return self.get_determine_wants_heads(wants, include_tags=include_tags)
665
def determine_wants_all(self, refs):
666
potential = set([v for v in refs.values() if not v == ZERO_SHA])
667
return list(potential - self._target_has_shas(potential))
670
class InterLocalGitLocalGitRepository(InterGitGitRepository):
672
def fetch_objects(self, determine_wants, mapping=None, limit=None, lossy=False):
674
raise LossyPushToSameVCS(self.source, self.target)
675
if limit is not None:
676
raise FetchLimitUnsupported(self)
677
refs = self.source._git.fetch(self.target._git, determine_wants)
678
return (None, None, refs)
681
def is_compatible(source, target):
682
"""Be compatible with GitRepository."""
683
return (isinstance(source, LocalGitRepository) and
684
isinstance(target, LocalGitRepository))
687
class InterRemoteGitLocalGitRepository(InterGitGitRepository):
689
def fetch_objects(self, determine_wants, mapping=None, limit=None, lossy=False):
691
raise LossyPushToSameVCS(self.source, self.target)
692
if limit is not None:
693
raise FetchLimitUnsupported(self)
694
graphwalker = self.target._git.get_graph_walker()
695
if CAPABILITY_THIN_PACK in self.source.controldir._client._fetch_capabilities:
696
# TODO(jelmer): Avoid reading entire file into memory and
697
# only processing it after the whole file has been fetched.
703
self.target._git.object_store.move_in_thin_pack(f)
708
f, commit, abort = self.target._git.object_store.add_pack()
710
refs = self.source.controldir.fetch_pack(
711
determine_wants, graphwalker, f.write)
713
return (None, None, refs)
714
except BaseException:
719
def is_compatible(source, target):
720
"""Be compatible with GitRepository."""
721
return (isinstance(source, RemoteGitRepository) and
722
isinstance(target, LocalGitRepository))