13
13
# You should have received a copy of the GNU General Public License
14
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
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
25
from bzrlib.errors import (
34
from ...errors import (
46
UninitializableFormat,
32
from bzrlib.transport import (
48
from ...transport import (
50
register_urlparse_netloc_protocol,
36
from bzrlib.plugins.git import (
37
54
lazy_check_versions,
55
user_agent_for_github,
39
57
lazy_check_versions()
41
from bzrlib.plugins.git.branch import (
44
from bzrlib.plugins.git.errors import (
69
BareLocalGitControlDirFormat,
45
72
GitSmartRemoteNotSupported,
48
from bzrlib.plugins.git.dir import (
51
from bzrlib.plugins.git.mapping import (
75
from .mapping import (
54
from bzrlib.plugins.git.repository import (
78
from .object_store import (
84
from .repository import (
57
from bzrlib.plugins.git.refs import (
59
88
branch_name_to_ref,
63
96
from dulwich.errors import (
66
99
from dulwich.pack import (
101
pack_objects_to_data,
103
from dulwich.protocol import ZERO_SHA
104
from dulwich.refs import (
108
from dulwich.repo import (
74
urlparse.uses_netloc.extend(['git', 'git+ssh'])
117
import urllib.parse as urlparse
118
from urllib.parse import splituser, splitnport
121
from urllib import splituser, splitnport
123
# urlparse only supports a limited number of schemes by default
124
register_urlparse_netloc_protocol('git')
125
register_urlparse_netloc_protocol('git+ssh')
76
127
from dulwich.pack import load_pack_index
130
class GitPushResult(PushResult):
132
def _lookup_revno(self, revid):
134
return _quick_lookup_revno(self.source_branch, self.target_branch,
136
except GitSmartRemoteNotSupported:
141
return self._lookup_revno(self.old_revid)
145
return self._lookup_revno(self.new_revid)
79
148
# Don't run any tests on GitSmartTransport as it is not intended to be
80
149
# a full implementation of Transport
81
150
def get_test_permutations():
89
158
:return: Tuple with host, port, username, path.
91
160
(scheme, netloc, loc, _, _) = urlparse.urlsplit(url)
92
path = urllib.unquote(loc)
161
path = urlparse.unquote(loc)
93
162
if path.startswith("/~"):
95
(username, hostport) = urllib.splituser(netloc)
96
(host, port) = urllib.splitnport(hostport, None)
164
(username, hostport) = splituser(netloc)
165
(host, port) = splitnport(hostport, None)
97
166
return (host, port, username, path)
169
class RemoteGitError(BzrError):
171
_fmt = "Remote server error: %(msg)s"
174
def parse_git_error(url, message):
175
"""Parse a remote git server error and return a bzr exception.
177
:param url: URL of the remote repository
178
:param message: Message sent by the remote git server
180
message = str(message).strip()
181
if message.startswith("Could not find Repository "):
182
return NotBranchError(url, message)
183
if message == "HEAD failed to update":
184
base_url, _ = urlutils.split_segment_parameters(url)
186
("Unable to update remote HEAD branch. To update the master "
187
"branch, specify the URL %s,branch=master.") % base_url)
188
# Don't know, just return it to the user as-is
189
return RemoteGitError(message)
100
192
class GitSmartTransport(Transport):
102
194
def __init__(self, url, _client=None):
310
class RemoteGitBranchFormat(GitBranchFormat):
312
def get_format_description(self):
313
return 'Remote Git Branch'
316
def _matchingcontroldir(self):
317
return RemoteGitControlDirFormat()
319
def initialize(self, a_controldir, name=None, repository=None,
320
append_revisions_only=None):
321
raise UninitializableFormat(self)
324
class DefaultProgressReporter(object):
326
_GIT_PROGRESS_PARTIAL_RE = re.compile(r"(.*?): +(\d+)% \((\d+)/(\d+)\)")
327
_GIT_PROGRESS_TOTAL_RE = re.compile(r"(.*?): (\d+)")
329
def __init__(self, pb):
332
def progress(self, text):
333
text = text.rstrip("\r\n")
334
if text.startswith('error: '):
335
trace.show_error('git: %s', text[len('error: '):])
337
trace.mutter("git: %s", text)
338
g = self._GIT_PROGRESS_PARTIAL_RE.match(text)
340
(text, pct, current, total) = g.groups()
341
self.pb.update(text, int(current), int(total))
343
g = self._GIT_PROGRESS_TOTAL_RE.match(text)
345
(text, total) = g.groups()
346
self.pb.update(text, None, int(total))
348
trace.note("%s", text)
198
351
class RemoteGitDir(GitDir):
200
def __init__(self, transport, lockfiles, format):
353
def __init__(self, transport, format, client, client_path):
201
354
self._format = format
202
355
self.root_transport = transport
203
356
self.transport = transport
204
self._lockfiles = lockfiles
205
357
self._mode_check_done = None
207
def _branch_name_to_ref(self, name, default=None):
208
return branch_name_to_ref(name, default=default)
358
self._client = client
359
self._client_path = client_path
360
self.base = self.root_transport.base
364
def _gitrepository_class(self):
365
return RemoteGitRepository
367
def fetch_pack(self, determine_wants, graph_walker, pack_data, progress=None):
369
pb = ui.ui_factory.nested_progress_bar()
370
progress = DefaultProgressReporter(pb).progress
374
result = self._client.fetch_pack(self._client_path, determine_wants,
375
graph_walker, pack_data, progress)
376
if result.refs is None:
378
self._refs = remote_refs_dict_to_container(result.refs, result.symrefs)
380
except GitProtocolError as e:
381
raise parse_git_error(self.transport.external_url(), e)
386
def send_pack(self, get_changed_refs, generate_pack_data, progress=None):
388
pb = ui.ui_factory.nested_progress_bar()
389
progress = DefaultProgressReporter(pb).progress
392
def get_changed_refs_wrapper(refs):
393
# TODO(jelmer): This drops symref information
394
self._refs = remote_refs_dict_to_container(refs)
395
return get_changed_refs(refs)
397
return self._client.send_pack(self._client_path,
398
get_changed_refs_wrapper, generate_pack_data, progress)
399
except GitProtocolError as e:
400
raise parse_git_error(self.transport.external_url(), e)
405
def create_branch(self, name=None, repository=None,
406
append_revisions_only=None, ref=None):
407
refname = self._get_selected_ref(name, ref)
408
if refname != b'HEAD' and refname in self.get_refs_container():
409
raise AlreadyBranchError(self.user_url)
410
if refname in self.get_refs_container():
411
ref_chain, unused_sha = self.get_refs_container().follow(self._get_selected_ref(None))
412
if ref_chain[0] == b'HEAD':
413
refname = ref_chain[1]
414
repo = self.open_repository()
415
return RemoteGitBranch(self, repo, refname)
417
def destroy_branch(self, name=None):
418
refname = self._get_selected_ref(name)
419
def get_changed_refs(old_refs):
421
if not refname in ret:
422
raise NotBranchError(self.user_url)
423
ret[refname] = dulwich.client.ZERO_SHA
425
def generate_pack_data(have, want, ofs_delta=False):
426
return pack_objects_to_data([])
427
self.send_pack(get_changed_refs, generate_pack_data)
431
return self.control_url
434
def user_transport(self):
435
return self.root_transport
438
def control_url(self):
439
return self.control_transport.base
442
def control_transport(self):
443
return self.root_transport
210
445
def open_repository(self):
211
return RemoteGitRepository(self, self._lockfiles)
446
return RemoteGitRepository(self)
213
def _open_branch(self, name=None, ignore_fallbacks=False,
448
def open_branch(self, name=None, unsupported=False,
449
ignore_fallbacks=False, ref=None, possible_transports=None,
215
451
repo = self.open_repository()
216
refname = self._branch_name_to_ref(name)
217
return RemoteGitBranch(self, repo, refname, self._lockfiles)
452
ref = self._get_selected_ref(name, ref)
453
if not nascent_ok and ref not in self.get_refs_container():
454
raise NotBranchError(self.root_transport.base,
456
ref_chain, unused_sha = self.get_refs_container().follow(ref)
457
return RemoteGitBranch(self, repo, ref_chain[-1])
219
459
def open_workingtree(self, recommend_upgrade=False):
220
460
raise NotLocalUrl(self.transport.base)
462
def has_workingtree(self):
465
def get_peeled(self, name):
466
return self.get_refs_container().get_peeled(name)
468
def get_refs_container(self):
469
if self._refs is not None:
471
result = self.fetch_pack(lambda x: None, None,
472
lambda x: None, lambda x: trace.mutter("git: %s" % x))
473
self._refs = remote_refs_dict_to_container(
474
result.refs, result.symrefs)
477
def push_branch(self, source, revision_id=None, overwrite=False,
478
remember=False, create_prefix=False, lossy=False,
480
"""Push the source branch into this ControlDir."""
481
if revision_id is None:
482
# No revision supplied by the user, default to the branch
484
revision_id = source.last_revision()
486
push_result = GitPushResult()
487
push_result.workingtree_updated = None
488
push_result.master_branch = None
489
push_result.source_branch = source
490
push_result.stacked_on = None
491
push_result.branch_push_result = None
492
repo = self.find_repository()
493
refname = self._get_selected_ref(name)
494
if isinstance(source, GitBranch) and lossy:
495
raise errors.LossyPushToSameVCS(source.controldir, self)
496
source_store = get_object_store(source.repository)
497
with source_store.lock_read():
498
def get_changed_refs(refs):
499
self._refs = remote_refs_dict_to_container(refs)
501
# TODO(jelmer): Unpeel if necessary
502
push_result.new_original_revid = revision_id
504
new_sha = source_store._lookup_revision_sha1(revision_id)
506
new_sha = repo.lookup_bzr_revision_id(revision_id)[0]
508
if remote_divergence(ret.get(refname), new_sha, source_store):
509
raise DivergedBranches(
510
source, self.open_branch(name, nascent_ok=True))
511
ret[refname] = new_sha
514
generate_pack_data = source_store.generate_lossy_pack_data
516
generate_pack_data = source_store.generate_pack_data
517
new_refs = self.send_pack(get_changed_refs, generate_pack_data)
518
push_result.new_revid = repo.lookup_foreign_revision_id(
521
old_remote = self._refs[refname]
523
old_remote = ZERO_SHA
524
push_result.old_revid = repo.lookup_foreign_revision_id(old_remote)
525
self._refs = remote_refs_dict_to_container(new_refs)
526
push_result.target_branch = self.open_branch(name)
527
if old_remote != ZERO_SHA:
528
push_result.branch_push_result = GitBranchPushResult()
529
push_result.branch_push_result.source_branch = source
530
push_result.branch_push_result.target_branch = push_result.target_branch
531
push_result.branch_push_result.local_branch = None
532
push_result.branch_push_result.master_branch = push_result.target_branch
533
push_result.branch_push_result.old_revid = push_result.old_revid
534
push_result.branch_push_result.new_revid = push_result.new_revid
535
push_result.branch_push_result.new_original_revid = push_result.new_original_revid
536
if source.get_push_location() is None or remember:
537
source.set_push_location(push_result.target_branch.base)
540
def _find_commondir(self):
541
# There is no way to find the commondir, if there is any.
223
545
class EmptyObjectStoreIterator(dict):
262
576
os.remove(self._data_path)
579
class BzrGitHttpClient(dulwich.client.HttpGitClient):
581
def __init__(self, transport, *args, **kwargs):
582
self.transport = transport
583
super(BzrGitHttpClient, self).__init__(transport.external_url(), *args, **kwargs)
585
def _http_request(self, url, headers=None, data=None,
586
allow_compression=False):
587
"""Perform HTTP request.
589
:param url: Request URL.
590
:param headers: Optional custom headers to override defaults.
591
:param data: Request data.
592
:param allow_compression: Allow GZipped communication.
593
:return: Tuple (`response`, `read`), where response is an `urllib3`
594
response object with additional `content_type` and
595
`redirect_location` properties, and `read` is a consumable read
596
method for the response data.
598
from breezy.transport.http._urllib2_wrappers import Request
599
headers['User-agent'] = user_agent_for_github()
600
headers["Pragma"] = "no-cache"
601
if allow_compression:
602
headers["Accept-Encoding"] = "gzip"
604
headers["Accept-Encoding"] = "identity"
607
('GET' if data is None else 'POST'),
609
accepted_errors=[200, 404])
611
response = self.transport._perform(request)
613
if response.code == 404:
614
raise NotGitRepository()
615
elif response.code != 200:
616
raise GitProtocolError("unexpected http resp %d for %s" %
617
(response.code, url))
619
# TODO: Optimization available by adding `preload_content=False` to the
620
# request and just passing the `read` method on instead of going via
621
# `BytesIO`, if we can guarantee that the entire response is consumed
622
# before issuing the next to still allow for connection reuse from the
624
if response.getheader("Content-Encoding") == "gzip":
625
read = gzip.GzipFile(fileobj=response).read
629
class WrapResponse(object):
631
def __init__(self, response):
632
self._response = response
633
self.status = response.code
634
self.content_type = response.getheader("Content-Type")
635
self.redirect_location = response.geturl()
638
self._response.close()
640
return WrapResponse(response), read
643
class RemoteGitControlDirFormat(GitControlDirFormat):
644
"""The .git directory control format."""
646
supports_workingtrees = False
649
def _known_formats(self):
650
return set([RemoteGitControlDirFormat()])
652
def get_branch_format(self):
653
return RemoteGitBranchFormat()
655
def is_initializable(self):
658
def is_supported(self):
661
def open(self, transport, _found=None):
662
"""Open this directory.
665
# we dont grok readonly - git isn't integrated with transport.
667
if url.startswith('readonly+'):
668
url = url[len('readonly+'):]
669
scheme = urlparse.urlsplit(transport.external_url())[0]
670
if isinstance(transport, GitSmartTransport):
671
client = transport._get_client()
672
client_path = transport._get_path()
673
elif scheme in ("http", "https"):
674
client = BzrGitHttpClient(transport)
675
client_path, _ = urlutils.split_segment_parameters(transport._path)
676
elif scheme == 'file':
677
client = dulwich.client.LocalGitClient()
678
client_path = transport.local_abspath('.')
680
raise NotBranchError(transport.base)
682
pass # TODO(jelmer): Actually probe for something
683
return RemoteGitDir(transport, self, client, client_path)
685
def get_format_description(self):
686
return "Remote Git Repository"
688
def initialize_on_transport(self, transport):
689
raise UninitializableFormat(self)
691
def supports_transport(self, transport):
693
external_url = transport.external_url()
694
except InProcessTransport:
695
raise NotBranchError(path=transport.base)
696
return (external_url.startswith("http:") or
697
external_url.startswith("https:") or
698
external_url.startswith("git+") or
699
external_url.startswith("git:"))
265
702
class RemoteGitRepository(GitRepository):
267
def __init__(self, gitdir, lockfiles):
268
GitRepository.__init__(self, gitdir, lockfiles)
272
def inventories(self):
273
raise GitSmartRemoteNotSupported()
277
raise GitSmartRemoteNotSupported()
281
raise GitSmartRemoteNotSupported()
284
if self._refs is not None:
286
self._refs = self.bzrdir.root_transport.fetch_pack(lambda x: [], None,
287
lambda x: None, lambda x: trace.mutter("git: %s" % x))
706
return self.control_url
708
def get_parent_map(self, revids):
709
raise GitSmartRemoteNotSupported(self.get_parent_map, self)
290
711
def fetch_pack(self, determine_wants, graph_walker, pack_data,
292
return self._transport.fetch_pack(determine_wants, graph_walker,
713
return self.controldir.fetch_pack(determine_wants, graph_walker,
293
714
pack_data, progress)
295
def send_pack(self, get_changed_refs, generate_pack_contents):
296
return self._transport.send_pack(get_changed_refs, generate_pack_contents)
716
def send_pack(self, get_changed_refs, generate_pack_data):
717
return self.controldir.send_pack(get_changed_refs, generate_pack_data)
298
719
def fetch_objects(self, determine_wants, graph_walker, resolve_ext_ref,
300
721
fd, path = tempfile.mkstemp(suffix=".pack")
301
self.fetch_pack(determine_wants, graph_walker,
302
lambda x: os.write(fd, x), progress)
723
self.fetch_pack(determine_wants, graph_walker,
724
lambda x: os.write(fd, x), progress)
304
727
if os.path.getsize(path) == 0:
305
728
return EmptyObjectStoreIterator()
306
729
return TemporaryPackIterator(path[:-len(".pack")], resolve_ext_ref)
308
def lookup_bzr_revision_id(self, bzr_revid):
731
def lookup_bzr_revision_id(self, bzr_revid, mapping=None):
309
732
# This won't work for any round-tripped bzr revisions, but it's a start..
311
734
return mapping_registry.revision_id_bzr_to_foreign(bzr_revid)
321
744
# Not really an easy way to parse foreign revids here..
322
745
return mapping.revision_id_foreign_to_bzr(foreign_revid)
325
class RemoteGitTagDict(tag.BasicTags):
327
def __init__(self, branch):
329
self.repository = branch.repository
331
def get_tag_dict(self):
333
for k, v in extract_tags(self.repository.get_refs()).iteritems():
334
tags[k] = self.branch.mapping.revision_id_foreign_to_bzr(v)
747
def revision_tree(self, revid):
748
raise GitSmartRemoteNotSupported(self.revision_tree, self)
750
def get_revisions(self, revids):
751
raise GitSmartRemoteNotSupported(self.get_revisions, self)
753
def has_revisions(self, revids):
754
raise GitSmartRemoteNotSupported(self.get_revisions, self)
757
class RemoteGitTagDict(GitTags):
337
759
def set_tag(self, name, revid):
338
# FIXME: Not supported yet, should do a push of a new ref
339
raise NotImplementedError(self.set_tag)
760
sha = self.branch.lookup_bzr_revision_id(revid)[0]
761
self._set_ref(name, sha)
763
def delete_tag(self, name):
764
self._set_ref(name, dulwich.client.ZERO_SHA)
766
def _set_ref(self, name, sha):
767
ref = tag_name_to_ref(name)
768
def get_changed_refs(old_refs):
770
if sha == dulwich.client.ZERO_SHA and ref not in ret:
771
raise NoSuchTag(name)
774
def generate_pack_data(have, want, ofs_delta=False):
775
return pack_objects_to_data([])
776
self.repository.send_pack(get_changed_refs, generate_pack_data)
342
779
class RemoteGitBranch(GitBranch):
344
def __init__(self, bzrdir, repository, name, lockfiles):
781
def __init__(self, controldir, repository, name):
346
super(RemoteGitBranch, self).__init__(bzrdir, repository, name,
349
def revision_history(self):
350
raise GitSmartRemoteNotSupported()
783
super(RemoteGitBranch, self).__init__(controldir, repository, name,
784
RemoteGitBranchFormat())
786
def last_revision_info(self):
787
raise GitSmartRemoteNotSupported(self.last_revision_info, self)
791
return self.control_url
794
def control_url(self):
797
def revision_id_to_revno(self, revision_id):
798
raise GitSmartRemoteNotSupported(self.revision_id_to_revno, self)
352
800
def last_revision(self):
353
801
return self.lookup_foreign_revision_id(self.head)
355
def _get_config(self):
356
class EmptyConfig(object):
358
def _get_configobj(self):
359
return config.ConfigObj()
365
805
if self._sha is not None:
367
heads = self.repository.get_refs()
368
name = self.bzrdir._branch_name_to_ref(self.name, "HEAD")
370
self._sha = heads[name]
372
raise NoSuchRef(self.name)
807
refs = self.controldir.get_refs_container()
808
name = branch_name_to_ref(self.name)
810
self._sha = refs[name]
812
raise NoSuchRef(name, self.repository.user_url, refs)
375
815
def _synchronize_history(self, destination, revision_id):
376
816
"""See Branch._synchronize_history()."""
377
817
destination.generate_revision_history(self.last_revision())
819
def _get_parent_location(self):
379
822
def get_push_location(self):
382
825
def set_push_location(self, url):
828
def _iter_tag_refs(self):
829
"""Iterate over the tag refs.
831
:param refs: Refs dictionary (name -> git sha1)
832
:return: iterator over (ref_name, tag_name, peeled_sha1, unpeeled_sha1)
834
refs = self.controldir.get_refs_container()
835
for ref_name, unpeeled in refs.as_dict().items():
837
tag_name = ref_to_tag_name(ref_name)
838
except (ValueError, UnicodeDecodeError):
840
peeled = refs.get_peeled(ref_name)
843
peeled = refs.peel_sha(unpeeled).id
845
# Let's just hope it's a commit
847
if type(tag_name) is not unicode:
848
raise TypeError(tag_name)
849
yield (ref_name, tag_name, peeled, unpeeled)
852
def remote_refs_dict_to_container(refs_dict, symrefs_dict={}):
855
for k, v in refs_dict.items():
861
for name, target in symrefs_dict.items():
862
base[name] = SYMREF + target
863
ret = DictRefsContainer(base)