1
# Copyright (C) 2008-2018 Jelmer Vernooij <jelmer@jelmer.uk>
2
# Copyright (C) 2007 Canonical Ltd
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
"""An adapter between a Git Repository and a Bazaar Branch"""
26
revision as _mod_revision,
31
from ..decorators import only_raises
32
from ..foreign import (
36
from .filegraph import (
37
GitFileLastChangeScanner,
38
GitFileParentProvider,
40
from .mapping import (
51
from dulwich.errors import (
54
from dulwich.objects import (
58
from dulwich.object_store import (
63
class GitCheck(check.Check):
65
def __init__(self, repository, check_repo=True):
66
self.repository = repository
67
self.check_repo = check_repo
68
self.checked_rev_cnt = 0
69
self.object_count = None
72
def check(self, callback_refs=None, check_repo=True):
73
if callback_refs is None:
75
with self.repository.lock_read(), \
76
ui.ui_factory.nested_progress_bar() as self.progress:
77
shas = set(self.repository._git.object_store)
78
self.object_count = len(shas)
79
# TODO(jelmer): Check more things
80
for i, sha in enumerate(shas):
81
self.progress.update('checking objects', i, self.object_count)
82
o = self.repository._git.object_store[sha]
85
except Exception as e:
86
self.problems.append((sha, e))
88
def _report_repo_results(self, verbose):
89
trace.note('checked repository {0} format {1}'.format(
90
self.repository.user_url,
91
self.repository._format))
92
trace.note('%6d objects', self.object_count)
93
for sha, problem in self.problems:
94
trace.note('%s: %s', sha, problem)
96
def report_results(self, verbose):
98
self._report_repo_results(verbose)
101
for optimiser in ['InterRemoteGitNonGitRepository',
102
'InterLocalGitNonGitRepository',
103
'InterLocalGitLocalGitRepository',
104
'InterRemoteGitLocalGitRepository',
105
'InterToLocalGitRepository',
106
'InterToRemoteGitRepository',
108
repository.InterRepository.register_lazy_optimiser(
109
'breezy.git.interrepo', optimiser)
112
class GitRepository(ForeignRepository):
113
"""An adapter to git repositories for bzr."""
116
vcs = foreign_vcs_git
119
def __init__(self, gitdir):
120
self._transport = gitdir.root_transport
121
super(GitRepository, self).__init__(GitRepositoryFormat(),
122
gitdir, control_files=None)
123
self.base = gitdir.root_transport.base
124
self._lock_mode = None
127
def add_fallback_repository(self, basis_url):
128
raise errors.UnstackableRepositoryFormat(self._format,
129
self.control_transport.base)
134
def get_physical_lock_status(self):
137
def lock_write(self):
138
"""See Branch.lock_write()."""
140
if self._lock_mode != 'w':
141
raise errors.ReadOnlyError(self)
142
self._lock_count += 1
144
self._lock_mode = 'w'
146
self._transaction = transactions.WriteTransaction()
147
return repository.RepositoryWriteLockResult(self.unlock, None)
149
def break_lock(self):
150
raise NotImplementedError(self.break_lock)
152
def dont_leave_lock_in_place(self):
153
raise NotImplementedError(self.dont_leave_lock_in_place)
155
def leave_lock_in_place(self):
156
raise NotImplementedError(self.leave_lock_in_place)
160
if self._lock_mode not in ('r', 'w'):
162
self._lock_count += 1
164
self._lock_mode = 'r'
166
self._transaction = transactions.ReadOnlyTransaction()
167
return lock.LogicalLockResult(self.unlock)
169
@only_raises(errors.LockNotHeld, errors.LockBroken)
171
if self._lock_count == 0:
172
raise errors.LockNotHeld(self)
173
if self._lock_count == 1 and self._lock_mode == 'w':
174
if self._write_group is not None:
175
self.abort_write_group()
176
self._lock_count -= 1
177
self._lock_mode = None
178
raise errors.BzrError(
179
'Must end write groups before releasing write locks.')
180
self._lock_count -= 1
181
if self._lock_count == 0:
182
self._lock_mode = None
183
transaction = self._transaction
184
self._transaction = None
187
def is_write_locked(self):
188
return (self._lock_mode == 'w')
191
return (self._lock_mode is not None)
193
def get_transaction(self):
194
"""See Repository.get_transaction()."""
195
if self._transaction is None:
196
return transactions.PassThroughTransaction()
198
return self._transaction
200
def reconcile(self, other=None, thorough=False):
201
"""Reconcile this repository."""
202
from ..reconcile import ReconcileResult
203
ret = ReconcileResult()
207
def supports_rich_root(self):
210
def get_mapping(self):
211
return default_mapping
213
def make_working_trees(self):
214
return not self._git.get_config().get_boolean(("core", ), "bare")
216
def revision_graph_can_have_wrong_parents(self):
219
def add_signature_text(self, revid, signature):
220
raise errors.UnsupportedOperation(self.add_signature_text, self)
222
def sign_revision(self, revision_id, gpg_strategy):
223
raise errors.UnsupportedOperation(self.add_signature_text, self)
226
class LocalGitRepository(GitRepository):
227
"""Git repository on the file system."""
229
def __init__(self, gitdir):
230
GitRepository.__init__(self, gitdir)
231
self._git = gitdir._git
232
self._file_change_scanner = GitFileLastChangeScanner(self)
233
self._transaction = None
235
def get_commit_builder(self, branch, parents, config, timestamp=None,
236
timezone=None, committer=None, revprops=None,
237
revision_id=None, lossy=False):
238
"""Obtain a CommitBuilder for this repository.
240
:param branch: Branch to commit to.
241
:param parents: Revision ids of the parents of the new revision.
242
:param config: Configuration to use.
243
:param timestamp: Optional timestamp recorded for commit.
244
:param timezone: Optional timezone for timestamp.
245
:param committer: Optional committer to set for commit.
246
:param revprops: Optional dictionary of revision properties.
247
:param revision_id: Optional revision id.
248
:param lossy: Whether to discard data that can not be natively
249
represented, when pushing to a foreign VCS
251
from .commit import (
254
builder = GitCommitBuilder(
255
self, parents, config, timestamp, timezone, committer, revprops,
257
self.start_write_group()
260
def get_file_graph(self):
261
return _mod_graph.Graph(GitFileParentProvider(
262
self._file_change_scanner))
264
def iter_files_bytes(self, desired_files):
265
"""Iterate through file versions.
267
Files will not necessarily be returned in the order they occur in
268
desired_files. No specific order is guaranteed.
270
Yields pairs of identifier, bytes_iterator. identifier is an opaque
271
value supplied by the caller as part of desired_files. It should
272
uniquely identify the file version in the caller's context. (Examples:
273
an index number or a TreeTransform trans_id.)
275
bytes_iterator is an iterable of bytestrings for the file. The
276
kind of iterable and length of the bytestrings are unspecified, but for
277
this implementation, it is a list of bytes produced by
278
VersionedFile.get_record_stream().
280
:param desired_files: a list of (file_id, revision_id, identifier)
284
for (file_id, revision_id, identifier) in desired_files:
285
per_revision.setdefault(revision_id, []).append(
286
(file_id, identifier))
287
for revid, files in per_revision.items():
289
(commit_id, mapping) = self.lookup_bzr_revision_id(revid)
290
except errors.NoSuchRevision:
291
raise errors.RevisionNotPresent(revid, self)
293
commit = self._git.object_store[commit_id]
295
raise errors.RevisionNotPresent(revid, self)
296
root_tree = commit.tree
297
for fileid, identifier in files:
299
path = mapping.parse_file_id(fileid)
301
raise errors.RevisionNotPresent((fileid, revid), self)
303
obj = tree_lookup_path(
304
self._git.object_store.__getitem__, root_tree,
305
encode_git_path(path))
306
if isinstance(obj, tuple):
307
(mode, item_id) = obj
308
obj = self._git.object_store[item_id]
310
raise errors.RevisionNotPresent((fileid, revid), self)
312
if obj.type_name == b"tree":
313
yield (identifier, [])
314
elif obj.type_name == b"blob":
315
yield (identifier, obj.chunked)
317
raise AssertionError("file text resolved to %r" % obj)
319
def gather_stats(self, revid=None, committers=None):
320
"""See Repository.gather_stats()."""
321
result = super(LocalGitRepository, self).gather_stats(
324
for sha in self._git.object_store:
325
o = self._git.object_store[sha]
326
if o.type_name == b"commit":
328
result['revisions'] = len(revs)
331
def _iter_revision_ids(self):
332
mapping = self.get_mapping()
333
for sha in self._git.object_store:
334
o = self._git.object_store[sha]
335
if not isinstance(o, Commit):
337
revid = mapping.revision_id_foreign_to_bzr(o.id)
340
def all_revision_ids(self):
342
for git_sha, revid in self._iter_revision_ids():
346
def _get_parents(self, revid, no_alternates=False):
347
if type(revid) != bytes:
350
(hexsha, mapping) = self.lookup_bzr_revision_id(revid)
351
except errors.NoSuchRevision:
353
# FIXME: Honor no_alternates setting
355
commit = self._git.object_store[hexsha]
359
for p in commit.parents:
361
ret.append(self.lookup_foreign_revision_id(p, mapping))
363
ret.append(mapping.revision_id_foreign_to_bzr(p))
366
def _get_parent_map_no_fallbacks(self, revids):
367
return self.get_parent_map(revids, no_alternates=True)
369
def get_parent_map(self, revids, no_alternates=False):
371
for revision_id in revids:
372
parents = self._get_parents(
373
revision_id, no_alternates=no_alternates)
374
if revision_id == _mod_revision.NULL_REVISION:
375
parent_map[revision_id] = ()
379
if len(parents) == 0:
380
parents = [_mod_revision.NULL_REVISION]
381
parent_map[revision_id] = tuple(parents)
384
def get_known_graph_ancestry(self, revision_ids):
385
"""Return the known graph for a set of revision ids and their ancestors.
387
pending = set(revision_ids)
391
for revid in pending:
392
if revid == _mod_revision.NULL_REVISION:
394
parents = self._get_parents(revid)
395
if parents is not None:
396
this_parent_map[revid] = parents
397
parent_map.update(this_parent_map)
399
for values in this_parent_map.values():
400
pending.update(values)
401
pending = pending.difference(parent_map)
402
return _mod_graph.KnownGraph(parent_map)
404
def get_signature_text(self, revision_id):
405
git_commit_id, mapping = self.lookup_bzr_revision_id(revision_id)
407
commit = self._git.object_store[git_commit_id]
409
raise errors.NoSuchRevision(self, revision_id)
410
if commit.gpgsig is None:
411
raise errors.NoSuchRevision(self, revision_id)
414
def check(self, revision_ids=None, callback_refs=None, check_repo=True):
415
result = GitCheck(self, check_repo=check_repo)
416
result.check(callback_refs)
419
def pack(self, hint=None, clean_obsolete_packs=False):
420
self._git.object_store.pack_loose_objects()
422
def lookup_foreign_revision_id(self, foreign_revid, mapping=None):
423
"""Lookup a revision id.
425
:param foreign_revid: Foreign revision id to look up
426
:param mapping: Mapping to use (use default mapping if not specified)
427
:raise KeyError: If foreign revision was not found
428
:return: bzr revision id
430
if not isinstance(foreign_revid, bytes):
431
raise TypeError(foreign_revid)
433
mapping = self.get_mapping()
434
if foreign_revid == ZERO_SHA:
435
return _mod_revision.NULL_REVISION
436
commit = self._git.object_store.peel_sha(foreign_revid)
437
if not isinstance(commit, Commit):
438
raise NotCommitError(commit.id)
439
revid = mapping.get_revision_id(commit)
440
# FIXME: check testament before doing this?
443
def has_signature_for_revision_id(self, revision_id):
444
"""Check whether a GPG signature is present for this revision.
446
This is never the case for Git repositories.
449
self.get_signature_text(revision_id)
450
except errors.NoSuchRevision:
455
def verify_revision_signature(self, revision_id, gpg_strategy):
456
"""Verify the signature on a revision.
458
:param revision_id: the revision to verify
459
:gpg_strategy: the GPGStrategy object to used
461
:return: gpg.SIGNATURE_VALID or a failed SIGNATURE_ value
463
from breezy import gpg
464
with self.lock_read():
465
git_commit_id, mapping = self.lookup_bzr_revision_id(revision_id)
467
commit = self._git.object_store[git_commit_id]
469
raise errors.NoSuchRevision(self, revision_id)
471
if commit.gpgsig is None:
472
return gpg.SIGNATURE_NOT_SIGNED, None
474
without_sig = Commit.from_string(commit.as_raw_string())
475
without_sig.gpgsig = None
477
(result, key, plain_text) = gpg_strategy.verify(
478
without_sig.as_raw_string(), commit.gpgsig)
481
def lookup_bzr_revision_id(self, bzr_revid, mapping=None):
482
"""Lookup a bzr revision id in a Git repository.
484
:param bzr_revid: Bazaar revision id
485
:param mapping: Optional mapping to use
486
:return: Tuple with git commit id, mapping that was used and supplement
490
(git_sha, mapping) = mapping_registry.revision_id_bzr_to_foreign(
492
except errors.InvalidRevisionId:
493
raise errors.NoSuchRevision(self, bzr_revid)
495
return (git_sha, mapping)
497
def get_revision(self, revision_id):
498
if not isinstance(revision_id, bytes):
499
raise errors.InvalidRevisionId(revision_id, self)
500
git_commit_id, mapping = self.lookup_bzr_revision_id(revision_id)
502
commit = self._git.object_store[git_commit_id]
504
raise errors.NoSuchRevision(self, revision_id)
505
revision, roundtrip_revid, verifiers = mapping.import_commit(
506
commit, self.lookup_foreign_revision_id, strict=False)
509
# FIXME: check verifiers ?
511
revision.revision_id = roundtrip_revid
514
def has_revision(self, revision_id):
515
"""See Repository.has_revision."""
516
if revision_id == _mod_revision.NULL_REVISION:
519
git_commit_id, mapping = self.lookup_bzr_revision_id(revision_id)
520
except errors.NoSuchRevision:
522
return (git_commit_id in self._git)
524
def has_revisions(self, revision_ids):
525
"""See Repository.has_revisions."""
526
return set(filter(self.has_revision, revision_ids))
528
def iter_revisions(self, revision_ids):
529
"""See Repository.get_revisions."""
530
for revid in revision_ids:
532
rev = self.get_revision(revid)
533
except errors.NoSuchRevision:
537
def revision_trees(self, revids):
538
"""See Repository.revision_trees."""
540
yield self.revision_tree(revid)
542
def revision_tree(self, revision_id):
543
"""See Repository.revision_tree."""
544
if revision_id is None:
545
raise ValueError('invalid revision id %s' % revision_id)
546
return GitRevisionTree(self, revision_id)
548
def set_make_working_trees(self, trees):
549
raise errors.UnsupportedOperation(self.set_make_working_trees, self)
551
def fetch_objects(self, determine_wants, graph_walker, resolve_ext_ref,
552
progress=None, limit=None):
553
return self._git.fetch_objects(determine_wants, graph_walker, progress,
557
class GitRepositoryFormat(repository.RepositoryFormat):
558
"""Git repository format."""
560
supports_versioned_directories = False
561
supports_tree_reference = True
562
rich_root_data = True
563
supports_leaving_lock = False
565
supports_funky_characters = True
566
supports_external_lookups = False
567
supports_full_versioned_files = False
568
supports_revision_signatures = False
569
supports_nesting_repositories = False
570
revision_graph_can_have_wrong_parents = False
571
supports_unreferenced_revisions = True
572
supports_setting_revision_ids = False
573
supports_storing_branch_nick = False
574
supports_overriding_transport = False
575
supports_custom_revision_properties = False
576
records_per_file_revision = False
579
def _matchingcontroldir(self):
580
from .dir import LocalGitControlDirFormat
581
return LocalGitControlDirFormat()
583
def get_format_description(self):
584
return "Git Repository"
586
def initialize(self, controldir, shared=False, _internal=False):
587
from .dir import GitDir
588
if not isinstance(controldir, GitDir):
589
raise errors.UninitializableFormat(self)
590
return controldir.open_repository()
592
def check_conversion_target(self, target_repo_format):
593
return target_repo_format.rich_root_data
595
def get_foreign_tests_repository_factory(self):
596
from .tests.test_repository import (
597
ForeignTestsRepositoryFactory,
599
return ForeignTestsRepositoryFactory()
601
def network_name(self):
605
def get_extra_interrepo_test_combinations():
606
from ..bzr.groupcompress_repo import RepositoryFormat2a
607
from . import interrepo
609
(interrepo.InterLocalGitNonGitRepository,
610
GitRepositoryFormat(), RepositoryFormat2a()),
611
(interrepo.InterLocalGitLocalGitRepository,
612
GitRepositoryFormat(), GitRepositoryFormat()),
613
(interrepo.InterToLocalGitRepository,
614
RepositoryFormat2a(), GitRepositoryFormat()),