2
0

test_client.py 60 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759
  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. from unittest.mock import patch
  32. import dulwich
  33. from dulwich import (
  34. client,
  35. )
  36. from dulwich.client import (
  37. InvalidWants,
  38. LocalGitClient,
  39. TraditionalGitClient,
  40. TCPGitClient,
  41. SSHGitClient,
  42. HttpGitClient,
  43. FetchPackResult,
  44. ReportStatusParser,
  45. SendPackError,
  46. StrangeHostname,
  47. SubprocessSSHVendor,
  48. PLinkSSHVendor,
  49. HangupException,
  50. GitProtocolError,
  51. check_wants,
  52. default_urllib3_manager,
  53. get_credentials_from_store,
  54. get_transport_and_path,
  55. get_transport_and_path_from_url,
  56. parse_rsync_url,
  57. _remote_error_from_stderr,
  58. )
  59. from dulwich.config import (
  60. ConfigDict,
  61. )
  62. from dulwich.tests import (
  63. TestCase,
  64. )
  65. from dulwich.protocol import (
  66. TCP_GIT_PORT,
  67. Protocol,
  68. )
  69. from dulwich.pack import (
  70. pack_objects_to_data,
  71. write_pack_data,
  72. write_pack_objects,
  73. )
  74. from dulwich.objects import Commit, Tree
  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, None
  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().setUp()
  109. self.rout = BytesIO()
  110. self.rin = BytesIO()
  111. self.client = DummyClient(lambda x: True, self.rin.read, self.rout.write)
  112. def test_caps(self):
  113. agent_cap = ("agent=dulwich/%d.%d.%d" % dulwich.__version__).encode("ascii")
  114. self.assertEqual(
  115. {
  116. b"multi_ack",
  117. b"side-band-64k",
  118. b"ofs-delta",
  119. b"thin-pack",
  120. b"multi_ack_detailed",
  121. b"shallow",
  122. agent_cap,
  123. },
  124. set(self.client._fetch_capabilities),
  125. )
  126. self.assertEqual(
  127. {
  128. b"delete-refs",
  129. b"ofs-delta",
  130. b"report-status",
  131. b"side-band-64k",
  132. agent_cap,
  133. },
  134. set(self.client._send_capabilities),
  135. )
  136. def test_archive_ack(self):
  137. self.rin.write(b"0009NACK\n" b"0000")
  138. self.rin.seek(0)
  139. self.client.archive(b"bla", b"HEAD", None, None)
  140. self.assertEqual(self.rout.getvalue(), b"0011argument HEAD0000")
  141. def test_fetch_empty(self):
  142. self.rin.write(b"0000")
  143. self.rin.seek(0)
  144. def check_heads(heads, **kwargs):
  145. self.assertEqual(heads, {})
  146. return []
  147. ret = self.client.fetch_pack(b"/", check_heads, None, None)
  148. self.assertEqual({}, ret.refs)
  149. self.assertEqual({}, ret.symrefs)
  150. def test_fetch_pack_ignores_magic_ref(self):
  151. self.rin.write(
  152. b"00000000000000000000000000000000000000000000 capabilities^{}"
  153. b"\x00 multi_ack "
  154. b"thin-pack side-band side-band-64k ofs-delta shallow no-progress "
  155. b"include-tag\n"
  156. b"0000"
  157. )
  158. self.rin.seek(0)
  159. def check_heads(heads, **kwargs):
  160. self.assertEqual({}, heads)
  161. return []
  162. ret = self.client.fetch_pack(b"bla", check_heads, None, None, None)
  163. self.assertEqual({}, ret.refs)
  164. self.assertEqual({}, ret.symrefs)
  165. self.assertEqual(self.rout.getvalue(), b"0000")
  166. def test_fetch_pack_none(self):
  167. self.rin.write(
  168. b"008855dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 HEAD\x00multi_ack "
  169. b"thin-pack side-band side-band-64k ofs-delta shallow no-progress "
  170. b"include-tag\n"
  171. b"0000"
  172. )
  173. self.rin.seek(0)
  174. ret = self.client.fetch_pack(b"bla", lambda heads, **kwargs: [], None, None, None)
  175. self.assertEqual(
  176. {b"HEAD": b"55dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7"}, ret.refs
  177. )
  178. self.assertEqual({}, ret.symrefs)
  179. self.assertEqual(self.rout.getvalue(), b"0000")
  180. def test_send_pack_no_sideband64k_with_update_ref_error(self) -> None:
  181. # No side-bank-64k reported by server shouldn't try to parse
  182. # side band data
  183. pkts = [
  184. b"55dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 capabilities^{}"
  185. b"\x00 report-status delete-refs ofs-delta\n",
  186. b"",
  187. b"unpack ok",
  188. b"ng refs/foo/bar pre-receive hook declined",
  189. b"",
  190. ]
  191. for pkt in pkts:
  192. if pkt == b"":
  193. self.rin.write(b"0000")
  194. else:
  195. self.rin.write(("%04x" % (len(pkt) + 4)).encode("ascii") + pkt)
  196. self.rin.seek(0)
  197. tree = Tree()
  198. commit = Commit()
  199. commit.tree = tree
  200. commit.parents = []
  201. commit.author = commit.committer = b"test user"
  202. commit.commit_time = commit.author_time = 1174773719
  203. commit.commit_timezone = commit.author_timezone = 0
  204. commit.encoding = b"UTF-8"
  205. commit.message = b"test message"
  206. def update_refs(refs):
  207. return {
  208. b"refs/foo/bar": commit.id,
  209. }
  210. def generate_pack_data(have, want, ofs_delta=False, progress=None):
  211. return pack_objects_to_data(
  212. [
  213. (commit, None),
  214. (tree, b""),
  215. ]
  216. )
  217. result = self.client.send_pack("blah", update_refs, generate_pack_data)
  218. self.assertEqual(
  219. {b"refs/foo/bar": "pre-receive hook declined"}, result.ref_status
  220. )
  221. self.assertEqual({b"refs/foo/bar": commit.id}, result.refs)
  222. def test_send_pack_none(self):
  223. # Set ref to current value
  224. self.rin.write(
  225. b"0078310ca9477129b8586fa2afc779c1f57cf64bba6c "
  226. b"refs/heads/master\x00 report-status delete-refs "
  227. b"side-band-64k quiet ofs-delta\n"
  228. b"0000"
  229. )
  230. self.rin.seek(0)
  231. def update_refs(refs):
  232. return {b"refs/heads/master": b"310ca9477129b8586fa2afc779c1f57cf64bba6c"}
  233. def generate_pack_data(have, want, ofs_delta=False, progress=None):
  234. return 0, []
  235. self.client.send_pack(b"/", update_refs, generate_pack_data)
  236. self.assertEqual(self.rout.getvalue(), b"0000")
  237. def test_send_pack_keep_and_delete(self):
  238. self.rin.write(
  239. b"0063310ca9477129b8586fa2afc779c1f57cf64bba6c "
  240. b"refs/heads/master\x00report-status delete-refs ofs-delta\n"
  241. b"003f310ca9477129b8586fa2afc779c1f57cf64bba6c refs/heads/keepme\n"
  242. b"0000000eunpack ok\n"
  243. b"0019ok refs/heads/master\n"
  244. b"0000"
  245. )
  246. self.rin.seek(0)
  247. def update_refs(refs):
  248. return {b"refs/heads/master": b"0" * 40}
  249. def generate_pack_data(have, want, ofs_delta=False, progress=None):
  250. return 0, []
  251. self.client.send_pack(b"/", update_refs, generate_pack_data)
  252. self.assertEqual(
  253. self.rout.getvalue(),
  254. b"008b310ca9477129b8586fa2afc779c1f57cf64bba6c "
  255. b"0000000000000000000000000000000000000000 "
  256. b"refs/heads/master\x00delete-refs ofs-delta report-status0000",
  257. )
  258. def test_send_pack_delete_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/master\n"
  264. b"0000"
  265. )
  266. self.rin.seek(0)
  267. def update_refs(refs):
  268. return {b"refs/heads/master": b"0" * 40}
  269. def generate_pack_data(have, want, ofs_delta=False, progress=None):
  270. return 0, []
  271. self.client.send_pack(b"/", update_refs, generate_pack_data)
  272. self.assertEqual(
  273. self.rout.getvalue(),
  274. b"008b310ca9477129b8586fa2afc779c1f57cf64bba6c "
  275. b"0000000000000000000000000000000000000000 "
  276. b"refs/heads/master\x00delete-refs ofs-delta report-status0000",
  277. )
  278. def test_send_pack_new_ref_only(self):
  279. self.rin.write(
  280. b"0063310ca9477129b8586fa2afc779c1f57cf64bba6c "
  281. b"refs/heads/master\x00report-status delete-refs ofs-delta\n"
  282. b"0000000eunpack ok\n"
  283. b"0019ok refs/heads/blah12\n"
  284. b"0000"
  285. )
  286. self.rin.seek(0)
  287. def update_refs(refs):
  288. return {
  289. b"refs/heads/blah12": b"310ca9477129b8586fa2afc779c1f57cf64bba6c",
  290. b"refs/heads/master": b"310ca9477129b8586fa2afc779c1f57cf64bba6c",
  291. }
  292. def generate_pack_data(have, want, ofs_delta=False, progress=None):
  293. return 0, []
  294. f = BytesIO()
  295. write_pack_objects(f.write, [])
  296. self.client.send_pack("/", update_refs, generate_pack_data)
  297. self.assertEqual(
  298. self.rout.getvalue(),
  299. b"008b0000000000000000000000000000000000000000 "
  300. b"310ca9477129b8586fa2afc779c1f57cf64bba6c "
  301. b"refs/heads/blah12\x00delete-refs ofs-delta report-status0000"
  302. + f.getvalue(),
  303. )
  304. def test_send_pack_new_ref(self):
  305. self.rin.write(
  306. b"0064310ca9477129b8586fa2afc779c1f57cf64bba6c "
  307. b"refs/heads/master\x00 report-status delete-refs ofs-delta\n"
  308. b"0000000eunpack ok\n"
  309. b"0019ok refs/heads/blah12\n"
  310. b"0000"
  311. )
  312. self.rin.seek(0)
  313. tree = Tree()
  314. commit = Commit()
  315. commit.tree = tree
  316. commit.parents = []
  317. commit.author = commit.committer = b"test user"
  318. commit.commit_time = commit.author_time = 1174773719
  319. commit.commit_timezone = commit.author_timezone = 0
  320. commit.encoding = b"UTF-8"
  321. commit.message = b"test message"
  322. def update_refs(refs):
  323. return {
  324. b"refs/heads/blah12": commit.id,
  325. b"refs/heads/master": b"310ca9477129b8586fa2afc779c1f57cf64bba6c",
  326. }
  327. def generate_pack_data(have, want, ofs_delta=False, progress=None):
  328. return pack_objects_to_data(
  329. [
  330. (commit, None),
  331. (tree, b""),
  332. ]
  333. )
  334. f = BytesIO()
  335. count, records = generate_pack_data(None, None)
  336. write_pack_data(f.write, records, num_records=count)
  337. self.client.send_pack(b"/", update_refs, generate_pack_data)
  338. self.assertEqual(
  339. self.rout.getvalue(),
  340. b"008b0000000000000000000000000000000000000000 "
  341. + commit.id
  342. + b" refs/heads/blah12\x00delete-refs ofs-delta report-status0000"
  343. + f.getvalue(),
  344. )
  345. def test_send_pack_no_deleteref_delete_only(self):
  346. pkts = [
  347. b"310ca9477129b8586fa2afc779c1f57cf64bba6c refs/heads/master"
  348. b"\x00 report-status ofs-delta\n",
  349. b"",
  350. b"",
  351. ]
  352. for pkt in pkts:
  353. if pkt == b"":
  354. self.rin.write(b"0000")
  355. else:
  356. self.rin.write(("%04x" % (len(pkt) + 4)).encode("ascii") + pkt)
  357. self.rin.seek(0)
  358. def update_refs(refs):
  359. return {b"refs/heads/master": b"0" * 40}
  360. def generate_pack_data(have, want, ofs_delta=False, progress=None):
  361. return 0, []
  362. result = self.client.send_pack(b"/", update_refs, generate_pack_data)
  363. self.assertEqual(
  364. result.ref_status,
  365. {b"refs/heads/master": "remote does not support deleting refs"},
  366. )
  367. self.assertEqual(
  368. result.refs,
  369. {b"refs/heads/master": b"310ca9477129b8586fa2afc779c1f57cf64bba6c"},
  370. )
  371. self.assertEqual(self.rout.getvalue(), b"0000")
  372. class TestGetTransportAndPath(TestCase):
  373. def test_tcp(self):
  374. c, path = get_transport_and_path("git://foo.com/bar/baz")
  375. self.assertIsInstance(c, TCPGitClient)
  376. self.assertEqual("foo.com", c._host)
  377. self.assertEqual(TCP_GIT_PORT, c._port)
  378. self.assertEqual("/bar/baz", path)
  379. def test_tcp_port(self):
  380. c, path = get_transport_and_path("git://foo.com:1234/bar/baz")
  381. self.assertIsInstance(c, TCPGitClient)
  382. self.assertEqual("foo.com", c._host)
  383. self.assertEqual(1234, c._port)
  384. self.assertEqual("/bar/baz", path)
  385. def test_git_ssh_explicit(self):
  386. c, path = get_transport_and_path("git+ssh://foo.com/bar/baz")
  387. self.assertIsInstance(c, SSHGitClient)
  388. self.assertEqual("foo.com", c.host)
  389. self.assertEqual(None, c.port)
  390. self.assertEqual(None, c.username)
  391. self.assertEqual("/bar/baz", path)
  392. def test_ssh_explicit(self):
  393. c, path = get_transport_and_path("ssh://foo.com/bar/baz")
  394. self.assertIsInstance(c, SSHGitClient)
  395. self.assertEqual("foo.com", c.host)
  396. self.assertEqual(None, c.port)
  397. self.assertEqual(None, c.username)
  398. self.assertEqual("/bar/baz", path)
  399. def test_ssh_port_explicit(self):
  400. c, path = get_transport_and_path("git+ssh://foo.com:1234/bar/baz")
  401. self.assertIsInstance(c, SSHGitClient)
  402. self.assertEqual("foo.com", c.host)
  403. self.assertEqual(1234, c.port)
  404. self.assertEqual("/bar/baz", path)
  405. def test_username_and_port_explicit_unknown_scheme(self):
  406. c, path = get_transport_and_path("unknown://git@server:7999/dply/stuff.git")
  407. self.assertIsInstance(c, SSHGitClient)
  408. self.assertEqual("unknown", c.host)
  409. self.assertEqual("//git@server:7999/dply/stuff.git", path)
  410. def test_username_and_port_explicit(self):
  411. c, path = get_transport_and_path("ssh://git@server:7999/dply/stuff.git")
  412. self.assertIsInstance(c, SSHGitClient)
  413. self.assertEqual("git", c.username)
  414. self.assertEqual("server", c.host)
  415. self.assertEqual(7999, c.port)
  416. self.assertEqual("/dply/stuff.git", path)
  417. def test_ssh_abspath_doubleslash(self):
  418. c, path = get_transport_and_path("git+ssh://foo.com//bar/baz")
  419. self.assertIsInstance(c, SSHGitClient)
  420. self.assertEqual("foo.com", c.host)
  421. self.assertEqual(None, c.port)
  422. self.assertEqual(None, c.username)
  423. self.assertEqual("//bar/baz", path)
  424. def test_ssh_port(self):
  425. c, path = get_transport_and_path("git+ssh://foo.com:1234/bar/baz")
  426. self.assertIsInstance(c, SSHGitClient)
  427. self.assertEqual("foo.com", c.host)
  428. self.assertEqual(1234, c.port)
  429. self.assertEqual("/bar/baz", path)
  430. def test_ssh_implicit(self):
  431. c, path = get_transport_and_path("foo:/bar/baz")
  432. self.assertIsInstance(c, SSHGitClient)
  433. self.assertEqual("foo", c.host)
  434. self.assertEqual(None, c.port)
  435. self.assertEqual(None, c.username)
  436. self.assertEqual("/bar/baz", path)
  437. def test_ssh_host(self):
  438. c, path = get_transport_and_path("foo.com:/bar/baz")
  439. self.assertIsInstance(c, SSHGitClient)
  440. self.assertEqual("foo.com", c.host)
  441. self.assertEqual(None, c.port)
  442. self.assertEqual(None, c.username)
  443. self.assertEqual("/bar/baz", path)
  444. def test_ssh_user_host(self):
  445. c, path = get_transport_and_path("user@foo.com:/bar/baz")
  446. self.assertIsInstance(c, SSHGitClient)
  447. self.assertEqual("foo.com", c.host)
  448. self.assertEqual(None, c.port)
  449. self.assertEqual("user", c.username)
  450. self.assertEqual("/bar/baz", path)
  451. def test_ssh_relpath(self):
  452. c, path = get_transport_and_path("foo:bar/baz")
  453. self.assertIsInstance(c, SSHGitClient)
  454. self.assertEqual("foo", c.host)
  455. self.assertEqual(None, c.port)
  456. self.assertEqual(None, c.username)
  457. self.assertEqual("bar/baz", path)
  458. def test_ssh_host_relpath(self):
  459. c, path = get_transport_and_path("foo.com:bar/baz")
  460. self.assertIsInstance(c, SSHGitClient)
  461. self.assertEqual("foo.com", c.host)
  462. self.assertEqual(None, c.port)
  463. self.assertEqual(None, c.username)
  464. self.assertEqual("bar/baz", path)
  465. def test_ssh_user_host_relpath(self):
  466. c, path = get_transport_and_path("user@foo.com:bar/baz")
  467. self.assertIsInstance(c, SSHGitClient)
  468. self.assertEqual("foo.com", c.host)
  469. self.assertEqual(None, c.port)
  470. self.assertEqual("user", c.username)
  471. self.assertEqual("bar/baz", path)
  472. def test_local(self):
  473. c, path = get_transport_and_path("foo.bar/baz")
  474. self.assertIsInstance(c, LocalGitClient)
  475. self.assertEqual("foo.bar/baz", path)
  476. @skipIf(sys.platform != "win32", "Behaviour only happens on windows.")
  477. def test_local_abs_windows_path(self):
  478. c, path = get_transport_and_path("C:\\foo.bar\\baz")
  479. self.assertIsInstance(c, LocalGitClient)
  480. self.assertEqual("C:\\foo.bar\\baz", path)
  481. def test_error(self):
  482. # Need to use a known urlparse.uses_netloc URL scheme to get the
  483. # expected parsing of the URL on Python versions less than 2.6.5
  484. c, path = get_transport_and_path("prospero://bar/baz")
  485. self.assertIsInstance(c, SSHGitClient)
  486. def test_http(self):
  487. url = "https://github.com/jelmer/dulwich"
  488. c, path = get_transport_and_path(url)
  489. self.assertIsInstance(c, HttpGitClient)
  490. self.assertEqual("/jelmer/dulwich", path)
  491. def test_http_auth(self):
  492. url = "https://user:passwd@github.com/jelmer/dulwich"
  493. c, path = get_transport_and_path(url)
  494. self.assertIsInstance(c, HttpGitClient)
  495. self.assertEqual("/jelmer/dulwich", path)
  496. self.assertEqual("user", c._username)
  497. self.assertEqual("passwd", c._password)
  498. def test_http_auth_with_username(self):
  499. url = "https://github.com/jelmer/dulwich"
  500. c, path = get_transport_and_path(url, username="user2", password="blah")
  501. self.assertIsInstance(c, HttpGitClient)
  502. self.assertEqual("/jelmer/dulwich", path)
  503. self.assertEqual("user2", c._username)
  504. self.assertEqual("blah", c._password)
  505. def test_http_auth_with_username_and_in_url(self):
  506. url = "https://user:passwd@github.com/jelmer/dulwich"
  507. c, path = get_transport_and_path(url, username="user2", password="blah")
  508. self.assertIsInstance(c, HttpGitClient)
  509. self.assertEqual("/jelmer/dulwich", path)
  510. self.assertEqual("user", c._username)
  511. self.assertEqual("passwd", c._password)
  512. def test_http_no_auth(self):
  513. url = "https://github.com/jelmer/dulwich"
  514. c, path = get_transport_and_path(url)
  515. self.assertIsInstance(c, HttpGitClient)
  516. self.assertEqual("/jelmer/dulwich", path)
  517. self.assertIs(None, c._username)
  518. self.assertIs(None, c._password)
  519. class TestGetTransportAndPathFromUrl(TestCase):
  520. def test_tcp(self):
  521. c, path = get_transport_and_path_from_url("git://foo.com/bar/baz")
  522. self.assertIsInstance(c, TCPGitClient)
  523. self.assertEqual("foo.com", c._host)
  524. self.assertEqual(TCP_GIT_PORT, c._port)
  525. self.assertEqual("/bar/baz", path)
  526. def test_tcp_port(self):
  527. c, path = get_transport_and_path_from_url("git://foo.com:1234/bar/baz")
  528. self.assertIsInstance(c, TCPGitClient)
  529. self.assertEqual("foo.com", c._host)
  530. self.assertEqual(1234, c._port)
  531. self.assertEqual("/bar/baz", path)
  532. def test_ssh_explicit(self):
  533. c, path = get_transport_and_path_from_url("git+ssh://foo.com/bar/baz")
  534. self.assertIsInstance(c, SSHGitClient)
  535. self.assertEqual("foo.com", c.host)
  536. self.assertEqual(None, c.port)
  537. self.assertEqual(None, c.username)
  538. self.assertEqual("/bar/baz", path)
  539. def test_ssh_port_explicit(self):
  540. c, path = get_transport_and_path_from_url("git+ssh://foo.com:1234/bar/baz")
  541. self.assertIsInstance(c, SSHGitClient)
  542. self.assertEqual("foo.com", c.host)
  543. self.assertEqual(1234, c.port)
  544. self.assertEqual("/bar/baz", path)
  545. def test_ssh_homepath(self):
  546. c, path = get_transport_and_path_from_url("git+ssh://foo.com/~/bar/baz")
  547. self.assertIsInstance(c, SSHGitClient)
  548. self.assertEqual("foo.com", c.host)
  549. self.assertEqual(None, c.port)
  550. self.assertEqual(None, c.username)
  551. self.assertEqual("/~/bar/baz", path)
  552. def test_ssh_port_homepath(self):
  553. c, path = get_transport_and_path_from_url("git+ssh://foo.com:1234/~/bar/baz")
  554. self.assertIsInstance(c, SSHGitClient)
  555. self.assertEqual("foo.com", c.host)
  556. self.assertEqual(1234, c.port)
  557. self.assertEqual("/~/bar/baz", path)
  558. def test_ssh_host_relpath(self):
  559. self.assertRaises(
  560. ValueError, get_transport_and_path_from_url, "foo.com:bar/baz"
  561. )
  562. def test_ssh_user_host_relpath(self):
  563. self.assertRaises(
  564. ValueError, get_transport_and_path_from_url, "user@foo.com:bar/baz"
  565. )
  566. def test_local_path(self):
  567. self.assertRaises(ValueError, get_transport_and_path_from_url, "foo.bar/baz")
  568. def test_error(self):
  569. # Need to use a known urlparse.uses_netloc URL scheme to get the
  570. # expected parsing of the URL on Python versions less than 2.6.5
  571. self.assertRaises(
  572. ValueError, get_transport_and_path_from_url, "prospero://bar/baz"
  573. )
  574. def test_http(self):
  575. url = "https://github.com/jelmer/dulwich"
  576. c, path = get_transport_and_path_from_url(url)
  577. self.assertIsInstance(c, HttpGitClient)
  578. self.assertEqual("https://github.com", c.get_url(b"/"))
  579. self.assertEqual("/jelmer/dulwich", path)
  580. def test_http_port(self):
  581. url = "https://github.com:9090/jelmer/dulwich"
  582. c, path = get_transport_and_path_from_url(url)
  583. self.assertEqual("https://github.com:9090", c.get_url(b"/"))
  584. self.assertIsInstance(c, HttpGitClient)
  585. self.assertEqual("/jelmer/dulwich", path)
  586. @patch("os.name", "posix")
  587. @patch("sys.platform", "linux")
  588. def test_file(self):
  589. c, path = get_transport_and_path_from_url("file:///home/jelmer/foo")
  590. self.assertIsInstance(c, LocalGitClient)
  591. self.assertEqual("/home/jelmer/foo", path)
  592. @patch("os.name", "nt")
  593. @patch("sys.platform", "win32")
  594. def test_file_win(self):
  595. # `_win32_url_to_path` uses urllib.request.url2pathname, which is set to
  596. # `ntutl2path.url2pathname` when `os.name==nt`
  597. from nturl2path import url2pathname
  598. with patch("dulwich.client.url2pathname", url2pathname):
  599. expected = "C:\\foo.bar\\baz"
  600. for file_url in [
  601. "file:C:/foo.bar/baz",
  602. "file:/C:/foo.bar/baz",
  603. "file://C:/foo.bar/baz",
  604. "file://C://foo.bar//baz",
  605. "file:///C:/foo.bar/baz",
  606. ]:
  607. c, path = get_transport_and_path(file_url)
  608. self.assertIsInstance(c, LocalGitClient)
  609. self.assertEqual(path, expected)
  610. for remote_url in [
  611. "file://host.example.com/C:/foo.bar/baz"
  612. "file://host.example.com/C:/foo.bar/baz"
  613. "file:////host.example/foo.bar/baz",
  614. ]:
  615. with self.assertRaises(NotImplementedError):
  616. c, path = get_transport_and_path(remote_url)
  617. class TestSSHVendor:
  618. def __init__(self):
  619. self.host = None
  620. self.command = ""
  621. self.username = None
  622. self.port = None
  623. self.password = None
  624. self.key_filename = None
  625. def run_command(
  626. self,
  627. host,
  628. command,
  629. username=None,
  630. port=None,
  631. password=None,
  632. key_filename=None,
  633. ssh_command=None,
  634. ):
  635. self.host = host
  636. self.command = command
  637. self.username = username
  638. self.port = port
  639. self.password = password
  640. self.key_filename = key_filename
  641. self.ssh_command = ssh_command
  642. class Subprocess:
  643. pass
  644. setattr(Subprocess, "read", lambda: None)
  645. setattr(Subprocess, "write", lambda: None)
  646. setattr(Subprocess, "close", lambda: None)
  647. setattr(Subprocess, "can_read", lambda: None)
  648. return Subprocess()
  649. class SSHGitClientTests(TestCase):
  650. def setUp(self):
  651. super().setUp()
  652. self.server = TestSSHVendor()
  653. self.real_vendor = client.get_ssh_vendor
  654. client.get_ssh_vendor = lambda: self.server
  655. self.client = SSHGitClient("git.samba.org")
  656. def tearDown(self):
  657. super().tearDown()
  658. client.get_ssh_vendor = self.real_vendor
  659. def test_get_url(self):
  660. path = "/tmp/repo.git"
  661. c = SSHGitClient("git.samba.org")
  662. url = c.get_url(path)
  663. self.assertEqual("ssh://git.samba.org/tmp/repo.git", url)
  664. def test_get_url_with_username_and_port(self):
  665. path = "/tmp/repo.git"
  666. c = SSHGitClient("git.samba.org", port=2222, username="user")
  667. url = c.get_url(path)
  668. self.assertEqual("ssh://user@git.samba.org:2222/tmp/repo.git", url)
  669. def test_default_command(self):
  670. self.assertEqual(b"git-upload-pack", self.client._get_cmd_path(b"upload-pack"))
  671. def test_alternative_command_path(self):
  672. self.client.alternative_paths[b"upload-pack"] = b"/usr/lib/git/git-upload-pack"
  673. self.assertEqual(
  674. b"/usr/lib/git/git-upload-pack",
  675. self.client._get_cmd_path(b"upload-pack"),
  676. )
  677. def test_alternative_command_path_spaces(self):
  678. self.client.alternative_paths[
  679. b"upload-pack"
  680. ] = b"/usr/lib/git/git-upload-pack -ibla"
  681. self.assertEqual(
  682. b"/usr/lib/git/git-upload-pack -ibla",
  683. self.client._get_cmd_path(b"upload-pack"),
  684. )
  685. def test_connect(self):
  686. server = self.server
  687. client = self.client
  688. client.username = b"username"
  689. client.port = 1337
  690. client._connect(b"command", b"/path/to/repo")
  691. self.assertEqual(b"username", server.username)
  692. self.assertEqual(1337, server.port)
  693. self.assertEqual("git-command '/path/to/repo'", server.command)
  694. client._connect(b"relative-command", b"/~/path/to/repo")
  695. self.assertEqual("git-relative-command '~/path/to/repo'", server.command)
  696. def test_ssh_command_precedence(self):
  697. self.overrideEnv("GIT_SSH", "/path/to/ssh")
  698. test_client = SSHGitClient("git.samba.org")
  699. self.assertEqual(test_client.ssh_command, "/path/to/ssh")
  700. self.overrideEnv("GIT_SSH_COMMAND", "/path/to/ssh -o Option=Value")
  701. test_client = SSHGitClient("git.samba.org")
  702. self.assertEqual(test_client.ssh_command, "/path/to/ssh -o Option=Value")
  703. test_client = SSHGitClient("git.samba.org", ssh_command="ssh -o Option1=Value1")
  704. self.assertEqual(test_client.ssh_command, "ssh -o Option1=Value1")
  705. class ReportStatusParserTests(TestCase):
  706. def test_invalid_pack(self):
  707. parser = ReportStatusParser()
  708. parser.handle_packet(b"unpack error - foo bar")
  709. parser.handle_packet(b"ok refs/foo/bar")
  710. parser.handle_packet(None)
  711. self.assertRaises(SendPackError, list, parser.check())
  712. def test_update_refs_error(self):
  713. parser = ReportStatusParser()
  714. parser.handle_packet(b"unpack ok")
  715. parser.handle_packet(b"ng refs/foo/bar need to pull")
  716. parser.handle_packet(None)
  717. self.assertEqual([(b"refs/foo/bar", "need to pull")], list(parser.check()))
  718. def test_ok(self):
  719. parser = ReportStatusParser()
  720. parser.handle_packet(b"unpack ok")
  721. parser.handle_packet(b"ok refs/foo/bar")
  722. parser.handle_packet(None)
  723. self.assertEqual([(b"refs/foo/bar", None)], list(parser.check()))
  724. class LocalGitClientTests(TestCase):
  725. def test_get_url(self):
  726. path = "/tmp/repo.git"
  727. c = LocalGitClient()
  728. url = c.get_url(path)
  729. self.assertEqual("file:///tmp/repo.git", url)
  730. def test_fetch_into_empty(self):
  731. c = LocalGitClient()
  732. target = tempfile.mkdtemp()
  733. self.addCleanup(shutil.rmtree, target)
  734. t = Repo.init_bare(target)
  735. s = open_repo("a.git")
  736. self.addCleanup(tear_down_repo, s)
  737. self.assertEqual(s.get_refs(), c.fetch(s.path, t).refs)
  738. def test_clone(self):
  739. c = LocalGitClient()
  740. s = open_repo("a.git")
  741. self.addCleanup(tear_down_repo, s)
  742. target = tempfile.mkdtemp()
  743. self.addCleanup(shutil.rmtree, target)
  744. result_repo = c.clone(s.path, target, mkdir=False)
  745. self.addCleanup(result_repo.close)
  746. expected = dict(s.get_refs())
  747. expected[b'refs/remotes/origin/HEAD'] = expected[b'HEAD']
  748. expected[b'refs/remotes/origin/master'] = expected[b'refs/heads/master']
  749. self.assertEqual(expected, result_repo.get_refs())
  750. def test_fetch_empty(self):
  751. c = LocalGitClient()
  752. s = open_repo("a.git")
  753. self.addCleanup(tear_down_repo, s)
  754. out = BytesIO()
  755. walker = {}
  756. ret = c.fetch_pack(
  757. s.path, lambda heads, **kwargs: [], graph_walker=walker, pack_data=out.write
  758. )
  759. self.assertEqual(
  760. {
  761. b"HEAD": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
  762. b"refs/heads/master": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
  763. b"refs/tags/mytag": b"28237f4dc30d0d462658d6b937b08a0f0b6ef55a",
  764. b"refs/tags/mytag-packed": b"b0931cadc54336e78a1d980420e3268903b57a50",
  765. },
  766. ret.refs,
  767. )
  768. self.assertEqual({b"HEAD": b"refs/heads/master"}, ret.symrefs)
  769. self.assertEqual(
  770. b"PACK\x00\x00\x00\x02\x00\x00\x00\x00\x02\x9d\x08"
  771. b"\x82;\xd8\xa8\xea\xb5\x10\xadj\xc7\\\x82<\xfd>\xd3\x1e",
  772. out.getvalue(),
  773. )
  774. def test_fetch_pack_none(self):
  775. c = LocalGitClient()
  776. s = open_repo("a.git")
  777. self.addCleanup(tear_down_repo, s)
  778. out = BytesIO()
  779. walker = MemoryRepo().get_graph_walker()
  780. ret = c.fetch_pack(
  781. s.path,
  782. lambda heads, **kwargs: [b"a90fa2d900a17e99b433217e988c4eb4a2e9a097"],
  783. graph_walker=walker,
  784. pack_data=out.write,
  785. )
  786. self.assertEqual({b"HEAD": b"refs/heads/master"}, ret.symrefs)
  787. self.assertEqual(
  788. {
  789. b"HEAD": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
  790. b"refs/heads/master": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
  791. b"refs/tags/mytag": b"28237f4dc30d0d462658d6b937b08a0f0b6ef55a",
  792. b"refs/tags/mytag-packed": b"b0931cadc54336e78a1d980420e3268903b57a50",
  793. },
  794. ret.refs,
  795. )
  796. # Hardcoding is not ideal, but we'll fix that some other day..
  797. self.assertTrue(
  798. out.getvalue().startswith(b"PACK\x00\x00\x00\x02\x00\x00\x00\x07")
  799. )
  800. def test_send_pack_without_changes(self):
  801. local = open_repo("a.git")
  802. self.addCleanup(tear_down_repo, local)
  803. target = open_repo("a.git")
  804. self.addCleanup(tear_down_repo, target)
  805. self.send_and_verify(b"master", local, target)
  806. def test_send_pack_with_changes(self):
  807. local = open_repo("a.git")
  808. self.addCleanup(tear_down_repo, local)
  809. target_path = tempfile.mkdtemp()
  810. self.addCleanup(shutil.rmtree, target_path)
  811. with Repo.init_bare(target_path) as target:
  812. self.send_and_verify(b"master", local, target)
  813. def test_get_refs(self):
  814. local = open_repo("refs.git")
  815. self.addCleanup(tear_down_repo, local)
  816. client = LocalGitClient()
  817. refs = client.get_refs(local.path)
  818. self.assertDictEqual(local.refs.as_dict(), refs)
  819. def send_and_verify(self, branch, local, target):
  820. """Send branch from local to remote repository and verify it worked."""
  821. client = LocalGitClient()
  822. ref_name = b"refs/heads/" + branch
  823. result = client.send_pack(
  824. target.path,
  825. lambda _: {ref_name: local.refs[ref_name]},
  826. local.generate_pack_data,
  827. )
  828. self.assertEqual(local.refs[ref_name], result.refs[ref_name])
  829. self.assertIs(None, result.agent)
  830. self.assertEqual({}, result.ref_status)
  831. obj_local = local.get_object(result.refs[ref_name])
  832. obj_target = target.get_object(result.refs[ref_name])
  833. self.assertEqual(obj_local, obj_target)
  834. class HttpGitClientTests(TestCase):
  835. def test_get_url(self):
  836. base_url = "https://github.com/jelmer/dulwich"
  837. path = "/jelmer/dulwich"
  838. c = HttpGitClient(base_url)
  839. url = c.get_url(path)
  840. self.assertEqual("https://github.com/jelmer/dulwich", url)
  841. def test_get_url_bytes_path(self):
  842. base_url = "https://github.com/jelmer/dulwich"
  843. path_bytes = b"/jelmer/dulwich"
  844. c = HttpGitClient(base_url)
  845. url = c.get_url(path_bytes)
  846. self.assertEqual("https://github.com/jelmer/dulwich", url)
  847. def test_get_url_with_username_and_passwd(self):
  848. base_url = "https://github.com/jelmer/dulwich"
  849. path = "/jelmer/dulwich"
  850. c = HttpGitClient(base_url, username="USERNAME", password="PASSWD")
  851. url = c.get_url(path)
  852. self.assertEqual("https://github.com/jelmer/dulwich", url)
  853. def test_init_username_passwd_set(self):
  854. url = "https://github.com/jelmer/dulwich"
  855. c = HttpGitClient(url, config=None, username="user", password="passwd")
  856. self.assertEqual("user", c._username)
  857. self.assertEqual("passwd", c._password)
  858. basic_auth = c.pool_manager.headers["authorization"]
  859. auth_string = "{}:{}".format("user", "passwd")
  860. b64_credentials = base64.b64encode(auth_string.encode("latin1"))
  861. expected_basic_auth = "Basic %s" % b64_credentials.decode("latin1")
  862. self.assertEqual(basic_auth, expected_basic_auth)
  863. def test_init_username_set_no_password(self):
  864. url = "https://github.com/jelmer/dulwich"
  865. c = HttpGitClient(url, config=None, username="user")
  866. self.assertEqual("user", c._username)
  867. self.assertIs(c._password, None)
  868. basic_auth = c.pool_manager.headers["authorization"]
  869. auth_string = b"user:"
  870. b64_credentials = base64.b64encode(auth_string)
  871. expected_basic_auth = f"Basic {b64_credentials.decode('ascii')}"
  872. self.assertEqual(basic_auth, expected_basic_auth)
  873. def test_init_no_username_passwd(self):
  874. url = "https://github.com/jelmer/dulwich"
  875. c = HttpGitClient(url, config=None)
  876. self.assertIs(None, c._username)
  877. self.assertIs(None, c._password)
  878. self.assertNotIn("authorization", c.pool_manager.headers)
  879. def test_from_parsedurl_username_only(self):
  880. username = "user"
  881. url = f"https://{username}@github.com/jelmer/dulwich"
  882. c = HttpGitClient.from_parsedurl(urlparse(url))
  883. self.assertEqual(c._username, username)
  884. self.assertEqual(c._password, None)
  885. basic_auth = c.pool_manager.headers["authorization"]
  886. auth_string = username.encode('ascii') + b":"
  887. b64_credentials = base64.b64encode(auth_string)
  888. expected_basic_auth = f"Basic {b64_credentials.decode('ascii')}"
  889. self.assertEqual(basic_auth, expected_basic_auth)
  890. def test_from_parsedurl_on_url_with_quoted_credentials(self):
  891. original_username = "john|the|first"
  892. quoted_username = urlquote(original_username)
  893. original_password = "Ya#1$2%3"
  894. quoted_password = urlquote(original_password)
  895. url = "https://{username}:{password}@github.com/jelmer/dulwich".format(
  896. username=quoted_username, password=quoted_password
  897. )
  898. c = HttpGitClient.from_parsedurl(urlparse(url))
  899. self.assertEqual(original_username, c._username)
  900. self.assertEqual(original_password, c._password)
  901. basic_auth = c.pool_manager.headers["authorization"]
  902. auth_string = "{}:{}".format(original_username, original_password)
  903. b64_credentials = base64.b64encode(auth_string.encode("latin1"))
  904. expected_basic_auth = "Basic %s" % b64_credentials.decode("latin1")
  905. self.assertEqual(basic_auth, expected_basic_auth)
  906. def test_url_redirect_location(self):
  907. from urllib3.response import HTTPResponse
  908. test_data = {
  909. "https://gitlab.com/inkscape/inkscape/": {
  910. "redirect_url": "https://gitlab.com/inkscape/inkscape.git/",
  911. "refs_data": (
  912. b"001e# service=git-upload-pack\n00000032"
  913. b"fb2bebf4919a011f0fd7cec085443d0031228e76 "
  914. b"HEAD\n0000"
  915. ),
  916. },
  917. "https://github.com/jelmer/dulwich/": {
  918. "redirect_url": "https://github.com/jelmer/dulwich/",
  919. "refs_data": (
  920. b"001e# service=git-upload-pack\n00000032"
  921. b"3ff25e09724aa4d86ea5bca7d5dd0399a3c8bfcf "
  922. b"HEAD\n0000"
  923. ),
  924. },
  925. }
  926. tail = "info/refs?service=git-upload-pack"
  927. # we need to mock urllib3.PoolManager as this test will fail
  928. # otherwise without an active internet connection
  929. class PoolManagerMock:
  930. def __init__(self):
  931. self.headers = {}
  932. def request(self, method, url, fields=None, headers=None, redirect=True, preload_content=True):
  933. base_url = url[: -len(tail)]
  934. redirect_base_url = test_data[base_url]["redirect_url"]
  935. redirect_url = redirect_base_url + tail
  936. headers = {
  937. "Content-Type": "application/x-git-upload-pack-advertisement"
  938. }
  939. body = test_data[base_url]["refs_data"]
  940. # urllib3 handles automatic redirection by default
  941. status = 200
  942. request_url = redirect_url
  943. # simulate urllib3 behavior when redirect parameter is False
  944. if redirect is False:
  945. request_url = url
  946. if redirect_base_url != base_url:
  947. body = b""
  948. headers["location"] = redirect_url
  949. status = 301
  950. return HTTPResponse(
  951. body=BytesIO(body),
  952. headers=headers,
  953. request_method=method,
  954. request_url=request_url,
  955. preload_content=preload_content,
  956. status=status,
  957. )
  958. pool_manager = PoolManagerMock()
  959. for base_url in test_data.keys():
  960. # instantiate HttpGitClient with mocked pool manager
  961. c = HttpGitClient(base_url, pool_manager=pool_manager, config=None)
  962. # call method that detects url redirection
  963. _, _, processed_url = c._discover_references(b"git-upload-pack", base_url)
  964. # send the same request as the method above without redirection
  965. resp = c.pool_manager.request("GET", base_url + tail, redirect=False)
  966. # check expected behavior of urllib3
  967. redirect_location = resp.get_redirect_location()
  968. if resp.status == 200:
  969. self.assertFalse(redirect_location)
  970. if redirect_location:
  971. # check that url redirection has been correctly detected
  972. self.assertEqual(processed_url, redirect_location[: -len(tail)])
  973. else:
  974. # check also the no redirection case
  975. self.assertEqual(processed_url, base_url)
  976. class TCPGitClientTests(TestCase):
  977. def test_get_url(self):
  978. host = "github.com"
  979. path = "/jelmer/dulwich"
  980. c = TCPGitClient(host)
  981. url = c.get_url(path)
  982. self.assertEqual("git://github.com/jelmer/dulwich", url)
  983. def test_get_url_with_port(self):
  984. host = "github.com"
  985. path = "/jelmer/dulwich"
  986. port = 9090
  987. c = TCPGitClient(host, port=port)
  988. url = c.get_url(path)
  989. self.assertEqual("git://github.com:9090/jelmer/dulwich", url)
  990. class DefaultUrllib3ManagerTest(TestCase):
  991. def test_no_config(self):
  992. manager = default_urllib3_manager(config=None)
  993. self.assertEqual(manager.connection_pool_kw["cert_reqs"], "CERT_REQUIRED")
  994. def test_config_no_proxy(self):
  995. import urllib3
  996. manager = default_urllib3_manager(config=ConfigDict())
  997. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  998. self.assertIsInstance(manager, urllib3.PoolManager)
  999. def test_config_no_proxy_custom_cls(self):
  1000. import urllib3
  1001. class CustomPoolManager(urllib3.PoolManager):
  1002. pass
  1003. manager = default_urllib3_manager(
  1004. config=ConfigDict(), pool_manager_cls=CustomPoolManager
  1005. )
  1006. self.assertIsInstance(manager, CustomPoolManager)
  1007. def test_config_ssl(self):
  1008. config = ConfigDict()
  1009. config.set(b"http", b"sslVerify", b"true")
  1010. manager = default_urllib3_manager(config=config)
  1011. self.assertEqual(manager.connection_pool_kw["cert_reqs"], "CERT_REQUIRED")
  1012. def test_config_no_ssl(self):
  1013. config = ConfigDict()
  1014. config.set(b"http", b"sslVerify", b"false")
  1015. manager = default_urllib3_manager(config=config)
  1016. self.assertEqual(manager.connection_pool_kw["cert_reqs"], "CERT_NONE")
  1017. def test_config_proxy(self):
  1018. import urllib3
  1019. config = ConfigDict()
  1020. config.set(b"http", b"proxy", b"http://localhost:3128/")
  1021. manager = default_urllib3_manager(config=config)
  1022. self.assertIsInstance(manager, urllib3.ProxyManager)
  1023. self.assertTrue(hasattr(manager, "proxy"))
  1024. self.assertEqual(manager.proxy.scheme, "http")
  1025. self.assertEqual(manager.proxy.host, "localhost")
  1026. self.assertEqual(manager.proxy.port, 3128)
  1027. def test_environment_proxy(self):
  1028. import urllib3
  1029. config = ConfigDict()
  1030. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1031. manager = default_urllib3_manager(config=config)
  1032. self.assertIsInstance(manager, urllib3.ProxyManager)
  1033. self.assertTrue(hasattr(manager, "proxy"))
  1034. self.assertEqual(manager.proxy.scheme, "http")
  1035. self.assertEqual(manager.proxy.host, "myproxy")
  1036. self.assertEqual(manager.proxy.port, 8080)
  1037. def test_environment_empty_proxy(self):
  1038. import urllib3
  1039. config = ConfigDict()
  1040. self.overrideEnv("http_proxy", "")
  1041. manager = default_urllib3_manager(config=config)
  1042. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  1043. self.assertIsInstance(manager, urllib3.PoolManager)
  1044. def test_environment_no_proxy_1(self):
  1045. import urllib3
  1046. config = ConfigDict()
  1047. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1048. self.overrideEnv("no_proxy", "xyz,abc.def.gh,abc.gh")
  1049. base_url = "http://xyz.abc.def.gh:8080/path/port"
  1050. manager = default_urllib3_manager(config=config, base_url=base_url)
  1051. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  1052. self.assertIsInstance(manager, urllib3.PoolManager)
  1053. def test_environment_no_proxy_2(self):
  1054. import urllib3
  1055. config = ConfigDict()
  1056. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1057. self.overrideEnv("no_proxy", "xyz,abc.def.gh,abc.gh,ample.com")
  1058. base_url = "http://ample.com/path/port"
  1059. manager = default_urllib3_manager(config=config, base_url=base_url)
  1060. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  1061. self.assertIsInstance(manager, urllib3.PoolManager)
  1062. def test_environment_no_proxy_3(self):
  1063. import urllib3
  1064. config = ConfigDict()
  1065. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1066. self.overrideEnv("no_proxy", "xyz,abc.def.gh,abc.gh,ample.com")
  1067. base_url = "http://ample.com:80/path/port"
  1068. manager = default_urllib3_manager(config=config, base_url=base_url)
  1069. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  1070. self.assertIsInstance(manager, urllib3.PoolManager)
  1071. def test_environment_no_proxy_4(self):
  1072. import urllib3
  1073. config = ConfigDict()
  1074. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1075. self.overrideEnv("no_proxy", "xyz,abc.def.gh,abc.gh,ample.com")
  1076. base_url = "http://www.ample.com/path/port"
  1077. manager = default_urllib3_manager(config=config, base_url=base_url)
  1078. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  1079. self.assertIsInstance(manager, urllib3.PoolManager)
  1080. def test_environment_no_proxy_5(self):
  1081. import urllib3
  1082. config = ConfigDict()
  1083. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1084. self.overrideEnv("no_proxy", "xyz,abc.def.gh,abc.gh,ample.com")
  1085. base_url = "http://www.example.com/path/port"
  1086. manager = default_urllib3_manager(config=config, base_url=base_url)
  1087. self.assertIsInstance(manager, urllib3.ProxyManager)
  1088. self.assertTrue(hasattr(manager, "proxy"))
  1089. self.assertEqual(manager.proxy.scheme, "http")
  1090. self.assertEqual(manager.proxy.host, "myproxy")
  1091. self.assertEqual(manager.proxy.port, 8080)
  1092. def test_environment_no_proxy_6(self):
  1093. import urllib3
  1094. config = ConfigDict()
  1095. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1096. self.overrideEnv("no_proxy", "xyz,abc.def.gh,abc.gh,ample.com")
  1097. base_url = "http://ample.com.org/path/port"
  1098. manager = default_urllib3_manager(config=config, base_url=base_url)
  1099. self.assertIsInstance(manager, urllib3.ProxyManager)
  1100. self.assertTrue(hasattr(manager, "proxy"))
  1101. self.assertEqual(manager.proxy.scheme, "http")
  1102. self.assertEqual(manager.proxy.host, "myproxy")
  1103. self.assertEqual(manager.proxy.port, 8080)
  1104. def test_environment_no_proxy_ipv4_address_1(self):
  1105. import urllib3
  1106. config = ConfigDict()
  1107. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1108. self.overrideEnv("no_proxy", "xyz,abc.def.gh,192.168.0.10,ample.com")
  1109. base_url = "http://192.168.0.10/path/port"
  1110. manager = default_urllib3_manager(config=config, base_url=base_url)
  1111. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  1112. self.assertIsInstance(manager, urllib3.PoolManager)
  1113. def test_environment_no_proxy_ipv4_address_2(self):
  1114. import urllib3
  1115. config = ConfigDict()
  1116. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1117. self.overrideEnv("no_proxy", "xyz,abc.def.gh,192.168.0.10,ample.com")
  1118. base_url = "http://192.168.0.10:8888/path/port"
  1119. manager = default_urllib3_manager(config=config, base_url=base_url)
  1120. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  1121. self.assertIsInstance(manager, urllib3.PoolManager)
  1122. def test_environment_no_proxy_ipv4_address_3(self):
  1123. import urllib3
  1124. config = ConfigDict()
  1125. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1126. self.overrideEnv("no_proxy", "xyz,abc.def.gh,ff80:1::/64,192.168.0.0/24,ample.com")
  1127. base_url = "http://192.168.0.10/path/port"
  1128. manager = default_urllib3_manager(config=config, base_url=base_url)
  1129. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  1130. self.assertIsInstance(manager, urllib3.PoolManager)
  1131. def test_environment_no_proxy_ipv6_address_1(self):
  1132. import urllib3
  1133. config = ConfigDict()
  1134. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1135. self.overrideEnv("no_proxy", "xyz,abc.def.gh,ff80:1::affe,ample.com")
  1136. base_url = "http://[ff80:1::affe]/path/port"
  1137. manager = default_urllib3_manager(config=config, base_url=base_url)
  1138. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  1139. self.assertIsInstance(manager, urllib3.PoolManager)
  1140. def test_environment_no_proxy_ipv6_address_2(self):
  1141. import urllib3
  1142. config = ConfigDict()
  1143. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1144. self.overrideEnv("no_proxy", "xyz,abc.def.gh,ff80:1::affe,ample.com")
  1145. base_url = "http://[ff80:1::affe]:1234/path/port"
  1146. manager = default_urllib3_manager(config=config, base_url=base_url)
  1147. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  1148. self.assertIsInstance(manager, urllib3.PoolManager)
  1149. def test_environment_no_proxy_ipv6_address_3(self):
  1150. import urllib3
  1151. config = ConfigDict()
  1152. self.overrideEnv("http_proxy", "http://myproxy:8080")
  1153. self.overrideEnv("no_proxy", "xyz,abc.def.gh,192.168.0.0/24,ff80:1::/64,ample.com")
  1154. base_url = "http://[ff80:1::affe]/path/port"
  1155. manager = default_urllib3_manager(config=config, base_url=base_url)
  1156. self.assertNotIsInstance(manager, urllib3.ProxyManager)
  1157. self.assertIsInstance(manager, urllib3.PoolManager)
  1158. def test_config_proxy_custom_cls(self):
  1159. import urllib3
  1160. class CustomProxyManager(urllib3.ProxyManager):
  1161. pass
  1162. config = ConfigDict()
  1163. config.set(b"http", b"proxy", b"http://localhost:3128/")
  1164. manager = default_urllib3_manager(
  1165. config=config, proxy_manager_cls=CustomProxyManager
  1166. )
  1167. self.assertIsInstance(manager, CustomProxyManager)
  1168. def test_config_no_verify_ssl(self):
  1169. manager = default_urllib3_manager(config=None, cert_reqs="CERT_NONE")
  1170. self.assertEqual(manager.connection_pool_kw["cert_reqs"], "CERT_NONE")
  1171. class SubprocessSSHVendorTests(TestCase):
  1172. def setUp(self):
  1173. # Monkey Patch client subprocess popen
  1174. self._orig_popen = dulwich.client.subprocess.Popen
  1175. dulwich.client.subprocess.Popen = DummyPopen
  1176. def tearDown(self):
  1177. dulwich.client.subprocess.Popen = self._orig_popen
  1178. def test_run_command_dashes(self):
  1179. vendor = SubprocessSSHVendor()
  1180. self.assertRaises(
  1181. StrangeHostname,
  1182. vendor.run_command,
  1183. "--weird-host",
  1184. "git-clone-url",
  1185. )
  1186. def test_run_command_password(self):
  1187. vendor = SubprocessSSHVendor()
  1188. self.assertRaises(
  1189. NotImplementedError,
  1190. vendor.run_command,
  1191. "host",
  1192. "git-clone-url",
  1193. password="12345",
  1194. )
  1195. def test_run_command_password_and_privkey(self):
  1196. vendor = SubprocessSSHVendor()
  1197. self.assertRaises(
  1198. NotImplementedError,
  1199. vendor.run_command,
  1200. "host",
  1201. "git-clone-url",
  1202. password="12345",
  1203. key_filename="/tmp/id_rsa",
  1204. )
  1205. def test_run_command_with_port_username_and_privkey(self):
  1206. expected = [
  1207. "ssh",
  1208. "-x",
  1209. "-p",
  1210. "2200",
  1211. "-i",
  1212. "/tmp/id_rsa",
  1213. "user@host",
  1214. "git-clone-url",
  1215. ]
  1216. vendor = SubprocessSSHVendor()
  1217. command = vendor.run_command(
  1218. "host",
  1219. "git-clone-url",
  1220. username="user",
  1221. port="2200",
  1222. key_filename="/tmp/id_rsa",
  1223. )
  1224. args = command.proc.args
  1225. self.assertListEqual(expected, args[0])
  1226. def test_run_with_ssh_command(self):
  1227. expected = [
  1228. "/path/to/ssh",
  1229. "-o",
  1230. "Option=Value",
  1231. "-x",
  1232. "host",
  1233. "git-clone-url",
  1234. ]
  1235. vendor = SubprocessSSHVendor()
  1236. command = vendor.run_command(
  1237. "host",
  1238. "git-clone-url",
  1239. ssh_command="/path/to/ssh -o Option=Value",
  1240. )
  1241. args = command.proc.args
  1242. self.assertListEqual(expected, args[0])
  1243. class PLinkSSHVendorTests(TestCase):
  1244. def setUp(self):
  1245. # Monkey Patch client subprocess popen
  1246. self._orig_popen = dulwich.client.subprocess.Popen
  1247. dulwich.client.subprocess.Popen = DummyPopen
  1248. def tearDown(self):
  1249. dulwich.client.subprocess.Popen = self._orig_popen
  1250. def test_run_command_dashes(self):
  1251. vendor = PLinkSSHVendor()
  1252. self.assertRaises(
  1253. StrangeHostname,
  1254. vendor.run_command,
  1255. "--weird-host",
  1256. "git-clone-url",
  1257. )
  1258. def test_run_command_password_and_privkey(self):
  1259. vendor = PLinkSSHVendor()
  1260. warnings.simplefilter("always", UserWarning)
  1261. self.addCleanup(warnings.resetwarnings)
  1262. warnings_list, restore_warnings = setup_warning_catcher()
  1263. self.addCleanup(restore_warnings)
  1264. command = vendor.run_command(
  1265. "host",
  1266. "git-clone-url",
  1267. password="12345",
  1268. key_filename="/tmp/id_rsa",
  1269. )
  1270. expected_warning = UserWarning(
  1271. "Invoking PLink with a password exposes the password in the "
  1272. "process list."
  1273. )
  1274. for w in warnings_list:
  1275. if type(w) == type(expected_warning) and w.args == expected_warning.args:
  1276. break
  1277. else:
  1278. raise AssertionError(
  1279. "Expected warning {!r} not in {!r}".format(expected_warning, warnings_list)
  1280. )
  1281. args = command.proc.args
  1282. if sys.platform == "win32":
  1283. binary = ["plink.exe", "-ssh"]
  1284. else:
  1285. binary = ["plink", "-ssh"]
  1286. expected = binary + [
  1287. "-pw",
  1288. "12345",
  1289. "-i",
  1290. "/tmp/id_rsa",
  1291. "host",
  1292. "git-clone-url",
  1293. ]
  1294. self.assertListEqual(expected, args[0])
  1295. def test_run_command_password(self):
  1296. if sys.platform == "win32":
  1297. binary = ["plink.exe", "-ssh"]
  1298. else:
  1299. binary = ["plink", "-ssh"]
  1300. expected = binary + ["-pw", "12345", "host", "git-clone-url"]
  1301. vendor = PLinkSSHVendor()
  1302. warnings.simplefilter("always", UserWarning)
  1303. self.addCleanup(warnings.resetwarnings)
  1304. warnings_list, restore_warnings = setup_warning_catcher()
  1305. self.addCleanup(restore_warnings)
  1306. command = vendor.run_command("host", "git-clone-url", password="12345")
  1307. expected_warning = UserWarning(
  1308. "Invoking PLink with a password exposes the password in the "
  1309. "process list."
  1310. )
  1311. for w in warnings_list:
  1312. if type(w) == type(expected_warning) and w.args == expected_warning.args:
  1313. break
  1314. else:
  1315. raise AssertionError(
  1316. "Expected warning {!r} not in {!r}".format(expected_warning, warnings_list)
  1317. )
  1318. args = command.proc.args
  1319. self.assertListEqual(expected, args[0])
  1320. def test_run_command_with_port_username_and_privkey(self):
  1321. if sys.platform == "win32":
  1322. binary = ["plink.exe", "-ssh"]
  1323. else:
  1324. binary = ["plink", "-ssh"]
  1325. expected = binary + [
  1326. "-P",
  1327. "2200",
  1328. "-i",
  1329. "/tmp/id_rsa",
  1330. "user@host",
  1331. "git-clone-url",
  1332. ]
  1333. vendor = PLinkSSHVendor()
  1334. command = vendor.run_command(
  1335. "host",
  1336. "git-clone-url",
  1337. username="user",
  1338. port="2200",
  1339. key_filename="/tmp/id_rsa",
  1340. )
  1341. args = command.proc.args
  1342. self.assertListEqual(expected, args[0])
  1343. def test_run_with_ssh_command(self):
  1344. expected = [
  1345. "/path/to/plink",
  1346. "-x",
  1347. "host",
  1348. "git-clone-url",
  1349. ]
  1350. vendor = SubprocessSSHVendor()
  1351. command = vendor.run_command(
  1352. "host",
  1353. "git-clone-url",
  1354. ssh_command="/path/to/plink",
  1355. )
  1356. args = command.proc.args
  1357. self.assertListEqual(expected, args[0])
  1358. class RsyncUrlTests(TestCase):
  1359. def test_simple(self):
  1360. self.assertEqual(parse_rsync_url("foo:bar/path"), (None, "foo", "bar/path"))
  1361. self.assertEqual(
  1362. parse_rsync_url("user@foo:bar/path"), ("user", "foo", "bar/path")
  1363. )
  1364. def test_path(self):
  1365. self.assertRaises(ValueError, parse_rsync_url, "/path")
  1366. class CheckWantsTests(TestCase):
  1367. def test_fine(self):
  1368. check_wants(
  1369. [b"2f3dc7a53fb752a6961d3a56683df46d4d3bf262"],
  1370. {b"refs/heads/blah": b"2f3dc7a53fb752a6961d3a56683df46d4d3bf262"},
  1371. )
  1372. def test_missing(self):
  1373. self.assertRaises(
  1374. InvalidWants,
  1375. check_wants,
  1376. [b"2f3dc7a53fb752a6961d3a56683df46d4d3bf262"],
  1377. {b"refs/heads/blah": b"3f3dc7a53fb752a6961d3a56683df46d4d3bf262"},
  1378. )
  1379. def test_annotated(self):
  1380. self.assertRaises(
  1381. InvalidWants,
  1382. check_wants,
  1383. [b"2f3dc7a53fb752a6961d3a56683df46d4d3bf262"],
  1384. {
  1385. b"refs/heads/blah": b"3f3dc7a53fb752a6961d3a56683df46d4d3bf262",
  1386. b"refs/heads/blah^{}": b"2f3dc7a53fb752a6961d3a56683df46d4d3bf262",
  1387. },
  1388. )
  1389. class FetchPackResultTests(TestCase):
  1390. def test_eq(self):
  1391. self.assertEqual(
  1392. FetchPackResult(
  1393. {b"refs/heads/master": b"2f3dc7a53fb752a6961d3a56683df46d4d3bf262"},
  1394. {},
  1395. b"user/agent",
  1396. ),
  1397. FetchPackResult(
  1398. {b"refs/heads/master": b"2f3dc7a53fb752a6961d3a56683df46d4d3bf262"},
  1399. {},
  1400. b"user/agent",
  1401. ),
  1402. )
  1403. class GitCredentialStoreTests(TestCase):
  1404. @classmethod
  1405. def setUpClass(cls):
  1406. with tempfile.NamedTemporaryFile(delete=False) as f:
  1407. f.write(b"https://user:pass@example.org\n")
  1408. cls.fname = f.name
  1409. @classmethod
  1410. def tearDownClass(cls):
  1411. os.unlink(cls.fname)
  1412. def test_nonmatching_scheme(self):
  1413. self.assertEqual(
  1414. get_credentials_from_store(b"http", b"example.org", fnames=[self.fname]),
  1415. None,
  1416. )
  1417. def test_nonmatching_hostname(self):
  1418. self.assertEqual(
  1419. get_credentials_from_store(b"https", b"noentry.org", fnames=[self.fname]),
  1420. None,
  1421. )
  1422. def test_match_without_username(self):
  1423. self.assertEqual(
  1424. get_credentials_from_store(b"https", b"example.org", fnames=[self.fname]),
  1425. (b"user", b"pass"),
  1426. )
  1427. def test_match_with_matching_username(self):
  1428. self.assertEqual(
  1429. get_credentials_from_store(
  1430. b"https", b"example.org", b"user", fnames=[self.fname]
  1431. ),
  1432. (b"user", b"pass"),
  1433. )
  1434. def test_no_match_with_nonmatching_username(self):
  1435. self.assertEqual(
  1436. get_credentials_from_store(
  1437. b"https", b"example.org", b"otheruser", fnames=[self.fname]
  1438. ),
  1439. None,
  1440. )
  1441. class RemoteErrorFromStderrTests(TestCase):
  1442. def test_nothing(self):
  1443. self.assertEqual(_remote_error_from_stderr(None), HangupException())
  1444. def test_error_line(self):
  1445. b = BytesIO(
  1446. b"""\
  1447. This is some random output.
  1448. ERROR: This is the actual error
  1449. with a tail
  1450. """
  1451. )
  1452. self.assertEqual(
  1453. _remote_error_from_stderr(b),
  1454. GitProtocolError("This is the actual error"),
  1455. )
  1456. def test_no_error_line(self):
  1457. b = BytesIO(
  1458. b"""\
  1459. This is output without an error line.
  1460. And this line is just random noise, too.
  1461. """
  1462. )
  1463. self.assertEqual(
  1464. _remote_error_from_stderr(b),
  1465. HangupException(
  1466. [
  1467. b"This is output without an error line.",
  1468. b"And this line is just random noise, too.",
  1469. ]
  1470. ),
  1471. )