client.py 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085
  1. # client.py -- Implementation of the server side git protocols
  2. # Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
  3. # Copyright (C) 2008 John Carr
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # or (at your option) a later version of the License.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  18. # MA 02110-1301, USA.
  19. """Client side support for the Git protocol.
  20. The Dulwich client supports the following capabilities:
  21. * thin-pack
  22. * multi_ack_detailed
  23. * multi_ack
  24. * side-band-64k
  25. * ofs-delta
  26. * report-status
  27. * delete-refs
  28. Known capabilities that are not supported:
  29. * shallow
  30. * no-progress
  31. * include-tag
  32. """
  33. __docformat__ = 'restructuredText'
  34. from cStringIO import StringIO
  35. import select
  36. import socket
  37. import subprocess
  38. import urllib2
  39. import urlparse
  40. from dulwich.errors import (
  41. GitProtocolError,
  42. NotGitRepository,
  43. SendPackError,
  44. UpdateRefsError,
  45. )
  46. from dulwich.protocol import (
  47. _RBUFSIZE,
  48. PktLineParser,
  49. Protocol,
  50. ProtocolFile,
  51. TCP_GIT_PORT,
  52. ZERO_SHA,
  53. extract_capabilities,
  54. )
  55. from dulwich.pack import (
  56. write_pack_objects,
  57. )
  58. from dulwich.refs import (
  59. read_info_refs,
  60. )
  61. # Python 2.6.6 included these in urlparse.uses_netloc upstream. Do
  62. # monkeypatching to enable similar behaviour in earlier Pythons:
  63. for scheme in ('git', 'git+ssh'):
  64. if scheme not in urlparse.uses_netloc:
  65. urlparse.uses_netloc.append(scheme)
  66. def _fileno_can_read(fileno):
  67. """Check if a file descriptor is readable."""
  68. return len(select.select([fileno], [], [], 0)[0]) > 0
  69. COMMON_CAPABILITIES = ['ofs-delta', 'side-band-64k']
  70. FETCH_CAPABILITIES = ['thin-pack', 'multi_ack', 'multi_ack_detailed'] + COMMON_CAPABILITIES
  71. SEND_CAPABILITIES = ['report-status'] + COMMON_CAPABILITIES
  72. class ReportStatusParser(object):
  73. """Handle status as reported by servers with the 'report-status' capability.
  74. """
  75. def __init__(self):
  76. self._done = False
  77. self._pack_status = None
  78. self._ref_status_ok = True
  79. self._ref_statuses = []
  80. def check(self):
  81. """Check if there were any errors and, if so, raise exceptions.
  82. :raise SendPackError: Raised when the server could not unpack
  83. :raise UpdateRefsError: Raised when refs could not be updated
  84. """
  85. if self._pack_status not in ('unpack ok', None):
  86. raise SendPackError(self._pack_status)
  87. if not self._ref_status_ok:
  88. ref_status = {}
  89. ok = set()
  90. for status in self._ref_statuses:
  91. if ' ' not in status:
  92. # malformed response, move on to the next one
  93. continue
  94. status, ref = status.split(' ', 1)
  95. if status == 'ng':
  96. if ' ' in ref:
  97. ref, status = ref.split(' ', 1)
  98. else:
  99. ok.add(ref)
  100. ref_status[ref] = status
  101. raise UpdateRefsError('%s failed to update' %
  102. ', '.join([ref for ref in ref_status
  103. if ref not in ok]),
  104. ref_status=ref_status)
  105. def handle_packet(self, pkt):
  106. """Handle a packet.
  107. :raise GitProtocolError: Raised when packets are received after a
  108. flush packet.
  109. """
  110. if self._done:
  111. raise GitProtocolError("received more data after status report")
  112. if pkt is None:
  113. self._done = True
  114. return
  115. if self._pack_status is None:
  116. self._pack_status = pkt.strip()
  117. else:
  118. ref_status = pkt.strip()
  119. self._ref_statuses.append(ref_status)
  120. if not ref_status.startswith('ok '):
  121. self._ref_status_ok = False
  122. def read_pkt_refs(proto):
  123. server_capabilities = None
  124. refs = {}
  125. # Receive refs from server
  126. for pkt in proto.read_pkt_seq():
  127. (sha, ref) = pkt.rstrip('\n').split(None, 1)
  128. if sha == 'ERR':
  129. raise GitProtocolError(ref)
  130. if server_capabilities is None:
  131. (ref, server_capabilities) = extract_capabilities(ref)
  132. refs[ref] = sha
  133. if len(refs) == 0:
  134. return None, set([])
  135. return refs, set(server_capabilities)
  136. # TODO(durin42): this doesn't correctly degrade if the server doesn't
  137. # support some capabilities. This should work properly with servers
  138. # that don't support multi_ack.
  139. class GitClient(object):
  140. """Git smart server client.
  141. """
  142. def __init__(self, thin_packs=True, report_activity=None):
  143. """Create a new GitClient instance.
  144. :param thin_packs: Whether or not thin packs should be retrieved
  145. :param report_activity: Optional callback for reporting transport
  146. activity.
  147. """
  148. self._report_activity = report_activity
  149. self._report_status_parser = None
  150. self._fetch_capabilities = set(FETCH_CAPABILITIES)
  151. self._send_capabilities = set(SEND_CAPABILITIES)
  152. if not thin_packs:
  153. self._fetch_capabilities.remove('thin-pack')
  154. def send_pack(self, path, determine_wants, generate_pack_contents,
  155. progress=None):
  156. """Upload a pack to a remote repository.
  157. :param path: Repository path
  158. :param generate_pack_contents: Function that can return a sequence of the
  159. shas of the objects to upload.
  160. :param progress: Optional progress function
  161. :raises SendPackError: if server rejects the pack data
  162. :raises UpdateRefsError: if the server supports report-status
  163. and rejects ref updates
  164. """
  165. raise NotImplementedError(self.send_pack)
  166. def fetch(self, path, target, determine_wants=None, progress=None):
  167. """Fetch into a target repository.
  168. :param path: Path to fetch from
  169. :param target: Target repository to fetch into
  170. :param determine_wants: Optional function to determine what refs
  171. to fetch
  172. :param progress: Optional progress function
  173. :return: remote refs as dictionary
  174. """
  175. if determine_wants is None:
  176. determine_wants = target.object_store.determine_wants_all
  177. f, commit, abort = target.object_store.add_pack()
  178. try:
  179. result = self.fetch_pack(path, determine_wants,
  180. target.get_graph_walker(), f.write, progress)
  181. except:
  182. abort()
  183. raise
  184. else:
  185. commit()
  186. return result
  187. def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
  188. progress=None):
  189. """Retrieve a pack from a git smart server.
  190. :param determine_wants: Callback that returns list of commits to fetch
  191. :param graph_walker: Object with next() and ack().
  192. :param pack_data: Callback called for each bit of data in the pack
  193. :param progress: Callback for progress reports (strings)
  194. """
  195. raise NotImplementedError(self.fetch_pack)
  196. def _parse_status_report(self, proto):
  197. unpack = proto.read_pkt_line().strip()
  198. if unpack != 'unpack ok':
  199. st = True
  200. # flush remaining error data
  201. while st is not None:
  202. st = proto.read_pkt_line()
  203. raise SendPackError(unpack)
  204. statuses = []
  205. errs = False
  206. ref_status = proto.read_pkt_line()
  207. while ref_status:
  208. ref_status = ref_status.strip()
  209. statuses.append(ref_status)
  210. if not ref_status.startswith('ok '):
  211. errs = True
  212. ref_status = proto.read_pkt_line()
  213. if errs:
  214. ref_status = {}
  215. ok = set()
  216. for status in statuses:
  217. if ' ' not in status:
  218. # malformed response, move on to the next one
  219. continue
  220. status, ref = status.split(' ', 1)
  221. if status == 'ng':
  222. if ' ' in ref:
  223. ref, status = ref.split(' ', 1)
  224. else:
  225. ok.add(ref)
  226. ref_status[ref] = status
  227. raise UpdateRefsError('%s failed to update' %
  228. ', '.join([ref for ref in ref_status
  229. if ref not in ok]),
  230. ref_status=ref_status)
  231. def _read_side_band64k_data(self, proto, channel_callbacks):
  232. """Read per-channel data.
  233. This requires the side-band-64k capability.
  234. :param proto: Protocol object to read from
  235. :param channel_callbacks: Dictionary mapping channels to packet
  236. handlers to use. None for a callback discards channel data.
  237. """
  238. for pkt in proto.read_pkt_seq():
  239. channel = ord(pkt[0])
  240. pkt = pkt[1:]
  241. try:
  242. cb = channel_callbacks[channel]
  243. except KeyError:
  244. raise AssertionError('Invalid sideband channel %d' % channel)
  245. else:
  246. if cb is not None:
  247. cb(pkt)
  248. def _handle_receive_pack_head(self, proto, capabilities, old_refs, new_refs):
  249. """Handle the head of a 'git-receive-pack' request.
  250. :param proto: Protocol object to read from
  251. :param capabilities: List of negotiated capabilities
  252. :param old_refs: Old refs, as received from the server
  253. :param new_refs: New refs
  254. :return: (have, want) tuple
  255. """
  256. want = []
  257. have = [x for x in old_refs.values() if not x == ZERO_SHA]
  258. sent_capabilities = False
  259. for refname in set(new_refs.keys() + old_refs.keys()):
  260. old_sha1 = old_refs.get(refname, ZERO_SHA)
  261. new_sha1 = new_refs.get(refname, ZERO_SHA)
  262. if old_sha1 != new_sha1:
  263. if sent_capabilities:
  264. proto.write_pkt_line('%s %s %s' % (old_sha1, new_sha1,
  265. refname))
  266. else:
  267. proto.write_pkt_line(
  268. '%s %s %s\0%s' % (old_sha1, new_sha1, refname,
  269. ' '.join(capabilities)))
  270. sent_capabilities = True
  271. if new_sha1 not in have and new_sha1 != ZERO_SHA:
  272. want.append(new_sha1)
  273. proto.write_pkt_line(None)
  274. return (have, want)
  275. def _handle_receive_pack_tail(self, proto, capabilities, progress=None):
  276. """Handle the tail of a 'git-receive-pack' request.
  277. :param proto: Protocol object to read from
  278. :param capabilities: List of negotiated capabilities
  279. :param progress: Optional progress reporting function
  280. """
  281. if "side-band-64k" in capabilities:
  282. if progress is None:
  283. progress = lambda x: None
  284. channel_callbacks = { 2: progress }
  285. if 'report-status' in capabilities:
  286. channel_callbacks[1] = PktLineParser(
  287. self._report_status_parser.handle_packet).parse
  288. self._read_side_band64k_data(proto, channel_callbacks)
  289. else:
  290. if 'report-status' in capabilities:
  291. for pkt in proto.read_pkt_seq():
  292. self._report_status_parser.handle_packet(pkt)
  293. if self._report_status_parser is not None:
  294. self._report_status_parser.check()
  295. # wait for EOF before returning
  296. data = proto.read()
  297. if data:
  298. raise SendPackError('Unexpected response %r' % data)
  299. def _handle_upload_pack_head(self, proto, capabilities, graph_walker,
  300. wants, can_read):
  301. """Handle the head of a 'git-upload-pack' request.
  302. :param proto: Protocol object to read from
  303. :param capabilities: List of negotiated capabilities
  304. :param graph_walker: GraphWalker instance to call .ack() on
  305. :param wants: List of commits to fetch
  306. :param can_read: function that returns a boolean that indicates
  307. whether there is extra graph data to read on proto
  308. """
  309. assert isinstance(wants, list) and type(wants[0]) == str
  310. proto.write_pkt_line('want %s %s\n' % (
  311. wants[0], ' '.join(capabilities)))
  312. for want in wants[1:]:
  313. proto.write_pkt_line('want %s\n' % want)
  314. proto.write_pkt_line(None)
  315. have = graph_walker.next()
  316. while have:
  317. proto.write_pkt_line('have %s\n' % have)
  318. if can_read():
  319. pkt = proto.read_pkt_line()
  320. parts = pkt.rstrip('\n').split(' ')
  321. if parts[0] == 'ACK':
  322. graph_walker.ack(parts[1])
  323. if parts[2] in ('continue', 'common'):
  324. pass
  325. elif parts[2] == 'ready':
  326. break
  327. else:
  328. raise AssertionError(
  329. "%s not in ('continue', 'ready', 'common)" %
  330. parts[2])
  331. have = graph_walker.next()
  332. proto.write_pkt_line('done\n')
  333. def _handle_upload_pack_tail(self, proto, capabilities, graph_walker,
  334. pack_data, progress=None, rbufsize=_RBUFSIZE):
  335. """Handle the tail of a 'git-upload-pack' request.
  336. :param proto: Protocol object to read from
  337. :param capabilities: List of negotiated capabilities
  338. :param graph_walker: GraphWalker instance to call .ack() on
  339. :param pack_data: Function to call with pack data
  340. :param progress: Optional progress reporting function
  341. :param rbufsize: Read buffer size
  342. """
  343. pkt = proto.read_pkt_line()
  344. while pkt:
  345. parts = pkt.rstrip('\n').split(' ')
  346. if parts[0] == 'ACK':
  347. graph_walker.ack(pkt.split(' ')[1])
  348. if len(parts) < 3 or parts[2] not in (
  349. 'ready', 'continue', 'common'):
  350. break
  351. pkt = proto.read_pkt_line()
  352. if "side-band-64k" in capabilities:
  353. if progress is None:
  354. # Just ignore progress data
  355. progress = lambda x: None
  356. self._read_side_band64k_data(proto, {1: pack_data, 2: progress})
  357. # wait for EOF before returning
  358. data = proto.read()
  359. if data:
  360. raise Exception('Unexpected response %r' % data)
  361. else:
  362. while True:
  363. data = proto.read(rbufsize)
  364. if data == "":
  365. break
  366. pack_data(data)
  367. class TraditionalGitClient(GitClient):
  368. """Traditional Git client."""
  369. def _connect(self, cmd, path):
  370. """Create a connection to the server.
  371. This method is abstract - concrete implementations should
  372. implement their own variant which connects to the server and
  373. returns an initialized Protocol object with the service ready
  374. for use and a can_read function which may be used to see if
  375. reads would block.
  376. :param cmd: The git service name to which we should connect.
  377. :param path: The path we should pass to the service.
  378. """
  379. raise NotImplementedError()
  380. def send_pack(self, path, determine_wants, generate_pack_contents,
  381. progress=None):
  382. """Upload a pack to a remote repository.
  383. :param path: Repository path
  384. :param generate_pack_contents: Function that can return a sequence of the
  385. shas of the objects to upload.
  386. :param progress: Optional callback called with progress updates
  387. :raises SendPackError: if server rejects the pack data
  388. :raises UpdateRefsError: if the server supports report-status
  389. and rejects ref updates
  390. """
  391. proto, unused_can_read = self._connect('receive-pack', path)
  392. old_refs, server_capabilities = read_pkt_refs(proto)
  393. negotiated_capabilities = self._send_capabilities & server_capabilities
  394. if 'report-status' in negotiated_capabilities:
  395. self._report_status_parser = ReportStatusParser()
  396. report_status_parser = self._report_status_parser
  397. try:
  398. new_refs = orig_new_refs = determine_wants(dict(old_refs))
  399. except:
  400. proto.write_pkt_line(None)
  401. raise
  402. if not 'delete-refs' in server_capabilities:
  403. # Server does not support deletions. Fail later.
  404. def remove_del(pair):
  405. if pair[1] == ZERO_SHA:
  406. if 'report-status' in negotiated_capabilities:
  407. report_status_parser._ref_statuses.append(
  408. 'ng %s remote does not support deleting refs'
  409. % pair[1])
  410. report_status_parser._ref_status_ok = False
  411. return False
  412. else:
  413. return True
  414. new_refs = dict(
  415. filter(
  416. remove_del,
  417. [(ref, sha) for ref, sha in new_refs.iteritems()]))
  418. if new_refs is None:
  419. proto.write_pkt_line(None)
  420. return old_refs
  421. if len(new_refs) == 0 and len(orig_new_refs):
  422. # NOOP - Original new refs filtered out by policy
  423. proto.write_pkt_line(None)
  424. if self._report_status_parser is not None:
  425. self._report_status_parser.check()
  426. return old_refs
  427. (have, want) = self._handle_receive_pack_head(proto,
  428. negotiated_capabilities, old_refs, new_refs)
  429. if not want and old_refs == new_refs:
  430. return new_refs
  431. objects = generate_pack_contents(have, want)
  432. if len(objects) > 0:
  433. entries, sha = write_pack_objects(proto.write_file(), objects)
  434. elif len(set(new_refs.values()) - set([ZERO_SHA])) > 0:
  435. # Check for valid create/update refs
  436. filtered_new_refs = \
  437. dict([(ref, sha) for ref, sha in new_refs.iteritems()
  438. if sha != ZERO_SHA])
  439. if len(set(filtered_new_refs.iteritems()) -
  440. set(old_refs.iteritems())) > 0:
  441. entries, sha = write_pack_objects(proto.write_file(), objects)
  442. self._handle_receive_pack_tail(proto, negotiated_capabilities,
  443. progress)
  444. return new_refs
  445. def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
  446. progress=None):
  447. """Retrieve a pack from a git smart server.
  448. :param determine_wants: Callback that returns list of commits to fetch
  449. :param graph_walker: Object with next() and ack().
  450. :param pack_data: Callback called for each bit of data in the pack
  451. :param progress: Callback for progress reports (strings)
  452. """
  453. proto, can_read = self._connect('upload-pack', path)
  454. refs, server_capabilities = read_pkt_refs(proto)
  455. negotiated_capabilities = self._fetch_capabilities & server_capabilities
  456. if refs is None:
  457. proto.write_pkt_line(None)
  458. return refs
  459. try:
  460. wants = determine_wants(refs)
  461. except:
  462. proto.write_pkt_line(None)
  463. raise
  464. if wants is not None:
  465. wants = [cid for cid in wants if cid != ZERO_SHA]
  466. if not wants:
  467. proto.write_pkt_line(None)
  468. return refs
  469. self._handle_upload_pack_head(proto, negotiated_capabilities,
  470. graph_walker, wants, can_read)
  471. self._handle_upload_pack_tail(proto, negotiated_capabilities,
  472. graph_walker, pack_data, progress)
  473. return refs
  474. def archive(self, path, committish, write_data, progress=None):
  475. proto, can_read = self._connect('upload-archive', path)
  476. proto.write_pkt_line("argument %s" % committish)
  477. proto.write_pkt_line(None)
  478. pkt = proto.read_pkt_line()
  479. if pkt == "NACK\n":
  480. return
  481. elif pkt == "ACK\n":
  482. pass
  483. elif pkt.startswith("ERR "):
  484. raise GitProtocolError(pkt[4:].rstrip("\n"))
  485. else:
  486. raise AssertionError("invalid response %r" % pkt)
  487. ret = proto.read_pkt_line()
  488. if ret is not None:
  489. raise AssertionError("expected pkt tail")
  490. self._read_side_band64k_data(proto, {1: write_data, 2: progress})
  491. class TCPGitClient(TraditionalGitClient):
  492. """A Git Client that works over TCP directly (i.e. git://)."""
  493. def __init__(self, host, port=None, *args, **kwargs):
  494. if port is None:
  495. port = TCP_GIT_PORT
  496. self._host = host
  497. self._port = port
  498. TraditionalGitClient.__init__(self, *args, **kwargs)
  499. def _connect(self, cmd, path):
  500. sockaddrs = socket.getaddrinfo(self._host, self._port,
  501. socket.AF_UNSPEC, socket.SOCK_STREAM)
  502. s = None
  503. err = socket.error("no address found for %s" % self._host)
  504. for (family, socktype, proto, canonname, sockaddr) in sockaddrs:
  505. s = socket.socket(family, socktype, proto)
  506. s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
  507. try:
  508. s.connect(sockaddr)
  509. break
  510. except socket.error as err:
  511. if s is not None:
  512. s.close()
  513. s = None
  514. if s is None:
  515. raise err
  516. # -1 means system default buffering
  517. rfile = s.makefile('rb', -1)
  518. # 0 means unbuffered
  519. wfile = s.makefile('wb', 0)
  520. proto = Protocol(rfile.read, wfile.write,
  521. report_activity=self._report_activity)
  522. if path.startswith("/~"):
  523. path = path[1:]
  524. proto.send_cmd('git-%s' % cmd, path, 'host=%s' % self._host)
  525. return proto, lambda: _fileno_can_read(s)
  526. class SubprocessWrapper(object):
  527. """A socket-like object that talks to a subprocess via pipes."""
  528. def __init__(self, proc):
  529. self.proc = proc
  530. self.read = proc.stdout.read
  531. self.write = proc.stdin.write
  532. def can_read(self):
  533. if subprocess.mswindows:
  534. from msvcrt import get_osfhandle
  535. from win32pipe import PeekNamedPipe
  536. handle = get_osfhandle(self.proc.stdout.fileno())
  537. return PeekNamedPipe(handle, 0)[2] != 0
  538. else:
  539. return _fileno_can_read(self.proc.stdout.fileno())
  540. def close(self):
  541. self.proc.stdin.close()
  542. self.proc.stdout.close()
  543. self.proc.wait()
  544. class SubprocessGitClient(TraditionalGitClient):
  545. """Git client that talks to a server using a subprocess."""
  546. def __init__(self, *args, **kwargs):
  547. self._connection = None
  548. self._stderr = None
  549. self._stderr = kwargs.get('stderr')
  550. if 'stderr' in kwargs:
  551. del kwargs['stderr']
  552. TraditionalGitClient.__init__(self, *args, **kwargs)
  553. def _connect(self, service, path):
  554. import subprocess
  555. argv = ['git', service, path]
  556. p = SubprocessWrapper(
  557. subprocess.Popen(argv, bufsize=0, stdin=subprocess.PIPE,
  558. stdout=subprocess.PIPE,
  559. stderr=self._stderr))
  560. return Protocol(p.read, p.write,
  561. report_activity=self._report_activity), p.can_read
  562. class LocalGitClient(GitClient):
  563. """Git Client that just uses a local Repo."""
  564. def __init__(self, thin_packs=True, report_activity=None):
  565. """Create a new LocalGitClient instance.
  566. :param path: Path to the local repository
  567. :param thin_packs: Whether or not thin packs should be retrieved
  568. :param report_activity: Optional callback for reporting transport
  569. activity.
  570. """
  571. self._report_activity = report_activity
  572. # Ignore the thin_packs argument
  573. def send_pack(self, path, determine_wants, generate_pack_contents,
  574. progress=None):
  575. """Upload a pack to a remote repository.
  576. :param path: Repository path
  577. :param generate_pack_contents: Function that can return a sequence of the
  578. shas of the objects to upload.
  579. :param progress: Optional progress function
  580. :raises SendPackError: if server rejects the pack data
  581. :raises UpdateRefsError: if the server supports report-status
  582. and rejects ref updates
  583. """
  584. raise NotImplementedError(self.send_pack)
  585. def fetch(self, path, target, determine_wants=None, progress=None):
  586. """Fetch into a target repository.
  587. :param path: Path to fetch from
  588. :param target: Target repository to fetch into
  589. :param determine_wants: Optional function to determine what refs
  590. to fetch
  591. :param progress: Optional progress function
  592. :return: remote refs as dictionary
  593. """
  594. from dulwich.repo import Repo
  595. r = Repo(path)
  596. return r.fetch(target, determine_wants=determine_wants, progress=progress)
  597. def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
  598. progress=None):
  599. """Retrieve a pack from a git smart server.
  600. :param determine_wants: Callback that returns list of commits to fetch
  601. :param graph_walker: Object with next() and ack().
  602. :param pack_data: Callback called for each bit of data in the pack
  603. :param progress: Callback for progress reports (strings)
  604. """
  605. from dulwich.repo import Repo
  606. r = Repo(path)
  607. objects_iter = r.fetch_objects(determine_wants, graph_walker, progress)
  608. # Did the process short-circuit (e.g. in a stateless RPC call)? Note
  609. # that the client still expects a 0-object pack in most cases.
  610. if objects_iter is None:
  611. return
  612. write_pack_objects(ProtocolFile(None, pack_data), objects_iter)
  613. # What Git client to use for local access
  614. default_local_git_client_cls = SubprocessGitClient
  615. class SSHVendor(object):
  616. """A client side SSH implementation."""
  617. def connect_ssh(self, host, command, username=None, port=None):
  618. import warnings
  619. warnings.warn(
  620. "SSHVendor.connect_ssh has been renamed to SSHVendor.run_command",
  621. DeprecationWarning)
  622. return self.run_command(host, command, username=username, port=port)
  623. def run_command(self, host, command, username=None, port=None):
  624. """Connect to an SSH server.
  625. Run a command remotely and return a file-like object for interaction
  626. with the remote command.
  627. :param host: Host name
  628. :param command: Command to run
  629. :param username: Optional ame of user to log in as
  630. :param port: Optional SSH port to use
  631. """
  632. raise NotImplementedError(self.run_command)
  633. class SubprocessSSHVendor(SSHVendor):
  634. """SSH vendor that shells out to the local 'ssh' command."""
  635. def run_command(self, host, command, username=None, port=None):
  636. import subprocess
  637. #FIXME: This has no way to deal with passwords..
  638. args = ['ssh', '-x']
  639. if port is not None:
  640. args.extend(['-p', str(port)])
  641. if username is not None:
  642. host = '%s@%s' % (username, host)
  643. args.append(host)
  644. proc = subprocess.Popen(args + command,
  645. stdin=subprocess.PIPE,
  646. stdout=subprocess.PIPE)
  647. return SubprocessWrapper(proc)
  648. try:
  649. import paramiko
  650. except ImportError:
  651. pass
  652. else:
  653. import threading
  654. class ParamikoWrapper(object):
  655. STDERR_READ_N = 2048 # 2k
  656. def __init__(self, client, channel, progress_stderr=None):
  657. self.client = client
  658. self.channel = channel
  659. self.progress_stderr = progress_stderr
  660. self.should_monitor = bool(progress_stderr) or True
  661. self.monitor_thread = None
  662. self.stderr = ''
  663. # Channel must block
  664. self.channel.setblocking(True)
  665. # Start
  666. if self.should_monitor:
  667. self.monitor_thread = threading.Thread(target=self.monitor_stderr)
  668. self.monitor_thread.start()
  669. def monitor_stderr(self):
  670. while self.should_monitor:
  671. # Block and read
  672. data = self.read_stderr(self.STDERR_READ_N)
  673. # Socket closed
  674. if not data:
  675. self.should_monitor = False
  676. break
  677. # Emit data
  678. if self.progress_stderr:
  679. self.progress_stderr(data)
  680. # Append to buffer
  681. self.stderr += data
  682. def stop_monitoring(self):
  683. # Stop StdErr thread
  684. if self.should_monitor:
  685. self.should_monitor = False
  686. self.monitor_thread.join()
  687. # Get left over data
  688. data = self.channel.in_stderr_buffer.empty()
  689. self.stderr += data
  690. def can_read(self):
  691. return self.channel.recv_ready()
  692. def write(self, data):
  693. return self.channel.sendall(data)
  694. def read_stderr(self, n):
  695. return self.channel.recv_stderr(n)
  696. def read(self, n=None):
  697. data = self.channel.recv(n)
  698. data_len = len(data)
  699. # Closed socket
  700. if not data:
  701. return
  702. # Read more if needed
  703. if n and data_len < n:
  704. diff_len = n - data_len
  705. return data + self.read(diff_len)
  706. return data
  707. def close(self):
  708. self.channel.close()
  709. self.stop_monitoring()
  710. def __del__(self):
  711. self.close()
  712. class ParamikoSSHVendor(object):
  713. def __init__(self):
  714. self.ssh_kwargs = {}
  715. def run_command(self, host, command, username=None, port=None,
  716. progress_stderr=None):
  717. # Paramiko needs an explicit port. None is not valid
  718. if port is None:
  719. port = 22
  720. client = paramiko.SSHClient()
  721. policy = paramiko.client.MissingHostKeyPolicy()
  722. client.set_missing_host_key_policy(policy)
  723. client.connect(host, username=username, port=port,
  724. **self.ssh_kwargs)
  725. # Open SSH session
  726. channel = client.get_transport().open_session()
  727. # Run commands
  728. apply(channel.exec_command, command)
  729. return ParamikoWrapper(client, channel,
  730. progress_stderr=progress_stderr)
  731. # Can be overridden by users
  732. get_ssh_vendor = SubprocessSSHVendor
  733. class SSHGitClient(TraditionalGitClient):
  734. def __init__(self, host, port=None, username=None, *args, **kwargs):
  735. self.host = host
  736. self.port = port
  737. self.username = username
  738. TraditionalGitClient.__init__(self, *args, **kwargs)
  739. self.alternative_paths = {}
  740. def _get_cmd_path(self, cmd):
  741. return self.alternative_paths.get(cmd, 'git-%s' % cmd)
  742. def _connect(self, cmd, path):
  743. if path.startswith("/~"):
  744. path = path[1:]
  745. con = get_ssh_vendor().run_command(
  746. self.host, ["%s '%s'" % (self._get_cmd_path(cmd), path)],
  747. port=self.port, username=self.username)
  748. return (Protocol(con.read, con.write, report_activity=self._report_activity),
  749. con.can_read)
  750. class HttpGitClient(GitClient):
  751. def __init__(self, base_url, dumb=None, *args, **kwargs):
  752. self.base_url = base_url.rstrip("/") + "/"
  753. self.dumb = dumb
  754. GitClient.__init__(self, *args, **kwargs)
  755. def _get_url(self, path):
  756. return urlparse.urljoin(self.base_url, path).rstrip("/") + "/"
  757. def _http_request(self, url, headers={}, data=None):
  758. req = urllib2.Request(url, headers=headers, data=data)
  759. try:
  760. resp = self._perform(req)
  761. except urllib2.HTTPError as e:
  762. if e.code == 404:
  763. raise NotGitRepository()
  764. if e.code != 200:
  765. raise GitProtocolError("unexpected http response %d" % e.code)
  766. return resp
  767. def _perform(self, req):
  768. """Perform a HTTP request.
  769. This is provided so subclasses can provide their own version.
  770. :param req: urllib2.Request instance
  771. :return: matching response
  772. """
  773. return urllib2.urlopen(req)
  774. def _discover_references(self, service, url):
  775. assert url[-1] == "/"
  776. url = urlparse.urljoin(url, "info/refs")
  777. headers = {}
  778. if self.dumb != False:
  779. url += "?service=%s" % service
  780. headers["Content-Type"] = "application/x-%s-request" % service
  781. resp = self._http_request(url, headers)
  782. self.dumb = (not resp.info().gettype().startswith("application/x-git-"))
  783. if not self.dumb:
  784. proto = Protocol(resp.read, None)
  785. # The first line should mention the service
  786. pkts = list(proto.read_pkt_seq())
  787. if pkts != [('# service=%s\n' % service)]:
  788. raise GitProtocolError(
  789. "unexpected first line %r from smart server" % pkts)
  790. return read_pkt_refs(proto)
  791. else:
  792. return read_info_refs(resp), set()
  793. def _smart_request(self, service, url, data):
  794. assert url[-1] == "/"
  795. url = urlparse.urljoin(url, service)
  796. headers = {"Content-Type": "application/x-%s-request" % service}
  797. resp = self._http_request(url, headers, data)
  798. if resp.info().gettype() != ("application/x-%s-result" % service):
  799. raise GitProtocolError("Invalid content-type from server: %s"
  800. % resp.info().gettype())
  801. return resp
  802. def send_pack(self, path, determine_wants, generate_pack_contents,
  803. progress=None):
  804. """Upload a pack to a remote repository.
  805. :param path: Repository path
  806. :param generate_pack_contents: Function that can return a sequence of the
  807. shas of the objects to upload.
  808. :param progress: Optional progress function
  809. :raises SendPackError: if server rejects the pack data
  810. :raises UpdateRefsError: if the server supports report-status
  811. and rejects ref updates
  812. """
  813. url = self._get_url(path)
  814. old_refs, server_capabilities = self._discover_references(
  815. "git-receive-pack", url)
  816. negotiated_capabilities = self._send_capabilities & server_capabilities
  817. if 'report-status' in negotiated_capabilities:
  818. self._report_status_parser = ReportStatusParser()
  819. new_refs = determine_wants(dict(old_refs))
  820. if new_refs is None:
  821. return old_refs
  822. if self.dumb:
  823. raise NotImplementedError(self.fetch_pack)
  824. req_data = StringIO()
  825. req_proto = Protocol(None, req_data.write)
  826. (have, want) = self._handle_receive_pack_head(
  827. req_proto, negotiated_capabilities, old_refs, new_refs)
  828. if not want and old_refs == new_refs:
  829. return new_refs
  830. objects = generate_pack_contents(have, want)
  831. if len(objects) > 0:
  832. entries, sha = write_pack_objects(req_proto.write_file(), objects)
  833. resp = self._smart_request("git-receive-pack", url,
  834. data=req_data.getvalue())
  835. resp_proto = Protocol(resp.read, None)
  836. self._handle_receive_pack_tail(resp_proto, negotiated_capabilities,
  837. progress)
  838. return new_refs
  839. def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
  840. progress=None):
  841. """Retrieve a pack from a git smart server.
  842. :param determine_wants: Callback that returns list of commits to fetch
  843. :param graph_walker: Object with next() and ack().
  844. :param pack_data: Callback called for each bit of data in the pack
  845. :param progress: Callback for progress reports (strings)
  846. :return: Dictionary with the refs of the remote repository
  847. """
  848. url = self._get_url(path)
  849. refs, server_capabilities = self._discover_references(
  850. "git-upload-pack", url)
  851. negotiated_capabilities = self._fetch_capabilities & server_capabilities
  852. wants = determine_wants(refs)
  853. if wants is not None:
  854. wants = [cid for cid in wants if cid != ZERO_SHA]
  855. if not wants:
  856. return refs
  857. if self.dumb:
  858. raise NotImplementedError(self.send_pack)
  859. req_data = StringIO()
  860. req_proto = Protocol(None, req_data.write)
  861. self._handle_upload_pack_head(req_proto,
  862. negotiated_capabilities, graph_walker, wants,
  863. lambda: False)
  864. resp = self._smart_request("git-upload-pack", url,
  865. data=req_data.getvalue())
  866. resp_proto = Protocol(resp.read, None)
  867. self._handle_upload_pack_tail(resp_proto, negotiated_capabilities,
  868. graph_walker, pack_data, progress)
  869. return refs
  870. def get_transport_and_path_from_url(url, **kwargs):
  871. """Obtain a git client from a URL.
  872. :param url: URL to open
  873. :param thin_packs: Whether or not thin packs should be retrieved
  874. :param report_activity: Optional callback for reporting transport
  875. activity.
  876. :return: Tuple with client instance and relative path.
  877. """
  878. parsed = urlparse.urlparse(url)
  879. if parsed.scheme == 'git':
  880. return (TCPGitClient(parsed.hostname, port=parsed.port, **kwargs),
  881. parsed.path)
  882. elif parsed.scheme == 'git+ssh':
  883. path = parsed.path
  884. if path.startswith('/'):
  885. path = parsed.path[1:]
  886. return SSHGitClient(parsed.hostname, port=parsed.port,
  887. username=parsed.username, **kwargs), path
  888. elif parsed.scheme in ('http', 'https'):
  889. return HttpGitClient(urlparse.urlunparse(parsed), **kwargs), parsed.path
  890. elif parsed.scheme == 'file':
  891. return default_local_git_client_cls(**kwargs), parsed.path
  892. raise ValueError("unknown scheme '%s'" % parsed.scheme)
  893. def get_transport_and_path(location, **kwargs):
  894. """Obtain a git client from a URL.
  895. :param location: URL or path
  896. :param thin_packs: Whether or not thin packs should be retrieved
  897. :param report_activity: Optional callback for reporting transport
  898. activity.
  899. :return: Tuple with client instance and relative path.
  900. """
  901. # First, try to parse it as a URL
  902. try:
  903. return get_transport_and_path_from_url(location, **kwargs)
  904. except ValueError:
  905. pass
  906. if ':' in location and not '@' in location:
  907. # SSH with no user@, zero or one leading slash.
  908. (hostname, path) = location.split(':')
  909. return SSHGitClient(hostname, **kwargs), path
  910. elif '@' in location and ':' in location:
  911. # SSH with user@host:foo.
  912. user_host, path = location.split(':')
  913. user, host = user_host.rsplit('@')
  914. return SSHGitClient(host, username=user, **kwargs), path
  915. # Otherwise, assume it's a local path.
  916. return default_local_git_client_cls(**kwargs), location