test_client.py 34 KB

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