1
# Copyright (C) 2007-2012 Jelmer Vernooij <jelmer@samba.org>
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
from __future__ import absolute_import
26
from ...errors import (
34
UninitializableFormat,
36
from ...transport import (
54
GitSmartRemoteNotSupported,
57
from .mapping import (
60
from .repository import (
70
from dulwich.errors import (
73
from dulwich.pack import (
76
from dulwich.repo import DictRefsContainer
83
# urlparse only supports a limited number of schemes by default
85
urlparse.uses_netloc.extend(['git', 'git+ssh'])
87
from dulwich.pack import load_pack_index
90
# Don't run any tests on GitSmartTransport as it is not intended to be
91
# a full implementation of Transport
92
def get_test_permutations():
96
def split_git_url(url):
100
:return: Tuple with host, port, username, path.
102
(scheme, netloc, loc, _, _) = urlparse.urlsplit(url)
103
path = urllib.unquote(loc)
104
if path.startswith("/~"):
106
(username, hostport) = urllib.splituser(netloc)
107
(host, port) = urllib.splitnport(hostport, None)
108
return (host, port, username, path)
111
class RemoteGitError(BzrError):
113
_fmt = "Remote server error: %(message)s"
116
def parse_git_error(url, message):
117
"""Parse a remote git server error and return a bzr exception.
119
:param url: URL of the remote repository
120
:param message: Message sent by the remote git server
122
message = str(message).strip()
123
if message.startswith("Could not find Repository "):
124
return NotBranchError(url, message)
125
if message == "HEAD failed to update":
126
base_url, _ = urlutils.split_segment_parameters(url)
128
("Unable to update remote HEAD branch. To update the master "
129
"branch, specify the URL %s,branch=master.") % base_url)
130
# Don't know, just return it to the user as-is
131
return RemoteGitError(message)
134
class GitSmartTransport(Transport):
136
def __init__(self, url, _client=None):
137
Transport.__init__(self, url)
138
(self._host, self._port, self._username, self._path) = \
140
if 'transport' in debug.debug_flags:
141
trace.mutter('host: %r, user: %r, port: %r, path: %r',
142
self._host, self._username, self._port, self._path)
143
self._client = _client
144
self._stripped_path = self._path.rsplit(",", 1)[0]
146
def external_url(self):
149
def has(self, relpath):
152
def _get_client(self, thin_packs):
153
raise NotImplementedError(self._get_client)
156
return self._stripped_path
159
raise NoSuchFile(path)
161
def abspath(self, relpath):
162
return urlutils.join(self.base, relpath)
164
def clone(self, offset=None):
165
"""See Transport.clone()."""
169
newurl = urlutils.join(self.base, offset)
171
return self.__class__(newurl, self._client)
174
class TCPGitSmartTransport(GitSmartTransport):
178
def _get_client(self, thin_packs):
179
if self._client is not None:
183
return dulwich.client.TCPGitClient(self._host, self._port,
184
thin_packs=thin_packs, report_activity=self._report_activity)
187
class SSHSocketWrapper(object):
189
def __init__(self, sock):
192
def read(self, len=None):
193
return self.sock.recv(len)
195
def write(self, data):
196
return self.sock.write(data)
199
return len(select.select([self.sock.fileno()], [], [], 0)[0]) > 0
202
class DulwichSSHVendor(dulwich.client.SSHVendor):
205
from ...transport import ssh
206
self.bzr_ssh_vendor = ssh._get_ssh_vendor()
208
def run_command(self, host, command, username=None, port=None):
209
connection = self.bzr_ssh_vendor.connect_ssh(username=username,
210
password=None, port=port, host=host, command=command)
211
(kind, io_object) = connection.get_sock_or_pipes()
213
return SSHSocketWrapper(io_object)
215
raise AssertionError("Unknown io object kind %r'" % kind)
218
#dulwich.client.get_ssh_vendor = DulwichSSHVendor
221
class SSHGitSmartTransport(GitSmartTransport):
226
path = self._stripped_path
227
if path.startswith("/~/"):
231
def _get_client(self, thin_packs):
232
if self._client is not None:
236
location_config = config.LocationConfig(self.base)
237
client = dulwich.client.SSHGitClient(self._host, self._port, self._username,
238
thin_packs=thin_packs, report_activity=self._report_activity)
239
# Set up alternate pack program paths
240
upload_pack = location_config.get_user_option('git_upload_pack')
242
client.alternative_paths["upload-pack"] = upload_pack
243
receive_pack = location_config.get_user_option('git_receive_pack')
245
client.alternative_paths["receive-pack"] = receive_pack
249
class RemoteGitDir(GitDir):
251
def __init__(self, transport, format, get_client, client_path):
252
self._format = format
253
self.root_transport = transport
254
self.transport = transport
255
self._mode_check_done = None
256
self._get_client = get_client
257
self._client_path = client_path
258
self.base = self.root_transport.base
261
def fetch_pack(self, determine_wants, graph_walker, pack_data, progress=None):
264
trace.info("git: %s" % text)
265
def wrap_determine_wants(refs_dict):
266
return determine_wants(remote_refs_dict_to_container(refs_dict))
267
client = self._get_client(thin_packs=True)
269
refs_dict = client.fetch_pack(self._client_path, wrap_determine_wants,
270
graph_walker, pack_data, progress)
271
if refs_dict is None:
273
self._refs = remote_refs_dict_to_container(refs_dict)
275
except GitProtocolError, e:
276
raise parse_git_error(self.transport.external_url(), e)
278
def send_pack(self, get_changed_refs, generate_pack_contents):
279
client = self._get_client(thin_packs=True)
281
return client.send_pack(self._client_path, get_changed_refs,
282
generate_pack_contents)
283
except GitProtocolError, e:
284
raise parse_git_error(self.transport.external_url(), e)
286
def _get_default_ref(self):
287
return "refs/heads/master"
289
def destroy_branch(self, name=None):
290
refname = self._get_selected_ref(name)
291
def get_changed_refs(old_refs):
293
if not refname in ret:
294
raise NotBranchError(self.user_url)
295
ret[refname] = "00" * 20
297
self.send_pack(get_changed_refs, lambda have, want: [])
301
return self.control_url
304
def user_transport(self):
305
return self.root_transport
308
def control_url(self):
309
return self.control_transport.base
312
def control_transport(self):
313
return self.root_transport
315
def open_repository(self):
316
return RemoteGitRepository(self)
318
def open_branch(self, name=None, unsupported=False,
319
ignore_fallbacks=False, ref=None, possible_transports=None):
320
repo = self.open_repository()
321
refname = self._get_selected_ref(name, ref)
322
return RemoteGitBranch(self, repo, refname)
324
def open_workingtree(self, recommend_upgrade=False):
325
raise NotLocalUrl(self.transport.base)
327
def get_peeled(self, name):
328
return self.get_refs_container().get_peeled(name)
330
def get_refs_container(self):
331
if self._refs is not None:
333
refs_dict = self.fetch_pack(lambda x: [], None,
334
lambda x: None, lambda x: trace.mutter("git: %s" % x))
335
self._refs = remote_refs_dict_to_container(refs_dict)
339
class EmptyObjectStoreIterator(dict):
341
def iterobjects(self):
345
class TemporaryPackIterator(Pack):
347
def __init__(self, path, resolve_ext_ref):
348
super(TemporaryPackIterator, self).__init__(
349
path, resolve_ext_ref=resolve_ext_ref)
350
self._idx_load = lambda: self._idx_load_or_generate(self._idx_path)
352
def _idx_load_or_generate(self, path):
353
if not os.path.exists(path):
354
pb = ui.ui_factory.nested_progress_bar()
356
def report_progress(cur, total):
357
pb.update("generating index", cur, total)
358
self.data.create_index(path,
359
progress=report_progress)
362
return load_pack_index(path)
365
if self._idx is not None:
367
os.remove(self._idx_path)
368
if self._data is not None:
370
os.remove(self._data_path)
373
class BzrGitHttpClient(dulwich.client.HttpGitClient):
375
def __init__(self, transport, *args, **kwargs):
376
self.transport = transport
377
super(BzrGitHttpClient, self).__init__(transport.external_url(), *args, **kwargs)
379
self._http_perform = getattr(self.transport, "_perform", urllib2.urlopen)
381
def _perform(self, req):
382
req.accepted_errors = (200, 404)
383
req.follow_redirections = True
384
req.redirected_to = None
385
return self._http_perform(req)
388
class RemoteGitControlDirFormat(GitControlDirFormat):
389
"""The .git directory control format."""
391
supports_workingtrees = False
394
def _known_formats(self):
395
return set([RemoteGitControlDirFormat()])
397
def is_initializable(self):
400
def is_supported(self):
403
def open(self, transport, _found=None):
404
"""Open this directory.
407
# we dont grok readonly - git isn't integrated with transport.
409
if url.startswith('readonly+'):
410
url = url[len('readonly+'):]
411
if isinstance(transport, GitSmartTransport):
412
get_client = transport._get_client
413
client_path = transport._get_path()
414
elif urlparse.urlsplit(transport.external_url())[0] in ("http", "https"):
415
def get_client(thin_packs):
416
return BzrGitHttpClient(transport, thin_packs=thin_packs)
417
client_path, _ = urlutils.split_segment_parameters(transport._path)
419
raise NotBranchError(transport.base)
420
return RemoteGitDir(transport, self, get_client, client_path)
422
def get_format_description(self):
423
return "Remote Git Repository"
425
def initialize_on_transport(self, transport):
426
raise UninitializableFormat(self)
428
def supports_transport(self, transport):
430
external_url = transport.external_url()
431
except InProcessTransport:
432
raise NotBranchError(path=transport.base)
433
return (external_url.startswith("http:") or
434
external_url.startswith("https:") or
435
external_url.startswith("git+") or
436
external_url.startswith("git:"))
439
class RemoteGitRepository(GitRepository):
443
return self.control_url
445
def get_parent_map(self, revids):
446
raise GitSmartRemoteNotSupported(self.get_parent_map, self)
448
def fetch_pack(self, determine_wants, graph_walker, pack_data,
450
return self.controldir.fetch_pack(determine_wants, graph_walker,
453
def send_pack(self, get_changed_refs, generate_pack_contents):
454
return self.controldir.send_pack(get_changed_refs, generate_pack_contents)
456
def fetch_objects(self, determine_wants, graph_walker, resolve_ext_ref,
458
fd, path = tempfile.mkstemp(suffix=".pack")
460
self.fetch_pack(determine_wants, graph_walker,
461
lambda x: os.write(fd, x), progress)
464
if os.path.getsize(path) == 0:
465
return EmptyObjectStoreIterator()
466
return TemporaryPackIterator(path[:-len(".pack")], resolve_ext_ref)
468
def lookup_bzr_revision_id(self, bzr_revid):
469
# This won't work for any round-tripped bzr revisions, but it's a start..
471
return mapping_registry.revision_id_bzr_to_foreign(bzr_revid)
472
except InvalidRevisionId:
473
raise NoSuchRevision(self, bzr_revid)
475
def lookup_foreign_revision_id(self, foreign_revid, mapping=None):
476
"""Lookup a revision id.
480
mapping = self.get_mapping()
481
# Not really an easy way to parse foreign revids here..
482
return mapping.revision_id_foreign_to_bzr(foreign_revid)
484
def revision_tree(self, revid):
485
raise GitSmartRemoteNotSupported(self.revision_tree, self)
487
def get_revisions(self, revids):
488
raise GitSmartRemoteNotSupported(self.get_revisions, self)
490
def has_revisions(self, revids):
491
raise GitSmartRemoteNotSupported(self.get_revisions, self)
494
class RemoteGitTagDict(GitTags):
496
def get_refs_container(self):
497
return self.repository.controldir.get_refs_container()
499
def set_tag(self, name, revid):
500
# FIXME: Not supported yet, should do a push of a new ref
501
raise NotImplementedError(self.set_tag)
504
class RemoteGitBranch(GitBranch):
506
def __init__(self, controldir, repository, name):
508
super(RemoteGitBranch, self).__init__(controldir, repository, name)
510
def last_revision_info(self):
511
raise GitSmartRemoteNotSupported(self.last_revision_info, self)
515
return self.control_url
518
def control_url(self):
521
def revision_id_to_revno(self, revision_id):
522
raise GitSmartRemoteNotSupported(self.revision_id_to_revno, self)
524
def last_revision(self):
525
return self.lookup_foreign_revision_id(self.head)
529
if self._sha is not None:
531
refs = self.controldir.get_refs_container()
532
name = branch_name_to_ref(self.name)
534
self._sha = refs[name]
536
raise NoSuchRef(name, self.repository.user_url, refs)
539
def _synchronize_history(self, destination, revision_id):
540
"""See Branch._synchronize_history()."""
541
destination.generate_revision_history(self.last_revision())
543
def get_push_location(self):
546
def set_push_location(self, url):
550
def remote_refs_dict_to_container(refs_dict):
553
for k, v in refs_dict.iteritems():
559
ret = DictRefsContainer(base)