test_client.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070
  1. # test_client.py -- Tests for the git protocol, client side
  2. # Copyright (C) 2009 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. from io import BytesIO
  21. import base64
  22. import sys
  23. import shutil
  24. import tempfile
  25. import mock
  26. try:
  27. from urllib import quote as urlquote
  28. except ImportError:
  29. from urllib.parse import quote as urlquote
  30. try:
  31. import urlparse
  32. except ImportError:
  33. import urllib.parse as urlparse
  34. import certifi
  35. import urllib3
  36. import dulwich
  37. from dulwich import (
  38. client,
  39. )
  40. from dulwich.client import (
  41. LocalGitClient,
  42. TraditionalGitClient,
  43. TCPGitClient,
  44. SSHGitClient,
  45. HttpGitClient,
  46. ReportStatusParser,
  47. SendPackError,
  48. StrangeHostname,
  49. SubprocessSSHVendor,
  50. PuttySSHVendor,
  51. UpdateRefsError,
  52. default_urllib3_manager,
  53. get_transport_and_path,
  54. get_transport_and_path_from_url,
  55. )
  56. from dulwich.config import (
  57. ConfigDict,
  58. )
  59. from dulwich.tests import (
  60. TestCase,
  61. )
  62. from dulwich.protocol import (
  63. TCP_GIT_PORT,
  64. Protocol,
  65. )
  66. from dulwich.pack import (
  67. pack_objects_to_data,
  68. write_pack_data,
  69. write_pack_objects,
  70. )
  71. from dulwich.objects import (
  72. Commit,
  73. Tree
  74. )
  75. from dulwich.repo import (
  76. MemoryRepo,
  77. Repo,
  78. )
  79. from dulwich.tests import skipIf
  80. from dulwich.tests.utils import (
  81. open_repo,
  82. tear_down_repo,
  83. )
  84. class DummyClient(TraditionalGitClient):
  85. def __init__(self, can_read, read, write):
  86. self.can_read = can_read
  87. self.read = read
  88. self.write = write
  89. TraditionalGitClient.__init__(self)
  90. def _connect(self, service, path):
  91. return Protocol(self.read, self.write), self.can_read
  92. # TODO(durin42): add unit-level tests of GitClient
  93. class GitClientTests(TestCase):
  94. def setUp(self):
  95. super(GitClientTests, self).setUp()
  96. self.rout = BytesIO()
  97. self.rin = BytesIO()
  98. self.client = DummyClient(lambda x: True, self.rin.read,
  99. self.rout.write)
  100. def test_caps(self):
  101. agent_cap = (
  102. 'agent=dulwich/%d.%d.%d' % dulwich.__version__).encode('ascii')
  103. self.assertEqual(set([b'multi_ack', b'side-band-64k', b'ofs-delta',
  104. b'thin-pack', b'multi_ack_detailed',
  105. agent_cap]),
  106. set(self.client._fetch_capabilities))
  107. self.assertEqual(set([b'ofs-delta', b'report-status', b'side-band-64k',
  108. agent_cap]),
  109. set(self.client._send_capabilities))
  110. def test_archive_ack(self):
  111. self.rin.write(
  112. b'0009NACK\n'
  113. b'0000')
  114. self.rin.seek(0)
  115. self.client.archive(b'bla', b'HEAD', None, None)
  116. self.assertEqual(self.rout.getvalue(), b'0011argument HEAD0000')
  117. def test_fetch_empty(self):
  118. self.rin.write(b'0000')
  119. self.rin.seek(0)
  120. def check_heads(heads):
  121. self.assertEqual(heads, {})
  122. return []
  123. ret = self.client.fetch_pack(b'/', check_heads, None, None)
  124. self.assertEqual({}, ret.refs)
  125. self.assertEqual({}, ret.symrefs)
  126. def test_fetch_pack_ignores_magic_ref(self):
  127. self.rin.write(
  128. b'00000000000000000000000000000000000000000000 capabilities^{}'
  129. b'\x00 multi_ack '
  130. b'thin-pack side-band side-band-64k ofs-delta shallow no-progress '
  131. b'include-tag\n'
  132. b'0000')
  133. self.rin.seek(0)
  134. def check_heads(heads):
  135. self.assertEqual({}, heads)
  136. return []
  137. ret = self.client.fetch_pack(b'bla', check_heads, None, None, None)
  138. self.assertEqual({}, ret.refs)
  139. self.assertEqual({}, ret.symrefs)
  140. self.assertEqual(self.rout.getvalue(), b'0000')
  141. def test_fetch_pack_none(self):
  142. self.rin.write(
  143. b'008855dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 HEAD\x00multi_ack '
  144. b'thin-pack side-band side-band-64k ofs-delta shallow no-progress '
  145. b'include-tag\n'
  146. b'0000')
  147. self.rin.seek(0)
  148. ret = self.client.fetch_pack(
  149. b'bla', lambda heads: [], None, None, None)
  150. self.assertEqual(
  151. {b'HEAD': b'55dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7'},
  152. ret.refs)
  153. self.assertEqual({}, ret.symrefs)
  154. self.assertEqual(self.rout.getvalue(), b'0000')
  155. def test_send_pack_no_sideband64k_with_update_ref_error(self):
  156. # No side-bank-64k reported by server shouldn't try to parse
  157. # side band data
  158. pkts = [b'55dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 capabilities^{}'
  159. b'\x00 report-status delete-refs ofs-delta\n',
  160. b'',
  161. b"unpack ok",
  162. b"ng refs/foo/bar pre-receive hook declined",
  163. b'']
  164. for pkt in pkts:
  165. if pkt == b'':
  166. self.rin.write(b"0000")
  167. else:
  168. self.rin.write(("%04x" % (len(pkt)+4)).encode('ascii') + pkt)
  169. self.rin.seek(0)
  170. tree = Tree()
  171. commit = Commit()
  172. commit.tree = tree
  173. commit.parents = []
  174. commit.author = commit.committer = b'test user'
  175. commit.commit_time = commit.author_time = 1174773719
  176. commit.commit_timezone = commit.author_timezone = 0
  177. commit.encoding = b'UTF-8'
  178. commit.message = b'test message'
  179. def determine_wants(refs):
  180. return {b'refs/foo/bar': commit.id, }
  181. def generate_pack_data(have, want, ofs_delta=False):
  182. return pack_objects_to_data([(commit, None), (tree, ''), ])
  183. self.assertRaises(UpdateRefsError,
  184. self.client.send_pack, "blah",
  185. determine_wants, generate_pack_data)
  186. def test_send_pack_none(self):
  187. self.rin.write(
  188. b'0078310ca9477129b8586fa2afc779c1f57cf64bba6c '
  189. b'refs/heads/master\x00 report-status delete-refs '
  190. b'side-band-64k quiet ofs-delta\n'
  191. b'0000')
  192. self.rin.seek(0)
  193. def determine_wants(refs):
  194. return {
  195. b'refs/heads/master':
  196. b'310ca9477129b8586fa2afc779c1f57cf64bba6c'
  197. }
  198. def generate_pack_data(have, want, ofs_delta=False):
  199. return 0, []
  200. self.client.send_pack(b'/', determine_wants, generate_pack_data)
  201. self.assertEqual(self.rout.getvalue(), b'0000')
  202. def test_send_pack_keep_and_delete(self):
  203. self.rin.write(
  204. b'0063310ca9477129b8586fa2afc779c1f57cf64bba6c '
  205. b'refs/heads/master\x00report-status delete-refs ofs-delta\n'
  206. b'003f310ca9477129b8586fa2afc779c1f57cf64bba6c refs/heads/keepme\n'
  207. b'0000000eunpack ok\n'
  208. b'0019ok refs/heads/master\n'
  209. b'0000')
  210. self.rin.seek(0)
  211. def determine_wants(refs):
  212. return {b'refs/heads/master': b'0' * 40}
  213. def generate_pack_data(have, want, ofs_delta=False):
  214. return 0, []
  215. self.client.send_pack(b'/', determine_wants, generate_pack_data)
  216. self.assertIn(
  217. self.rout.getvalue(),
  218. [b'007f310ca9477129b8586fa2afc779c1f57cf64bba6c '
  219. b'0000000000000000000000000000000000000000 '
  220. b'refs/heads/master\x00report-status ofs-delta0000',
  221. b'007f310ca9477129b8586fa2afc779c1f57cf64bba6c '
  222. b'0000000000000000000000000000000000000000 '
  223. b'refs/heads/master\x00ofs-delta report-status0000'])
  224. def test_send_pack_delete_only(self):
  225. self.rin.write(
  226. b'0063310ca9477129b8586fa2afc779c1f57cf64bba6c '
  227. b'refs/heads/master\x00report-status delete-refs ofs-delta\n'
  228. b'0000000eunpack ok\n'
  229. b'0019ok refs/heads/master\n'
  230. b'0000')
  231. self.rin.seek(0)
  232. def determine_wants(refs):
  233. return {b'refs/heads/master': b'0' * 40}
  234. def generate_pack_data(have, want, ofs_delta=False):
  235. return 0, []
  236. self.client.send_pack(b'/', determine_wants, generate_pack_data)
  237. self.assertIn(
  238. self.rout.getvalue(),
  239. [b'007f310ca9477129b8586fa2afc779c1f57cf64bba6c '
  240. b'0000000000000000000000000000000000000000 '
  241. b'refs/heads/master\x00report-status ofs-delta0000',
  242. b'007f310ca9477129b8586fa2afc779c1f57cf64bba6c '
  243. b'0000000000000000000000000000000000000000 '
  244. b'refs/heads/master\x00ofs-delta report-status0000'])
  245. def test_send_pack_new_ref_only(self):
  246. self.rin.write(
  247. b'0063310ca9477129b8586fa2afc779c1f57cf64bba6c '
  248. b'refs/heads/master\x00report-status delete-refs ofs-delta\n'
  249. b'0000000eunpack ok\n'
  250. b'0019ok refs/heads/blah12\n'
  251. b'0000')
  252. self.rin.seek(0)
  253. def determine_wants(refs):
  254. return {
  255. b'refs/heads/blah12':
  256. b'310ca9477129b8586fa2afc779c1f57cf64bba6c',
  257. b'refs/heads/master':
  258. b'310ca9477129b8586fa2afc779c1f57cf64bba6c'
  259. }
  260. def generate_pack_data(have, want, ofs_delta=False):
  261. return 0, []
  262. f = BytesIO()
  263. write_pack_objects(f, {})
  264. self.client.send_pack('/', determine_wants, generate_pack_data)
  265. self.assertIn(
  266. self.rout.getvalue(),
  267. [b'007f0000000000000000000000000000000000000000 '
  268. b'310ca9477129b8586fa2afc779c1f57cf64bba6c '
  269. b'refs/heads/blah12\x00report-status ofs-delta0000' +
  270. f.getvalue(),
  271. b'007f0000000000000000000000000000000000000000 '
  272. b'310ca9477129b8586fa2afc779c1f57cf64bba6c '
  273. b'refs/heads/blah12\x00ofs-delta report-status0000' +
  274. f.getvalue()])
  275. def test_send_pack_new_ref(self):
  276. self.rin.write(
  277. b'0064310ca9477129b8586fa2afc779c1f57cf64bba6c '
  278. b'refs/heads/master\x00 report-status delete-refs ofs-delta\n'
  279. b'0000000eunpack ok\n'
  280. b'0019ok refs/heads/blah12\n'
  281. b'0000')
  282. self.rin.seek(0)
  283. tree = Tree()
  284. commit = Commit()
  285. commit.tree = tree
  286. commit.parents = []
  287. commit.author = commit.committer = b'test user'
  288. commit.commit_time = commit.author_time = 1174773719
  289. commit.commit_timezone = commit.author_timezone = 0
  290. commit.encoding = b'UTF-8'
  291. commit.message = b'test message'
  292. def determine_wants(refs):
  293. return {
  294. b'refs/heads/blah12': commit.id,
  295. b'refs/heads/master':
  296. b'310ca9477129b8586fa2afc779c1f57cf64bba6c'
  297. }
  298. def generate_pack_data(have, want, ofs_delta=False):
  299. return pack_objects_to_data([(commit, None), (tree, b''), ])
  300. f = BytesIO()
  301. write_pack_data(f, *generate_pack_data(None, None))
  302. self.client.send_pack(b'/', determine_wants, generate_pack_data)
  303. self.assertIn(
  304. self.rout.getvalue(),
  305. [b'007f0000000000000000000000000000000000000000 ' + commit.id +
  306. b' refs/heads/blah12\x00report-status ofs-delta0000' +
  307. f.getvalue(),
  308. b'007f0000000000000000000000000000000000000000 ' + commit.id +
  309. b' refs/heads/blah12\x00ofs-delta report-status0000' +
  310. f.getvalue()])
  311. def test_send_pack_no_deleteref_delete_only(self):
  312. pkts = [b'310ca9477129b8586fa2afc779c1f57cf64bba6c refs/heads/master'
  313. b'\x00 report-status ofs-delta\n',
  314. b'',
  315. b'']
  316. for pkt in pkts:
  317. if pkt == b'':
  318. self.rin.write(b"0000")
  319. else:
  320. self.rin.write(("%04x" % (len(pkt)+4)).encode('ascii') + pkt)
  321. self.rin.seek(0)
  322. def determine_wants(refs):
  323. return {b'refs/heads/master': b'0' * 40}
  324. def generate_pack_data(have, want, ofs_delta=False):
  325. return 0, []
  326. self.assertRaises(UpdateRefsError,
  327. self.client.send_pack, b"/",
  328. determine_wants, generate_pack_data)
  329. self.assertEqual(self.rout.getvalue(), b'0000')
  330. class TestGetTransportAndPath(TestCase):
  331. def test_tcp(self):
  332. c, path = get_transport_and_path('git://foo.com/bar/baz')
  333. self.assertTrue(isinstance(c, TCPGitClient))
  334. self.assertEqual('foo.com', c._host)
  335. self.assertEqual(TCP_GIT_PORT, c._port)
  336. self.assertEqual('/bar/baz', path)
  337. def test_tcp_port(self):
  338. c, path = get_transport_and_path('git://foo.com:1234/bar/baz')
  339. self.assertTrue(isinstance(c, TCPGitClient))
  340. self.assertEqual('foo.com', c._host)
  341. self.assertEqual(1234, c._port)
  342. self.assertEqual('/bar/baz', path)
  343. def test_git_ssh_explicit(self):
  344. c, path = get_transport_and_path('git+ssh://foo.com/bar/baz')
  345. self.assertTrue(isinstance(c, SSHGitClient))
  346. self.assertEqual('foo.com', c.host)
  347. self.assertEqual(None, c.port)
  348. self.assertEqual(None, c.username)
  349. self.assertEqual('/bar/baz', path)
  350. def test_ssh_explicit(self):
  351. c, path = get_transport_and_path('ssh://foo.com/bar/baz')
  352. self.assertTrue(isinstance(c, SSHGitClient))
  353. self.assertEqual('foo.com', c.host)
  354. self.assertEqual(None, c.port)
  355. self.assertEqual(None, c.username)
  356. self.assertEqual('/bar/baz', path)
  357. def test_ssh_port_explicit(self):
  358. c, path = get_transport_and_path(
  359. 'git+ssh://foo.com:1234/bar/baz')
  360. self.assertTrue(isinstance(c, SSHGitClient))
  361. self.assertEqual('foo.com', c.host)
  362. self.assertEqual(1234, c.port)
  363. self.assertEqual('/bar/baz', path)
  364. def test_username_and_port_explicit_unknown_scheme(self):
  365. c, path = get_transport_and_path(
  366. 'unknown://git@server:7999/dply/stuff.git')
  367. self.assertTrue(isinstance(c, SSHGitClient))
  368. self.assertEqual('unknown', c.host)
  369. self.assertEqual('//git@server:7999/dply/stuff.git', path)
  370. def test_username_and_port_explicit(self):
  371. c, path = get_transport_and_path(
  372. 'ssh://git@server:7999/dply/stuff.git')
  373. self.assertTrue(isinstance(c, SSHGitClient))
  374. self.assertEqual('git', c.username)
  375. self.assertEqual('server', c.host)
  376. self.assertEqual(7999, c.port)
  377. self.assertEqual('/dply/stuff.git', path)
  378. def test_ssh_abspath_doubleslash(self):
  379. c, path = get_transport_and_path('git+ssh://foo.com//bar/baz')
  380. self.assertTrue(isinstance(c, SSHGitClient))
  381. self.assertEqual('foo.com', c.host)
  382. self.assertEqual(None, c.port)
  383. self.assertEqual(None, c.username)
  384. self.assertEqual('//bar/baz', path)
  385. def test_ssh_port(self):
  386. c, path = get_transport_and_path(
  387. 'git+ssh://foo.com:1234/bar/baz')
  388. self.assertTrue(isinstance(c, SSHGitClient))
  389. self.assertEqual('foo.com', c.host)
  390. self.assertEqual(1234, c.port)
  391. self.assertEqual('/bar/baz', path)
  392. def test_ssh_implicit(self):
  393. c, path = get_transport_and_path('foo:/bar/baz')
  394. self.assertTrue(isinstance(c, SSHGitClient))
  395. self.assertEqual('foo', c.host)
  396. self.assertEqual(None, c.port)
  397. self.assertEqual(None, c.username)
  398. self.assertEqual('/bar/baz', path)
  399. def test_ssh_host(self):
  400. c, path = get_transport_and_path('foo.com:/bar/baz')
  401. self.assertTrue(isinstance(c, SSHGitClient))
  402. self.assertEqual('foo.com', c.host)
  403. self.assertEqual(None, c.port)
  404. self.assertEqual(None, c.username)
  405. self.assertEqual('/bar/baz', path)
  406. def test_ssh_user_host(self):
  407. c, path = get_transport_and_path('user@foo.com:/bar/baz')
  408. self.assertTrue(isinstance(c, SSHGitClient))
  409. self.assertEqual('foo.com', c.host)
  410. self.assertEqual(None, c.port)
  411. self.assertEqual('user', c.username)
  412. self.assertEqual('/bar/baz', path)
  413. def test_ssh_relpath(self):
  414. c, path = get_transport_and_path('foo:bar/baz')
  415. self.assertTrue(isinstance(c, SSHGitClient))
  416. self.assertEqual('foo', c.host)
  417. self.assertEqual(None, c.port)
  418. self.assertEqual(None, c.username)
  419. self.assertEqual('bar/baz', path)
  420. def test_ssh_host_relpath(self):
  421. c, path = get_transport_and_path('foo.com:bar/baz')
  422. self.assertTrue(isinstance(c, SSHGitClient))
  423. self.assertEqual('foo.com', c.host)
  424. self.assertEqual(None, c.port)
  425. self.assertEqual(None, c.username)
  426. self.assertEqual('bar/baz', path)
  427. def test_ssh_user_host_relpath(self):
  428. c, path = get_transport_and_path('user@foo.com:bar/baz')
  429. self.assertTrue(isinstance(c, SSHGitClient))
  430. self.assertEqual('foo.com', c.host)
  431. self.assertEqual(None, c.port)
  432. self.assertEqual('user', c.username)
  433. self.assertEqual('bar/baz', path)
  434. def test_local(self):
  435. c, path = get_transport_and_path('foo.bar/baz')
  436. self.assertTrue(isinstance(c, LocalGitClient))
  437. self.assertEqual('foo.bar/baz', path)
  438. @skipIf(sys.platform != 'win32', 'Behaviour only happens on windows.')
  439. def test_local_abs_windows_path(self):
  440. c, path = get_transport_and_path('C:\\foo.bar\\baz')
  441. self.assertTrue(isinstance(c, LocalGitClient))
  442. self.assertEqual('C:\\foo.bar\\baz', path)
  443. def test_error(self):
  444. # Need to use a known urlparse.uses_netloc URL scheme to get the
  445. # expected parsing of the URL on Python versions less than 2.6.5
  446. c, path = get_transport_and_path('prospero://bar/baz')
  447. self.assertTrue(isinstance(c, SSHGitClient))
  448. def test_http(self):
  449. url = 'https://github.com/jelmer/dulwich'
  450. c, path = get_transport_and_path(url)
  451. self.assertTrue(isinstance(c, HttpGitClient))
  452. self.assertEqual('/jelmer/dulwich', path)
  453. def test_http_auth(self):
  454. url = 'https://user:passwd@github.com/jelmer/dulwich'
  455. c, path = get_transport_and_path(url)
  456. self.assertTrue(isinstance(c, HttpGitClient))
  457. self.assertEqual('/jelmer/dulwich', path)
  458. self.assertEqual('user', c._username)
  459. self.assertEqual('passwd', c._password)
  460. def test_http_no_auth(self):
  461. url = 'https://github.com/jelmer/dulwich'
  462. c, path = get_transport_and_path(url)
  463. self.assertTrue(isinstance(c, HttpGitClient))
  464. self.assertEqual('/jelmer/dulwich', path)
  465. self.assertIs(None, c._username)
  466. self.assertIs(None, c._password)
  467. class TestGetTransportAndPathFromUrl(TestCase):
  468. def test_tcp(self):
  469. c, path = get_transport_and_path_from_url('git://foo.com/bar/baz')
  470. self.assertTrue(isinstance(c, TCPGitClient))
  471. self.assertEqual('foo.com', c._host)
  472. self.assertEqual(TCP_GIT_PORT, c._port)
  473. self.assertEqual('/bar/baz', path)
  474. def test_tcp_port(self):
  475. c, path = get_transport_and_path_from_url('git://foo.com:1234/bar/baz')
  476. self.assertTrue(isinstance(c, TCPGitClient))
  477. self.assertEqual('foo.com', c._host)
  478. self.assertEqual(1234, c._port)
  479. self.assertEqual('/bar/baz', path)
  480. def test_ssh_explicit(self):
  481. c, path = get_transport_and_path_from_url('git+ssh://foo.com/bar/baz')
  482. self.assertTrue(isinstance(c, SSHGitClient))
  483. self.assertEqual('foo.com', c.host)
  484. self.assertEqual(None, c.port)
  485. self.assertEqual(None, c.username)
  486. self.assertEqual('/bar/baz', path)
  487. def test_ssh_port_explicit(self):
  488. c, path = get_transport_and_path_from_url(
  489. 'git+ssh://foo.com:1234/bar/baz')
  490. self.assertTrue(isinstance(c, SSHGitClient))
  491. self.assertEqual('foo.com', c.host)
  492. self.assertEqual(1234, c.port)
  493. self.assertEqual('/bar/baz', path)
  494. def test_ssh_homepath(self):
  495. c, path = get_transport_and_path_from_url(
  496. 'git+ssh://foo.com/~/bar/baz')
  497. self.assertTrue(isinstance(c, SSHGitClient))
  498. self.assertEqual('foo.com', c.host)
  499. self.assertEqual(None, c.port)
  500. self.assertEqual(None, c.username)
  501. self.assertEqual('/~/bar/baz', path)
  502. def test_ssh_port_homepath(self):
  503. c, path = get_transport_and_path_from_url(
  504. 'git+ssh://foo.com:1234/~/bar/baz')
  505. self.assertTrue(isinstance(c, SSHGitClient))
  506. self.assertEqual('foo.com', c.host)
  507. self.assertEqual(1234, c.port)
  508. self.assertEqual('/~/bar/baz', path)
  509. def test_ssh_host_relpath(self):
  510. self.assertRaises(
  511. ValueError, get_transport_and_path_from_url,
  512. 'foo.com:bar/baz')
  513. def test_ssh_user_host_relpath(self):
  514. self.assertRaises(
  515. ValueError, get_transport_and_path_from_url,
  516. 'user@foo.com:bar/baz')
  517. def test_local_path(self):
  518. self.assertRaises(
  519. ValueError, get_transport_and_path_from_url,
  520. 'foo.bar/baz')
  521. def test_error(self):
  522. # Need to use a known urlparse.uses_netloc URL scheme to get the
  523. # expected parsing of the URL on Python versions less than 2.6.5
  524. self.assertRaises(
  525. ValueError, get_transport_and_path_from_url,
  526. 'prospero://bar/baz')
  527. def test_http(self):
  528. url = 'https://github.com/jelmer/dulwich'
  529. c, path = get_transport_and_path_from_url(url)
  530. self.assertTrue(isinstance(c, HttpGitClient))
  531. self.assertEqual('/jelmer/dulwich', path)
  532. def test_file(self):
  533. c, path = get_transport_and_path_from_url('file:///home/jelmer/foo')
  534. self.assertTrue(isinstance(c, LocalGitClient))
  535. self.assertEqual('/home/jelmer/foo', path)
  536. class TestSSHVendor(object):
  537. def __init__(self):
  538. self.host = None
  539. self.command = ""
  540. self.username = None
  541. self.port = None
  542. self.password = None
  543. self.key_filename = None
  544. def run_command(self, host, command, username=None, port=None,
  545. password=None, key_filename=None):
  546. self.host = host
  547. self.command = command
  548. self.username = username
  549. self.port = port
  550. self.password = password
  551. self.key_filename = key_filename
  552. class Subprocess:
  553. pass
  554. setattr(Subprocess, 'read', lambda: None)
  555. setattr(Subprocess, 'write', lambda: None)
  556. setattr(Subprocess, 'close', lambda: None)
  557. setattr(Subprocess, 'can_read', lambda: None)
  558. return Subprocess()
  559. class SSHGitClientTests(TestCase):
  560. def setUp(self):
  561. super(SSHGitClientTests, self).setUp()
  562. self.server = TestSSHVendor()
  563. self.real_vendor = client.get_ssh_vendor
  564. client.get_ssh_vendor = lambda: self.server
  565. self.client = SSHGitClient('git.samba.org')
  566. def tearDown(self):
  567. super(SSHGitClientTests, self).tearDown()
  568. client.get_ssh_vendor = self.real_vendor
  569. def test_get_url(self):
  570. path = '/tmp/repo.git'
  571. c = SSHGitClient('git.samba.org')
  572. url = c.get_url(path)
  573. self.assertEqual('ssh://git.samba.org/tmp/repo.git', url)
  574. def test_get_url_with_username_and_port(self):
  575. path = '/tmp/repo.git'
  576. c = SSHGitClient('git.samba.org', port=2222, username='user')
  577. url = c.get_url(path)
  578. self.assertEqual('ssh://user@git.samba.org:2222/tmp/repo.git', url)
  579. def test_default_command(self):
  580. self.assertEqual(
  581. b'git-upload-pack',
  582. self.client._get_cmd_path(b'upload-pack'))
  583. def test_alternative_command_path(self):
  584. self.client.alternative_paths[b'upload-pack'] = (
  585. b'/usr/lib/git/git-upload-pack')
  586. self.assertEqual(
  587. b'/usr/lib/git/git-upload-pack',
  588. self.client._get_cmd_path(b'upload-pack'))
  589. def test_alternative_command_path_spaces(self):
  590. self.client.alternative_paths[b'upload-pack'] = (
  591. b'/usr/lib/git/git-upload-pack -ibla')
  592. self.assertEqual(b"/usr/lib/git/git-upload-pack -ibla",
  593. self.client._get_cmd_path(b'upload-pack'))
  594. def test_connect(self):
  595. server = self.server
  596. client = self.client
  597. client.username = b"username"
  598. client.port = 1337
  599. client._connect(b"command", b"/path/to/repo")
  600. self.assertEqual(b"username", server.username)
  601. self.assertEqual(1337, server.port)
  602. self.assertEqual("git-command '/path/to/repo'", server.command)
  603. client._connect(b"relative-command", b"/~/path/to/repo")
  604. self.assertEqual("git-relative-command '~/path/to/repo'",
  605. server.command)
  606. class ReportStatusParserTests(TestCase):
  607. def test_invalid_pack(self):
  608. parser = ReportStatusParser()
  609. parser.handle_packet(b"unpack error - foo bar")
  610. parser.handle_packet(b"ok refs/foo/bar")
  611. parser.handle_packet(None)
  612. self.assertRaises(SendPackError, parser.check)
  613. def test_update_refs_error(self):
  614. parser = ReportStatusParser()
  615. parser.handle_packet(b"unpack ok")
  616. parser.handle_packet(b"ng refs/foo/bar need to pull")
  617. parser.handle_packet(None)
  618. self.assertRaises(UpdateRefsError, parser.check)
  619. def test_ok(self):
  620. parser = ReportStatusParser()
  621. parser.handle_packet(b"unpack ok")
  622. parser.handle_packet(b"ok refs/foo/bar")
  623. parser.handle_packet(None)
  624. parser.check()
  625. class LocalGitClientTests(TestCase):
  626. def test_get_url(self):
  627. path = "/tmp/repo.git"
  628. c = LocalGitClient()
  629. url = c.get_url(path)
  630. self.assertEqual('file:///tmp/repo.git', url)
  631. def test_fetch_into_empty(self):
  632. c = LocalGitClient()
  633. t = MemoryRepo()
  634. s = open_repo('a.git')
  635. self.addCleanup(tear_down_repo, s)
  636. self.assertEqual(s.get_refs(), c.fetch(s.path, t).refs)
  637. def test_fetch_empty(self):
  638. c = LocalGitClient()
  639. s = open_repo('a.git')
  640. self.addCleanup(tear_down_repo, s)
  641. out = BytesIO()
  642. walker = {}
  643. ret = c.fetch_pack(
  644. s.path, lambda heads: [], graph_walker=walker, pack_data=out.write)
  645. self.assertEqual({
  646. b'HEAD': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  647. b'refs/heads/master': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  648. b'refs/tags/mytag': b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
  649. b'refs/tags/mytag-packed':
  650. b'b0931cadc54336e78a1d980420e3268903b57a50'
  651. }, ret.refs)
  652. self.assertEqual(
  653. {b'HEAD': b'refs/heads/master'},
  654. ret.symrefs)
  655. self.assertEqual(
  656. b"PACK\x00\x00\x00\x02\x00\x00\x00\x00\x02\x9d\x08"
  657. b"\x82;\xd8\xa8\xea\xb5\x10\xadj\xc7\\\x82<\xfd>\xd3\x1e",
  658. out.getvalue())
  659. def test_fetch_pack_none(self):
  660. c = LocalGitClient()
  661. s = open_repo('a.git')
  662. self.addCleanup(tear_down_repo, s)
  663. out = BytesIO()
  664. walker = MemoryRepo().get_graph_walker()
  665. ret = c.fetch_pack(
  666. s.path,
  667. lambda heads: [b"a90fa2d900a17e99b433217e988c4eb4a2e9a097"],
  668. graph_walker=walker, pack_data=out.write)
  669. self.assertEqual({b'HEAD': b'refs/heads/master'}, ret.symrefs)
  670. self.assertEqual({
  671. b'HEAD': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  672. b'refs/heads/master': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  673. b'refs/tags/mytag': b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
  674. b'refs/tags/mytag-packed':
  675. b'b0931cadc54336e78a1d980420e3268903b57a50'
  676. }, ret.refs)
  677. # Hardcoding is not ideal, but we'll fix that some other day..
  678. self.assertTrue(out.getvalue().startswith(
  679. b'PACK\x00\x00\x00\x02\x00\x00\x00\x07'))
  680. def test_send_pack_without_changes(self):
  681. local = open_repo('a.git')
  682. self.addCleanup(tear_down_repo, local)
  683. target = open_repo('a.git')
  684. self.addCleanup(tear_down_repo, target)
  685. self.send_and_verify(b"master", local, target)
  686. def test_send_pack_with_changes(self):
  687. local = open_repo('a.git')
  688. self.addCleanup(tear_down_repo, local)
  689. target_path = tempfile.mkdtemp()
  690. self.addCleanup(shutil.rmtree, target_path)
  691. with Repo.init_bare(target_path) as target:
  692. self.send_and_verify(b"master", local, target)
  693. def test_get_refs(self):
  694. local = open_repo('refs.git')
  695. self.addCleanup(tear_down_repo, local)
  696. client = LocalGitClient()
  697. refs = client.get_refs(local.path)
  698. self.assertDictEqual(local.refs.as_dict(), refs)
  699. def send_and_verify(self, branch, local, target):
  700. """Send branch from local to remote repository and verify it worked."""
  701. client = LocalGitClient()
  702. ref_name = b"refs/heads/" + branch
  703. new_refs = client.send_pack(target.path,
  704. lambda _: {ref_name: local.refs[ref_name]},
  705. local.object_store.generate_pack_data)
  706. self.assertEqual(local.refs[ref_name], new_refs[ref_name])
  707. obj_local = local.get_object(new_refs[ref_name])
  708. obj_target = target.get_object(new_refs[ref_name])
  709. self.assertEqual(obj_local, obj_target)
  710. class HttpGitClientTests(TestCase):
  711. @staticmethod
  712. def b64encode(s):
  713. """Python 2/3 compatible Base64 encoder. Returns string."""
  714. try:
  715. return base64.b64encode(s)
  716. except TypeError:
  717. return base64.b64encode(s.encode('latin1')).decode('ascii')
  718. def test_get_url(self):
  719. base_url = 'https://github.com/jelmer/dulwich'
  720. path = '/jelmer/dulwich'
  721. c = HttpGitClient(base_url)
  722. url = c.get_url(path)
  723. self.assertEqual('https://github.com/jelmer/dulwich', url)
  724. def test_get_url_bytes_path(self):
  725. base_url = 'https://github.com/jelmer/dulwich'
  726. path_bytes = b'/jelmer/dulwich'
  727. c = HttpGitClient(base_url)
  728. url = c.get_url(path_bytes)
  729. self.assertEqual('https://github.com/jelmer/dulwich', url)
  730. def test_get_url_with_username_and_passwd(self):
  731. base_url = 'https://github.com/jelmer/dulwich'
  732. path = '/jelmer/dulwich'
  733. c = HttpGitClient(base_url, username='USERNAME', password='PASSWD')
  734. url = c.get_url(path)
  735. self.assertEqual('https://github.com/jelmer/dulwich', url)
  736. def test_init_username_passwd_set(self):
  737. url = 'https://github.com/jelmer/dulwich'
  738. c = HttpGitClient(url, config=None, username='user', password='passwd')
  739. self.assertEqual('user', c._username)
  740. self.assertEqual('passwd', c._password)
  741. basic_auth = c.pool_manager.headers['authorization']
  742. auth_string = '%s:%s' % ('user', 'passwd')
  743. b64_credentials = self.b64encode(auth_string)
  744. expected_basic_auth = 'Basic %s' % b64_credentials
  745. self.assertEqual(basic_auth, expected_basic_auth)
  746. def test_init_no_username_passwd(self):
  747. url = 'https://github.com/jelmer/dulwich'
  748. c = HttpGitClient(url, config=None)
  749. self.assertIs(None, c._username)
  750. self.assertIs(None, c._password)
  751. self.assertNotIn('authorization', c.pool_manager.headers)
  752. def test_from_parsedurl_on_url_with_quoted_credentials(self):
  753. original_username = 'john|the|first'
  754. quoted_username = urlquote(original_username)
  755. original_password = 'Ya#1$2%3'
  756. quoted_password = urlquote(original_password)
  757. url = 'https://{username}:{password}@github.com/jelmer/dulwich'.format(
  758. username=quoted_username,
  759. password=quoted_password
  760. )
  761. c = HttpGitClient.from_parsedurl(urlparse.urlparse(url))
  762. self.assertEqual(original_username, c._username)
  763. self.assertEqual(original_password, c._password)
  764. basic_auth = c.pool_manager.headers['authorization']
  765. auth_string = '%s:%s' % (original_username, original_password)
  766. b64_credentials = self.b64encode(auth_string)
  767. expected_basic_auth = 'Basic %s' % str(b64_credentials)
  768. self.assertEqual(basic_auth, expected_basic_auth)
  769. class TCPGitClientTests(TestCase):
  770. def test_get_url(self):
  771. host = 'github.com'
  772. path = '/jelmer/dulwich'
  773. c = TCPGitClient(host)
  774. url = c.get_url(path)
  775. self.assertEqual('git://github.com/jelmer/dulwich', url)
  776. def test_get_url_with_port(self):
  777. host = 'github.com'
  778. path = '/jelmer/dulwich'
  779. port = 9090
  780. c = TCPGitClient(host, port=port)
  781. url = c.get_url(path)
  782. self.assertEqual('git://github.com:9090/jelmer/dulwich', url)
  783. class DefaultUrllib3ManagerTest(TestCase):
  784. def assert_verify_ssl(self, manager, assertion=True):
  785. pool_keywords = tuple(manager.connection_pool_kw.items())
  786. assert_method = self.assertIn if assertion else self.assertNotIn
  787. assert_method(('cert_reqs', 'CERT_REQUIRED'), pool_keywords)
  788. assert_method(('ca_certs', certifi.where()), pool_keywords)
  789. def test_no_config(self):
  790. manager = default_urllib3_manager(config=None)
  791. self.assert_verify_ssl(manager)
  792. def test_config_no_proxy(self):
  793. manager = default_urllib3_manager(config=ConfigDict())
  794. self.assert_verify_ssl(manager)
  795. def test_config_proxy(self):
  796. config = ConfigDict()
  797. config.set(b'http', b'proxy', b'http://localhost:3128/')
  798. manager = default_urllib3_manager(config=config)
  799. self.assertIsInstance(manager, urllib3.ProxyManager)
  800. self.assertTrue(hasattr(manager, 'proxy'))
  801. self.assertEqual(manager.proxy.scheme, 'http')
  802. self.assertEqual(manager.proxy.host, 'localhost')
  803. self.assertEqual(manager.proxy.port, 3128)
  804. self.assert_verify_ssl(manager)
  805. def test_config_no_verify_ssl(self):
  806. manager = default_urllib3_manager(config=None, verify_ssl=False)
  807. self.assert_verify_ssl(manager, assertion=False)
  808. class SubprocessSSHVendorTests(TestCase):
  809. def test_run_command_dashes(self):
  810. vendor = SubprocessSSHVendor()
  811. self.assertRaises(StrangeHostname, vendor.run_command, '--weird-host',
  812. 'git-clone-url')
  813. def test_run_command_password(self):
  814. vendor = SubprocessSSHVendor()
  815. self.assertRaises(NotImplementedError, vendor.run_command, 'host',
  816. 'git-clone-url', password='12345')
  817. def test_run_command_password_and_privkey(self):
  818. vendor = SubprocessSSHVendor()
  819. self.assertRaises(NotImplementedError, vendor.run_command,
  820. 'host', 'git-clone-url',
  821. password='12345', key_filename='/tmp/id_rsa')
  822. @mock.patch('dulwich.client.subprocess.Popen')
  823. def test_run_command_with_port_username_and_privkey(self, mocked_popen):
  824. expected = ['ssh', '-x', '-p', '2200',
  825. '-i', '/tmp/id_rsa', 'user@host', 'git-clone-url']
  826. mocked_popen.return_value.stdout = BytesIO(b"stdout")
  827. mocked_popen.return_value.stderr = BytesIO(b"stderr")
  828. mocked_popen.return_value.wait.return_value = False
  829. mocked_popen.return_value.returncode = 0
  830. mocked_popen.return_value.communicate.return_value = ('Running', '')
  831. vendor = SubprocessSSHVendor()
  832. vendor.run_command('host', 'git-clone-url',
  833. username='user', port='2200',
  834. key_filename='/tmp/id_rsa')
  835. args, kwargs = mocked_popen.call_args
  836. self.assertListEqual(expected, args[0])
  837. class PuttySSHVendorTests(TestCase):
  838. def test_run_command_dashes(self):
  839. vendor = PuttySSHVendor()
  840. self.assertRaises(StrangeHostname, vendor.run_command, '--weird-host',
  841. 'git-clone-url')
  842. def test_run_command_password_and_privkey(self):
  843. vendor = PuttySSHVendor()
  844. self.assertRaises(NotImplementedError, vendor.run_command,
  845. 'host', 'git-clone-url',
  846. password='12345', key_filename='/tmp/id_rsa')
  847. @mock.patch('dulwich.client.subprocess.Popen')
  848. def test_run_command_password(self, mocked_popen):
  849. if sys.platform == 'win32':
  850. binary = ['putty.exe', '-ssh']
  851. else:
  852. binary = ['putty', '-ssh']
  853. expected = binary + ['-pw', '12345', 'host', 'git-clone-url']
  854. mocked_popen.return_value.stdout = BytesIO(b"stdout")
  855. mocked_popen.return_value.stderr = BytesIO(b"stderr")
  856. mocked_popen.return_value.wait.return_value = False
  857. mocked_popen.return_value.returncode = 0
  858. mocked_popen.return_value.communicate.return_value = ('Running', '')
  859. vendor = PuttySSHVendor()
  860. vendor.run_command('host', 'git-clone-url', password='12345')
  861. args, kwargs = mocked_popen.call_args
  862. self.assertListEqual(expected, args[0])
  863. @mock.patch('dulwich.client.subprocess.Popen')
  864. def test_run_command_with_port_username_and_privkey(self, mocked_popen):
  865. if sys.platform == 'win32':
  866. binary = ['putty.exe', '-ssh']
  867. else:
  868. binary = ['putty', '-ssh']
  869. expected = binary + [
  870. '-P', '2200', '-i', '/tmp/id_rsa',
  871. 'user@host', 'git-clone-url']
  872. mocked_popen.return_value.stdout = BytesIO(b"stdout")
  873. mocked_popen.return_value.stderr = BytesIO(b"stderr")
  874. mocked_popen.return_value.wait.return_value = False
  875. mocked_popen.return_value.returncode = 0
  876. mocked_popen.return_value.communicate.return_value = ('Running', '')
  877. vendor = PuttySSHVendor()
  878. vendor.run_command('host', 'git-clone-url',
  879. username='user', port='2200',
  880. key_filename='/tmp/id_rsa')
  881. args, kwargs = mocked_popen.call_args
  882. self.assertListEqual(expected, args[0])