test_client.py 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390
  1. # test_client.py -- Tests for the git protocol, client side
  2. # Copyright (C) 2009 Jelmer Vernooij <jelmer@jelmer.uk>
  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 os
  23. import sys
  24. import shutil
  25. import tempfile
  26. import warnings
  27. from urllib.parse import (
  28. quote as urlquote,
  29. urlparse,
  30. )
  31. import dulwich
  32. from dulwich import (
  33. client,
  34. )
  35. from dulwich.client import (
  36. InvalidWants,
  37. LocalGitClient,
  38. TraditionalGitClient,
  39. TCPGitClient,
  40. SSHGitClient,
  41. HttpGitClient,
  42. FetchPackResult,
  43. ReportStatusParser,
  44. SendPackError,
  45. StrangeHostname,
  46. SubprocessSSHVendor,
  47. PLinkSSHVendor,
  48. HangupException,
  49. GitProtocolError,
  50. check_wants,
  51. default_urllib3_manager,
  52. get_credentials_from_store,
  53. get_transport_and_path,
  54. get_transport_and_path_from_url,
  55. parse_rsync_url,
  56. _remote_error_from_stderr,
  57. )
  58. from dulwich.config import (
  59. ConfigDict,
  60. )
  61. from dulwich.tests import (
  62. TestCase,
  63. )
  64. from dulwich.protocol import (
  65. TCP_GIT_PORT,
  66. Protocol,
  67. )
  68. from dulwich.pack import (
  69. pack_objects_to_data,
  70. write_pack_data,
  71. write_pack_objects,
  72. )
  73. from dulwich.objects import (
  74. Commit,
  75. Tree
  76. )
  77. from dulwich.repo import (
  78. MemoryRepo,
  79. Repo,
  80. )
  81. from dulwich.tests import skipIf
  82. from dulwich.tests.utils import (
  83. open_repo,
  84. tear_down_repo,
  85. setup_warning_catcher,
  86. )
  87. class DummyClient(TraditionalGitClient):
  88. def __init__(self, can_read, read, write):
  89. self.can_read = can_read
  90. self.read = read
  91. self.write = write
  92. TraditionalGitClient.__init__(self)
  93. def _connect(self, service, path):
  94. return Protocol(self.read, self.write), self.can_read, None
  95. class DummyPopen():
  96. def __init__(self, *args, **kwards):
  97. self.stdin = BytesIO(b"stdin")
  98. self.stdout = BytesIO(b"stdout")
  99. self.stderr = BytesIO(b"stderr")
  100. self.returncode = 0
  101. self.args = args
  102. self.kwargs = kwards
  103. def communicate(self, *args, **kwards):
  104. return ('Running', '')
  105. def wait(self, *args, **kwards):
  106. return False
  107. # TODO(durin42): add unit-level tests of GitClient
  108. class GitClientTests(TestCase):
  109. def setUp(self):
  110. super(GitClientTests, self).setUp()
  111. self.rout = BytesIO()
  112. self.rin = BytesIO()
  113. self.client = DummyClient(lambda x: True, self.rin.read,
  114. self.rout.write)
  115. def test_caps(self):
  116. agent_cap = (
  117. 'agent=dulwich/%d.%d.%d' % dulwich.__version__).encode('ascii')
  118. self.assertEqual(set([b'multi_ack', b'side-band-64k', b'ofs-delta',
  119. b'thin-pack', b'multi_ack_detailed', b'shallow',
  120. agent_cap]),
  121. set(self.client._fetch_capabilities))
  122. self.assertEqual(
  123. set([b'delete-refs', b'ofs-delta', b'report-status',
  124. b'side-band-64k', agent_cap]),
  125. set(self.client._send_capabilities))
  126. def test_archive_ack(self):
  127. self.rin.write(
  128. b'0009NACK\n'
  129. b'0000')
  130. self.rin.seek(0)
  131. self.client.archive(b'bla', b'HEAD', None, None)
  132. self.assertEqual(self.rout.getvalue(), b'0011argument HEAD0000')
  133. def test_fetch_empty(self):
  134. self.rin.write(b'0000')
  135. self.rin.seek(0)
  136. def check_heads(heads):
  137. self.assertEqual(heads, {})
  138. return []
  139. ret = self.client.fetch_pack(b'/', check_heads, None, None)
  140. self.assertEqual({}, ret.refs)
  141. self.assertEqual({}, ret.symrefs)
  142. def test_fetch_pack_ignores_magic_ref(self):
  143. self.rin.write(
  144. b'00000000000000000000000000000000000000000000 capabilities^{}'
  145. b'\x00 multi_ack '
  146. b'thin-pack side-band side-band-64k ofs-delta shallow no-progress '
  147. b'include-tag\n'
  148. b'0000')
  149. self.rin.seek(0)
  150. def check_heads(heads):
  151. self.assertEqual({}, heads)
  152. return []
  153. ret = self.client.fetch_pack(b'bla', check_heads, None, None, None)
  154. self.assertEqual({}, ret.refs)
  155. self.assertEqual({}, ret.symrefs)
  156. self.assertEqual(self.rout.getvalue(), b'0000')
  157. def test_fetch_pack_none(self):
  158. self.rin.write(
  159. b'008855dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 HEAD\x00multi_ack '
  160. b'thin-pack side-band side-band-64k ofs-delta shallow no-progress '
  161. b'include-tag\n'
  162. b'0000')
  163. self.rin.seek(0)
  164. ret = self.client.fetch_pack(
  165. b'bla', lambda heads: [], None, None, None)
  166. self.assertEqual(
  167. {b'HEAD': b'55dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7'},
  168. ret.refs)
  169. self.assertEqual({}, ret.symrefs)
  170. self.assertEqual(self.rout.getvalue(), b'0000')
  171. def test_send_pack_no_sideband64k_with_update_ref_error(self):
  172. # No side-bank-64k reported by server shouldn't try to parse
  173. # side band data
  174. pkts = [b'55dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 capabilities^{}'
  175. b'\x00 report-status delete-refs ofs-delta\n',
  176. b'',
  177. b"unpack ok",
  178. b"ng refs/foo/bar pre-receive hook declined",
  179. b'']
  180. for pkt in pkts:
  181. if pkt == b'':
  182. self.rin.write(b"0000")
  183. else:
  184. self.rin.write(("%04x" % (len(pkt)+4)).encode('ascii') + pkt)
  185. self.rin.seek(0)
  186. tree = Tree()
  187. commit = Commit()
  188. commit.tree = tree
  189. commit.parents = []
  190. commit.author = commit.committer = b'test user'
  191. commit.commit_time = commit.author_time = 1174773719
  192. commit.commit_timezone = commit.author_timezone = 0
  193. commit.encoding = b'UTF-8'
  194. commit.message = b'test message'
  195. def update_refs(refs):
  196. return {b'refs/foo/bar': commit.id, }
  197. def generate_pack_data(have, want, ofs_delta=False):
  198. return pack_objects_to_data([(commit, None), (tree, ''), ])
  199. result = self.client.send_pack("blah", update_refs, generate_pack_data)
  200. self.assertEqual(
  201. {b'refs/foo/bar': 'pre-receive hook declined'},
  202. result.ref_status)
  203. self.assertEqual({b'refs/foo/bar': commit.id}, result.refs)
  204. def test_send_pack_none(self):
  205. # Set ref to current value
  206. self.rin.write(
  207. b'0078310ca9477129b8586fa2afc779c1f57cf64bba6c '
  208. b'refs/heads/master\x00 report-status delete-refs '
  209. b'side-band-64k quiet ofs-delta\n'
  210. b'0000')
  211. self.rin.seek(0)
  212. def update_refs(refs):
  213. return {
  214. b'refs/heads/master':
  215. b'310ca9477129b8586fa2afc779c1f57cf64bba6c'
  216. }
  217. def generate_pack_data(have, want, ofs_delta=False):
  218. return 0, []
  219. self.client.send_pack(b'/', update_refs, generate_pack_data)
  220. self.assertEqual(self.rout.getvalue(), b'0000')
  221. def test_send_pack_keep_and_delete(self):
  222. self.rin.write(
  223. b'0063310ca9477129b8586fa2afc779c1f57cf64bba6c '
  224. b'refs/heads/master\x00report-status delete-refs ofs-delta\n'
  225. b'003f310ca9477129b8586fa2afc779c1f57cf64bba6c refs/heads/keepme\n'
  226. b'0000000eunpack ok\n'
  227. b'0019ok refs/heads/master\n'
  228. b'0000')
  229. self.rin.seek(0)
  230. def update_refs(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'/', update_refs, generate_pack_data)
  235. self.assertEqual(
  236. self.rout.getvalue(),
  237. b'008b310ca9477129b8586fa2afc779c1f57cf64bba6c '
  238. b'0000000000000000000000000000000000000000 '
  239. b'refs/heads/master\x00delete-refs ofs-delta report-status0000')
  240. def test_send_pack_delete_only(self):
  241. self.rin.write(
  242. b'0063310ca9477129b8586fa2afc779c1f57cf64bba6c '
  243. b'refs/heads/master\x00report-status delete-refs ofs-delta\n'
  244. b'0000000eunpack ok\n'
  245. b'0019ok refs/heads/master\n'
  246. b'0000')
  247. self.rin.seek(0)
  248. def update_refs(refs):
  249. return {b'refs/heads/master': b'0' * 40}
  250. def generate_pack_data(have, want, ofs_delta=False):
  251. return 0, []
  252. self.client.send_pack(b'/', update_refs, generate_pack_data)
  253. self.assertEqual(
  254. self.rout.getvalue(),
  255. b'008b310ca9477129b8586fa2afc779c1f57cf64bba6c '
  256. b'0000000000000000000000000000000000000000 '
  257. b'refs/heads/master\x00delete-refs ofs-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 update_refs(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('/', update_refs, generate_pack_data)
  278. self.assertEqual(
  279. self.rout.getvalue(),
  280. b'008b0000000000000000000000000000000000000000 '
  281. b'310ca9477129b8586fa2afc779c1f57cf64bba6c '
  282. b'refs/heads/blah12\x00delete-refs ofs-delta report-status0000' +
  283. f.getvalue())
  284. def test_send_pack_new_ref(self):
  285. self.rin.write(
  286. b'0064310ca9477129b8586fa2afc779c1f57cf64bba6c '
  287. b'refs/heads/master\x00 report-status delete-refs ofs-delta\n'
  288. b'0000000eunpack ok\n'
  289. b'0019ok refs/heads/blah12\n'
  290. b'0000')
  291. self.rin.seek(0)
  292. tree = Tree()
  293. commit = Commit()
  294. commit.tree = tree
  295. commit.parents = []
  296. commit.author = commit.committer = b'test user'
  297. commit.commit_time = commit.author_time = 1174773719
  298. commit.commit_timezone = commit.author_timezone = 0
  299. commit.encoding = b'UTF-8'
  300. commit.message = b'test message'
  301. def update_refs(refs):
  302. return {
  303. b'refs/heads/blah12': commit.id,
  304. b'refs/heads/master':
  305. b'310ca9477129b8586fa2afc779c1f57cf64bba6c'
  306. }
  307. def generate_pack_data(have, want, ofs_delta=False):
  308. return pack_objects_to_data([(commit, None), (tree, b''), ])
  309. f = BytesIO()
  310. write_pack_data(f, *generate_pack_data(None, None))
  311. self.client.send_pack(b'/', update_refs, generate_pack_data)
  312. self.assertEqual(
  313. self.rout.getvalue(),
  314. b'008b0000000000000000000000000000000000000000 ' + commit.id +
  315. b' refs/heads/blah12\x00delete-refs ofs-delta report-status0000' +
  316. f.getvalue())
  317. def test_send_pack_no_deleteref_delete_only(self):
  318. pkts = [b'310ca9477129b8586fa2afc779c1f57cf64bba6c refs/heads/master'
  319. b'\x00 report-status ofs-delta\n',
  320. b'',
  321. b'']
  322. for pkt in pkts:
  323. if pkt == b'':
  324. self.rin.write(b"0000")
  325. else:
  326. self.rin.write(("%04x" % (len(pkt)+4)).encode('ascii') + pkt)
  327. self.rin.seek(0)
  328. def update_refs(refs):
  329. return {b'refs/heads/master': b'0' * 40}
  330. def generate_pack_data(have, want, ofs_delta=False):
  331. return 0, []
  332. result = self.client.send_pack(b"/", update_refs, generate_pack_data)
  333. self.assertEqual(
  334. result.ref_status,
  335. {b'refs/heads/master': 'remote does not support deleting refs'})
  336. self.assertEqual(
  337. result.refs,
  338. {b'refs/heads/master':
  339. b'310ca9477129b8586fa2afc779c1f57cf64bba6c'})
  340. self.assertEqual(self.rout.getvalue(), b'0000')
  341. class TestGetTransportAndPath(TestCase):
  342. def test_tcp(self):
  343. c, path = get_transport_and_path('git://foo.com/bar/baz')
  344. self.assertTrue(isinstance(c, TCPGitClient))
  345. self.assertEqual('foo.com', c._host)
  346. self.assertEqual(TCP_GIT_PORT, c._port)
  347. self.assertEqual('/bar/baz', path)
  348. def test_tcp_port(self):
  349. c, path = get_transport_and_path('git://foo.com:1234/bar/baz')
  350. self.assertTrue(isinstance(c, TCPGitClient))
  351. self.assertEqual('foo.com', c._host)
  352. self.assertEqual(1234, c._port)
  353. self.assertEqual('/bar/baz', path)
  354. def test_git_ssh_explicit(self):
  355. c, path = get_transport_and_path('git+ssh://foo.com/bar/baz')
  356. self.assertTrue(isinstance(c, SSHGitClient))
  357. self.assertEqual('foo.com', c.host)
  358. self.assertEqual(None, c.port)
  359. self.assertEqual(None, c.username)
  360. self.assertEqual('/bar/baz', path)
  361. def test_ssh_explicit(self):
  362. c, path = get_transport_and_path('ssh://foo.com/bar/baz')
  363. self.assertTrue(isinstance(c, SSHGitClient))
  364. self.assertEqual('foo.com', c.host)
  365. self.assertEqual(None, c.port)
  366. self.assertEqual(None, c.username)
  367. self.assertEqual('/bar/baz', path)
  368. def test_ssh_port_explicit(self):
  369. c, path = get_transport_and_path(
  370. 'git+ssh://foo.com:1234/bar/baz')
  371. self.assertTrue(isinstance(c, SSHGitClient))
  372. self.assertEqual('foo.com', c.host)
  373. self.assertEqual(1234, c.port)
  374. self.assertEqual('/bar/baz', path)
  375. def test_username_and_port_explicit_unknown_scheme(self):
  376. c, path = get_transport_and_path(
  377. 'unknown://git@server:7999/dply/stuff.git')
  378. self.assertTrue(isinstance(c, SSHGitClient))
  379. self.assertEqual('unknown', c.host)
  380. self.assertEqual('//git@server:7999/dply/stuff.git', path)
  381. def test_username_and_port_explicit(self):
  382. c, path = get_transport_and_path(
  383. 'ssh://git@server:7999/dply/stuff.git')
  384. self.assertTrue(isinstance(c, SSHGitClient))
  385. self.assertEqual('git', c.username)
  386. self.assertEqual('server', c.host)
  387. self.assertEqual(7999, c.port)
  388. self.assertEqual('/dply/stuff.git', path)
  389. def test_ssh_abspath_doubleslash(self):
  390. c, path = get_transport_and_path('git+ssh://foo.com//bar/baz')
  391. self.assertTrue(isinstance(c, SSHGitClient))
  392. self.assertEqual('foo.com', c.host)
  393. self.assertEqual(None, c.port)
  394. self.assertEqual(None, c.username)
  395. self.assertEqual('//bar/baz', path)
  396. def test_ssh_port(self):
  397. c, path = get_transport_and_path(
  398. 'git+ssh://foo.com:1234/bar/baz')
  399. self.assertTrue(isinstance(c, SSHGitClient))
  400. self.assertEqual('foo.com', c.host)
  401. self.assertEqual(1234, c.port)
  402. self.assertEqual('/bar/baz', path)
  403. def test_ssh_implicit(self):
  404. c, path = get_transport_and_path('foo:/bar/baz')
  405. self.assertTrue(isinstance(c, SSHGitClient))
  406. self.assertEqual('foo', c.host)
  407. self.assertEqual(None, c.port)
  408. self.assertEqual(None, c.username)
  409. self.assertEqual('/bar/baz', path)
  410. def test_ssh_host(self):
  411. c, path = get_transport_and_path('foo.com:/bar/baz')
  412. self.assertTrue(isinstance(c, SSHGitClient))
  413. self.assertEqual('foo.com', c.host)
  414. self.assertEqual(None, c.port)
  415. self.assertEqual(None, c.username)
  416. self.assertEqual('/bar/baz', path)
  417. def test_ssh_user_host(self):
  418. c, path = get_transport_and_path('user@foo.com:/bar/baz')
  419. self.assertTrue(isinstance(c, SSHGitClient))
  420. self.assertEqual('foo.com', c.host)
  421. self.assertEqual(None, c.port)
  422. self.assertEqual('user', c.username)
  423. self.assertEqual('/bar/baz', path)
  424. def test_ssh_relpath(self):
  425. c, path = get_transport_and_path('foo:bar/baz')
  426. self.assertTrue(isinstance(c, SSHGitClient))
  427. self.assertEqual('foo', c.host)
  428. self.assertEqual(None, c.port)
  429. self.assertEqual(None, c.username)
  430. self.assertEqual('bar/baz', path)
  431. def test_ssh_host_relpath(self):
  432. c, path = get_transport_and_path('foo.com:bar/baz')
  433. self.assertTrue(isinstance(c, SSHGitClient))
  434. self.assertEqual('foo.com', c.host)
  435. self.assertEqual(None, c.port)
  436. self.assertEqual(None, c.username)
  437. self.assertEqual('bar/baz', path)
  438. def test_ssh_user_host_relpath(self):
  439. c, path = get_transport_and_path('user@foo.com:bar/baz')
  440. self.assertTrue(isinstance(c, SSHGitClient))
  441. self.assertEqual('foo.com', c.host)
  442. self.assertEqual(None, c.port)
  443. self.assertEqual('user', c.username)
  444. self.assertEqual('bar/baz', path)
  445. def test_local(self):
  446. c, path = get_transport_and_path('foo.bar/baz')
  447. self.assertTrue(isinstance(c, LocalGitClient))
  448. self.assertEqual('foo.bar/baz', path)
  449. @skipIf(sys.platform != 'win32', 'Behaviour only happens on windows.')
  450. def test_local_abs_windows_path(self):
  451. c, path = get_transport_and_path('C:\\foo.bar\\baz')
  452. self.assertTrue(isinstance(c, LocalGitClient))
  453. self.assertEqual('C:\\foo.bar\\baz', path)
  454. def test_error(self):
  455. # Need to use a known urlparse.uses_netloc URL scheme to get the
  456. # expected parsing of the URL on Python versions less than 2.6.5
  457. c, path = get_transport_and_path('prospero://bar/baz')
  458. self.assertTrue(isinstance(c, SSHGitClient))
  459. def test_http(self):
  460. url = 'https://github.com/jelmer/dulwich'
  461. c, path = get_transport_and_path(url)
  462. self.assertTrue(isinstance(c, HttpGitClient))
  463. self.assertEqual('/jelmer/dulwich', path)
  464. def test_http_auth(self):
  465. url = 'https://user:passwd@github.com/jelmer/dulwich'
  466. c, path = get_transport_and_path(url)
  467. self.assertTrue(isinstance(c, HttpGitClient))
  468. self.assertEqual('/jelmer/dulwich', path)
  469. self.assertEqual('user', c._username)
  470. self.assertEqual('passwd', c._password)
  471. def test_http_auth_with_username(self):
  472. url = 'https://github.com/jelmer/dulwich'
  473. c, path = get_transport_and_path(
  474. url, username='user2', password='blah')
  475. self.assertTrue(isinstance(c, HttpGitClient))
  476. self.assertEqual('/jelmer/dulwich', path)
  477. self.assertEqual('user2', c._username)
  478. self.assertEqual('blah', c._password)
  479. def test_http_auth_with_username_and_in_url(self):
  480. url = 'https://user:passwd@github.com/jelmer/dulwich'
  481. c, path = get_transport_and_path(
  482. url, username='user2', password='blah')
  483. self.assertTrue(isinstance(c, HttpGitClient))
  484. self.assertEqual('/jelmer/dulwich', path)
  485. self.assertEqual('user', c._username)
  486. self.assertEqual('passwd', c._password)
  487. def test_http_no_auth(self):
  488. url = 'https://github.com/jelmer/dulwich'
  489. c, path = get_transport_and_path(url)
  490. self.assertTrue(isinstance(c, HttpGitClient))
  491. self.assertEqual('/jelmer/dulwich', path)
  492. self.assertIs(None, c._username)
  493. self.assertIs(None, c._password)
  494. class TestGetTransportAndPathFromUrl(TestCase):
  495. def test_tcp(self):
  496. c, path = get_transport_and_path_from_url('git://foo.com/bar/baz')
  497. self.assertTrue(isinstance(c, TCPGitClient))
  498. self.assertEqual('foo.com', c._host)
  499. self.assertEqual(TCP_GIT_PORT, c._port)
  500. self.assertEqual('/bar/baz', path)
  501. def test_tcp_port(self):
  502. c, path = get_transport_and_path_from_url('git://foo.com:1234/bar/baz')
  503. self.assertTrue(isinstance(c, TCPGitClient))
  504. self.assertEqual('foo.com', c._host)
  505. self.assertEqual(1234, c._port)
  506. self.assertEqual('/bar/baz', path)
  507. def test_ssh_explicit(self):
  508. c, path = get_transport_and_path_from_url('git+ssh://foo.com/bar/baz')
  509. self.assertTrue(isinstance(c, SSHGitClient))
  510. self.assertEqual('foo.com', c.host)
  511. self.assertEqual(None, c.port)
  512. self.assertEqual(None, c.username)
  513. self.assertEqual('/bar/baz', path)
  514. def test_ssh_port_explicit(self):
  515. c, path = get_transport_and_path_from_url(
  516. 'git+ssh://foo.com:1234/bar/baz')
  517. self.assertTrue(isinstance(c, SSHGitClient))
  518. self.assertEqual('foo.com', c.host)
  519. self.assertEqual(1234, c.port)
  520. self.assertEqual('/bar/baz', path)
  521. def test_ssh_homepath(self):
  522. c, path = get_transport_and_path_from_url(
  523. 'git+ssh://foo.com/~/bar/baz')
  524. self.assertTrue(isinstance(c, SSHGitClient))
  525. self.assertEqual('foo.com', c.host)
  526. self.assertEqual(None, c.port)
  527. self.assertEqual(None, c.username)
  528. self.assertEqual('/~/bar/baz', path)
  529. def test_ssh_port_homepath(self):
  530. c, path = get_transport_and_path_from_url(
  531. 'git+ssh://foo.com:1234/~/bar/baz')
  532. self.assertTrue(isinstance(c, SSHGitClient))
  533. self.assertEqual('foo.com', c.host)
  534. self.assertEqual(1234, c.port)
  535. self.assertEqual('/~/bar/baz', path)
  536. def test_ssh_host_relpath(self):
  537. self.assertRaises(
  538. ValueError, get_transport_and_path_from_url,
  539. 'foo.com:bar/baz')
  540. def test_ssh_user_host_relpath(self):
  541. self.assertRaises(
  542. ValueError, get_transport_and_path_from_url,
  543. 'user@foo.com:bar/baz')
  544. def test_local_path(self):
  545. self.assertRaises(
  546. ValueError, get_transport_and_path_from_url,
  547. 'foo.bar/baz')
  548. def test_error(self):
  549. # Need to use a known urlparse.uses_netloc URL scheme to get the
  550. # expected parsing of the URL on Python versions less than 2.6.5
  551. self.assertRaises(
  552. ValueError, get_transport_and_path_from_url,
  553. 'prospero://bar/baz')
  554. def test_http(self):
  555. url = 'https://github.com/jelmer/dulwich'
  556. c, path = get_transport_and_path_from_url(url)
  557. self.assertTrue(isinstance(c, HttpGitClient))
  558. self.assertEqual('https://github.com', c.get_url(b'/'))
  559. self.assertEqual('/jelmer/dulwich', path)
  560. def test_http_port(self):
  561. url = 'https://github.com:9090/jelmer/dulwich'
  562. c, path = get_transport_and_path_from_url(url)
  563. self.assertEqual('https://github.com:9090', c.get_url(b'/'))
  564. self.assertTrue(isinstance(c, HttpGitClient))
  565. self.assertEqual('/jelmer/dulwich', path)
  566. def test_file(self):
  567. c, path = get_transport_and_path_from_url('file:///home/jelmer/foo')
  568. self.assertTrue(isinstance(c, LocalGitClient))
  569. self.assertEqual('/home/jelmer/foo', path)
  570. class TestSSHVendor(object):
  571. def __init__(self):
  572. self.host = None
  573. self.command = ""
  574. self.username = None
  575. self.port = None
  576. self.password = None
  577. self.key_filename = None
  578. def run_command(self, host, command, username=None, port=None,
  579. password=None, key_filename=None):
  580. self.host = host
  581. self.command = command
  582. self.username = username
  583. self.port = port
  584. self.password = password
  585. self.key_filename = key_filename
  586. class Subprocess:
  587. pass
  588. setattr(Subprocess, 'read', lambda: None)
  589. setattr(Subprocess, 'write', lambda: None)
  590. setattr(Subprocess, 'close', lambda: None)
  591. setattr(Subprocess, 'can_read', lambda: None)
  592. return Subprocess()
  593. class SSHGitClientTests(TestCase):
  594. def setUp(self):
  595. super(SSHGitClientTests, self).setUp()
  596. self.server = TestSSHVendor()
  597. self.real_vendor = client.get_ssh_vendor
  598. client.get_ssh_vendor = lambda: self.server
  599. self.client = SSHGitClient('git.samba.org')
  600. def tearDown(self):
  601. super(SSHGitClientTests, self).tearDown()
  602. client.get_ssh_vendor = self.real_vendor
  603. def test_get_url(self):
  604. path = '/tmp/repo.git'
  605. c = SSHGitClient('git.samba.org')
  606. url = c.get_url(path)
  607. self.assertEqual('ssh://git.samba.org/tmp/repo.git', url)
  608. def test_get_url_with_username_and_port(self):
  609. path = '/tmp/repo.git'
  610. c = SSHGitClient('git.samba.org', port=2222, username='user')
  611. url = c.get_url(path)
  612. self.assertEqual('ssh://user@git.samba.org:2222/tmp/repo.git', url)
  613. def test_default_command(self):
  614. self.assertEqual(
  615. b'git-upload-pack',
  616. self.client._get_cmd_path(b'upload-pack'))
  617. def test_alternative_command_path(self):
  618. self.client.alternative_paths[b'upload-pack'] = (
  619. b'/usr/lib/git/git-upload-pack')
  620. self.assertEqual(
  621. b'/usr/lib/git/git-upload-pack',
  622. self.client._get_cmd_path(b'upload-pack'))
  623. def test_alternative_command_path_spaces(self):
  624. self.client.alternative_paths[b'upload-pack'] = (
  625. b'/usr/lib/git/git-upload-pack -ibla')
  626. self.assertEqual(b"/usr/lib/git/git-upload-pack -ibla",
  627. self.client._get_cmd_path(b'upload-pack'))
  628. def test_connect(self):
  629. server = self.server
  630. client = self.client
  631. client.username = b"username"
  632. client.port = 1337
  633. client._connect(b"command", b"/path/to/repo")
  634. self.assertEqual(b"username", server.username)
  635. self.assertEqual(1337, server.port)
  636. self.assertEqual("git-command '/path/to/repo'", server.command)
  637. client._connect(b"relative-command", b"/~/path/to/repo")
  638. self.assertEqual("git-relative-command '~/path/to/repo'",
  639. server.command)
  640. class ReportStatusParserTests(TestCase):
  641. def test_invalid_pack(self):
  642. parser = ReportStatusParser()
  643. parser.handle_packet(b"unpack error - foo bar")
  644. parser.handle_packet(b"ok refs/foo/bar")
  645. parser.handle_packet(None)
  646. self.assertRaises(SendPackError, list, parser.check())
  647. def test_update_refs_error(self):
  648. parser = ReportStatusParser()
  649. parser.handle_packet(b"unpack ok")
  650. parser.handle_packet(b"ng refs/foo/bar need to pull")
  651. parser.handle_packet(None)
  652. self.assertEqual(
  653. [(b'refs/foo/bar', 'need to pull')], list(parser.check()))
  654. def test_ok(self):
  655. parser = ReportStatusParser()
  656. parser.handle_packet(b"unpack ok")
  657. parser.handle_packet(b"ok refs/foo/bar")
  658. parser.handle_packet(None)
  659. self.assertEqual([(b'refs/foo/bar', None)], list(parser.check()))
  660. class LocalGitClientTests(TestCase):
  661. def test_get_url(self):
  662. path = "/tmp/repo.git"
  663. c = LocalGitClient()
  664. url = c.get_url(path)
  665. self.assertEqual('file:///tmp/repo.git', url)
  666. def test_fetch_into_empty(self):
  667. c = LocalGitClient()
  668. t = MemoryRepo()
  669. s = open_repo('a.git')
  670. self.addCleanup(tear_down_repo, s)
  671. self.assertEqual(s.get_refs(), c.fetch(s.path, t).refs)
  672. def test_fetch_empty(self):
  673. c = LocalGitClient()
  674. s = open_repo('a.git')
  675. self.addCleanup(tear_down_repo, s)
  676. out = BytesIO()
  677. walker = {}
  678. ret = c.fetch_pack(
  679. s.path, lambda heads: [], graph_walker=walker, pack_data=out.write)
  680. self.assertEqual({
  681. b'HEAD': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  682. b'refs/heads/master': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  683. b'refs/tags/mytag': b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
  684. b'refs/tags/mytag-packed':
  685. b'b0931cadc54336e78a1d980420e3268903b57a50'
  686. }, ret.refs)
  687. self.assertEqual(
  688. {b'HEAD': b'refs/heads/master'},
  689. ret.symrefs)
  690. self.assertEqual(
  691. b"PACK\x00\x00\x00\x02\x00\x00\x00\x00\x02\x9d\x08"
  692. b"\x82;\xd8\xa8\xea\xb5\x10\xadj\xc7\\\x82<\xfd>\xd3\x1e",
  693. out.getvalue())
  694. def test_fetch_pack_none(self):
  695. c = LocalGitClient()
  696. s = open_repo('a.git')
  697. self.addCleanup(tear_down_repo, s)
  698. out = BytesIO()
  699. walker = MemoryRepo().get_graph_walker()
  700. ret = c.fetch_pack(
  701. s.path,
  702. lambda heads: [b"a90fa2d900a17e99b433217e988c4eb4a2e9a097"],
  703. graph_walker=walker, pack_data=out.write)
  704. self.assertEqual({b'HEAD': b'refs/heads/master'}, ret.symrefs)
  705. self.assertEqual({
  706. b'HEAD': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  707. b'refs/heads/master': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  708. b'refs/tags/mytag': b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
  709. b'refs/tags/mytag-packed':
  710. b'b0931cadc54336e78a1d980420e3268903b57a50'
  711. }, ret.refs)
  712. # Hardcoding is not ideal, but we'll fix that some other day..
  713. self.assertTrue(out.getvalue().startswith(
  714. b'PACK\x00\x00\x00\x02\x00\x00\x00\x07'))
  715. def test_send_pack_without_changes(self):
  716. local = open_repo('a.git')
  717. self.addCleanup(tear_down_repo, local)
  718. target = open_repo('a.git')
  719. self.addCleanup(tear_down_repo, target)
  720. self.send_and_verify(b"master", local, target)
  721. def test_send_pack_with_changes(self):
  722. local = open_repo('a.git')
  723. self.addCleanup(tear_down_repo, local)
  724. target_path = tempfile.mkdtemp()
  725. self.addCleanup(shutil.rmtree, target_path)
  726. with Repo.init_bare(target_path) as target:
  727. self.send_and_verify(b"master", local, target)
  728. def test_get_refs(self):
  729. local = open_repo('refs.git')
  730. self.addCleanup(tear_down_repo, local)
  731. client = LocalGitClient()
  732. refs = client.get_refs(local.path)
  733. self.assertDictEqual(local.refs.as_dict(), refs)
  734. def send_and_verify(self, branch, local, target):
  735. """Send branch from local to remote repository and verify it worked."""
  736. client = LocalGitClient()
  737. ref_name = b"refs/heads/" + branch
  738. result = client.send_pack(target.path,
  739. lambda _: {ref_name: local.refs[ref_name]},
  740. local.generate_pack_data)
  741. self.assertEqual(local.refs[ref_name], result.refs[ref_name])
  742. self.assertIs(None, result.agent)
  743. self.assertEqual({}, result.ref_status)
  744. obj_local = local.get_object(result.refs[ref_name])
  745. obj_target = target.get_object(result.refs[ref_name])
  746. self.assertEqual(obj_local, obj_target)
  747. class HttpGitClientTests(TestCase):
  748. def test_get_url(self):
  749. base_url = 'https://github.com/jelmer/dulwich'
  750. path = '/jelmer/dulwich'
  751. c = HttpGitClient(base_url)
  752. url = c.get_url(path)
  753. self.assertEqual('https://github.com/jelmer/dulwich', url)
  754. def test_get_url_bytes_path(self):
  755. base_url = 'https://github.com/jelmer/dulwich'
  756. path_bytes = b'/jelmer/dulwich'
  757. c = HttpGitClient(base_url)
  758. url = c.get_url(path_bytes)
  759. self.assertEqual('https://github.com/jelmer/dulwich', url)
  760. def test_get_url_with_username_and_passwd(self):
  761. base_url = 'https://github.com/jelmer/dulwich'
  762. path = '/jelmer/dulwich'
  763. c = HttpGitClient(base_url, username='USERNAME', password='PASSWD')
  764. url = c.get_url(path)
  765. self.assertEqual('https://github.com/jelmer/dulwich', url)
  766. def test_init_username_passwd_set(self):
  767. url = 'https://github.com/jelmer/dulwich'
  768. c = HttpGitClient(url, config=None, username='user', password='passwd')
  769. self.assertEqual('user', c._username)
  770. self.assertEqual('passwd', c._password)
  771. basic_auth = c.pool_manager.headers['authorization']
  772. auth_string = '%s:%s' % ('user', 'passwd')
  773. b64_credentials = base64.b64encode(auth_string.encode('latin1'))
  774. expected_basic_auth = 'Basic %s' % b64_credentials.decode('latin1')
  775. self.assertEqual(basic_auth, expected_basic_auth)
  776. def test_init_no_username_passwd(self):
  777. url = 'https://github.com/jelmer/dulwich'
  778. c = HttpGitClient(url, config=None)
  779. self.assertIs(None, c._username)
  780. self.assertIs(None, c._password)
  781. self.assertNotIn('authorization', c.pool_manager.headers)
  782. def test_from_parsedurl_on_url_with_quoted_credentials(self):
  783. original_username = 'john|the|first'
  784. quoted_username = urlquote(original_username)
  785. original_password = 'Ya#1$2%3'
  786. quoted_password = urlquote(original_password)
  787. url = 'https://{username}:{password}@github.com/jelmer/dulwich'.format(
  788. username=quoted_username,
  789. password=quoted_password
  790. )
  791. c = HttpGitClient.from_parsedurl(urlparse(url))
  792. self.assertEqual(original_username, c._username)
  793. self.assertEqual(original_password, c._password)
  794. basic_auth = c.pool_manager.headers['authorization']
  795. auth_string = '%s:%s' % (original_username, original_password)
  796. b64_credentials = base64.b64encode(auth_string.encode('latin1'))
  797. expected_basic_auth = 'Basic %s' % b64_credentials.decode('latin1')
  798. self.assertEqual(basic_auth, expected_basic_auth)
  799. def test_url_redirect_location(self):
  800. from urllib3.response import HTTPResponse
  801. test_data = {
  802. 'https://gitlab.com/inkscape/inkscape/': {
  803. 'redirect_url': 'https://gitlab.com/inkscape/inkscape.git/',
  804. 'refs_data': (b'001e# service=git-upload-pack\n00000032'
  805. b'fb2bebf4919a011f0fd7cec085443d0031228e76 '
  806. b'HEAD\n0000')
  807. },
  808. 'https://github.com/jelmer/dulwich/': {
  809. 'redirect_url': 'https://github.com/jelmer/dulwich/',
  810. 'refs_data': (b'001e# service=git-upload-pack\n00000032'
  811. b'3ff25e09724aa4d86ea5bca7d5dd0399a3c8bfcf '
  812. b'HEAD\n0000')
  813. }
  814. }
  815. tail = 'info/refs?service=git-upload-pack'
  816. # we need to mock urllib3.PoolManager as this test will fail
  817. # otherwise without an active internet connection
  818. class PoolManagerMock():
  819. def __init__(self):
  820. self.headers = {}
  821. def request(self, method, url, fields=None, headers=None,
  822. redirect=True):
  823. base_url = url[:-len(tail)]
  824. redirect_base_url = test_data[base_url]['redirect_url']
  825. redirect_url = redirect_base_url + tail
  826. headers = {
  827. 'Content-Type':
  828. 'application/x-git-upload-pack-advertisement'
  829. }
  830. body = test_data[base_url]['refs_data']
  831. # urllib3 handles automatic redirection by default
  832. status = 200
  833. request_url = redirect_url
  834. # simulate urllib3 behavior when redirect parameter is False
  835. if redirect is False:
  836. request_url = url
  837. if redirect_base_url != base_url:
  838. body = ''
  839. headers['location'] = redirect_url
  840. status = 301
  841. return HTTPResponse(body=body,
  842. headers=headers,
  843. request_method=method,
  844. request_url=request_url,
  845. status=status)
  846. pool_manager = PoolManagerMock()
  847. for base_url in test_data.keys():
  848. # instantiate HttpGitClient with mocked pool manager
  849. c = HttpGitClient(base_url, pool_manager=pool_manager,
  850. config=None)
  851. # call method that detects url redirection
  852. _, _, processed_url = c._discover_references(b'git-upload-pack',
  853. base_url)
  854. # send the same request as the method above without redirection
  855. resp = c.pool_manager.request('GET', base_url + tail,
  856. redirect=False)
  857. # check expected behavior of urllib3
  858. redirect_location = resp.get_redirect_location()
  859. if resp.status == 200:
  860. self.assertFalse(redirect_location)
  861. if redirect_location:
  862. # check that url redirection has been correctly detected
  863. self.assertEqual(processed_url, redirect_location[:-len(tail)])
  864. else:
  865. # check also the no redirection case
  866. self.assertEqual(processed_url, base_url)
  867. class TCPGitClientTests(TestCase):
  868. def test_get_url(self):
  869. host = 'github.com'
  870. path = '/jelmer/dulwich'
  871. c = TCPGitClient(host)
  872. url = c.get_url(path)
  873. self.assertEqual('git://github.com/jelmer/dulwich', url)
  874. def test_get_url_with_port(self):
  875. host = 'github.com'
  876. path = '/jelmer/dulwich'
  877. port = 9090
  878. c = TCPGitClient(host, port=port)
  879. url = c.get_url(path)
  880. self.assertEqual('git://github.com:9090/jelmer/dulwich', url)
  881. class DefaultUrllib3ManagerTest(TestCase):
  882. def test_no_config(self):
  883. manager = default_urllib3_manager(config=None)
  884. self.assertEqual(manager.connection_pool_kw['cert_reqs'],
  885. 'CERT_REQUIRED')
  886. def test_config_no_proxy(self):
  887. import urllib3
  888. manager = default_urllib3_manager(config=ConfigDict())
  889. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  890. self.assertIsInstance(manager, urllib3.PoolManager)
  891. def test_config_no_proxy_custom_cls(self):
  892. import urllib3
  893. class CustomPoolManager(urllib3.PoolManager):
  894. pass
  895. manager = default_urllib3_manager(config=ConfigDict(),
  896. pool_manager_cls=CustomPoolManager)
  897. self.assertIsInstance(manager, CustomPoolManager)
  898. def test_config_ssl(self):
  899. config = ConfigDict()
  900. config.set(b'http', b'sslVerify', b'true')
  901. manager = default_urllib3_manager(config=config)
  902. self.assertEqual(manager.connection_pool_kw['cert_reqs'],
  903. 'CERT_REQUIRED')
  904. def test_config_no_ssl(self):
  905. config = ConfigDict()
  906. config.set(b'http', b'sslVerify', b'false')
  907. manager = default_urllib3_manager(config=config)
  908. self.assertEqual(manager.connection_pool_kw['cert_reqs'],
  909. 'CERT_NONE')
  910. def test_config_proxy(self):
  911. import urllib3
  912. config = ConfigDict()
  913. config.set(b'http', b'proxy', b'http://localhost:3128/')
  914. manager = default_urllib3_manager(config=config)
  915. self.assertIsInstance(manager, urllib3.ProxyManager)
  916. self.assertTrue(hasattr(manager, 'proxy'))
  917. self.assertEqual(manager.proxy.scheme, 'http')
  918. self.assertEqual(manager.proxy.host, 'localhost')
  919. self.assertEqual(manager.proxy.port, 3128)
  920. def test_config_proxy_custom_cls(self):
  921. import urllib3
  922. class CustomProxyManager(urllib3.ProxyManager):
  923. pass
  924. config = ConfigDict()
  925. config.set(b'http', b'proxy', b'http://localhost:3128/')
  926. manager = default_urllib3_manager(config=config,
  927. proxy_manager_cls=CustomProxyManager)
  928. self.assertIsInstance(manager, CustomProxyManager)
  929. def test_config_no_verify_ssl(self):
  930. manager = default_urllib3_manager(config=None, cert_reqs="CERT_NONE")
  931. self.assertEqual(manager.connection_pool_kw['cert_reqs'], 'CERT_NONE')
  932. class SubprocessSSHVendorTests(TestCase):
  933. def setUp(self):
  934. # Monkey Patch client subprocess popen
  935. self._orig_popen = dulwich.client.subprocess.Popen
  936. dulwich.client.subprocess.Popen = DummyPopen
  937. def tearDown(self):
  938. dulwich.client.subprocess.Popen = self._orig_popen
  939. def test_run_command_dashes(self):
  940. vendor = SubprocessSSHVendor()
  941. self.assertRaises(StrangeHostname, vendor.run_command, '--weird-host',
  942. 'git-clone-url')
  943. def test_run_command_password(self):
  944. vendor = SubprocessSSHVendor()
  945. self.assertRaises(NotImplementedError, vendor.run_command, 'host',
  946. 'git-clone-url', password='12345')
  947. def test_run_command_password_and_privkey(self):
  948. vendor = SubprocessSSHVendor()
  949. self.assertRaises(NotImplementedError, vendor.run_command,
  950. 'host', 'git-clone-url',
  951. password='12345', key_filename='/tmp/id_rsa')
  952. def test_run_command_with_port_username_and_privkey(self):
  953. expected = ['ssh', '-x', '-p', '2200',
  954. '-i', '/tmp/id_rsa', 'user@host', 'git-clone-url']
  955. vendor = SubprocessSSHVendor()
  956. command = vendor.run_command(
  957. 'host', 'git-clone-url',
  958. username='user', port='2200',
  959. key_filename='/tmp/id_rsa')
  960. args = command.proc.args
  961. self.assertListEqual(expected, args[0])
  962. class PLinkSSHVendorTests(TestCase):
  963. def setUp(self):
  964. # Monkey Patch client subprocess popen
  965. self._orig_popen = dulwich.client.subprocess.Popen
  966. dulwich.client.subprocess.Popen = DummyPopen
  967. def tearDown(self):
  968. dulwich.client.subprocess.Popen = self._orig_popen
  969. def test_run_command_dashes(self):
  970. vendor = PLinkSSHVendor()
  971. self.assertRaises(StrangeHostname, vendor.run_command, '--weird-host',
  972. 'git-clone-url')
  973. def test_run_command_password_and_privkey(self):
  974. vendor = PLinkSSHVendor()
  975. warnings.simplefilter("always", UserWarning)
  976. self.addCleanup(warnings.resetwarnings)
  977. warnings_list, restore_warnings = setup_warning_catcher()
  978. self.addCleanup(restore_warnings)
  979. command = vendor.run_command(
  980. 'host', 'git-clone-url', password='12345',
  981. key_filename='/tmp/id_rsa')
  982. expected_warning = UserWarning(
  983. 'Invoking PLink with a password exposes the password in the '
  984. 'process list.')
  985. for w in warnings_list:
  986. if (type(w) == type(expected_warning) and
  987. w.args == expected_warning.args):
  988. break
  989. else:
  990. raise AssertionError(
  991. 'Expected warning %r not in %r' %
  992. (expected_warning, warnings_list))
  993. args = command.proc.args
  994. if sys.platform == 'win32':
  995. binary = ['plink.exe', '-ssh']
  996. else:
  997. binary = ['plink', '-ssh']
  998. expected = binary + [
  999. '-pw', '12345', '-i', '/tmp/id_rsa', 'host', 'git-clone-url']
  1000. self.assertListEqual(expected, args[0])
  1001. def test_run_command_password(self):
  1002. if sys.platform == 'win32':
  1003. binary = ['plink.exe', '-ssh']
  1004. else:
  1005. binary = ['plink', '-ssh']
  1006. expected = binary + ['-pw', '12345', 'host', 'git-clone-url']
  1007. vendor = PLinkSSHVendor()
  1008. warnings.simplefilter("always", UserWarning)
  1009. self.addCleanup(warnings.resetwarnings)
  1010. warnings_list, restore_warnings = setup_warning_catcher()
  1011. self.addCleanup(restore_warnings)
  1012. command = vendor.run_command('host', 'git-clone-url', password='12345')
  1013. expected_warning = UserWarning(
  1014. 'Invoking PLink with a password exposes the password in the '
  1015. 'process list.')
  1016. for w in warnings_list:
  1017. if (type(w) == type(expected_warning) and
  1018. w.args == expected_warning.args):
  1019. break
  1020. else:
  1021. raise AssertionError(
  1022. 'Expected warning %r not in %r' %
  1023. (expected_warning, warnings_list))
  1024. args = command.proc.args
  1025. self.assertListEqual(expected, args[0])
  1026. def test_run_command_with_port_username_and_privkey(self):
  1027. if sys.platform == 'win32':
  1028. binary = ['plink.exe', '-ssh']
  1029. else:
  1030. binary = ['plink', '-ssh']
  1031. expected = binary + [
  1032. '-P', '2200', '-i', '/tmp/id_rsa',
  1033. 'user@host', 'git-clone-url']
  1034. vendor = PLinkSSHVendor()
  1035. command = vendor.run_command(
  1036. 'host', 'git-clone-url',
  1037. username='user', port='2200',
  1038. key_filename='/tmp/id_rsa')
  1039. args = command.proc.args
  1040. self.assertListEqual(expected, args[0])
  1041. class RsyncUrlTests(TestCase):
  1042. def test_simple(self):
  1043. self.assertEqual(
  1044. parse_rsync_url('foo:bar/path'),
  1045. (None, 'foo', 'bar/path'))
  1046. self.assertEqual(
  1047. parse_rsync_url('user@foo:bar/path'),
  1048. ('user', 'foo', 'bar/path'))
  1049. def test_path(self):
  1050. self.assertRaises(ValueError, parse_rsync_url, '/path')
  1051. class CheckWantsTests(TestCase):
  1052. def test_fine(self):
  1053. check_wants(
  1054. [b'2f3dc7a53fb752a6961d3a56683df46d4d3bf262'],
  1055. {b'refs/heads/blah': b'2f3dc7a53fb752a6961d3a56683df46d4d3bf262'})
  1056. def test_missing(self):
  1057. self.assertRaises(
  1058. InvalidWants, check_wants,
  1059. [b'2f3dc7a53fb752a6961d3a56683df46d4d3bf262'],
  1060. {b'refs/heads/blah': b'3f3dc7a53fb752a6961d3a56683df46d4d3bf262'})
  1061. def test_annotated(self):
  1062. self.assertRaises(
  1063. InvalidWants, check_wants,
  1064. [b'2f3dc7a53fb752a6961d3a56683df46d4d3bf262'],
  1065. {b'refs/heads/blah': b'3f3dc7a53fb752a6961d3a56683df46d4d3bf262',
  1066. b'refs/heads/blah^{}':
  1067. b'2f3dc7a53fb752a6961d3a56683df46d4d3bf262'})
  1068. class FetchPackResultTests(TestCase):
  1069. def test_eq(self):
  1070. self.assertEqual(
  1071. FetchPackResult(
  1072. {b'refs/heads/master':
  1073. b'2f3dc7a53fb752a6961d3a56683df46d4d3bf262'}, {},
  1074. b'user/agent'),
  1075. FetchPackResult(
  1076. {b'refs/heads/master':
  1077. b'2f3dc7a53fb752a6961d3a56683df46d4d3bf262'}, {},
  1078. b'user/agent'))
  1079. class GitCredentialStoreTests(TestCase):
  1080. @classmethod
  1081. def setUpClass(cls):
  1082. with tempfile.NamedTemporaryFile(delete=False) as f:
  1083. f.write(b'https://user:pass@example.org')
  1084. cls.fname = f.name
  1085. @classmethod
  1086. def tearDownClass(cls):
  1087. os.unlink(cls.fname)
  1088. def test_nonmatching_scheme(self):
  1089. self.assertEqual(
  1090. get_credentials_from_store(
  1091. b'http', b'example.org', fnames=[self.fname]),
  1092. None)
  1093. def test_nonmatching_hostname(self):
  1094. self.assertEqual(
  1095. get_credentials_from_store(
  1096. b'https', b'noentry.org', fnames=[self.fname]),
  1097. None)
  1098. def test_match_without_username(self):
  1099. self.assertEqual(
  1100. get_credentials_from_store(
  1101. b'https', b'example.org', fnames=[self.fname]),
  1102. (b'user', b'pass'))
  1103. def test_match_with_matching_username(self):
  1104. self.assertEqual(
  1105. get_credentials_from_store(
  1106. b'https', b'example.org', b'user', fnames=[self.fname]),
  1107. (b'user', b'pass'))
  1108. def test_no_match_with_nonmatching_username(self):
  1109. self.assertEqual(
  1110. get_credentials_from_store(
  1111. b'https', b'example.org', b'otheruser', fnames=[self.fname]),
  1112. None)
  1113. class RemoteErrorFromStderrTests(TestCase):
  1114. def test_nothing(self):
  1115. self.assertEqual(
  1116. _remote_error_from_stderr(None), HangupException())
  1117. def test_error_line(self):
  1118. b = BytesIO(b"""\
  1119. This is some random output.
  1120. ERROR: This is the actual error
  1121. with a tail
  1122. """)
  1123. self.assertEqual(
  1124. _remote_error_from_stderr(b),
  1125. GitProtocolError("This is the actual error"))
  1126. def test_no_error_line(self):
  1127. b = BytesIO(b"""\
  1128. This is output without an error line.
  1129. And this line is just random noise, too.
  1130. """)
  1131. self.assertEqual(
  1132. _remote_error_from_stderr(b),
  1133. HangupException([
  1134. b"This is output without an error line.",
  1135. b"And this line is just random noise, too."]))