test_patch.py 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117
  1. # test_patch.py -- tests for patch.py
  2. # Copyright (C) 2010 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as published by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Tests for patch.py."""
  22. from io import BytesIO, StringIO
  23. from typing import NoReturn
  24. from dulwich.object_store import MemoryObjectStore
  25. from dulwich.objects import S_IFGITLINK, ZERO_SHA, Blob, Commit, Tree
  26. from dulwich.patch import (
  27. DiffAlgorithmNotAvailable,
  28. commit_patch_id,
  29. get_summary,
  30. git_am_patch_split,
  31. patch_id,
  32. unified_diff_with_algorithm,
  33. write_blob_diff,
  34. write_commit_patch,
  35. write_object_diff,
  36. write_tree_diff,
  37. )
  38. from dulwich.tests.utils import make_commit
  39. from . import DependencyMissing, SkipTest, TestCase
  40. class WriteCommitPatchTests(TestCase):
  41. def test_simple_bytesio(self) -> None:
  42. f = BytesIO()
  43. c = make_commit(
  44. author=b"Jelmer <jelmer@samba.org>",
  45. committer=b"Jelmer <jelmer@samba.org>",
  46. author_time=1271350201,
  47. commit_time=1271350201,
  48. author_timezone=0,
  49. commit_timezone=0,
  50. message=b"This is the first line\nAnd this is the second line.\n",
  51. tree=Tree().id,
  52. )
  53. write_commit_patch(f, c, b"CONTENTS", (1, 1), version="custom")
  54. f.seek(0)
  55. lines = f.readlines()
  56. self.assertTrue(
  57. lines[0].startswith(b"From 0b0d34d1b5b596c928adc9a727a4b9e03d025298")
  58. )
  59. self.assertEqual(lines[1], b"From: Jelmer <jelmer@samba.org>\n")
  60. self.assertTrue(lines[2].startswith(b"Date: "))
  61. self.assertEqual(
  62. [
  63. b"Subject: [PATCH 1/1] This is the first line\n",
  64. b"And this is the second line.\n",
  65. b"\n",
  66. b"\n",
  67. b"---\n",
  68. ],
  69. lines[3:8],
  70. )
  71. self.assertEqual([b"CONTENTS-- \n", b"custom\n"], lines[-2:])
  72. if len(lines) >= 12:
  73. # diffstat may not be present
  74. self.assertEqual(lines[8], b" 0 files changed\n")
  75. class ReadGitAmPatch(TestCase):
  76. def test_extract_string(self) -> None:
  77. text = b"""\
  78. From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
  79. From: Jelmer Vernooij <jelmer@samba.org>
  80. Date: Thu, 15 Apr 2010 15:40:28 +0200
  81. Subject: [PATCH 1/2] Remove executable bit from prey.ico (triggers a warning).
  82. ---
  83. pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  84. 1 files changed, 0 insertions(+), 0 deletions(-)
  85. mode change 100755 => 100644 pixmaps/prey.ico
  86. --
  87. 1.7.0.4
  88. """
  89. c, diff, version = git_am_patch_split(StringIO(text.decode("utf-8")), "utf-8")
  90. self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.committer)
  91. self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.author)
  92. self.assertEqual(
  93. b"Remove executable bit from prey.ico (triggers a warning).\n",
  94. c.message,
  95. )
  96. self.assertEqual(
  97. b""" pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  98. 1 files changed, 0 insertions(+), 0 deletions(-)
  99. mode change 100755 => 100644 pixmaps/prey.ico
  100. """,
  101. diff,
  102. )
  103. self.assertEqual(b"1.7.0.4", version)
  104. def test_extract_bytes(self) -> None:
  105. text = b"""\
  106. From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
  107. From: Jelmer Vernooij <jelmer@samba.org>
  108. Date: Thu, 15 Apr 2010 15:40:28 +0200
  109. Subject: [PATCH 1/2] Remove executable bit from prey.ico (triggers a warning).
  110. ---
  111. pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  112. 1 files changed, 0 insertions(+), 0 deletions(-)
  113. mode change 100755 => 100644 pixmaps/prey.ico
  114. --
  115. 1.7.0.4
  116. """
  117. c, diff, version = git_am_patch_split(BytesIO(text))
  118. self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.committer)
  119. self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.author)
  120. self.assertEqual(
  121. b"Remove executable bit from prey.ico (triggers a warning).\n",
  122. c.message,
  123. )
  124. self.assertEqual(
  125. b""" pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  126. 1 files changed, 0 insertions(+), 0 deletions(-)
  127. mode change 100755 => 100644 pixmaps/prey.ico
  128. """,
  129. diff,
  130. )
  131. self.assertEqual(b"1.7.0.4", version)
  132. def test_extract_spaces(self) -> None:
  133. text = b"""From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
  134. From: Jelmer Vernooij <jelmer@samba.org>
  135. Date: Thu, 15 Apr 2010 15:40:28 +0200
  136. Subject: [Dulwich-users] [PATCH] Added unit tests for
  137. dulwich.object_store.tree_lookup_path.
  138. * dulwich/tests/test_object_store.py
  139. (TreeLookupPathTests): This test case contains a few tests that ensure the
  140. tree_lookup_path function works as expected.
  141. ---
  142. pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  143. 1 files changed, 0 insertions(+), 0 deletions(-)
  144. mode change 100755 => 100644 pixmaps/prey.ico
  145. --
  146. 1.7.0.4
  147. """
  148. c, _diff, _version = git_am_patch_split(BytesIO(text), "utf-8")
  149. self.assertEqual(
  150. b"""\
  151. Added unit tests for dulwich.object_store.tree_lookup_path.
  152. * dulwich/tests/test_object_store.py
  153. (TreeLookupPathTests): This test case contains a few tests that ensure the
  154. tree_lookup_path function works as expected.
  155. """,
  156. c.message,
  157. )
  158. def test_extract_pseudo_from_header(self) -> None:
  159. text = b"""From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
  160. From: Jelmer Vernooij <jelmer@samba.org>
  161. Date: Thu, 15 Apr 2010 15:40:28 +0200
  162. Subject: [Dulwich-users] [PATCH] Added unit tests for
  163. dulwich.object_store.tree_lookup_path.
  164. From: Jelmer Vernooij <jelmer@debian.org>
  165. * dulwich/tests/test_object_store.py
  166. (TreeLookupPathTests): This test case contains a few tests that ensure the
  167. tree_lookup_path function works as expected.
  168. ---
  169. pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  170. 1 files changed, 0 insertions(+), 0 deletions(-)
  171. mode change 100755 => 100644 pixmaps/prey.ico
  172. --
  173. 1.7.0.4
  174. """
  175. c, _diff, _version = git_am_patch_split(BytesIO(text), "utf-8")
  176. self.assertEqual(b"Jelmer Vernooij <jelmer@debian.org>", c.author)
  177. self.assertEqual(
  178. b"""\
  179. Added unit tests for dulwich.object_store.tree_lookup_path.
  180. * dulwich/tests/test_object_store.py
  181. (TreeLookupPathTests): This test case contains a few tests that ensure the
  182. tree_lookup_path function works as expected.
  183. """,
  184. c.message,
  185. )
  186. def test_extract_no_version_tail(self) -> None:
  187. text = b"""\
  188. From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
  189. From: Jelmer Vernooij <jelmer@samba.org>
  190. Date: Thu, 15 Apr 2010 15:40:28 +0200
  191. Subject: [Dulwich-users] [PATCH] Added unit tests for
  192. dulwich.object_store.tree_lookup_path.
  193. From: Jelmer Vernooij <jelmer@debian.org>
  194. ---
  195. pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  196. 1 files changed, 0 insertions(+), 0 deletions(-)
  197. mode change 100755 => 100644 pixmaps/prey.ico
  198. """
  199. _c, _diff, version = git_am_patch_split(BytesIO(text), "utf-8")
  200. self.assertEqual(None, version)
  201. def test_extract_mercurial(self) -> NoReturn:
  202. raise SkipTest(
  203. "git_am_patch_split doesn't handle Mercurial patches properly yet"
  204. )
  205. expected_diff = """\
  206. diff --git a/dulwich/tests/test_patch.py b/dulwich/tests/test_patch.py
  207. --- a/dulwich/tests/test_patch.py
  208. +++ b/dulwich/tests/test_patch.py
  209. @@ -158,7 +158,7 @@
  210. '''
  211. c, diff, version = git_am_patch_split(BytesIO(text))
  212. - self.assertIs(None, version)
  213. + self.assertEqual(None, version)
  214. class DiffTests(TestCase):
  215. """
  216. text = f"""\
  217. From dulwich-users-bounces+jelmer=samba.org@lists.launchpad.net \
  218. Mon Nov 29 00:58:18 2010
  219. Date: Sun, 28 Nov 2010 17:57:27 -0600
  220. From: Augie Fackler <durin42@gmail.com>
  221. To: dulwich-users <dulwich-users@lists.launchpad.net>
  222. Subject: [Dulwich-users] [PATCH] test_patch: fix tests on Python 2.6
  223. Content-Transfer-Encoding: 8bit
  224. Change-Id: I5e51313d4ae3a65c3f00c665002a7489121bb0d6
  225. {expected_diff}
  226. _______________________________________________
  227. Mailing list: https://launchpad.net/~dulwich-users
  228. Post to : dulwich-users@lists.launchpad.net
  229. Unsubscribe : https://launchpad.net/~dulwich-users
  230. More help : https://help.launchpad.net/ListHelp
  231. """
  232. _c, diff, version = git_am_patch_split(BytesIO(text))
  233. self.assertEqual(expected_diff, diff)
  234. self.assertEqual(None, version)
  235. class DiffTests(TestCase):
  236. """Tests for write_blob_diff and write_tree_diff."""
  237. def test_blob_diff(self) -> None:
  238. f = BytesIO()
  239. write_blob_diff(
  240. f,
  241. (b"foo.txt", 0o644, Blob.from_string(b"old\nsame\n")),
  242. (b"bar.txt", 0o644, Blob.from_string(b"new\nsame\n")),
  243. )
  244. self.assertEqual(
  245. [
  246. b"diff --git a/foo.txt b/bar.txt",
  247. b"index 3b0f961..a116b51 644",
  248. b"--- a/foo.txt",
  249. b"+++ b/bar.txt",
  250. b"@@ -1,2 +1,2 @@",
  251. b"-old",
  252. b"+new",
  253. b" same",
  254. ],
  255. f.getvalue().splitlines(),
  256. )
  257. def test_blob_add(self) -> None:
  258. f = BytesIO()
  259. write_blob_diff(
  260. f,
  261. (None, None, None),
  262. (b"bar.txt", 0o644, Blob.from_string(b"new\nsame\n")),
  263. )
  264. self.assertEqual(
  265. [
  266. b"diff --git a/bar.txt b/bar.txt",
  267. b"new file mode 644",
  268. b"index 0000000..a116b51",
  269. b"--- /dev/null",
  270. b"+++ b/bar.txt",
  271. b"@@ -0,0 +1,2 @@",
  272. b"+new",
  273. b"+same",
  274. ],
  275. f.getvalue().splitlines(),
  276. )
  277. def test_blob_remove(self) -> None:
  278. f = BytesIO()
  279. write_blob_diff(
  280. f,
  281. (b"bar.txt", 0o644, Blob.from_string(b"new\nsame\n")),
  282. (None, None, None),
  283. )
  284. self.assertEqual(
  285. [
  286. b"diff --git a/bar.txt b/bar.txt",
  287. b"deleted file mode 644",
  288. b"index a116b51..0000000",
  289. b"--- a/bar.txt",
  290. b"+++ /dev/null",
  291. b"@@ -1,2 +0,0 @@",
  292. b"-new",
  293. b"-same",
  294. ],
  295. f.getvalue().splitlines(),
  296. )
  297. def test_tree_diff(self) -> None:
  298. f = BytesIO()
  299. store = MemoryObjectStore()
  300. added = Blob.from_string(b"add\n")
  301. removed = Blob.from_string(b"removed\n")
  302. changed1 = Blob.from_string(b"unchanged\nremoved\n")
  303. changed2 = Blob.from_string(b"unchanged\nadded\n")
  304. unchanged = Blob.from_string(b"unchanged\n")
  305. tree1 = Tree()
  306. tree1.add(b"removed.txt", 0o644, removed.id)
  307. tree1.add(b"changed.txt", 0o644, changed1.id)
  308. tree1.add(b"unchanged.txt", 0o644, changed1.id)
  309. tree2 = Tree()
  310. tree2.add(b"added.txt", 0o644, added.id)
  311. tree2.add(b"changed.txt", 0o644, changed2.id)
  312. tree2.add(b"unchanged.txt", 0o644, changed1.id)
  313. store.add_objects(
  314. [
  315. (o, None)
  316. for o in [
  317. tree1,
  318. tree2,
  319. added,
  320. removed,
  321. changed1,
  322. changed2,
  323. unchanged,
  324. ]
  325. ]
  326. )
  327. write_tree_diff(f, store, tree1.id, tree2.id)
  328. self.assertEqual(
  329. [
  330. b"diff --git a/added.txt b/added.txt",
  331. b"new file mode 644",
  332. b"index 0000000..76d4bb8",
  333. b"--- /dev/null",
  334. b"+++ b/added.txt",
  335. b"@@ -0,0 +1 @@",
  336. b"+add",
  337. b"diff --git a/changed.txt b/changed.txt",
  338. b"index bf84e48..1be2436 644",
  339. b"--- a/changed.txt",
  340. b"+++ b/changed.txt",
  341. b"@@ -1,2 +1,2 @@",
  342. b" unchanged",
  343. b"-removed",
  344. b"+added",
  345. b"diff --git a/removed.txt b/removed.txt",
  346. b"deleted file mode 644",
  347. b"index 2c3f0b3..0000000",
  348. b"--- a/removed.txt",
  349. b"+++ /dev/null",
  350. b"@@ -1 +0,0 @@",
  351. b"-removed",
  352. ],
  353. f.getvalue().splitlines(),
  354. )
  355. def test_tree_diff_submodule(self) -> None:
  356. f = BytesIO()
  357. store = MemoryObjectStore()
  358. tree1 = Tree()
  359. tree1.add(
  360. b"asubmodule",
  361. S_IFGITLINK,
  362. b"06d0bdd9e2e20377b3180e4986b14c8549b393e4",
  363. )
  364. tree2 = Tree()
  365. tree2.add(
  366. b"asubmodule",
  367. S_IFGITLINK,
  368. b"cc975646af69f279396d4d5e1379ac6af80ee637",
  369. )
  370. store.add_objects([(o, None) for o in [tree1, tree2]])
  371. write_tree_diff(f, store, tree1.id, tree2.id)
  372. self.assertEqual(
  373. [
  374. b"diff --git a/asubmodule b/asubmodule",
  375. b"index 06d0bdd..cc97564 160000",
  376. b"--- a/asubmodule",
  377. b"+++ b/asubmodule",
  378. b"@@ -1 +1 @@",
  379. b"-Subproject commit 06d0bdd9e2e20377b3180e4986b14c8549b393e4",
  380. b"+Subproject commit cc975646af69f279396d4d5e1379ac6af80ee637",
  381. ],
  382. f.getvalue().splitlines(),
  383. )
  384. def test_object_diff_blob(self) -> None:
  385. f = BytesIO()
  386. b1 = Blob.from_string(b"old\nsame\n")
  387. b2 = Blob.from_string(b"new\nsame\n")
  388. store = MemoryObjectStore()
  389. store.add_objects([(b1, None), (b2, None)])
  390. write_object_diff(
  391. f, store, (b"foo.txt", 0o644, b1.id), (b"bar.txt", 0o644, b2.id)
  392. )
  393. self.assertEqual(
  394. [
  395. b"diff --git a/foo.txt b/bar.txt",
  396. b"index 3b0f961..a116b51 644",
  397. b"--- a/foo.txt",
  398. b"+++ b/bar.txt",
  399. b"@@ -1,2 +1,2 @@",
  400. b"-old",
  401. b"+new",
  402. b" same",
  403. ],
  404. f.getvalue().splitlines(),
  405. )
  406. def test_object_diff_add_blob(self) -> None:
  407. f = BytesIO()
  408. store = MemoryObjectStore()
  409. b2 = Blob.from_string(b"new\nsame\n")
  410. store.add_object(b2)
  411. write_object_diff(f, store, (None, None, None), (b"bar.txt", 0o644, b2.id))
  412. self.assertEqual(
  413. [
  414. b"diff --git a/bar.txt b/bar.txt",
  415. b"new file mode 644",
  416. b"index 0000000..a116b51",
  417. b"--- /dev/null",
  418. b"+++ b/bar.txt",
  419. b"@@ -0,0 +1,2 @@",
  420. b"+new",
  421. b"+same",
  422. ],
  423. f.getvalue().splitlines(),
  424. )
  425. def test_object_diff_remove_blob(self) -> None:
  426. f = BytesIO()
  427. b1 = Blob.from_string(b"new\nsame\n")
  428. store = MemoryObjectStore()
  429. store.add_object(b1)
  430. write_object_diff(f, store, (b"bar.txt", 0o644, b1.id), (None, None, None))
  431. self.assertEqual(
  432. [
  433. b"diff --git a/bar.txt b/bar.txt",
  434. b"deleted file mode 644",
  435. b"index a116b51..0000000",
  436. b"--- a/bar.txt",
  437. b"+++ /dev/null",
  438. b"@@ -1,2 +0,0 @@",
  439. b"-new",
  440. b"-same",
  441. ],
  442. f.getvalue().splitlines(),
  443. )
  444. def test_object_diff_bin_blob_force(self) -> None:
  445. f = BytesIO()
  446. # Prepare two slightly different PNG headers
  447. b1 = Blob.from_string(
  448. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  449. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  450. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  451. b"\x08\x04\x00\x00\x00\x05\x04\x8b"
  452. )
  453. b2 = Blob.from_string(
  454. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  455. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  456. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  457. b"\x08\x03\x00\x00\x00\x98\xd3\xb3"
  458. )
  459. store = MemoryObjectStore()
  460. store.add_objects([(b1, None), (b2, None)])
  461. write_object_diff(
  462. f,
  463. store,
  464. (b"foo.png", 0o644, b1.id),
  465. (b"bar.png", 0o644, b2.id),
  466. diff_binary=True,
  467. )
  468. self.assertEqual(
  469. [
  470. b"diff --git a/foo.png b/bar.png",
  471. b"index f73e47d..06364b7 644",
  472. b"--- a/foo.png",
  473. b"+++ b/bar.png",
  474. b"@@ -1,4 +1,4 @@",
  475. b" \x89PNG",
  476. b" \x1a",
  477. b" \x00\x00\x00",
  478. b"-IHDR\x00\x00\x01\xd5\x00\x00\x00"
  479. b"\x9f\x08\x04\x00\x00\x00\x05\x04\x8b",
  480. b"\\ No newline at end of file",
  481. b"+IHDR\x00\x00\x01\xd5\x00\x00\x00\x9f"
  482. b"\x08\x03\x00\x00\x00\x98\xd3\xb3",
  483. b"\\ No newline at end of file",
  484. ],
  485. f.getvalue().splitlines(),
  486. )
  487. def test_object_diff_bin_blob(self) -> None:
  488. f = BytesIO()
  489. # Prepare two slightly different PNG headers
  490. b1 = Blob.from_string(
  491. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  492. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  493. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  494. b"\x08\x04\x00\x00\x00\x05\x04\x8b"
  495. )
  496. b2 = Blob.from_string(
  497. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  498. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  499. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  500. b"\x08\x03\x00\x00\x00\x98\xd3\xb3"
  501. )
  502. store = MemoryObjectStore()
  503. store.add_objects([(b1, None), (b2, None)])
  504. write_object_diff(
  505. f, store, (b"foo.png", 0o644, b1.id), (b"bar.png", 0o644, b2.id)
  506. )
  507. self.assertEqual(
  508. [
  509. b"diff --git a/foo.png b/bar.png",
  510. b"index f73e47d..06364b7 644",
  511. b"Binary files a/foo.png and b/bar.png differ",
  512. ],
  513. f.getvalue().splitlines(),
  514. )
  515. def test_object_diff_add_bin_blob(self) -> None:
  516. f = BytesIO()
  517. b2 = Blob.from_string(
  518. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  519. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  520. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  521. b"\x08\x03\x00\x00\x00\x98\xd3\xb3"
  522. )
  523. store = MemoryObjectStore()
  524. store.add_object(b2)
  525. write_object_diff(f, store, (None, None, None), (b"bar.png", 0o644, b2.id))
  526. self.assertEqual(
  527. [
  528. b"diff --git a/bar.png b/bar.png",
  529. b"new file mode 644",
  530. b"index 0000000..06364b7",
  531. b"Binary files /dev/null and b/bar.png differ",
  532. ],
  533. f.getvalue().splitlines(),
  534. )
  535. def test_object_diff_remove_bin_blob(self) -> None:
  536. f = BytesIO()
  537. b1 = Blob.from_string(
  538. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  539. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  540. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  541. b"\x08\x04\x00\x00\x00\x05\x04\x8b"
  542. )
  543. store = MemoryObjectStore()
  544. store.add_object(b1)
  545. write_object_diff(f, store, (b"foo.png", 0o644, b1.id), (None, None, None))
  546. self.assertEqual(
  547. [
  548. b"diff --git a/foo.png b/foo.png",
  549. b"deleted file mode 644",
  550. b"index f73e47d..0000000",
  551. b"Binary files a/foo.png and /dev/null differ",
  552. ],
  553. f.getvalue().splitlines(),
  554. )
  555. def test_object_diff_kind_change(self) -> None:
  556. f = BytesIO()
  557. b1 = Blob.from_string(b"new\nsame\n")
  558. store = MemoryObjectStore()
  559. store.add_object(b1)
  560. write_object_diff(
  561. f,
  562. store,
  563. (b"bar.txt", 0o644, b1.id),
  564. (
  565. b"bar.txt",
  566. 0o160000,
  567. b"06d0bdd9e2e20377b3180e4986b14c8549b393e4",
  568. ),
  569. )
  570. self.assertEqual(
  571. [
  572. b"diff --git a/bar.txt b/bar.txt",
  573. b"old file mode 644",
  574. b"new file mode 160000",
  575. b"index a116b51..06d0bdd 160000",
  576. b"--- a/bar.txt",
  577. b"+++ b/bar.txt",
  578. b"@@ -1,2 +1 @@",
  579. b"-new",
  580. b"-same",
  581. b"+Subproject commit 06d0bdd9e2e20377b3180e4986b14c8549b393e4",
  582. ],
  583. f.getvalue().splitlines(),
  584. )
  585. class GetSummaryTests(TestCase):
  586. def test_simple(self) -> None:
  587. c = make_commit(
  588. author=b"Jelmer <jelmer@samba.org>",
  589. committer=b"Jelmer <jelmer@samba.org>",
  590. author_time=1271350201,
  591. commit_time=1271350201,
  592. author_timezone=0,
  593. commit_timezone=0,
  594. message=b"This is the first line\nAnd this is the second line.\n",
  595. tree=Tree().id,
  596. )
  597. self.assertEqual("This-is-the-first-line", get_summary(c))
  598. class DiffAlgorithmTests(TestCase):
  599. """Tests for diff algorithm selection."""
  600. def test_unified_diff_with_myers(self) -> None:
  601. """Test unified_diff_with_algorithm with default myers algorithm."""
  602. a = [b"line1\n", b"line2\n", b"line3\n"]
  603. b = [b"line1\n", b"line2 modified\n", b"line3\n"]
  604. result = list(
  605. unified_diff_with_algorithm(
  606. a, b, fromfile=b"a.txt", tofile=b"b.txt", algorithm="myers"
  607. )
  608. )
  609. # Should contain diff headers and the change
  610. self.assertTrue(any(b"---" in line for line in result))
  611. self.assertTrue(any(b"+++" in line for line in result))
  612. self.assertTrue(any(b"-line2" in line for line in result))
  613. self.assertTrue(any(b"+line2 modified" in line for line in result))
  614. def test_unified_diff_with_patience_not_available(self) -> None:
  615. """Test that DiffAlgorithmNotAvailable is raised when patience not available."""
  616. # Temporarily mock _get_sequence_matcher to simulate ImportError
  617. import dulwich.patch
  618. original = dulwich.patch._get_sequence_matcher
  619. def mock_get_sequence_matcher(algorithm, a, b):
  620. if algorithm == "patience":
  621. raise DiffAlgorithmNotAvailable(
  622. "patience", "Install with: pip install 'dulwich[patiencediff]'"
  623. )
  624. return original(algorithm, a, b)
  625. try:
  626. dulwich.patch._get_sequence_matcher = mock_get_sequence_matcher
  627. a = [b"line1\n", b"line2\n", b"line3\n"]
  628. b = [b"line1\n", b"line2 modified\n", b"line3\n"]
  629. with self.assertRaises(DiffAlgorithmNotAvailable) as cm:
  630. list(
  631. unified_diff_with_algorithm(
  632. a, b, fromfile=b"a.txt", tofile=b"b.txt", algorithm="patience"
  633. )
  634. )
  635. self.assertIn("patience", str(cm.exception))
  636. self.assertIn("pip install", str(cm.exception))
  637. finally:
  638. dulwich.patch._get_sequence_matcher = original
  639. class PatienceDiffTests(TestCase):
  640. """Tests for patience diff algorithm support."""
  641. def setUp(self) -> None:
  642. super().setUp()
  643. # Skip all patience diff tests if patiencediff is not available
  644. try:
  645. import patiencediff # noqa: F401
  646. except ImportError:
  647. raise DependencyMissing("patiencediff")
  648. def test_unified_diff_with_patience_available(self) -> None:
  649. """Test unified_diff_with_algorithm with patience if available."""
  650. a = [b"line1\n", b"line2\n", b"line3\n"]
  651. b = [b"line1\n", b"line2 modified\n", b"line3\n"]
  652. result = list(
  653. unified_diff_with_algorithm(
  654. a, b, fromfile=b"a.txt", tofile=b"b.txt", algorithm="patience"
  655. )
  656. )
  657. # Should contain diff headers and the change
  658. self.assertTrue(any(b"---" in line for line in result))
  659. self.assertTrue(any(b"+++" in line for line in result))
  660. self.assertTrue(any(b"-line2" in line for line in result))
  661. self.assertTrue(any(b"+line2 modified" in line for line in result))
  662. def test_unified_diff_with_patience_not_available(self) -> None:
  663. """Test that DiffAlgorithmNotAvailable is raised when patience not available."""
  664. # Temporarily mock _get_sequence_matcher to simulate ImportError
  665. import dulwich.patch
  666. original = dulwich.patch._get_sequence_matcher
  667. def mock_get_sequence_matcher(algorithm, a, b):
  668. if algorithm == "patience":
  669. raise DiffAlgorithmNotAvailable(
  670. "patience", "Install with: pip install 'dulwich[patiencediff]'"
  671. )
  672. return original(algorithm, a, b)
  673. try:
  674. dulwich.patch._get_sequence_matcher = mock_get_sequence_matcher
  675. a = [b"line1\n", b"line2\n", b"line3\n"]
  676. b = [b"line1\n", b"line2 modified\n", b"line3\n"]
  677. with self.assertRaises(DiffAlgorithmNotAvailable) as cm:
  678. list(
  679. unified_diff_with_algorithm(
  680. a, b, fromfile=b"a.txt", tofile=b"b.txt", algorithm="patience"
  681. )
  682. )
  683. self.assertIn("patience", str(cm.exception))
  684. self.assertIn("pip install", str(cm.exception))
  685. finally:
  686. dulwich.patch._get_sequence_matcher = original
  687. def test_write_blob_diff_with_patience(self) -> None:
  688. """Test write_blob_diff with patience algorithm if available."""
  689. f = BytesIO()
  690. old_blob = Blob()
  691. old_blob.data = b"line1\nline2\nline3\n"
  692. new_blob = Blob()
  693. new_blob.data = b"line1\nline2 modified\nline3\n"
  694. write_blob_diff(
  695. f,
  696. (b"file.txt", 0o100644, old_blob),
  697. (b"file.txt", 0o100644, new_blob),
  698. diff_algorithm="patience",
  699. )
  700. diff = f.getvalue()
  701. self.assertIn(b"diff --git", diff)
  702. self.assertIn(b"-line2", diff)
  703. self.assertIn(b"+line2 modified", diff)
  704. def test_write_object_diff_with_patience(self) -> None:
  705. """Test write_object_diff with patience algorithm if available."""
  706. f = BytesIO()
  707. store = MemoryObjectStore()
  708. old_blob = Blob()
  709. old_blob.data = b"line1\nline2\nline3\n"
  710. store.add_object(old_blob)
  711. new_blob = Blob()
  712. new_blob.data = b"line1\nline2 modified\nline3\n"
  713. store.add_object(new_blob)
  714. write_object_diff(
  715. f,
  716. store,
  717. (b"file.txt", 0o100644, old_blob.id),
  718. (b"file.txt", 0o100644, new_blob.id),
  719. diff_algorithm="patience",
  720. )
  721. diff = f.getvalue()
  722. self.assertIn(b"diff --git", diff)
  723. self.assertIn(b"-line2", diff)
  724. self.assertIn(b"+line2 modified", diff)
  725. class PatchIdTests(TestCase):
  726. """Tests for patch_id and commit_patch_id functions."""
  727. def test_patch_id_simple(self) -> None:
  728. """Test patch_id computation with a simple diff."""
  729. diff = b"""diff --git a/file.txt b/file.txt
  730. index 3b0f961..a116b51 644
  731. --- a/file.txt
  732. +++ b/file.txt
  733. @@ -1,2 +1,2 @@
  734. -old
  735. +new
  736. same
  737. """
  738. pid = patch_id(diff)
  739. # Patch ID should be a 40-byte hex string
  740. self.assertEqual(40, len(pid))
  741. self.assertTrue(all(c in b"0123456789abcdef" for c in pid))
  742. def test_patch_id_same_for_equivalent_diffs(self) -> None:
  743. """Test that equivalent patches have the same ID."""
  744. # Two diffs with different line numbers but same changes
  745. diff1 = b"""diff --git a/file.txt b/file.txt
  746. --- a/file.txt
  747. +++ b/file.txt
  748. @@ -1,3 +1,3 @@
  749. context
  750. -old line
  751. +new line
  752. context
  753. """
  754. diff2 = b"""diff --git a/file.txt b/file.txt
  755. --- a/file.txt
  756. +++ b/file.txt
  757. @@ -10,3 +10,3 @@
  758. context
  759. -old line
  760. +new line
  761. context
  762. """
  763. pid1 = patch_id(diff1)
  764. pid2 = patch_id(diff2)
  765. # Same patch content should give same patch ID
  766. self.assertEqual(pid1, pid2)
  767. def test_commit_patch_id(self) -> None:
  768. """Test commit_patch_id computation."""
  769. store = MemoryObjectStore()
  770. # Create two trees
  771. blob1 = Blob.from_string(b"content1\n")
  772. blob2 = Blob.from_string(b"content2\n")
  773. store.add_objects([(blob1, None), (blob2, None)])
  774. tree1 = Tree()
  775. tree1.add(b"file.txt", 0o644, blob1.id)
  776. store.add_object(tree1)
  777. tree2 = Tree()
  778. tree2.add(b"file.txt", 0o644, blob2.id)
  779. store.add_object(tree2)
  780. # Create a commit
  781. commit = Commit()
  782. commit.tree = tree2.id
  783. commit.parents = [ZERO_SHA] # Fake parent
  784. commit.author = commit.committer = b"Test <test@example.com>"
  785. commit.author_time = commit.commit_time = 1234567890
  786. commit.author_timezone = commit.commit_timezone = 0
  787. commit.message = b"Test commit\n"
  788. commit.encoding = b"UTF-8"
  789. store.add_object(commit)
  790. # Create parent commit
  791. parent_commit = Commit()
  792. parent_commit.tree = tree1.id
  793. parent_commit.parents = []
  794. parent_commit.author = parent_commit.committer = b"Test <test@example.com>"
  795. parent_commit.author_time = parent_commit.commit_time = 1234567880
  796. parent_commit.author_timezone = parent_commit.commit_timezone = 0
  797. parent_commit.message = b"Parent commit\n"
  798. parent_commit.encoding = b"UTF-8"
  799. store.add_object(parent_commit)
  800. # Update commit to have real parent
  801. commit.parents = [parent_commit.id]
  802. store.add_object(commit)
  803. # Compute patch ID
  804. pid = commit_patch_id(store, commit.id)
  805. self.assertEqual(40, len(pid))
  806. self.assertTrue(all(c in b"0123456789abcdef" for c in pid))
  807. class MailinfoTests(TestCase):
  808. """Tests for mailinfo functionality."""
  809. def test_basic_parsing(self):
  810. """Test basic email parsing."""
  811. from io import BytesIO
  812. from dulwich.patch import mailinfo
  813. email_content = b"""From: John Doe <john@example.com>
  814. Date: Mon, 1 Jan 2024 12:00:00 +0000
  815. Subject: [PATCH] Add new feature
  816. Message-ID: <test@example.com>
  817. This is the commit message.
  818. More details here.
  819. ---
  820. file.txt | 1 +
  821. 1 file changed, 1 insertion(+)
  822. diff --git a/file.txt b/file.txt
  823. --- a/file.txt
  824. +++ b/file.txt
  825. @@ -1 +1,2 @@
  826. line1
  827. +line2
  828. --
  829. 2.39.0
  830. """
  831. result = mailinfo(BytesIO(email_content))
  832. self.assertEqual("John Doe", result.author_name)
  833. self.assertEqual("john@example.com", result.author_email)
  834. self.assertEqual("Add new feature", result.subject)
  835. self.assertIn("This is the commit message.", result.message)
  836. self.assertIn("More details here.", result.message)
  837. self.assertIn("diff --git a/file.txt b/file.txt", result.patch)
  838. def test_subject_munging(self):
  839. """Test subject line munging."""
  840. from io import BytesIO
  841. from dulwich.patch import mailinfo
  842. # Test with [PATCH] tag
  843. email = b"""From: Test <test@example.com>
  844. Subject: [PATCH 1/2] Fix bug
  845. Body
  846. """
  847. result = mailinfo(BytesIO(email))
  848. self.assertEqual("Fix bug", result.subject)
  849. # Test with Re: prefix
  850. email = b"""From: Test <test@example.com>
  851. Subject: Re: [PATCH] Fix bug
  852. Body
  853. """
  854. result = mailinfo(BytesIO(email))
  855. self.assertEqual("Fix bug", result.subject)
  856. # Test with multiple brackets
  857. email = b"""From: Test <test@example.com>
  858. Subject: [RFC][PATCH] New feature
  859. Body
  860. """
  861. result = mailinfo(BytesIO(email))
  862. self.assertEqual("New feature", result.subject)
  863. def test_keep_subject(self):
  864. """Test -k flag (keep subject intact)."""
  865. from io import BytesIO
  866. from dulwich.patch import mailinfo
  867. email = b"""From: Test <test@example.com>
  868. Subject: [PATCH 1/2] Fix bug
  869. Body
  870. """
  871. result = mailinfo(BytesIO(email), keep_subject=True)
  872. self.assertEqual("[PATCH 1/2] Fix bug", result.subject)
  873. def test_keep_non_patch(self):
  874. """Test -b flag (only strip [PATCH])."""
  875. from io import BytesIO
  876. from dulwich.patch import mailinfo
  877. email = b"""From: Test <test@example.com>
  878. Subject: [RFC][PATCH] New feature
  879. Body
  880. """
  881. result = mailinfo(BytesIO(email), keep_non_patch=True)
  882. self.assertEqual("[RFC] New feature", result.subject)
  883. def test_scissors(self):
  884. """Test scissors line handling."""
  885. from io import BytesIO
  886. from dulwich.patch import mailinfo
  887. email = b"""From: Test <test@example.com>
  888. Subject: Test
  889. Ignore this part
  890. -- >8 --
  891. Keep this part
  892. ---
  893. diff --git a/file.txt b/file.txt
  894. """
  895. result = mailinfo(BytesIO(email), scissors=True)
  896. self.assertIn("Keep this part", result.message)
  897. self.assertNotIn("Ignore this part", result.message)
  898. def test_message_id(self):
  899. """Test -m flag (include Message-ID)."""
  900. from io import BytesIO
  901. from dulwich.patch import mailinfo
  902. email = b"""From: Test <test@example.com>
  903. Subject: Test
  904. Message-ID: <12345@example.com>
  905. Body text
  906. """
  907. result = mailinfo(BytesIO(email), message_id=True)
  908. self.assertIn("Message-ID: <12345@example.com>", result.message)
  909. self.assertEqual("<12345@example.com>", result.message_id)
  910. def test_encoding(self):
  911. """Test encoding handling."""
  912. from io import BytesIO
  913. from dulwich.patch import mailinfo
  914. # Use explicit UTF-8 bytes with MIME encoded subject
  915. email = (
  916. b"From: Test <test@example.com>\n"
  917. b"Subject: =?utf-8?q?Test_with_UTF-8=3A_caf=C3=A9?=\n"
  918. b"Content-Type: text/plain; charset=utf-8\n"
  919. b"Content-Transfer-Encoding: 8bit\n"
  920. b"\n"
  921. b"Body with UTF-8: " + "naïve".encode() + b"\n"
  922. )
  923. result = mailinfo(BytesIO(email), encoding="utf-8")
  924. # The subject should be decoded from MIME encoding
  925. self.assertIn("caf", result.subject)
  926. self.assertIn("na", result.message)
  927. def test_patch_separation(self):
  928. """Test separation of message from patch."""
  929. from io import BytesIO
  930. from dulwich.patch import mailinfo
  931. email = b"""From: Test <test@example.com>
  932. Subject: Test
  933. Commit message line 1
  934. Commit message line 2
  935. ---
  936. file.txt | 1 +
  937. 1 file changed, 1 insertion(+)
  938. diff --git a/file.txt b/file.txt
  939. """
  940. result = mailinfo(BytesIO(email))
  941. self.assertIn("Commit message line 1", result.message)
  942. self.assertIn("Commit message line 2", result.message)
  943. self.assertIn("---", result.patch)
  944. self.assertIn("diff --git", result.patch)
  945. self.assertNotIn("---", result.message)
  946. def test_no_subject(self):
  947. """Test handling of missing subject."""
  948. from io import BytesIO
  949. from dulwich.patch import mailinfo
  950. email = b"""From: Test <test@example.com>
  951. Body text
  952. """
  953. result = mailinfo(BytesIO(email))
  954. self.assertEqual("(no subject)", result.subject)
  955. def test_missing_from_header(self):
  956. """Test error on missing From header."""
  957. from io import BytesIO
  958. from dulwich.patch import mailinfo
  959. email = b"""Subject: Test
  960. Body text
  961. """
  962. with self.assertRaises(ValueError) as cm:
  963. mailinfo(BytesIO(email))
  964. self.assertIn("From", str(cm.exception))