client.py 48 KB

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