gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / dulwich / client.py @ 959
History | View | Annotate | Download (48.5 KB)
1 |
# client.py -- Implementation of the client side git protocols
|
---|---|
2 |
# Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
|
3 |
#
|
4 |
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
|
5 |
# General Public License as public by the Free Software Foundation; version 2.0
|
6 |
# or (at your option) any later version. You can redistribute it and/or
|
7 |
# modify it under the terms of either of these two licenses.
|
8 |
#
|
9 |
# Unless required by applicable law or agreed to in writing, software
|
10 |
# distributed under the License is distributed on an "AS IS" BASIS,
|
11 |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12 |
# See the License for the specific language governing permissions and
|
13 |
# limitations under the License.
|
14 |
#
|
15 |
# You should have received a copy of the licenses; if not, see
|
16 |
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
|
17 |
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
|
18 |
# License, Version 2.0.
|
19 |
#
|
20 |
|
21 |
"""Client side support for the Git protocol.
|
22 |
|
23 |
The Dulwich client supports the following capabilities:
|
24 |
|
25 |
* thin-pack
|
26 |
* multi_ack_detailed
|
27 |
* multi_ack
|
28 |
* side-band-64k
|
29 |
* ofs-delta
|
30 |
* quiet
|
31 |
* report-status
|
32 |
* delete-refs
|
33 |
|
34 |
Known capabilities that are not supported:
|
35 |
|
36 |
* shallow
|
37 |
* no-progress
|
38 |
* include-tag
|
39 |
"""
|
40 |
|
41 |
__docformat__ = 'restructuredText'
|
42 |
|
43 |
from contextlib import closing |
44 |
from io import BytesIO, BufferedReader |
45 |
import dulwich |
46 |
import select |
47 |
import socket |
48 |
import subprocess |
49 |
import sys |
50 |
|
51 |
try:
|
52 |
from urllib import quote as urlquote |
53 |
from urllib import unquote as urlunquote |
54 |
except ImportError: |
55 |
from urllib.parse import quote as urlquote |
56 |
from urllib.parse import unquote as urlunquote |
57 |
|
58 |
try:
|
59 |
import urllib2 |
60 |
import urlparse |
61 |
except ImportError: |
62 |
import urllib.request as urllib2 |
63 |
import urllib.parse as urlparse |
64 |
|
65 |
from dulwich.errors import ( |
66 |
GitProtocolError, |
67 |
NotGitRepository, |
68 |
SendPackError, |
69 |
UpdateRefsError, |
70 |
) |
71 |
from dulwich.protocol import ( |
72 |
_RBUFSIZE, |
73 |
capability_agent, |
74 |
CAPABILITY_DELETE_REFS, |
75 |
CAPABILITY_MULTI_ACK, |
76 |
CAPABILITY_MULTI_ACK_DETAILED, |
77 |
CAPABILITY_OFS_DELTA, |
78 |
CAPABILITY_QUIET, |
79 |
CAPABILITY_REPORT_STATUS, |
80 |
CAPABILITY_SIDE_BAND_64K, |
81 |
CAPABILITY_THIN_PACK, |
82 |
CAPABILITIES_REF, |
83 |
COMMAND_DONE, |
84 |
COMMAND_HAVE, |
85 |
COMMAND_WANT, |
86 |
SIDE_BAND_CHANNEL_DATA, |
87 |
SIDE_BAND_CHANNEL_PROGRESS, |
88 |
SIDE_BAND_CHANNEL_FATAL, |
89 |
PktLineParser, |
90 |
Protocol, |
91 |
ProtocolFile, |
92 |
TCP_GIT_PORT, |
93 |
ZERO_SHA, |
94 |
extract_capabilities, |
95 |
) |
96 |
from dulwich.pack import ( |
97 |
write_pack_objects, |
98 |
) |
99 |
from dulwich.refs import ( |
100 |
read_info_refs, |
101 |
) |
102 |
|
103 |
|
104 |
def _fileno_can_read(fileno): |
105 |
"""Check if a file descriptor is readable."""
|
106 |
return len(select.select([fileno], [], [], 0)[0]) > 0 |
107 |
|
108 |
|
109 |
COMMON_CAPABILITIES = [CAPABILITY_OFS_DELTA, CAPABILITY_SIDE_BAND_64K] |
110 |
FETCH_CAPABILITIES = ([CAPABILITY_THIN_PACK, CAPABILITY_MULTI_ACK, |
111 |
CAPABILITY_MULTI_ACK_DETAILED] + |
112 |
COMMON_CAPABILITIES) |
113 |
SEND_CAPABILITIES = [CAPABILITY_REPORT_STATUS] + COMMON_CAPABILITIES |
114 |
|
115 |
|
116 |
class ReportStatusParser(object): |
117 |
"""Handle status as reported by servers with 'report-status' capability.
|
118 |
"""
|
119 |
|
120 |
def __init__(self): |
121 |
self._done = False |
122 |
self._pack_status = None |
123 |
self._ref_status_ok = True |
124 |
self._ref_statuses = []
|
125 |
|
126 |
def check(self): |
127 |
"""Check if there were any errors and, if so, raise exceptions.
|
128 |
|
129 |
:raise SendPackError: Raised when the server could not unpack
|
130 |
:raise UpdateRefsError: Raised when refs could not be updated
|
131 |
"""
|
132 |
if self._pack_status not in (b'unpack ok', None): |
133 |
raise SendPackError(self._pack_status) |
134 |
if not self._ref_status_ok: |
135 |
ref_status = {} |
136 |
ok = set()
|
137 |
for status in self._ref_statuses: |
138 |
if b' ' not in status: |
139 |
# malformed response, move on to the next one
|
140 |
continue
|
141 |
status, ref = status.split(b' ', 1) |
142 |
|
143 |
if status == b'ng': |
144 |
if b' ' in ref: |
145 |
ref, status = ref.split(b' ', 1) |
146 |
else:
|
147 |
ok.add(ref) |
148 |
ref_status[ref] = status |
149 |
# TODO(jelmer): don't assume encoding of refs is ascii.
|
150 |
raise UpdateRefsError(', '.join([ |
151 |
ref.decode('ascii') for ref in ref_status if ref not in ok]) + |
152 |
' failed to update', ref_status=ref_status)
|
153 |
|
154 |
def handle_packet(self, pkt): |
155 |
"""Handle a packet.
|
156 |
|
157 |
:raise GitProtocolError: Raised when packets are received after a
|
158 |
flush packet.
|
159 |
"""
|
160 |
if self._done: |
161 |
raise GitProtocolError("received more data after status report") |
162 |
if pkt is None: |
163 |
self._done = True |
164 |
return
|
165 |
if self._pack_status is None: |
166 |
self._pack_status = pkt.strip()
|
167 |
else:
|
168 |
ref_status = pkt.strip() |
169 |
self._ref_statuses.append(ref_status)
|
170 |
if not ref_status.startswith(b'ok '): |
171 |
self._ref_status_ok = False |
172 |
|
173 |
|
174 |
def read_pkt_refs(proto): |
175 |
server_capabilities = None
|
176 |
refs = {} |
177 |
# Receive refs from server
|
178 |
for pkt in proto.read_pkt_seq(): |
179 |
(sha, ref) = pkt.rstrip(b'\n').split(None, 1) |
180 |
if sha == b'ERR': |
181 |
raise GitProtocolError(ref)
|
182 |
if server_capabilities is None: |
183 |
(ref, server_capabilities) = extract_capabilities(ref) |
184 |
refs[ref] = sha |
185 |
|
186 |
if len(refs) == 0: |
187 |
return None, set([]) |
188 |
if refs == {CAPABILITIES_REF: ZERO_SHA}:
|
189 |
refs = {} |
190 |
return refs, set(server_capabilities) |
191 |
|
192 |
|
193 |
# TODO(durin42): this doesn't correctly degrade if the server doesn't
|
194 |
# support some capabilities. This should work properly with servers
|
195 |
# that don't support multi_ack.
|
196 |
class GitClient(object): |
197 |
"""Git smart server client.
|
198 |
|
199 |
"""
|
200 |
|
201 |
def __init__(self, thin_packs=True, report_activity=None, quiet=False): |
202 |
"""Create a new GitClient instance.
|
203 |
|
204 |
:param thin_packs: Whether or not thin packs should be retrieved
|
205 |
:param report_activity: Optional callback for reporting transport
|
206 |
activity.
|
207 |
"""
|
208 |
self._report_activity = report_activity
|
209 |
self._report_status_parser = None |
210 |
self._fetch_capabilities = set(FETCH_CAPABILITIES) |
211 |
self._fetch_capabilities.add(capability_agent())
|
212 |
self._send_capabilities = set(SEND_CAPABILITIES) |
213 |
self._send_capabilities.add(capability_agent())
|
214 |
if quiet:
|
215 |
self._send_capabilities.add(CAPABILITY_QUIET)
|
216 |
if not thin_packs: |
217 |
self._fetch_capabilities.remove(CAPABILITY_THIN_PACK)
|
218 |
|
219 |
def get_url(self, path): |
220 |
"""Retrieves full url to given path.
|
221 |
|
222 |
:param path: Repository path (as string)
|
223 |
:return: Url to path (as string)
|
224 |
"""
|
225 |
raise NotImplementedError(self.get_url) |
226 |
|
227 |
@classmethod
|
228 |
def from_parsedurl(cls, parsedurl, **kwargs): |
229 |
"""Create an instance of this client from a urlparse.parsed object.
|
230 |
|
231 |
:param parsedurl: Result of urlparse.urlparse()
|
232 |
:return: A `GitClient` object
|
233 |
"""
|
234 |
raise NotImplementedError(cls.from_parsedurl) |
235 |
|
236 |
def send_pack(self, path, determine_wants, generate_pack_contents, |
237 |
progress=None, write_pack=write_pack_objects):
|
238 |
"""Upload a pack to a remote repository.
|
239 |
|
240 |
:param path: Repository path (as bytestring)
|
241 |
:param generate_pack_contents: Function that can return a sequence of
|
242 |
the shas of the objects to upload.
|
243 |
:param progress: Optional progress function
|
244 |
:param write_pack: Function called with (file, iterable of objects) to
|
245 |
write the objects returned by generate_pack_contents to the server.
|
246 |
|
247 |
:raises SendPackError: if server rejects the pack data
|
248 |
:raises UpdateRefsError: if the server supports report-status
|
249 |
and rejects ref updates
|
250 |
:return: new_refs dictionary containing the changes that were made
|
251 |
{refname: new_ref}, including deleted refs.
|
252 |
"""
|
253 |
raise NotImplementedError(self.send_pack) |
254 |
|
255 |
def fetch(self, path, target, determine_wants=None, progress=None): |
256 |
"""Fetch into a target repository.
|
257 |
|
258 |
:param path: Path to fetch from (as bytestring)
|
259 |
:param target: Target repository to fetch into
|
260 |
:param determine_wants: Optional function to determine what refs
|
261 |
to fetch
|
262 |
:param progress: Optional progress function
|
263 |
:return: Dictionary with all remote refs (not just those fetched)
|
264 |
"""
|
265 |
if determine_wants is None: |
266 |
determine_wants = target.object_store.determine_wants_all |
267 |
if CAPABILITY_THIN_PACK in self._fetch_capabilities: |
268 |
# TODO(jelmer): Avoid reading entire file into memory and
|
269 |
# only processing it after the whole file has been fetched.
|
270 |
f = BytesIO() |
271 |
def commit(): |
272 |
if f.tell():
|
273 |
f.seek(0)
|
274 |
target.object_store.add_thin_pack(f.read, None)
|
275 |
def abort(): |
276 |
pass
|
277 |
else:
|
278 |
f, commit, abort = target.object_store.add_pack() |
279 |
try:
|
280 |
result = self.fetch_pack(
|
281 |
path, determine_wants, target.get_graph_walker(), f.write, |
282 |
progress) |
283 |
except:
|
284 |
abort() |
285 |
raise
|
286 |
else:
|
287 |
commit() |
288 |
return result
|
289 |
|
290 |
def fetch_pack(self, path, determine_wants, graph_walker, pack_data, |
291 |
progress=None):
|
292 |
"""Retrieve a pack from a git smart server.
|
293 |
|
294 |
:param determine_wants: Callback that returns list of commits to fetch
|
295 |
:param graph_walker: Object with next() and ack().
|
296 |
:param pack_data: Callback called for each bit of data in the pack
|
297 |
:param progress: Callback for progress reports (strings)
|
298 |
:return: Dictionary with all remote refs (not just those fetched)
|
299 |
"""
|
300 |
raise NotImplementedError(self.fetch_pack) |
301 |
|
302 |
def get_refs(self, path): |
303 |
"""Retrieve the current refs from a git smart server.
|
304 |
|
305 |
:param path: Path to the repo to fetch from. (as bytestring)
|
306 |
"""
|
307 |
raise NotImplementedError(self.get_refs) |
308 |
|
309 |
def _parse_status_report(self, proto): |
310 |
unpack = proto.read_pkt_line().strip() |
311 |
if unpack != b'unpack ok': |
312 |
st = True
|
313 |
# flush remaining error data
|
314 |
while st is not None: |
315 |
st = proto.read_pkt_line() |
316 |
raise SendPackError(unpack)
|
317 |
statuses = [] |
318 |
errs = False
|
319 |
ref_status = proto.read_pkt_line() |
320 |
while ref_status:
|
321 |
ref_status = ref_status.strip() |
322 |
statuses.append(ref_status) |
323 |
if not ref_status.startswith(b'ok '): |
324 |
errs = True
|
325 |
ref_status = proto.read_pkt_line() |
326 |
|
327 |
if errs:
|
328 |
ref_status = {} |
329 |
ok = set()
|
330 |
for status in statuses: |
331 |
if b' ' not in status: |
332 |
# malformed response, move on to the next one
|
333 |
continue
|
334 |
status, ref = status.split(b' ', 1) |
335 |
|
336 |
if status == b'ng': |
337 |
if b' ' in ref: |
338 |
ref, status = ref.split(b' ', 1) |
339 |
else:
|
340 |
ok.add(ref) |
341 |
ref_status[ref] = status |
342 |
raise UpdateRefsError(', '.join([ref for ref in ref_status |
343 |
if ref not in ok]) + |
344 |
b' failed to update',
|
345 |
ref_status=ref_status) |
346 |
|
347 |
def _read_side_band64k_data(self, proto, channel_callbacks): |
348 |
"""Read per-channel data.
|
349 |
|
350 |
This requires the side-band-64k capability.
|
351 |
|
352 |
:param proto: Protocol object to read from
|
353 |
:param channel_callbacks: Dictionary mapping channels to packet
|
354 |
handlers to use. None for a callback discards channel data.
|
355 |
"""
|
356 |
for pkt in proto.read_pkt_seq(): |
357 |
channel = ord(pkt[:1]) |
358 |
pkt = pkt[1:]
|
359 |
try:
|
360 |
cb = channel_callbacks[channel] |
361 |
except KeyError: |
362 |
raise AssertionError('Invalid sideband channel %d' % channel) |
363 |
else:
|
364 |
if cb is not None: |
365 |
cb(pkt) |
366 |
|
367 |
def _handle_receive_pack_head(self, proto, capabilities, old_refs, |
368 |
new_refs): |
369 |
"""Handle the head of a 'git-receive-pack' request.
|
370 |
|
371 |
:param proto: Protocol object to read from
|
372 |
:param capabilities: List of negotiated capabilities
|
373 |
:param old_refs: Old refs, as received from the server
|
374 |
:param new_refs: Refs to change
|
375 |
:return: (have, want) tuple
|
376 |
"""
|
377 |
want = [] |
378 |
have = [x for x in old_refs.values() if not x == ZERO_SHA] |
379 |
sent_capabilities = False
|
380 |
|
381 |
for refname in new_refs: |
382 |
if not isinstance(refname, bytes): |
383 |
raise TypeError('refname is not a bytestring: %r' % refname) |
384 |
old_sha1 = old_refs.get(refname, ZERO_SHA) |
385 |
if not isinstance(old_sha1, bytes): |
386 |
raise TypeError('old sha1 for %s is not a bytestring: %r' % |
387 |
(refname, old_sha1)) |
388 |
new_sha1 = new_refs.get(refname, ZERO_SHA) |
389 |
if not isinstance(new_sha1, bytes): |
390 |
raise TypeError('old sha1 for %s is not a bytestring %r' % |
391 |
(refname, new_sha1)) |
392 |
|
393 |
if old_sha1 != new_sha1:
|
394 |
if sent_capabilities:
|
395 |
proto.write_pkt_line(old_sha1 + b' ' + new_sha1 + b' ' + refname) |
396 |
else:
|
397 |
proto.write_pkt_line( |
398 |
old_sha1 + b' ' + new_sha1 + b' ' + refname + b'\0' + |
399 |
b' '.join(capabilities))
|
400 |
sent_capabilities = True
|
401 |
if new_sha1 not in have and new_sha1 != ZERO_SHA: |
402 |
want.append(new_sha1) |
403 |
proto.write_pkt_line(None)
|
404 |
return (have, want)
|
405 |
|
406 |
def _handle_receive_pack_tail(self, proto, capabilities, progress=None): |
407 |
"""Handle the tail of a 'git-receive-pack' request.
|
408 |
|
409 |
:param proto: Protocol object to read from
|
410 |
:param capabilities: List of negotiated capabilities
|
411 |
:param progress: Optional progress reporting function
|
412 |
"""
|
413 |
if b"side-band-64k" in capabilities: |
414 |
if progress is None: |
415 |
progress = lambda x: None |
416 |
channel_callbacks = {2: progress}
|
417 |
if CAPABILITY_REPORT_STATUS in capabilities: |
418 |
channel_callbacks[1] = PktLineParser(
|
419 |
self._report_status_parser.handle_packet).parse
|
420 |
self._read_side_band64k_data(proto, channel_callbacks)
|
421 |
else:
|
422 |
if CAPABILITY_REPORT_STATUS in capabilities: |
423 |
for pkt in proto.read_pkt_seq(): |
424 |
self._report_status_parser.handle_packet(pkt)
|
425 |
if self._report_status_parser is not None: |
426 |
self._report_status_parser.check()
|
427 |
|
428 |
def _handle_upload_pack_head(self, proto, capabilities, graph_walker, |
429 |
wants, can_read): |
430 |
"""Handle the head of a 'git-upload-pack' request.
|
431 |
|
432 |
:param proto: Protocol object to read from
|
433 |
:param capabilities: List of negotiated capabilities
|
434 |
:param graph_walker: GraphWalker instance to call .ack() on
|
435 |
:param wants: List of commits to fetch
|
436 |
:param can_read: function that returns a boolean that indicates
|
437 |
whether there is extra graph data to read on proto
|
438 |
"""
|
439 |
assert isinstance(wants, list) and isinstance(wants[0], bytes) |
440 |
proto.write_pkt_line(COMMAND_WANT + b' ' + wants[0] + b' ' + b' '.join(capabilities) + b'\n') |
441 |
for want in wants[1:]: |
442 |
proto.write_pkt_line(COMMAND_WANT + b' ' + want + b'\n') |
443 |
proto.write_pkt_line(None)
|
444 |
have = next(graph_walker)
|
445 |
while have:
|
446 |
proto.write_pkt_line(COMMAND_HAVE + b' ' + have + b'\n') |
447 |
if can_read():
|
448 |
pkt = proto.read_pkt_line() |
449 |
parts = pkt.rstrip(b'\n').split(b' ') |
450 |
if parts[0] == b'ACK': |
451 |
graph_walker.ack(parts[1])
|
452 |
if parts[2] in (b'continue', b'common'): |
453 |
pass
|
454 |
elif parts[2] == b'ready': |
455 |
break
|
456 |
else:
|
457 |
raise AssertionError( |
458 |
"%s not in ('continue', 'ready', 'common)" %
|
459 |
parts[2])
|
460 |
have = next(graph_walker)
|
461 |
proto.write_pkt_line(COMMAND_DONE + b'\n')
|
462 |
|
463 |
def _handle_upload_pack_tail(self, proto, capabilities, graph_walker, |
464 |
pack_data, progress=None, rbufsize=_RBUFSIZE):
|
465 |
"""Handle the tail of a 'git-upload-pack' request.
|
466 |
|
467 |
:param proto: Protocol object to read from
|
468 |
:param capabilities: List of negotiated capabilities
|
469 |
:param graph_walker: GraphWalker instance to call .ack() on
|
470 |
:param pack_data: Function to call with pack data
|
471 |
:param progress: Optional progress reporting function
|
472 |
:param rbufsize: Read buffer size
|
473 |
"""
|
474 |
pkt = proto.read_pkt_line() |
475 |
while pkt:
|
476 |
parts = pkt.rstrip(b'\n').split(b' ') |
477 |
if parts[0] == b'ACK': |
478 |
graph_walker.ack(parts[1])
|
479 |
if len(parts) < 3 or parts[2] not in ( |
480 |
b'ready', b'continue', b'common'): |
481 |
break
|
482 |
pkt = proto.read_pkt_line() |
483 |
if CAPABILITY_SIDE_BAND_64K in capabilities: |
484 |
if progress is None: |
485 |
# Just ignore progress data
|
486 |
progress = lambda x: None |
487 |
self._read_side_band64k_data(proto, {
|
488 |
SIDE_BAND_CHANNEL_DATA: pack_data, |
489 |
SIDE_BAND_CHANNEL_PROGRESS: progress} |
490 |
) |
491 |
else:
|
492 |
while True: |
493 |
data = proto.read(rbufsize) |
494 |
if data == b"": |
495 |
break
|
496 |
pack_data(data) |
497 |
|
498 |
|
499 |
class TraditionalGitClient(GitClient): |
500 |
"""Traditional Git client."""
|
501 |
|
502 |
DEFAULT_ENCODING = 'utf-8'
|
503 |
|
504 |
def __init__(self, path_encoding=DEFAULT_ENCODING, **kwargs): |
505 |
self._remote_path_encoding = path_encoding
|
506 |
super(TraditionalGitClient, self).__init__(**kwargs) |
507 |
|
508 |
def _connect(self, cmd, path): |
509 |
"""Create a connection to the server.
|
510 |
|
511 |
This method is abstract - concrete implementations should
|
512 |
implement their own variant which connects to the server and
|
513 |
returns an initialized Protocol object with the service ready
|
514 |
for use and a can_read function which may be used to see if
|
515 |
reads would block.
|
516 |
|
517 |
:param cmd: The git service name to which we should connect.
|
518 |
:param path: The path we should pass to the service. (as bytestirng)
|
519 |
"""
|
520 |
raise NotImplementedError() |
521 |
|
522 |
def send_pack(self, path, determine_wants, generate_pack_contents, |
523 |
progress=None, write_pack=write_pack_objects):
|
524 |
"""Upload a pack to a remote repository.
|
525 |
|
526 |
:param path: Repository path (as bytestring)
|
527 |
:param generate_pack_contents: Function that can return a sequence of
|
528 |
the shas of the objects to upload.
|
529 |
:param progress: Optional callback called with progress updates
|
530 |
:param write_pack: Function called with (file, iterable of objects) to
|
531 |
write the objects returned by generate_pack_contents to the server.
|
532 |
|
533 |
:raises SendPackError: if server rejects the pack data
|
534 |
:raises UpdateRefsError: if the server supports report-status
|
535 |
and rejects ref updates
|
536 |
:return: new_refs dictionary containing the changes that were made
|
537 |
{refname: new_ref}, including deleted refs.
|
538 |
"""
|
539 |
proto, unused_can_read = self._connect(b'receive-pack', path) |
540 |
with proto:
|
541 |
old_refs, server_capabilities = read_pkt_refs(proto) |
542 |
negotiated_capabilities = self._send_capabilities & server_capabilities
|
543 |
|
544 |
if CAPABILITY_REPORT_STATUS in negotiated_capabilities: |
545 |
self._report_status_parser = ReportStatusParser()
|
546 |
report_status_parser = self._report_status_parser
|
547 |
|
548 |
try:
|
549 |
new_refs = orig_new_refs = determine_wants(dict(old_refs))
|
550 |
except:
|
551 |
proto.write_pkt_line(None)
|
552 |
raise
|
553 |
|
554 |
if not CAPABILITY_DELETE_REFS in server_capabilities: |
555 |
# Server does not support deletions. Fail later.
|
556 |
new_refs = dict(orig_new_refs)
|
557 |
for ref, sha in orig_new_refs.items(): |
558 |
if sha == ZERO_SHA:
|
559 |
if CAPABILITY_REPORT_STATUS in negotiated_capabilities: |
560 |
report_status_parser._ref_statuses.append( |
561 |
b'ng ' + sha + b' remote does not support deleting refs') |
562 |
report_status_parser._ref_status_ok = False
|
563 |
del new_refs[ref]
|
564 |
|
565 |
if new_refs is None: |
566 |
proto.write_pkt_line(None)
|
567 |
return old_refs
|
568 |
|
569 |
if len(new_refs) == 0 and len(orig_new_refs): |
570 |
# NOOP - Original new refs filtered out by policy
|
571 |
proto.write_pkt_line(None)
|
572 |
if report_status_parser is not None: |
573 |
report_status_parser.check() |
574 |
return old_refs
|
575 |
|
576 |
(have, want) = self._handle_receive_pack_head(
|
577 |
proto, negotiated_capabilities, old_refs, new_refs) |
578 |
if not want and set(new_refs.items()).issubset(set(old_refs.items())): |
579 |
return new_refs
|
580 |
objects = generate_pack_contents(have, want) |
581 |
|
582 |
dowrite = len(objects) > 0 |
583 |
dowrite = dowrite or any(old_refs.get(ref) != sha |
584 |
for (ref, sha) in new_refs.items() |
585 |
if sha != ZERO_SHA)
|
586 |
if dowrite:
|
587 |
write_pack(proto.write_file(), objects) |
588 |
|
589 |
self._handle_receive_pack_tail(
|
590 |
proto, negotiated_capabilities, progress) |
591 |
return new_refs
|
592 |
|
593 |
def fetch_pack(self, path, determine_wants, graph_walker, pack_data, |
594 |
progress=None):
|
595 |
"""Retrieve a pack from a git smart server.
|
596 |
|
597 |
:param determine_wants: Callback that returns list of commits to fetch
|
598 |
:param graph_walker: Object with next() and ack().
|
599 |
:param pack_data: Callback called for each bit of data in the pack
|
600 |
:param progress: Callback for progress reports (strings)
|
601 |
:return: Dictionary with all remote refs (not just those fetched)
|
602 |
"""
|
603 |
proto, can_read = self._connect(b'upload-pack', path) |
604 |
with proto:
|
605 |
refs, server_capabilities = read_pkt_refs(proto) |
606 |
negotiated_capabilities = ( |
607 |
self._fetch_capabilities & server_capabilities)
|
608 |
|
609 |
if refs is None: |
610 |
proto.write_pkt_line(None)
|
611 |
return refs
|
612 |
|
613 |
try:
|
614 |
wants = determine_wants(refs) |
615 |
except:
|
616 |
proto.write_pkt_line(None)
|
617 |
raise
|
618 |
if wants is not None: |
619 |
wants = [cid for cid in wants if cid != ZERO_SHA] |
620 |
if not wants: |
621 |
proto.write_pkt_line(None)
|
622 |
return refs
|
623 |
self._handle_upload_pack_head(
|
624 |
proto, negotiated_capabilities, graph_walker, wants, can_read) |
625 |
self._handle_upload_pack_tail(
|
626 |
proto, negotiated_capabilities, graph_walker, pack_data, progress) |
627 |
return refs
|
628 |
|
629 |
def get_refs(self, path): |
630 |
"""Retrieve the current refs from a git smart server."""
|
631 |
# stock `git ls-remote` uses upload-pack
|
632 |
proto, _ = self._connect(b'upload-pack', path) |
633 |
with proto:
|
634 |
refs, _ = read_pkt_refs(proto) |
635 |
return refs
|
636 |
|
637 |
def archive(self, path, committish, write_data, progress=None, |
638 |
write_error=None):
|
639 |
proto, can_read = self._connect(b'upload-archive', path) |
640 |
with proto:
|
641 |
proto.write_pkt_line(b"argument " + committish)
|
642 |
proto.write_pkt_line(None)
|
643 |
pkt = proto.read_pkt_line() |
644 |
if pkt == b"NACK\n": |
645 |
return
|
646 |
elif pkt == b"ACK\n": |
647 |
pass
|
648 |
elif pkt.startswith(b"ERR "): |
649 |
raise GitProtocolError(pkt[4:].rstrip(b"\n")) |
650 |
else:
|
651 |
raise AssertionError("invalid response %r" % pkt) |
652 |
ret = proto.read_pkt_line() |
653 |
if ret is not None: |
654 |
raise AssertionError("expected pkt tail") |
655 |
self._read_side_band64k_data(proto, {
|
656 |
SIDE_BAND_CHANNEL_DATA: write_data, |
657 |
SIDE_BAND_CHANNEL_PROGRESS: progress, |
658 |
SIDE_BAND_CHANNEL_FATAL: write_error}) |
659 |
|
660 |
|
661 |
class TCPGitClient(TraditionalGitClient): |
662 |
"""A Git Client that works over TCP directly (i.e. git://)."""
|
663 |
|
664 |
def __init__(self, host, port=None, **kwargs): |
665 |
if port is None: |
666 |
port = TCP_GIT_PORT |
667 |
self._host = host
|
668 |
self._port = port
|
669 |
super(TCPGitClient, self).__init__(**kwargs) |
670 |
|
671 |
@classmethod
|
672 |
def from_parsedurl(cls, parsedurl, **kwargs): |
673 |
return cls(parsedurl.hostname, port=parsedurl.port, **kwargs)
|
674 |
|
675 |
def get_url(self, path): |
676 |
netloc = self._host
|
677 |
if self._port is not None and self._port != TCP_GIT_PORT: |
678 |
netloc += ":%d" % self._port |
679 |
return urlparse.urlunsplit(("git", netloc, path, '', '')) |
680 |
|
681 |
def _connect(self, cmd, path): |
682 |
if type(cmd) is not bytes: |
683 |
raise TypeError(cmd) |
684 |
if type(path) is not bytes: |
685 |
path = path.encode(self._remote_path_encoding)
|
686 |
sockaddrs = socket.getaddrinfo( |
687 |
self._host, self._port, socket.AF_UNSPEC, socket.SOCK_STREAM) |
688 |
s = None
|
689 |
err = socket.error("no address found for %s" % self._host) |
690 |
for (family, socktype, proto, canonname, sockaddr) in sockaddrs: |
691 |
s = socket.socket(family, socktype, proto) |
692 |
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
693 |
try:
|
694 |
s.connect(sockaddr) |
695 |
break
|
696 |
except socket.error as err: |
697 |
if s is not None: |
698 |
s.close() |
699 |
s = None
|
700 |
if s is None: |
701 |
raise err
|
702 |
# -1 means system default buffering
|
703 |
rfile = s.makefile('rb', -1) |
704 |
# 0 means unbuffered
|
705 |
wfile = s.makefile('wb', 0) |
706 |
def close(): |
707 |
rfile.close() |
708 |
wfile.close() |
709 |
s.close() |
710 |
|
711 |
proto = Protocol(rfile.read, wfile.write, close, |
712 |
report_activity=self._report_activity)
|
713 |
if path.startswith(b"/~"): |
714 |
path = path[1:]
|
715 |
# TODO(jelmer): Alternative to ascii?
|
716 |
proto.send_cmd(b'git-' + cmd, path, b'host=' + self._host.encode('ascii')) |
717 |
return proto, lambda: _fileno_can_read(s) |
718 |
|
719 |
|
720 |
class SubprocessWrapper(object): |
721 |
"""A socket-like object that talks to a subprocess via pipes."""
|
722 |
|
723 |
def __init__(self, proc): |
724 |
self.proc = proc
|
725 |
if sys.version_info[0] == 2: |
726 |
self.read = proc.stdout.read
|
727 |
else:
|
728 |
self.read = BufferedReader(proc.stdout).read
|
729 |
self.write = proc.stdin.write
|
730 |
|
731 |
def can_read(self): |
732 |
if sys.platform == 'win32': |
733 |
from msvcrt import get_osfhandle |
734 |
from win32pipe import PeekNamedPipe |
735 |
handle = get_osfhandle(self.proc.stdout.fileno())
|
736 |
data, total_bytes_avail, msg_bytes_left = PeekNamedPipe(handle, 0)
|
737 |
return total_bytes_avail != 0 |
738 |
else:
|
739 |
return _fileno_can_read(self.proc.stdout.fileno()) |
740 |
|
741 |
def close(self): |
742 |
self.proc.stdin.close()
|
743 |
self.proc.stdout.close()
|
744 |
if self.proc.stderr: |
745 |
self.proc.stderr.close()
|
746 |
self.proc.wait()
|
747 |
|
748 |
|
749 |
def find_git_command(): |
750 |
"""Find command to run for system Git (usually C Git).
|
751 |
"""
|
752 |
if sys.platform == 'win32': # support .exe, .bat and .cmd |
753 |
try: # to avoid overhead |
754 |
import win32api |
755 |
except ImportError: # run through cmd.exe with some overhead |
756 |
return ['cmd', '/c', 'git'] |
757 |
else:
|
758 |
status, git = win32api.FindExecutable('git')
|
759 |
return [git]
|
760 |
else:
|
761 |
return ['git'] |
762 |
|
763 |
|
764 |
class SubprocessGitClient(TraditionalGitClient): |
765 |
"""Git client that talks to a server using a subprocess."""
|
766 |
|
767 |
def __init__(self, **kwargs): |
768 |
self._connection = None |
769 |
self._stderr = None |
770 |
self._stderr = kwargs.get('stderr') |
771 |
if 'stderr' in kwargs: |
772 |
del kwargs['stderr'] |
773 |
super(SubprocessGitClient, self).__init__(**kwargs) |
774 |
|
775 |
@classmethod
|
776 |
def from_parsedurl(cls, parsedurl, **kwargs): |
777 |
return cls(**kwargs)
|
778 |
|
779 |
git_command = None
|
780 |
|
781 |
def _connect(self, service, path): |
782 |
if type(service) is not bytes: |
783 |
raise TypeError(service) |
784 |
if type(path) is not bytes: |
785 |
path = path.encode(self._remote_path_encoding)
|
786 |
if self.git_command is None: |
787 |
git_command = find_git_command() |
788 |
argv = git_command + [service.decode('ascii'), path]
|
789 |
p = SubprocessWrapper( |
790 |
subprocess.Popen(argv, bufsize=0, stdin=subprocess.PIPE,
|
791 |
stdout=subprocess.PIPE, |
792 |
stderr=self._stderr))
|
793 |
return Protocol(p.read, p.write, p.close,
|
794 |
report_activity=self._report_activity), p.can_read
|
795 |
|
796 |
|
797 |
class LocalGitClient(GitClient): |
798 |
"""Git Client that just uses a local Repo."""
|
799 |
|
800 |
def __init__(self, thin_packs=True, report_activity=None): |
801 |
"""Create a new LocalGitClient instance.
|
802 |
|
803 |
:param thin_packs: Whether or not thin packs should be retrieved
|
804 |
:param report_activity: Optional callback for reporting transport
|
805 |
activity.
|
806 |
"""
|
807 |
self._report_activity = report_activity
|
808 |
# Ignore the thin_packs argument
|
809 |
|
810 |
def get_url(self, path): |
811 |
return urlparse.urlunsplit(('file', '', path, '', '')) |
812 |
|
813 |
@classmethod
|
814 |
def from_parsedurl(cls, parsedurl, **kwargs): |
815 |
return cls(**kwargs)
|
816 |
|
817 |
@classmethod
|
818 |
def _open_repo(cls, path): |
819 |
from dulwich.repo import Repo |
820 |
if not isinstance(path, str): |
821 |
path = path.decode(sys.getfilesystemencoding()) |
822 |
return closing(Repo(path))
|
823 |
|
824 |
def send_pack(self, path, determine_wants, generate_pack_contents, |
825 |
progress=None, write_pack=write_pack_objects):
|
826 |
"""Upload a pack to a remote repository.
|
827 |
|
828 |
:param path: Repository path (as bytestring)
|
829 |
:param generate_pack_contents: Function that can return a sequence of
|
830 |
the shas of the objects to upload.
|
831 |
:param progress: Optional progress function
|
832 |
:param write_pack: Function called with (file, iterable of objects) to
|
833 |
write the objects returned by generate_pack_contents to the server.
|
834 |
|
835 |
:raises SendPackError: if server rejects the pack data
|
836 |
:raises UpdateRefsError: if the server supports report-status
|
837 |
and rejects ref updates
|
838 |
:return: new_refs dictionary containing the changes that were made
|
839 |
{refname: new_ref}, including deleted refs.
|
840 |
"""
|
841 |
if not progress: |
842 |
progress = lambda x: None |
843 |
|
844 |
with self._open_repo(path) as target: |
845 |
old_refs = target.get_refs() |
846 |
new_refs = determine_wants(dict(old_refs))
|
847 |
|
848 |
have = [sha1 for sha1 in old_refs.values() if sha1 != ZERO_SHA] |
849 |
want = [] |
850 |
for refname, new_sha1 in new_refs.items(): |
851 |
if new_sha1 not in have and not new_sha1 in want and new_sha1 != ZERO_SHA: |
852 |
want.append(new_sha1) |
853 |
|
854 |
if not want and set(new_refs.items()).issubset(set(old_refs.items())): |
855 |
return new_refs
|
856 |
|
857 |
target.object_store.add_objects(generate_pack_contents(have, want)) |
858 |
|
859 |
for refname, new_sha1 in new_refs.items(): |
860 |
old_sha1 = old_refs.get(refname, ZERO_SHA) |
861 |
if new_sha1 != ZERO_SHA:
|
862 |
if not target.refs.set_if_equals(refname, old_sha1, new_sha1): |
863 |
progress('unable to set %s to %s' % (refname, new_sha1))
|
864 |
else:
|
865 |
if not target.refs.remove_if_equals(refname, old_sha1): |
866 |
progress('unable to remove %s' % refname)
|
867 |
|
868 |
return new_refs
|
869 |
|
870 |
def fetch(self, path, target, determine_wants=None, progress=None): |
871 |
"""Fetch into a target repository.
|
872 |
|
873 |
:param path: Path to fetch from (as bytestring)
|
874 |
:param target: Target repository to fetch into
|
875 |
:param determine_wants: Optional function to determine what refs
|
876 |
to fetch
|
877 |
:param progress: Optional progress function
|
878 |
:return: Dictionary with all remote refs (not just those fetched)
|
879 |
"""
|
880 |
with self._open_repo(path) as r: |
881 |
return r.fetch(target, determine_wants=determine_wants,
|
882 |
progress=progress) |
883 |
|
884 |
def fetch_pack(self, path, determine_wants, graph_walker, pack_data, |
885 |
progress=None):
|
886 |
"""Retrieve a pack from a git smart server.
|
887 |
|
888 |
:param determine_wants: Callback that returns list of commits to fetch
|
889 |
:param graph_walker: Object with next() and ack().
|
890 |
:param pack_data: Callback called for each bit of data in the pack
|
891 |
:param progress: Callback for progress reports (strings)
|
892 |
:return: Dictionary with all remote refs (not just those fetched)
|
893 |
"""
|
894 |
with self._open_repo(path) as r: |
895 |
objects_iter = r.fetch_objects(determine_wants, graph_walker, progress) |
896 |
|
897 |
# Did the process short-circuit (e.g. in a stateless RPC call)? Note
|
898 |
# that the client still expects a 0-object pack in most cases.
|
899 |
if objects_iter is None: |
900 |
return
|
901 |
write_pack_objects(ProtocolFile(None, pack_data), objects_iter)
|
902 |
return r.get_refs()
|
903 |
|
904 |
def get_refs(self, path): |
905 |
"""Retrieve the current refs from a git smart server."""
|
906 |
|
907 |
with self._open_repo(path) as target: |
908 |
return target.get_refs()
|
909 |
|
910 |
|
911 |
# What Git client to use for local access
|
912 |
default_local_git_client_cls = LocalGitClient |
913 |
|
914 |
|
915 |
class SSHVendor(object): |
916 |
"""A client side SSH implementation."""
|
917 |
|
918 |
def connect_ssh(self, host, command, username=None, port=None): |
919 |
# This function was deprecated in 0.9.1
|
920 |
import warnings |
921 |
warnings.warn( |
922 |
"SSHVendor.connect_ssh has been renamed to SSHVendor.run_command",
|
923 |
DeprecationWarning)
|
924 |
return self.run_command(host, command, username=username, port=port) |
925 |
|
926 |
def run_command(self, host, command, username=None, port=None): |
927 |
"""Connect to an SSH server.
|
928 |
|
929 |
Run a command remotely and return a file-like object for interaction
|
930 |
with the remote command.
|
931 |
|
932 |
:param host: Host name
|
933 |
:param command: Command to run (as argv array)
|
934 |
:param username: Optional ame of user to log in as
|
935 |
:param port: Optional SSH port to use
|
936 |
"""
|
937 |
raise NotImplementedError(self.run_command) |
938 |
|
939 |
|
940 |
class SubprocessSSHVendor(SSHVendor): |
941 |
"""SSH vendor that shells out to the local 'ssh' command."""
|
942 |
|
943 |
def run_command(self, host, command, username=None, port=None): |
944 |
if not isinstance(command, bytes): |
945 |
raise TypeError(command) |
946 |
|
947 |
#FIXME: This has no way to deal with passwords..
|
948 |
args = ['ssh', '-x'] |
949 |
if port is not None: |
950 |
args.extend(['-p', str(port)]) |
951 |
if username is not None: |
952 |
host = '%s@%s' % (username, host)
|
953 |
args.append(host) |
954 |
proc = subprocess.Popen(args + [command], |
955 |
stdin=subprocess.PIPE, |
956 |
stdout=subprocess.PIPE) |
957 |
return SubprocessWrapper(proc)
|
958 |
|
959 |
|
960 |
def ParamikoSSHVendor(**kwargs): |
961 |
import warnings |
962 |
warnings.warn( |
963 |
"ParamikoSSHVendor has been moved to dulwich.contrib.paramiko_vendor.",
|
964 |
DeprecationWarning)
|
965 |
from dulwich.contrib.paramiko_vendor import ParamikoSSHVendor |
966 |
return ParamikoSSHVendor(**kwargs)
|
967 |
|
968 |
|
969 |
# Can be overridden by users
|
970 |
get_ssh_vendor = SubprocessSSHVendor |
971 |
|
972 |
|
973 |
class SSHGitClient(TraditionalGitClient): |
974 |
|
975 |
def __init__(self, host, port=None, username=None, vendor=None, **kwargs): |
976 |
self.host = host
|
977 |
self.port = port
|
978 |
self.username = username
|
979 |
super(SSHGitClient, self).__init__(**kwargs) |
980 |
self.alternative_paths = {}
|
981 |
if vendor is not None: |
982 |
self.ssh_vendor = vendor
|
983 |
else:
|
984 |
self.ssh_vendor = get_ssh_vendor()
|
985 |
|
986 |
def get_url(self, path): |
987 |
netloc = self.host
|
988 |
if self.port is not None: |
989 |
netloc += ":%d" % self.port |
990 |
|
991 |
if self.username is not None: |
992 |
netloc = urlquote(self.username, '@/:') + "@" + netloc |
993 |
|
994 |
return urlparse.urlunsplit(('ssh', netloc, path, '', '')) |
995 |
|
996 |
@classmethod
|
997 |
def from_parsedurl(cls, parsedurl, **kwargs): |
998 |
return cls(host=parsedurl.hostname, port=parsedurl.port,
|
999 |
username=parsedurl.username, **kwargs) |
1000 |
|
1001 |
def _get_cmd_path(self, cmd): |
1002 |
cmd = self.alternative_paths.get(cmd, b'git-' + cmd) |
1003 |
assert isinstance(cmd, bytes) |
1004 |
return cmd
|
1005 |
|
1006 |
def _connect(self, cmd, path): |
1007 |
if type(cmd) is not bytes: |
1008 |
raise TypeError(cmd) |
1009 |
if type(path) is not bytes: |
1010 |
path = path.encode(self._remote_path_encoding)
|
1011 |
if path.startswith(b"/~"): |
1012 |
path = path[1:]
|
1013 |
argv = self._get_cmd_path(cmd) + b" '" + path + b"'" |
1014 |
con = self.ssh_vendor.run_command(
|
1015 |
self.host, argv, port=self.port, username=self.username) |
1016 |
return (Protocol(con.read, con.write, con.close,
|
1017 |
report_activity=self._report_activity),
|
1018 |
con.can_read) |
1019 |
|
1020 |
|
1021 |
def default_user_agent_string(): |
1022 |
return "dulwich/%s" % ".".join([str(x) for x in dulwich.__version__]) |
1023 |
|
1024 |
|
1025 |
def default_urllib2_opener(config): |
1026 |
if config is not None: |
1027 |
proxy_server = config.get("http", "proxy") |
1028 |
else:
|
1029 |
proxy_server = None
|
1030 |
handlers = [] |
1031 |
if proxy_server is not None: |
1032 |
handlers.append(urllib2.ProxyHandler({"http": proxy_server}))
|
1033 |
opener = urllib2.build_opener(*handlers) |
1034 |
if config is not None: |
1035 |
user_agent = config.get("http", "useragent") |
1036 |
else:
|
1037 |
user_agent = None
|
1038 |
if user_agent is None: |
1039 |
user_agent = default_user_agent_string() |
1040 |
opener.addheaders = [('User-agent', user_agent)]
|
1041 |
return opener
|
1042 |
|
1043 |
|
1044 |
class HttpGitClient(GitClient): |
1045 |
|
1046 |
def __init__(self, base_url, dumb=None, opener=None, config=None, |
1047 |
username=None, password=None, **kwargs): |
1048 |
self._base_url = base_url.rstrip("/") + "/" |
1049 |
self._username = username
|
1050 |
self._password = password
|
1051 |
self.dumb = dumb
|
1052 |
if opener is None: |
1053 |
self.opener = default_urllib2_opener(config)
|
1054 |
else:
|
1055 |
self.opener = opener
|
1056 |
if username is not None: |
1057 |
pass_man = urllib2.HTTPPasswordMgrWithDefaultRealm() |
1058 |
pass_man.add_password(None, base_url, username, password)
|
1059 |
self.opener.add_handler(urllib2.HTTPBasicAuthHandler(pass_man))
|
1060 |
GitClient.__init__(self, **kwargs)
|
1061 |
|
1062 |
def get_url(self, path): |
1063 |
return self._get_url(path).rstrip("/") |
1064 |
|
1065 |
@classmethod
|
1066 |
def from_parsedurl(cls, parsedurl, **kwargs): |
1067 |
auth, host = urllib2.splituser(parsedurl.netloc) |
1068 |
password = parsedurl.password |
1069 |
if password is not None: |
1070 |
password = urlunquote(password) |
1071 |
username = parsedurl.username |
1072 |
if username is not None: |
1073 |
username = urlunquote(username) |
1074 |
# TODO(jelmer): This also strips the username
|
1075 |
parsedurl = parsedurl._replace(netloc=host) |
1076 |
return cls(urlparse.urlunparse(parsedurl),
|
1077 |
password=password, username=username, **kwargs) |
1078 |
|
1079 |
def __repr__(self): |
1080 |
return "%s(%r, dumb=%r)" % (type(self).__name__, self._base_url, self.dumb) |
1081 |
|
1082 |
def _get_url(self, path): |
1083 |
return urlparse.urljoin(self._base_url, path).rstrip("/") + "/" |
1084 |
|
1085 |
def _http_request(self, url, headers={}, data=None): |
1086 |
req = urllib2.Request(url, headers=headers, data=data) |
1087 |
try:
|
1088 |
resp = self.opener.open(req)
|
1089 |
except urllib2.HTTPError as e: |
1090 |
if e.code == 404: |
1091 |
raise NotGitRepository()
|
1092 |
if e.code != 200: |
1093 |
raise GitProtocolError("unexpected http response %d" % e.code) |
1094 |
return resp
|
1095 |
|
1096 |
def _discover_references(self, service, url): |
1097 |
assert url[-1] == "/" |
1098 |
url = urlparse.urljoin(url, "info/refs")
|
1099 |
headers = {} |
1100 |
if self.dumb is not False: |
1101 |
url += "?service=%s" % service.decode('ascii') |
1102 |
headers["Content-Type"] = "application/x-%s-request" % ( |
1103 |
service.decode('ascii'))
|
1104 |
resp = self._http_request(url, headers)
|
1105 |
try:
|
1106 |
content_type = resp.info().gettype() |
1107 |
except AttributeError: |
1108 |
content_type = resp.info().get_content_type() |
1109 |
try:
|
1110 |
self.dumb = (not content_type.startswith("application/x-git-")) |
1111 |
if not self.dumb: |
1112 |
proto = Protocol(resp.read, None)
|
1113 |
# The first line should mention the service
|
1114 |
try:
|
1115 |
[pkt] = list(proto.read_pkt_seq())
|
1116 |
except ValueError: |
1117 |
raise GitProtocolError(
|
1118 |
"unexpected number of packets received")
|
1119 |
if pkt.rstrip(b'\n') != (b'# service=' + service): |
1120 |
raise GitProtocolError(
|
1121 |
"unexpected first line %r from smart server" % pkt)
|
1122 |
return read_pkt_refs(proto)
|
1123 |
else:
|
1124 |
return read_info_refs(resp), set() |
1125 |
finally:
|
1126 |
resp.close() |
1127 |
|
1128 |
def _smart_request(self, service, url, data): |
1129 |
assert url[-1] == "/" |
1130 |
url = urlparse.urljoin(url, service) |
1131 |
headers = { |
1132 |
"Content-Type": "application/x-%s-request" % service |
1133 |
} |
1134 |
resp = self._http_request(url, headers, data)
|
1135 |
try:
|
1136 |
content_type = resp.info().gettype() |
1137 |
except AttributeError: |
1138 |
content_type = resp.info().get_content_type() |
1139 |
if content_type != (
|
1140 |
"application/x-%s-result" % service):
|
1141 |
raise GitProtocolError("Invalid content-type from server: %s" |
1142 |
% content_type) |
1143 |
return resp
|
1144 |
|
1145 |
def send_pack(self, path, determine_wants, generate_pack_contents, |
1146 |
progress=None, write_pack=write_pack_objects):
|
1147 |
"""Upload a pack to a remote repository.
|
1148 |
|
1149 |
:param path: Repository path (as bytestring)
|
1150 |
:param generate_pack_contents: Function that can return a sequence of
|
1151 |
the shas of the objects to upload.
|
1152 |
:param progress: Optional progress function
|
1153 |
:param write_pack: Function called with (file, iterable of objects) to
|
1154 |
write the objects returned by generate_pack_contents to the server.
|
1155 |
|
1156 |
:raises SendPackError: if server rejects the pack data
|
1157 |
:raises UpdateRefsError: if the server supports report-status
|
1158 |
and rejects ref updates
|
1159 |
:return: new_refs dictionary containing the changes that were made
|
1160 |
{refname: new_ref}, including deleted refs.
|
1161 |
"""
|
1162 |
url = self._get_url(path)
|
1163 |
old_refs, server_capabilities = self._discover_references(
|
1164 |
b"git-receive-pack", url)
|
1165 |
negotiated_capabilities = self._send_capabilities & server_capabilities
|
1166 |
|
1167 |
if CAPABILITY_REPORT_STATUS in negotiated_capabilities: |
1168 |
self._report_status_parser = ReportStatusParser()
|
1169 |
|
1170 |
new_refs = determine_wants(dict(old_refs))
|
1171 |
if new_refs is None: |
1172 |
# Determine wants function is aborting the push.
|
1173 |
return old_refs
|
1174 |
if self.dumb: |
1175 |
raise NotImplementedError(self.fetch_pack) |
1176 |
req_data = BytesIO() |
1177 |
req_proto = Protocol(None, req_data.write)
|
1178 |
(have, want) = self._handle_receive_pack_head(
|
1179 |
req_proto, negotiated_capabilities, old_refs, new_refs) |
1180 |
if not want and set(new_refs.items()).issubset(set(old_refs.items())): |
1181 |
return new_refs
|
1182 |
objects = generate_pack_contents(have, want) |
1183 |
if len(objects) > 0: |
1184 |
write_pack(req_proto.write_file(), objects) |
1185 |
resp = self._smart_request("git-receive-pack", url, |
1186 |
data=req_data.getvalue()) |
1187 |
try:
|
1188 |
resp_proto = Protocol(resp.read, None)
|
1189 |
self._handle_receive_pack_tail(resp_proto, negotiated_capabilities,
|
1190 |
progress) |
1191 |
return new_refs
|
1192 |
finally:
|
1193 |
resp.close() |
1194 |
|
1195 |
|
1196 |
def fetch_pack(self, path, determine_wants, graph_walker, pack_data, |
1197 |
progress=None):
|
1198 |
"""Retrieve a pack from a git smart server.
|
1199 |
|
1200 |
:param determine_wants: Callback that returns list of commits to fetch
|
1201 |
:param graph_walker: Object with next() and ack().
|
1202 |
:param pack_data: Callback called for each bit of data in the pack
|
1203 |
:param progress: Callback for progress reports (strings)
|
1204 |
:return: Dictionary with all remote refs (not just those fetched)
|
1205 |
"""
|
1206 |
url = self._get_url(path)
|
1207 |
refs, server_capabilities = self._discover_references(
|
1208 |
b"git-upload-pack", url)
|
1209 |
negotiated_capabilities = self._fetch_capabilities & server_capabilities
|
1210 |
wants = determine_wants(refs) |
1211 |
if wants is not None: |
1212 |
wants = [cid for cid in wants if cid != ZERO_SHA] |
1213 |
if not wants: |
1214 |
return refs
|
1215 |
if self.dumb: |
1216 |
raise NotImplementedError(self.send_pack) |
1217 |
req_data = BytesIO() |
1218 |
req_proto = Protocol(None, req_data.write)
|
1219 |
self._handle_upload_pack_head(
|
1220 |
req_proto, negotiated_capabilities, graph_walker, wants, |
1221 |
lambda: False) |
1222 |
resp = self._smart_request(
|
1223 |
"git-upload-pack", url, data=req_data.getvalue())
|
1224 |
try:
|
1225 |
resp_proto = Protocol(resp.read, None)
|
1226 |
self._handle_upload_pack_tail(resp_proto, negotiated_capabilities,
|
1227 |
graph_walker, pack_data, progress) |
1228 |
return refs
|
1229 |
finally:
|
1230 |
resp.close() |
1231 |
|
1232 |
def get_refs(self, path): |
1233 |
"""Retrieve the current refs from a git smart server."""
|
1234 |
url = self._get_url(path)
|
1235 |
refs, _ = self._discover_references(
|
1236 |
b"git-upload-pack", url)
|
1237 |
return refs
|
1238 |
|
1239 |
|
1240 |
def get_transport_and_path_from_url(url, config=None, **kwargs): |
1241 |
"""Obtain a git client from a URL.
|
1242 |
|
1243 |
:param url: URL to open (a unicode string)
|
1244 |
:param config: Optional config object
|
1245 |
:param thin_packs: Whether or not thin packs should be retrieved
|
1246 |
:param report_activity: Optional callback for reporting transport
|
1247 |
activity.
|
1248 |
:return: Tuple with client instance and relative path.
|
1249 |
"""
|
1250 |
parsed = urlparse.urlparse(url) |
1251 |
if parsed.scheme == 'git': |
1252 |
return (TCPGitClient.from_parsedurl(parsed, **kwargs),
|
1253 |
parsed.path) |
1254 |
elif parsed.scheme in ('git+ssh', 'ssh'): |
1255 |
path = parsed.path |
1256 |
if path.startswith('/'): |
1257 |
path = parsed.path[1:]
|
1258 |
return SSHGitClient.from_parsedurl(parsed, **kwargs), path
|
1259 |
elif parsed.scheme in ('http', 'https'): |
1260 |
return HttpGitClient.from_parsedurl(
|
1261 |
parsed, config=config, **kwargs), parsed.path |
1262 |
elif parsed.scheme == 'file': |
1263 |
return default_local_git_client_cls.from_parsedurl(
|
1264 |
parsed, **kwargs), parsed.path |
1265 |
|
1266 |
raise ValueError("unknown scheme '%s'" % parsed.scheme) |
1267 |
|
1268 |
|
1269 |
def get_transport_and_path(location, **kwargs): |
1270 |
"""Obtain a git client from a URL.
|
1271 |
|
1272 |
:param location: URL or path (a string)
|
1273 |
:param config: Optional config object
|
1274 |
:param thin_packs: Whether or not thin packs should be retrieved
|
1275 |
:param report_activity: Optional callback for reporting transport
|
1276 |
activity.
|
1277 |
:return: Tuple with client instance and relative path.
|
1278 |
"""
|
1279 |
# First, try to parse it as a URL
|
1280 |
try:
|
1281 |
return get_transport_and_path_from_url(location, **kwargs)
|
1282 |
except ValueError: |
1283 |
pass
|
1284 |
|
1285 |
if (sys.platform == 'win32' and |
1286 |
location[0].isalpha() and location[1:3] == ':\\'): |
1287 |
# Windows local path
|
1288 |
return default_local_git_client_cls(**kwargs), location
|
1289 |
|
1290 |
if ':' in location and not '@' in location: |
1291 |
# SSH with no user@, zero or one leading slash.
|
1292 |
(hostname, path) = location.split(':', 1) |
1293 |
return SSHGitClient(hostname, **kwargs), path
|
1294 |
elif ':' in location: |
1295 |
# SSH with user@host:foo.
|
1296 |
user_host, path = location.split(':', 1) |
1297 |
if '@' in user_host: |
1298 |
user, host = user_host.rsplit('@', 1) |
1299 |
else:
|
1300 |
user = None
|
1301 |
host = user_host |
1302 |
return SSHGitClient(host, username=user, **kwargs), path
|
1303 |
|
1304 |
# Otherwise, assume it's a local path.
|
1305 |
return default_local_git_client_cls(**kwargs), location
|