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 (
60
DetermineWantsRecorder,
62
from .mapping import (
65
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):
147
if not config.GlobalConfig().suppress_warning('slow_intervcs_push'):
149
'Pushing from a Bazaar to a Git repository. '
150
'For better performance, push into a Bazaar repository.')
153
class InterToLocalGitRepository(InterToGitRepository):
154
"""InterBranch implementation between a Bazaar and a Git repository."""
156
def __init__(self, source, target):
157
super(InterToLocalGitRepository, self).__init__(source, target)
158
self.target_store = self.target.controldir._git.object_store
159
self.target_refs = self.target.controldir._git.refs
161
def _commit_needs_fetching(self, sha_id):
163
return (sha_id not in self.target_store)
164
except NoSuchRevision:
168
def _revision_needs_fetching(self, sha_id, revid):
169
if revid == NULL_REVISION:
173
sha_id = self.source_store._lookup_revision_sha1(revid)
176
return self._commit_needs_fetching(sha_id)
178
def missing_revisions(self, stop_revisions):
179
"""Find the revisions that are missing from the target repository.
181
:param stop_revisions: Revisions to check for (tuples with
183
:return: sequence of missing revisions, in topological order
184
:raise: NoSuchRevision if the stop_revisions are not present in
189
for (sha1, revid) in stop_revisions:
190
if sha1 is not None and revid is not None:
191
revid_sha_map[revid] = sha1
192
stop_revids.append(revid)
193
elif sha1 is not None:
194
if self._commit_needs_fetching(sha1):
195
for (kind, (revid, tree_sha, verifiers)) in self.source_store.lookup_git_sha(sha1):
196
revid_sha_map[revid] = sha1
197
stop_revids.append(revid)
201
stop_revids.append(revid)
203
graph = self.source.get_graph()
204
pb = ui.ui_factory.nested_progress_bar()
208
for revid in stop_revids:
209
sha1 = revid_sha_map.get(revid)
210
if (not revid in missing and
211
self._revision_needs_fetching(sha1, revid)):
213
new_stop_revids.append(revid)
215
parent_map = graph.get_parent_map(new_stop_revids)
216
for parent_revids in parent_map.itervalues():
217
stop_revids.update(parent_revids)
218
pb.update("determining revisions to fetch", len(missing))
221
return graph.iter_topo_order(missing)
223
def _get_target_bzr_refs(self):
224
"""Return a dictionary with references.
226
:return: Dictionary with reference names as keys and tuples
227
with Git SHA, Bazaar revid as values.
231
for k in self.target._git.refs.allkeys():
233
v = self.target._git.refs[k]
238
for (kind, type_data) in self.source_store.lookup_git_sha(v):
239
if kind == "commit" and self.source.has_revision(type_data[0]):
246
bzr_refs[k] = (v, revid)
249
def fetch_refs(self, update_refs, lossy, overwrite=False):
251
with self.source_store.lock_read():
252
old_refs = self._get_target_bzr_refs()
253
new_refs = update_refs(old_refs)
254
revidmap = self.fetch_objects(
255
[(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)
256
for name, (gitid, revid) in new_refs.iteritems():
259
gitid = revidmap[revid][0]
261
gitid = self.source_store._lookup_revision_sha1(revid)
262
if len(gitid) != 40 and not gitid.startswith('ref: '):
263
raise AssertionError("invalid ref contents: %r" % gitid)
264
self.target_refs[name] = gitid
265
return revidmap, old_refs, new_refs
267
def fetch_objects(self, revs, lossy, limit=None):
268
if not lossy and not self.mapping.roundtripping:
269
for git_sha, bzr_revid in revs:
270
if bzr_revid is not None and needs_roundtripping(self.source, bzr_revid):
271
raise NoPushSupport(self.source, self.target, self.mapping,
273
with self.source_store.lock_read():
274
todo = list(self.missing_revisions(revs))[:limit]
276
pb = ui.ui_factory.nested_progress_bar()
278
object_generator = MissingObjectsIterator(
279
self.source_store, self.source, pb)
280
for (old_revid, git_sha) in object_generator.import_revisions(
283
new_revid = self.mapping.revision_id_foreign_to_bzr(git_sha)
285
new_revid = old_revid
287
self.mapping.revision_id_bzr_to_foreign(old_revid)
288
except InvalidRevisionId:
289
refname = self.mapping.revid_as_refname(old_revid)
290
self.target_refs[refname] = git_sha
291
revidmap[old_revid] = (git_sha, new_revid)
292
self.target_store.add_objects(object_generator)
297
def fetch(self, revision_id=None, pb=None, find_ghosts=False,
298
fetch_spec=None, mapped_refs=None):
299
if mapped_refs is not None:
300
stop_revisions = mapped_refs
301
elif revision_id is not None:
302
stop_revisions = [(None, revision_id)]
303
elif fetch_spec is not None:
304
recipe = fetch_spec.get_recipe()
305
if recipe[0] in ("search", "proxy-search"):
306
stop_revisions = [(None, revid) for revid in recipe[1]]
308
raise AssertionError("Unsupported search result type %s" % recipe[0])
310
stop_revisions = [(None, revid) for revid in self.source.all_revision_ids()]
313
self.fetch_objects(stop_revisions, lossy=False)
314
except NoPushSupport:
315
raise NoRoundtrippingSupport(self.source, self.target)
318
def is_compatible(source, target):
319
"""Be compatible with GitRepository."""
320
return (not isinstance(source, GitRepository) and
321
isinstance(target, LocalGitRepository))
324
class InterToRemoteGitRepository(InterToGitRepository):
326
def fetch_refs(self, update_refs, lossy, overwrite=False):
327
"""Import the gist of the ancestry of a particular revision."""
328
if not lossy and not self.mapping.roundtripping:
329
raise NoPushSupport(self.source, self.target, self.mapping)
330
unpeel_map = UnpeelMap.from_repository(self.source)
332
def determine_wants(old_refs):
334
self.old_refs = dict([(k, (v, None)) for (k, v) in old_refs.iteritems()])
335
self.new_refs = update_refs(self.old_refs)
336
for name, (gitid, revid) in self.new_refs.iteritems():
338
git_sha = self.source_store._lookup_revision_sha1(revid)
339
gitid = unpeel_map.re_unpeel_tag(git_sha, old_refs.get(name))
341
if remote_divergence(old_refs.get(name), gitid, self.source_store):
342
raise DivergedBranches(self.source, self.target)
346
with self.source_store.lock_read():
347
new_refs = self.target.send_pack(determine_wants,
348
self.source_store.generate_lossy_pack_data)
350
return revidmap, self.old_refs, self.new_refs
353
def is_compatible(source, target):
354
"""Be compatible with GitRepository."""
355
return (not isinstance(source, GitRepository) and
356
isinstance(target, RemoteGitRepository))
359
class InterFromGitRepository(InterRepository):
361
_matching_repo_format = GitRepositoryFormat()
363
def _target_has_shas(self, shas):
364
raise NotImplementedError(self._target_has_shas)
366
def get_determine_wants_heads(self, wants, include_tags=False):
368
def determine_wants(refs):
369
potential = set(wants)
371
for k, unpeeled in refs.iteritems():
372
if k.endswith("^{}"):
376
if unpeeled == ZERO_SHA:
378
potential.add(unpeeled)
379
return list(potential - self._target_has_shas(potential))
380
return determine_wants
382
def determine_wants_all(self, refs):
383
raise NotImplementedError(self.determine_wants_all)
386
def _get_repo_format_to_test():
389
def copy_content(self, revision_id=None):
390
"""See InterRepository.copy_content."""
391
self.fetch(revision_id, find_ghosts=False)
393
def search_missing_revision_ids(self,
394
find_ghosts=True, revision_ids=None, if_present_ids=None,
396
if limit is not None:
397
raise FetchLimitUnsupported(self)
401
todo.extend(revision_ids)
403
todo.extend(revision_ids)
404
with self.lock_read():
405
for revid in revision_ids:
406
if revid == NULL_REVISION:
408
git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
409
git_shas.append(git_sha)
410
walker = Walker(self.source._git.object_store,
411
include=git_shas, exclude=[
412
sha for sha in self.target.controldir.get_refs_container().as_dict().values()
414
missing_revids = set()
416
missing_revids.add(self.source.lookup_foreign_revision_id(entry.commit.id))
417
return self.source.revision_ids_to_search_result(missing_revids)
420
class InterGitNonGitRepository(InterFromGitRepository):
421
"""Base InterRepository that copies revisions from a Git into a non-Git
424
def _target_has_shas(self, shas):
428
revid = self.source.lookup_foreign_revision_id(sha)
429
except NotCommitError:
430
# Commit is definitely not present
434
return set([revids[r] for r in self.target.has_revisions(revids)])
436
def determine_wants_all(self, refs):
438
for k, v in refs.iteritems():
439
# For non-git target repositories, only worry about peeled
442
potential.add(self.source.controldir.get_peeled(k) or v)
443
return list(potential - self._target_has_shas(potential))
445
def get_determine_wants_heads(self, wants, include_tags=False):
447
def determine_wants(refs):
448
potential = set(wants)
450
for k, unpeeled in refs.iteritems():
453
if unpeeled == ZERO_SHA:
455
potential.add(self.source.controldir.get_peeled(k) or unpeeled)
456
return list(potential - self._target_has_shas(potential))
457
return determine_wants
459
def _warn_slow(self):
460
if not config.GlobalConfig().suppress_warning('slow_intervcs_push'):
462
'Fetching from Git to Bazaar repository. '
463
'For better performance, fetch into a Git repository.')
465
def fetch_objects(self, determine_wants, mapping, limit=None, lossy=False):
466
"""Fetch objects from a remote server.
468
:param determine_wants: determine_wants callback
469
:param mapping: BzrGitMapping to use
470
:param limit: Maximum number of commits to import.
471
:return: Tuple with pack hint, last imported revision id and remote refs
473
raise NotImplementedError(self.fetch_objects)
475
def get_determine_wants_revids(self, revids, include_tags=False):
477
for revid in set(revids):
478
if self.target.has_revision(revid):
480
git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
482
return self.get_determine_wants_heads(wants, include_tags=include_tags)
484
def fetch(self, revision_id=None, find_ghosts=False,
485
mapping=None, fetch_spec=None, include_tags=False):
487
mapping = self.source.get_mapping()
488
if revision_id is not None:
489
interesting_heads = [revision_id]
490
elif fetch_spec is not None:
491
recipe = fetch_spec.get_recipe()
492
if recipe[0] in ("search", "proxy-search"):
493
interesting_heads = recipe[1]
495
raise AssertionError("Unsupported search result type %s" %
498
interesting_heads = None
500
if interesting_heads is not None:
501
determine_wants = self.get_determine_wants_revids(
502
interesting_heads, include_tags=include_tags)
504
determine_wants = self.determine_wants_all
506
(pack_hint, _, remote_refs) = self.fetch_objects(determine_wants,
508
if pack_hint is not None and self.target._format.pack_compresses:
509
self.target.pack(hint=pack_hint)
513
class InterRemoteGitNonGitRepository(InterGitNonGitRepository):
514
"""InterRepository that copies revisions from a remote Git into a non-Git
517
def get_target_heads(self):
518
# FIXME: This should be more efficient
519
all_revs = self.target.all_revision_ids()
520
parent_map = self.target.get_parent_map(all_revs)
522
map(all_parents.update, parent_map.itervalues())
523
return set(all_revs) - all_parents
525
def fetch_objects(self, determine_wants, mapping, limit=None, lossy=False):
526
"""See `InterGitNonGitRepository`."""
528
store = get_object_store(self.target, mapping)
529
with store.lock_write():
530
heads = self.get_target_heads()
531
graph_walker = ObjectStoreGraphWalker(
532
[store._lookup_revision_sha1(head) for head in heads],
533
lambda sha: store[sha].parents)
534
wants_recorder = DetermineWantsRecorder(determine_wants)
536
pb = ui.ui_factory.nested_progress_bar()
538
objects_iter = self.source.fetch_objects(
539
wants_recorder, graph_walker, store.get_raw)
540
trace.mutter("Importing %d new revisions",
541
len(wants_recorder.wants))
542
(pack_hint, last_rev) = import_git_objects(self.target,
543
mapping, objects_iter, store, wants_recorder.wants, pb,
545
return (pack_hint, last_rev, wants_recorder.remote_refs)
550
def is_compatible(source, target):
551
"""Be compatible with GitRepository."""
552
if not isinstance(source, RemoteGitRepository):
554
if not target.supports_rich_root():
556
if isinstance(target, GitRepository):
558
if not getattr(target._format, "supports_full_versioned_files", True):
563
class InterLocalGitNonGitRepository(InterGitNonGitRepository):
564
"""InterRepository that copies revisions from a local Git into a non-Git
567
def fetch_objects(self, determine_wants, mapping, limit=None, lossy=False):
568
"""See `InterGitNonGitRepository`."""
570
remote_refs = self.source.controldir.get_refs_container().as_dict()
571
wants = determine_wants(remote_refs)
573
pb = ui.ui_factory.nested_progress_bar()
574
target_git_object_retriever = get_object_store(self.target, mapping)
576
target_git_object_retriever.lock_write()
578
(pack_hint, last_rev) = import_git_objects(self.target,
579
mapping, self.source._git.object_store,
580
target_git_object_retriever, wants, pb, limit)
581
return (pack_hint, last_rev, remote_refs)
583
target_git_object_retriever.unlock()
588
def is_compatible(source, target):
589
"""Be compatible with GitRepository."""
590
if not isinstance(source, LocalGitRepository):
592
if not target.supports_rich_root():
594
if isinstance(target, GitRepository):
596
if not getattr(target._format, "supports_full_versioned_files", True):
601
class InterGitGitRepository(InterFromGitRepository):
602
"""InterRepository that copies between Git repositories."""
604
def fetch_refs(self, update_refs, lossy, overwrite=False):
606
raise LossyPushToSameVCS(self.source, self.target)
607
old_refs = self.target.controldir.get_refs_container()
609
def determine_wants(heads):
610
old_refs = dict([(k, (v, None)) for (k, v) in heads.as_dict().iteritems()])
611
new_refs = update_refs(old_refs)
612
ref_changes.update(new_refs)
613
return [sha1 for (sha1, bzr_revid) in new_refs.itervalues()]
614
self.fetch_objects(determine_wants, lossy=lossy)
615
for k, (git_sha, bzr_revid) in ref_changes.iteritems():
616
self.target._git.refs[k] = git_sha
617
new_refs = self.target.controldir.get_refs_container()
618
return None, old_refs, new_refs
620
def fetch_objects(self, determine_wants, mapping=None, limit=None, lossy=False):
621
raise NotImplementedError(self.fetch_objects)
623
def _target_has_shas(self, shas):
624
return set([sha for sha in shas if sha in self.target._git.object_store])
626
def fetch(self, revision_id=None, find_ghosts=False,
627
mapping=None, fetch_spec=None, branches=None, limit=None, include_tags=False):
629
mapping = self.source.get_mapping()
630
if revision_id is not None:
632
elif fetch_spec is not None:
633
recipe = fetch_spec.get_recipe()
634
if recipe[0] in ("search", "proxy-search"):
637
raise AssertionError(
638
"Unsupported search result type %s" % recipe[0])
640
if branches is not None:
641
def determine_wants(refs):
643
for name, value in refs.iteritems():
644
if value == ZERO_SHA:
647
if name in branches or (include_tags and is_tag(name)):
650
elif fetch_spec is None and revision_id is None:
651
determine_wants = self.determine_wants_all
653
determine_wants = self.get_determine_wants_revids(args, include_tags=include_tags)
654
wants_recorder = DetermineWantsRecorder(determine_wants)
655
self.fetch_objects(wants_recorder, mapping, limit=limit)
656
return wants_recorder.remote_refs
658
def get_determine_wants_revids(self, revids, include_tags=False):
660
for revid in set(revids):
661
if revid == NULL_REVISION:
663
git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
665
return self.get_determine_wants_heads(wants, include_tags=include_tags)
667
def determine_wants_all(self, refs):
668
potential = set([v for v in refs.values() if not v == ZERO_SHA])
669
return list(potential - self._target_has_shas(potential))
672
class InterLocalGitLocalGitRepository(InterGitGitRepository):
674
def fetch_objects(self, determine_wants, mapping=None, limit=None, lossy=False):
676
raise LossyPushToSameVCS(self.source, self.target)
677
if limit is not None:
678
raise FetchLimitUnsupported(self)
679
refs = self.source._git.fetch(self.target._git, determine_wants)
680
return (None, None, refs)
683
def is_compatible(source, target):
684
"""Be compatible with GitRepository."""
685
return (isinstance(source, LocalGitRepository) and
686
isinstance(target, LocalGitRepository))
689
class InterRemoteGitLocalGitRepository(InterGitGitRepository):
691
def fetch_objects(self, determine_wants, mapping=None, limit=None, lossy=False):
693
raise LossyPushToSameVCS(self.source, self.target)
694
if limit is not None:
695
raise FetchLimitUnsupported(self)
696
graphwalker = self.target._git.get_graph_walker()
697
if CAPABILITY_THIN_PACK in self.source.controldir._client._fetch_capabilities:
698
# TODO(jelmer): Avoid reading entire file into memory and
699
# only processing it after the whole file has been fetched.
705
self.target._git.object_store.move_in_thin_pack(f)
710
f, commit, abort = self.target._git.object_store.add_pack()
712
refs = self.source.controldir.fetch_pack(
713
determine_wants, graphwalker, f.write)
715
return (None, None, refs)
716
except BaseException:
721
def is_compatible(source, target):
722
"""Be compatible with GitRepository."""
723
return (isinstance(source, RemoteGitRepository) and
724
isinstance(target, LocalGitRepository))