test_client.py 40 KB

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