1
# Copyright (C) 2007-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
"""Remote dirs, repositories and branches."""
19
from __future__ import absolute_import
21
from io import BytesIO
36
from ..errors import (
48
UninitializableFormat,
50
from ..revisiontree import RevisionTree
51
from ..transport import (
53
register_urlparse_netloc_protocol,
58
user_agent_for_github,
72
BareLocalGitControlDirFormat,
75
GitSmartRemoteNotSupported,
78
from .mapping import (
81
from .object_store import (
87
from .repository import (
99
from dulwich.errors import (
102
from dulwich.pack import (
104
pack_objects_to_data,
106
from dulwich.protocol import ZERO_SHA
107
from dulwich.refs import (
111
from dulwich.repo import (
120
import urllib.parse as urlparse
121
from urllib.parse import splituser, splitnport
124
from urllib import splituser, splitnport
126
# urlparse only supports a limited number of schemes by default
127
register_urlparse_netloc_protocol('git')
128
register_urlparse_netloc_protocol('git+ssh')
130
from dulwich.pack import load_pack_index
133
class GitPushResult(PushResult):
135
def _lookup_revno(self, revid):
137
return _quick_lookup_revno(self.source_branch, self.target_branch,
139
except GitSmartRemoteNotSupported:
144
return self._lookup_revno(self.old_revid)
148
return self._lookup_revno(self.new_revid)
151
# Don't run any tests on GitSmartTransport as it is not intended to be
152
# a full implementation of Transport
153
def get_test_permutations():
157
def split_git_url(url):
161
:return: Tuple with host, port, username, path.
163
(scheme, netloc, loc, _, _) = urlparse.urlsplit(url)
164
path = urlparse.unquote(loc)
165
if path.startswith("/~"):
167
(username, hostport) = splituser(netloc)
168
(host, port) = splitnport(hostport, None)
169
return (host, port, username, path)
172
class RemoteGitError(BzrError):
174
_fmt = "Remote server error: %(msg)s"
177
def parse_git_error(url, message):
178
"""Parse a remote git server error and return a bzr exception.
180
:param url: URL of the remote repository
181
:param message: Message sent by the remote git server
183
message = str(message).strip()
184
if message.startswith("Could not find Repository "):
185
return NotBranchError(url, message)
186
if message == "HEAD failed to update":
187
base_url, _ = urlutils.split_segment_parameters(url)
189
("Unable to update remote HEAD branch. To update the master "
190
"branch, specify the URL %s,branch=master.") % base_url)
191
# Don't know, just return it to the user as-is
192
return RemoteGitError(message)
195
class GitSmartTransport(Transport):
197
def __init__(self, url, _client=None):
198
Transport.__init__(self, url)
199
(self._host, self._port, self._username, self._path) = \
201
if 'transport' in debug.debug_flags:
202
trace.mutter('host: %r, user: %r, port: %r, path: %r',
203
self._host, self._username, self._port, self._path)
204
self._client = _client
205
self._stripped_path = self._path.rsplit(",", 1)[0]
207
def external_url(self):
210
def has(self, relpath):
213
def _get_client(self):
214
raise NotImplementedError(self._get_client)
217
return self._stripped_path
220
raise NoSuchFile(path)
222
def abspath(self, relpath):
223
return urlutils.join(self.base, relpath)
225
def clone(self, offset=None):
226
"""See Transport.clone()."""
230
newurl = urlutils.join(self.base, offset)
232
return self.__class__(newurl, self._client)
235
class TCPGitSmartTransport(GitSmartTransport):
239
def _get_client(self):
240
if self._client is not None:
245
# return dulwich.client.LocalGitClient()
246
return dulwich.client.SubprocessGitClient()
247
return dulwich.client.TCPGitClient(self._host, self._port,
248
report_activity=self._report_activity)
251
class SSHSocketWrapper(object):
253
def __init__(self, sock):
256
def read(self, len=None):
257
return self.sock.recv(len)
259
def write(self, data):
260
return self.sock.write(data)
263
return len(select.select([self.sock.fileno()], [], [], 0)[0]) > 0
266
class DulwichSSHVendor(dulwich.client.SSHVendor):
269
from ..transport import ssh
270
self.bzr_ssh_vendor = ssh._get_ssh_vendor()
272
def run_command(self, host, command, username=None, port=None):
273
connection = self.bzr_ssh_vendor.connect_ssh(username=username,
274
password=None, port=port, host=host, command=command)
275
(kind, io_object) = connection.get_sock_or_pipes()
277
return SSHSocketWrapper(io_object)
279
raise AssertionError("Unknown io object kind %r'" % kind)
282
#dulwich.client.get_ssh_vendor = DulwichSSHVendor
285
class SSHGitSmartTransport(GitSmartTransport):
290
path = self._stripped_path
291
if path.startswith("/~/"):
295
def _get_client(self):
296
if self._client is not None:
300
location_config = config.LocationConfig(self.base)
301
client = dulwich.client.SSHGitClient(self._host, self._port, self._username,
302
report_activity=self._report_activity)
303
# Set up alternate pack program paths
304
upload_pack = location_config.get_user_option('git_upload_pack')
306
client.alternative_paths["upload-pack"] = upload_pack
307
receive_pack = location_config.get_user_option('git_receive_pack')
309
client.alternative_paths["receive-pack"] = receive_pack
313
class RemoteGitBranchFormat(GitBranchFormat):
315
def get_format_description(self):
316
return 'Remote Git Branch'
319
def _matchingcontroldir(self):
320
return RemoteGitControlDirFormat()
322
def initialize(self, a_controldir, name=None, repository=None,
323
append_revisions_only=None):
324
raise UninitializableFormat(self)
327
class DefaultProgressReporter(object):
329
_GIT_PROGRESS_PARTIAL_RE = re.compile(r"(.*?): +(\d+)% \((\d+)/(\d+)\)")
330
_GIT_PROGRESS_TOTAL_RE = re.compile(r"(.*?): (\d+)")
332
def __init__(self, pb):
335
def progress(self, text):
336
text = text.rstrip("\r\n")
337
if text.startswith('error: '):
338
trace.show_error('git: %s', text[len('error: '):])
340
trace.mutter("git: %s", text)
341
g = self._GIT_PROGRESS_PARTIAL_RE.match(text)
343
(text, pct, current, total) = g.groups()
344
self.pb.update(text, int(current), int(total))
346
g = self._GIT_PROGRESS_TOTAL_RE.match(text)
348
(text, total) = g.groups()
349
self.pb.update(text, None, int(total))
351
trace.note("%s", text)
354
class RemoteGitDir(GitDir):
356
def __init__(self, transport, format, client, client_path):
357
self._format = format
358
self.root_transport = transport
359
self.transport = transport
360
self._mode_check_done = None
361
self._client = client
362
self._client_path = client_path
363
self.base = self.root_transport.base
367
def _gitrepository_class(self):
368
return RemoteGitRepository
370
def archive(self, format, committish, write_data, progress=None, write_error=None,
371
subdirs=None, prefix=None):
372
if format not in ('tar', 'zip'):
373
raise errors.NoSuchExportFormat(format)
375
pb = ui.ui_factory.nested_progress_bar()
376
progress = DefaultProgressReporter(pb).progress
380
self._client.archive(self._client_path, committish,
381
write_data, progress, write_error, format=format,
382
subdirs=subdirs, prefix=prefix)
383
except GitProtocolError as e:
384
raise parse_git_error(self.transport.external_url(), e)
389
def fetch_pack(self, determine_wants, graph_walker, pack_data, progress=None):
391
pb = ui.ui_factory.nested_progress_bar()
392
progress = DefaultProgressReporter(pb).progress
396
result = self._client.fetch_pack(self._client_path, determine_wants,
397
graph_walker, pack_data, progress)
398
if result.refs is None:
400
self._refs = remote_refs_dict_to_container(result.refs, result.symrefs)
402
except GitProtocolError as e:
403
raise parse_git_error(self.transport.external_url(), e)
408
def send_pack(self, get_changed_refs, generate_pack_data, progress=None):
410
pb = ui.ui_factory.nested_progress_bar()
411
progress = DefaultProgressReporter(pb).progress
414
def get_changed_refs_wrapper(refs):
415
# TODO(jelmer): This drops symref information
416
self._refs = remote_refs_dict_to_container(refs)
417
return get_changed_refs(refs)
419
return self._client.send_pack(self._client_path,
420
get_changed_refs_wrapper, generate_pack_data, progress)
421
except GitProtocolError as e:
422
raise parse_git_error(self.transport.external_url(), e)
427
def create_branch(self, name=None, repository=None,
428
append_revisions_only=None, ref=None):
429
refname = self._get_selected_ref(name, ref)
430
if refname != b'HEAD' and refname in self.get_refs_container():
431
raise AlreadyBranchError(self.user_url)
432
if refname in self.get_refs_container():
433
ref_chain, unused_sha = self.get_refs_container().follow(self._get_selected_ref(None))
434
if ref_chain[0] == b'HEAD':
435
refname = ref_chain[1]
436
repo = self.open_repository()
437
return RemoteGitBranch(self, repo, refname)
439
def destroy_branch(self, name=None):
440
refname = self._get_selected_ref(name)
441
def get_changed_refs(old_refs):
443
if not refname in ret:
444
raise NotBranchError(self.user_url)
445
ret[refname] = dulwich.client.ZERO_SHA
447
def generate_pack_data(have, want, ofs_delta=False):
448
return pack_objects_to_data([])
449
self.send_pack(get_changed_refs, generate_pack_data)
453
return self.control_url
456
def user_transport(self):
457
return self.root_transport
460
def control_url(self):
461
return self.control_transport.base
464
def control_transport(self):
465
return self.root_transport
467
def open_repository(self):
468
return RemoteGitRepository(self)
470
def open_branch(self, name=None, unsupported=False,
471
ignore_fallbacks=False, ref=None, possible_transports=None,
473
repo = self.open_repository()
474
ref = self._get_selected_ref(name, ref)
475
if not nascent_ok and ref not in self.get_refs_container():
476
raise NotBranchError(self.root_transport.base,
478
ref_chain, unused_sha = self.get_refs_container().follow(ref)
479
return RemoteGitBranch(self, repo, ref_chain[-1])
481
def open_workingtree(self, recommend_upgrade=False):
482
raise NotLocalUrl(self.transport.base)
484
def has_workingtree(self):
487
def get_peeled(self, name):
488
return self.get_refs_container().get_peeled(name)
490
def get_refs_container(self):
491
if self._refs is not None:
493
result = self.fetch_pack(lambda x: None, None,
494
lambda x: None, lambda x: trace.mutter("git: %s" % x))
495
self._refs = remote_refs_dict_to_container(
496
result.refs, result.symrefs)
499
def push_branch(self, source, revision_id=None, overwrite=False,
500
remember=False, create_prefix=False, lossy=False,
502
"""Push the source branch into this ControlDir."""
503
if revision_id is None:
504
# No revision supplied by the user, default to the branch
506
revision_id = source.last_revision()
508
push_result = GitPushResult()
509
push_result.workingtree_updated = None
510
push_result.master_branch = None
511
push_result.source_branch = source
512
push_result.stacked_on = None
513
push_result.branch_push_result = None
514
repo = self.find_repository()
515
refname = self._get_selected_ref(name)
516
if isinstance(source, GitBranch) and lossy:
517
raise errors.LossyPushToSameVCS(source.controldir, self)
518
source_store = get_object_store(source.repository)
519
with source_store.lock_read():
520
def get_changed_refs(refs):
521
self._refs = remote_refs_dict_to_container(refs)
523
# TODO(jelmer): Unpeel if necessary
524
push_result.new_original_revid = revision_id
526
new_sha = source_store._lookup_revision_sha1(revision_id)
528
new_sha = repo.lookup_bzr_revision_id(revision_id)[0]
530
if remote_divergence(ret.get(refname), new_sha, source_store):
531
raise DivergedBranches(
532
source, self.open_branch(name, nascent_ok=True))
533
ret[refname] = new_sha
536
generate_pack_data = source_store.generate_lossy_pack_data
538
generate_pack_data = source_store.generate_pack_data
539
new_refs = self.send_pack(get_changed_refs, generate_pack_data)
540
push_result.new_revid = repo.lookup_foreign_revision_id(
543
old_remote = self._refs[refname]
545
old_remote = ZERO_SHA
546
push_result.old_revid = repo.lookup_foreign_revision_id(old_remote)
547
self._refs = remote_refs_dict_to_container(new_refs)
548
push_result.target_branch = self.open_branch(name)
549
if old_remote != ZERO_SHA:
550
push_result.branch_push_result = GitBranchPushResult()
551
push_result.branch_push_result.source_branch = source
552
push_result.branch_push_result.target_branch = push_result.target_branch
553
push_result.branch_push_result.local_branch = None
554
push_result.branch_push_result.master_branch = push_result.target_branch
555
push_result.branch_push_result.old_revid = push_result.old_revid
556
push_result.branch_push_result.new_revid = push_result.new_revid
557
push_result.branch_push_result.new_original_revid = push_result.new_original_revid
558
if source.get_push_location() is None or remember:
559
source.set_push_location(push_result.target_branch.base)
562
def _find_commondir(self):
563
# There is no way to find the commondir, if there is any.
567
class EmptyObjectStoreIterator(dict):
569
def iterobjects(self):
573
class TemporaryPackIterator(Pack):
575
def __init__(self, path, resolve_ext_ref):
576
super(TemporaryPackIterator, self).__init__(
577
path, resolve_ext_ref=resolve_ext_ref)
578
self._idx_load = lambda: self._idx_load_or_generate(self._idx_path)
580
def _idx_load_or_generate(self, path):
581
if not os.path.exists(path):
582
pb = ui.ui_factory.nested_progress_bar()
584
def report_progress(cur, total):
585
pb.update("generating index", cur, total)
586
self.data.create_index(path,
587
progress=report_progress)
590
return load_pack_index(path)
593
if self._idx is not None:
595
os.remove(self._idx_path)
596
if self._data is not None:
598
os.remove(self._data_path)
601
class BzrGitHttpClient(dulwich.client.HttpGitClient):
603
def __init__(self, transport, *args, **kwargs):
604
self.transport = transport
605
super(BzrGitHttpClient, self).__init__(transport.external_url(), *args, **kwargs)
607
def _http_request(self, url, headers=None, data=None,
608
allow_compression=False):
609
"""Perform HTTP request.
611
:param url: Request URL.
612
:param headers: Optional custom headers to override defaults.
613
:param data: Request data.
614
:param allow_compression: Allow GZipped communication.
615
:return: Tuple (`response`, `read`), where response is an `urllib3`
616
response object with additional `content_type` and
617
`redirect_location` properties, and `read` is a consumable read
618
method for the response data.
620
from breezy.transport.http._urllib2_wrappers import Request
621
headers['User-agent'] = user_agent_for_github()
622
headers["Pragma"] = "no-cache"
623
if allow_compression:
624
headers["Accept-Encoding"] = "gzip"
626
headers["Accept-Encoding"] = "identity"
629
('GET' if data is None else 'POST'),
631
accepted_errors=[200, 404])
633
response = self.transport._perform(request)
635
if response.code == 404:
636
raise NotGitRepository()
637
elif response.code != 200:
638
raise GitProtocolError("unexpected http resp %d for %s" %
639
(response.code, url))
641
# TODO: Optimization available by adding `preload_content=False` to the
642
# request and just passing the `read` method on instead of going via
643
# `BytesIO`, if we can guarantee that the entire response is consumed
644
# before issuing the next to still allow for connection reuse from the
646
if response.getheader("Content-Encoding") == "gzip":
647
read = gzip.GzipFile(fileobj=response).read
651
class WrapResponse(object):
653
def __init__(self, response):
654
self._response = response
655
self.status = response.code
656
self.content_type = response.getheader("Content-Type")
657
self.redirect_location = response.geturl()
660
self._response.close()
662
return WrapResponse(response), read
665
class RemoteGitControlDirFormat(GitControlDirFormat):
666
"""The .git directory control format."""
668
supports_workingtrees = False
671
def _known_formats(self):
672
return set([RemoteGitControlDirFormat()])
674
def get_branch_format(self):
675
return RemoteGitBranchFormat()
677
def is_initializable(self):
680
def is_supported(self):
683
def open(self, transport, _found=None):
684
"""Open this directory.
687
# we dont grok readonly - git isn't integrated with transport.
689
if url.startswith('readonly+'):
690
url = url[len('readonly+'):]
691
scheme = urlparse.urlsplit(transport.external_url())[0]
692
if isinstance(transport, GitSmartTransport):
693
client = transport._get_client()
694
client_path = transport._get_path()
695
elif scheme in ("http", "https"):
696
client = BzrGitHttpClient(transport)
697
client_path, _ = urlutils.split_segment_parameters(transport._path)
698
elif scheme == 'file':
699
client = dulwich.client.LocalGitClient()
700
client_path = transport.local_abspath('.')
702
raise NotBranchError(transport.base)
704
pass # TODO(jelmer): Actually probe for something
705
return RemoteGitDir(transport, self, client, client_path)
707
def get_format_description(self):
708
return "Remote Git Repository"
710
def initialize_on_transport(self, transport):
711
raise UninitializableFormat(self)
713
def supports_transport(self, transport):
715
external_url = transport.external_url()
716
except InProcessTransport:
717
raise NotBranchError(path=transport.base)
718
return (external_url.startswith("http:") or
719
external_url.startswith("https:") or
720
external_url.startswith("git+") or
721
external_url.startswith("git:"))
724
class GitRemoteRevisionTree(RevisionTree):
726
def archive(self, format, name, root=None, subdir=None, force_mtime=None):
727
"""Create an archive of this tree.
729
:param format: Format name (e.g. 'tar')
730
:param name: target file name
731
:param root: Root directory name (or None)
732
:param subdir: Subdirectory to export (or None)
733
:return: Iterator over archive chunks
735
commit = self._repository.lookup_bzr_revision_id(
736
self.get_revision_id())[0]
737
f = tempfile.SpooledTemporaryFile()
738
# git-upload-archive(1) generaly only supports refs. So let's see if we
742
self._repository.controldir.get_refs_container().as_dict().items()}
744
committish = reverse_refs[commit]
746
# No? Maybe the user has uploadArchive.allowUnreachable enabled.
747
# Let's hope for the best.
749
self._repository.archive(
750
format, committish, f.write,
751
subdirs=([subdir] if subdir else None),
752
prefix=(root+'/') if root else '')
754
return osutils.file_iterator(f)
757
class RemoteGitRepository(GitRepository):
761
return self.control_url
763
def get_parent_map(self, revids):
764
raise GitSmartRemoteNotSupported(self.get_parent_map, self)
766
def archive(self, *args, **kwargs):
767
return self.controldir.archive(*args, **kwargs)
769
def fetch_pack(self, determine_wants, graph_walker, pack_data,
771
return self.controldir.fetch_pack(determine_wants, graph_walker,
774
def send_pack(self, get_changed_refs, generate_pack_data):
775
return self.controldir.send_pack(get_changed_refs, generate_pack_data)
777
def fetch_objects(self, determine_wants, graph_walker, resolve_ext_ref,
779
fd, path = tempfile.mkstemp(suffix=".pack")
781
self.fetch_pack(determine_wants, graph_walker,
782
lambda x: os.write(fd, x), progress)
785
if os.path.getsize(path) == 0:
786
return EmptyObjectStoreIterator()
787
return TemporaryPackIterator(path[:-len(".pack")], resolve_ext_ref)
789
def lookup_bzr_revision_id(self, bzr_revid, mapping=None):
790
# This won't work for any round-tripped bzr revisions, but it's a start..
792
return mapping_registry.revision_id_bzr_to_foreign(bzr_revid)
793
except InvalidRevisionId:
794
raise NoSuchRevision(self, bzr_revid)
796
def lookup_foreign_revision_id(self, foreign_revid, mapping=None):
797
"""Lookup a revision id.
801
mapping = self.get_mapping()
802
# Not really an easy way to parse foreign revids here..
803
return mapping.revision_id_foreign_to_bzr(foreign_revid)
805
def revision_tree(self, revid):
806
return GitRemoteRevisionTree(self, revid)
808
def get_revisions(self, revids):
809
raise GitSmartRemoteNotSupported(self.get_revisions, self)
811
def has_revisions(self, revids):
812
raise GitSmartRemoteNotSupported(self.get_revisions, self)
815
class RemoteGitTagDict(GitTags):
817
def set_tag(self, name, revid):
818
sha = self.branch.lookup_bzr_revision_id(revid)[0]
819
self._set_ref(name, sha)
821
def delete_tag(self, name):
822
self._set_ref(name, dulwich.client.ZERO_SHA)
824
def _set_ref(self, name, sha):
825
ref = tag_name_to_ref(name)
826
def get_changed_refs(old_refs):
828
if sha == dulwich.client.ZERO_SHA and ref not in ret:
829
raise NoSuchTag(name)
832
def generate_pack_data(have, want, ofs_delta=False):
833
return pack_objects_to_data([])
834
self.repository.send_pack(get_changed_refs, generate_pack_data)
837
class RemoteGitBranch(GitBranch):
839
def __init__(self, controldir, repository, name):
841
super(RemoteGitBranch, self).__init__(controldir, repository, name,
842
RemoteGitBranchFormat())
844
def last_revision_info(self):
845
raise GitSmartRemoteNotSupported(self.last_revision_info, self)
849
return self.control_url
852
def control_url(self):
855
def revision_id_to_revno(self, revision_id):
856
raise GitSmartRemoteNotSupported(self.revision_id_to_revno, self)
858
def last_revision(self):
859
return self.lookup_foreign_revision_id(self.head)
863
if self._sha is not None:
865
refs = self.controldir.get_refs_container()
866
name = branch_name_to_ref(self.name)
868
self._sha = refs[name]
870
raise NoSuchRef(name, self.repository.user_url, refs)
873
def _synchronize_history(self, destination, revision_id):
874
"""See Branch._synchronize_history()."""
875
destination.generate_revision_history(self.last_revision())
877
def _get_parent_location(self):
880
def get_push_location(self):
883
def set_push_location(self, url):
886
def _iter_tag_refs(self):
887
"""Iterate over the tag refs.
889
:param refs: Refs dictionary (name -> git sha1)
890
:return: iterator over (ref_name, tag_name, peeled_sha1, unpeeled_sha1)
892
refs = self.controldir.get_refs_container()
893
for ref_name, unpeeled in refs.as_dict().items():
895
tag_name = ref_to_tag_name(ref_name)
896
except (ValueError, UnicodeDecodeError):
898
peeled = refs.get_peeled(ref_name)
901
peeled = refs.peel_sha(unpeeled).id
903
# Let's just hope it's a commit
905
if type(tag_name) is not unicode:
906
raise TypeError(tag_name)
907
yield (ref_name, tag_name, peeled, unpeeled)
910
def remote_refs_dict_to_container(refs_dict, symrefs_dict={}):
913
for k, v in refs_dict.items():
919
for name, target in symrefs_dict.items():
920
base[name] = SYMREF + target
921
ret = DictRefsContainer(base)