test_patch.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  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, Blob, Commit, Tree
  26. from dulwich.patch import (
  27. DiffAlgorithmNotAvailable,
  28. get_summary,
  29. git_am_patch_split,
  30. unified_diff_with_algorithm,
  31. write_blob_diff,
  32. write_commit_patch,
  33. write_object_diff,
  34. write_tree_diff,
  35. )
  36. from . import SkipTest, TestCase
  37. class WriteCommitPatchTests(TestCase):
  38. def test_simple_bytesio(self) -> None:
  39. f = BytesIO()
  40. c = Commit()
  41. c.committer = c.author = b"Jelmer <jelmer@samba.org>"
  42. c.commit_time = c.author_time = 1271350201
  43. c.commit_timezone = c.author_timezone = 0
  44. c.message = b"This is the first line\nAnd this is the second line.\n"
  45. c.tree = Tree().id
  46. write_commit_patch(f, c, b"CONTENTS", (1, 1), version="custom")
  47. f.seek(0)
  48. lines = f.readlines()
  49. self.assertTrue(
  50. lines[0].startswith(b"From 0b0d34d1b5b596c928adc9a727a4b9e03d025298")
  51. )
  52. self.assertEqual(lines[1], b"From: Jelmer <jelmer@samba.org>\n")
  53. self.assertTrue(lines[2].startswith(b"Date: "))
  54. self.assertEqual(
  55. [
  56. b"Subject: [PATCH 1/1] This is the first line\n",
  57. b"And this is the second line.\n",
  58. b"\n",
  59. b"\n",
  60. b"---\n",
  61. ],
  62. lines[3:8],
  63. )
  64. self.assertEqual([b"CONTENTS-- \n", b"custom\n"], lines[-2:])
  65. if len(lines) >= 12:
  66. # diffstat may not be present
  67. self.assertEqual(lines[8], b" 0 files changed\n")
  68. class ReadGitAmPatch(TestCase):
  69. def test_extract_string(self) -> None:
  70. text = b"""\
  71. From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
  72. From: Jelmer Vernooij <jelmer@samba.org>
  73. Date: Thu, 15 Apr 2010 15:40:28 +0200
  74. Subject: [PATCH 1/2] Remove executable bit from prey.ico (triggers a warning).
  75. ---
  76. pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  77. 1 files changed, 0 insertions(+), 0 deletions(-)
  78. mode change 100755 => 100644 pixmaps/prey.ico
  79. --
  80. 1.7.0.4
  81. """
  82. c, diff, version = git_am_patch_split(StringIO(text.decode("utf-8")), "utf-8")
  83. self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.committer)
  84. self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.author)
  85. self.assertEqual(
  86. b"Remove executable bit from prey.ico (triggers a warning).\n",
  87. c.message,
  88. )
  89. self.assertEqual(
  90. b""" pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  91. 1 files changed, 0 insertions(+), 0 deletions(-)
  92. mode change 100755 => 100644 pixmaps/prey.ico
  93. """,
  94. diff,
  95. )
  96. self.assertEqual(b"1.7.0.4", version)
  97. def test_extract_bytes(self) -> None:
  98. text = b"""\
  99. From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
  100. From: Jelmer Vernooij <jelmer@samba.org>
  101. Date: Thu, 15 Apr 2010 15:40:28 +0200
  102. Subject: [PATCH 1/2] Remove executable bit from prey.ico (triggers a warning).
  103. ---
  104. pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  105. 1 files changed, 0 insertions(+), 0 deletions(-)
  106. mode change 100755 => 100644 pixmaps/prey.ico
  107. --
  108. 1.7.0.4
  109. """
  110. c, diff, version = git_am_patch_split(BytesIO(text))
  111. self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.committer)
  112. self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.author)
  113. self.assertEqual(
  114. b"Remove executable bit from prey.ico (triggers a warning).\n",
  115. c.message,
  116. )
  117. self.assertEqual(
  118. b""" pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  119. 1 files changed, 0 insertions(+), 0 deletions(-)
  120. mode change 100755 => 100644 pixmaps/prey.ico
  121. """,
  122. diff,
  123. )
  124. self.assertEqual(b"1.7.0.4", version)
  125. def test_extract_spaces(self) -> None:
  126. text = b"""From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
  127. From: Jelmer Vernooij <jelmer@samba.org>
  128. Date: Thu, 15 Apr 2010 15:40:28 +0200
  129. Subject: [Dulwich-users] [PATCH] Added unit tests for
  130. dulwich.object_store.tree_lookup_path.
  131. * dulwich/tests/test_object_store.py
  132. (TreeLookupPathTests): This test case contains a few tests that ensure the
  133. tree_lookup_path function works as expected.
  134. ---
  135. pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  136. 1 files changed, 0 insertions(+), 0 deletions(-)
  137. mode change 100755 => 100644 pixmaps/prey.ico
  138. --
  139. 1.7.0.4
  140. """
  141. c, _diff, _version = git_am_patch_split(BytesIO(text), "utf-8")
  142. self.assertEqual(
  143. b"""\
  144. Added unit tests for dulwich.object_store.tree_lookup_path.
  145. * dulwich/tests/test_object_store.py
  146. (TreeLookupPathTests): This test case contains a few tests that ensure the
  147. tree_lookup_path function works as expected.
  148. """,
  149. c.message,
  150. )
  151. def test_extract_pseudo_from_header(self) -> None:
  152. text = b"""From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
  153. From: Jelmer Vernooij <jelmer@samba.org>
  154. Date: Thu, 15 Apr 2010 15:40:28 +0200
  155. Subject: [Dulwich-users] [PATCH] Added unit tests for
  156. dulwich.object_store.tree_lookup_path.
  157. From: Jelmer Vernooij <jelmer@debian.org>
  158. * dulwich/tests/test_object_store.py
  159. (TreeLookupPathTests): This test case contains a few tests that ensure the
  160. tree_lookup_path function works as expected.
  161. ---
  162. pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  163. 1 files changed, 0 insertions(+), 0 deletions(-)
  164. mode change 100755 => 100644 pixmaps/prey.ico
  165. --
  166. 1.7.0.4
  167. """
  168. c, _diff, _version = git_am_patch_split(BytesIO(text), "utf-8")
  169. self.assertEqual(b"Jelmer Vernooij <jelmer@debian.org>", c.author)
  170. self.assertEqual(
  171. b"""\
  172. Added unit tests for dulwich.object_store.tree_lookup_path.
  173. * dulwich/tests/test_object_store.py
  174. (TreeLookupPathTests): This test case contains a few tests that ensure the
  175. tree_lookup_path function works as expected.
  176. """,
  177. c.message,
  178. )
  179. def test_extract_no_version_tail(self) -> None:
  180. text = b"""\
  181. From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
  182. From: Jelmer Vernooij <jelmer@samba.org>
  183. Date: Thu, 15 Apr 2010 15:40:28 +0200
  184. Subject: [Dulwich-users] [PATCH] Added unit tests for
  185. dulwich.object_store.tree_lookup_path.
  186. From: Jelmer Vernooij <jelmer@debian.org>
  187. ---
  188. pixmaps/prey.ico | Bin 9662 -> 9662 bytes
  189. 1 files changed, 0 insertions(+), 0 deletions(-)
  190. mode change 100755 => 100644 pixmaps/prey.ico
  191. """
  192. _c, _diff, version = git_am_patch_split(BytesIO(text), "utf-8")
  193. self.assertEqual(None, version)
  194. def test_extract_mercurial(self) -> NoReturn:
  195. raise SkipTest(
  196. "git_am_patch_split doesn't handle Mercurial patches properly yet"
  197. )
  198. expected_diff = """\
  199. diff --git a/dulwich/tests/test_patch.py b/dulwich/tests/test_patch.py
  200. --- a/dulwich/tests/test_patch.py
  201. +++ b/dulwich/tests/test_patch.py
  202. @@ -158,7 +158,7 @@
  203. '''
  204. c, diff, version = git_am_patch_split(BytesIO(text))
  205. - self.assertIs(None, version)
  206. + self.assertEqual(None, version)
  207. class DiffTests(TestCase):
  208. """
  209. text = f"""\
  210. From dulwich-users-bounces+jelmer=samba.org@lists.launchpad.net \
  211. Mon Nov 29 00:58:18 2010
  212. Date: Sun, 28 Nov 2010 17:57:27 -0600
  213. From: Augie Fackler <durin42@gmail.com>
  214. To: dulwich-users <dulwich-users@lists.launchpad.net>
  215. Subject: [Dulwich-users] [PATCH] test_patch: fix tests on Python 2.6
  216. Content-Transfer-Encoding: 8bit
  217. Change-Id: I5e51313d4ae3a65c3f00c665002a7489121bb0d6
  218. {expected_diff}
  219. _______________________________________________
  220. Mailing list: https://launchpad.net/~dulwich-users
  221. Post to : dulwich-users@lists.launchpad.net
  222. Unsubscribe : https://launchpad.net/~dulwich-users
  223. More help : https://help.launchpad.net/ListHelp
  224. """
  225. _c, diff, version = git_am_patch_split(BytesIO(text))
  226. self.assertEqual(expected_diff, diff)
  227. self.assertEqual(None, version)
  228. class DiffTests(TestCase):
  229. """Tests for write_blob_diff and write_tree_diff."""
  230. def test_blob_diff(self) -> None:
  231. f = BytesIO()
  232. write_blob_diff(
  233. f,
  234. (b"foo.txt", 0o644, Blob.from_string(b"old\nsame\n")),
  235. (b"bar.txt", 0o644, Blob.from_string(b"new\nsame\n")),
  236. )
  237. self.assertEqual(
  238. [
  239. b"diff --git a/foo.txt b/bar.txt",
  240. b"index 3b0f961..a116b51 644",
  241. b"--- a/foo.txt",
  242. b"+++ b/bar.txt",
  243. b"@@ -1,2 +1,2 @@",
  244. b"-old",
  245. b"+new",
  246. b" same",
  247. ],
  248. f.getvalue().splitlines(),
  249. )
  250. def test_blob_add(self) -> None:
  251. f = BytesIO()
  252. write_blob_diff(
  253. f,
  254. (None, None, None),
  255. (b"bar.txt", 0o644, Blob.from_string(b"new\nsame\n")),
  256. )
  257. self.assertEqual(
  258. [
  259. b"diff --git a/bar.txt b/bar.txt",
  260. b"new file mode 644",
  261. b"index 0000000..a116b51",
  262. b"--- /dev/null",
  263. b"+++ b/bar.txt",
  264. b"@@ -0,0 +1,2 @@",
  265. b"+new",
  266. b"+same",
  267. ],
  268. f.getvalue().splitlines(),
  269. )
  270. def test_blob_remove(self) -> None:
  271. f = BytesIO()
  272. write_blob_diff(
  273. f,
  274. (b"bar.txt", 0o644, Blob.from_string(b"new\nsame\n")),
  275. (None, None, None),
  276. )
  277. self.assertEqual(
  278. [
  279. b"diff --git a/bar.txt b/bar.txt",
  280. b"deleted file mode 644",
  281. b"index a116b51..0000000",
  282. b"--- a/bar.txt",
  283. b"+++ /dev/null",
  284. b"@@ -1,2 +0,0 @@",
  285. b"-new",
  286. b"-same",
  287. ],
  288. f.getvalue().splitlines(),
  289. )
  290. def test_tree_diff(self) -> None:
  291. f = BytesIO()
  292. store = MemoryObjectStore()
  293. added = Blob.from_string(b"add\n")
  294. removed = Blob.from_string(b"removed\n")
  295. changed1 = Blob.from_string(b"unchanged\nremoved\n")
  296. changed2 = Blob.from_string(b"unchanged\nadded\n")
  297. unchanged = Blob.from_string(b"unchanged\n")
  298. tree1 = Tree()
  299. tree1.add(b"removed.txt", 0o644, removed.id)
  300. tree1.add(b"changed.txt", 0o644, changed1.id)
  301. tree1.add(b"unchanged.txt", 0o644, changed1.id)
  302. tree2 = Tree()
  303. tree2.add(b"added.txt", 0o644, added.id)
  304. tree2.add(b"changed.txt", 0o644, changed2.id)
  305. tree2.add(b"unchanged.txt", 0o644, changed1.id)
  306. store.add_objects(
  307. [
  308. (o, None)
  309. for o in [
  310. tree1,
  311. tree2,
  312. added,
  313. removed,
  314. changed1,
  315. changed2,
  316. unchanged,
  317. ]
  318. ]
  319. )
  320. write_tree_diff(f, store, tree1.id, tree2.id)
  321. self.assertEqual(
  322. [
  323. b"diff --git a/added.txt b/added.txt",
  324. b"new file mode 644",
  325. b"index 0000000..76d4bb8",
  326. b"--- /dev/null",
  327. b"+++ b/added.txt",
  328. b"@@ -0,0 +1 @@",
  329. b"+add",
  330. b"diff --git a/changed.txt b/changed.txt",
  331. b"index bf84e48..1be2436 644",
  332. b"--- a/changed.txt",
  333. b"+++ b/changed.txt",
  334. b"@@ -1,2 +1,2 @@",
  335. b" unchanged",
  336. b"-removed",
  337. b"+added",
  338. b"diff --git a/removed.txt b/removed.txt",
  339. b"deleted file mode 644",
  340. b"index 2c3f0b3..0000000",
  341. b"--- a/removed.txt",
  342. b"+++ /dev/null",
  343. b"@@ -1 +0,0 @@",
  344. b"-removed",
  345. ],
  346. f.getvalue().splitlines(),
  347. )
  348. def test_tree_diff_submodule(self) -> None:
  349. f = BytesIO()
  350. store = MemoryObjectStore()
  351. tree1 = Tree()
  352. tree1.add(
  353. b"asubmodule",
  354. S_IFGITLINK,
  355. b"06d0bdd9e2e20377b3180e4986b14c8549b393e4",
  356. )
  357. tree2 = Tree()
  358. tree2.add(
  359. b"asubmodule",
  360. S_IFGITLINK,
  361. b"cc975646af69f279396d4d5e1379ac6af80ee637",
  362. )
  363. store.add_objects([(o, None) for o in [tree1, tree2]])
  364. write_tree_diff(f, store, tree1.id, tree2.id)
  365. self.assertEqual(
  366. [
  367. b"diff --git a/asubmodule b/asubmodule",
  368. b"index 06d0bdd..cc97564 160000",
  369. b"--- a/asubmodule",
  370. b"+++ b/asubmodule",
  371. b"@@ -1 +1 @@",
  372. b"-Subproject commit 06d0bdd9e2e20377b3180e4986b14c8549b393e4",
  373. b"+Subproject commit cc975646af69f279396d4d5e1379ac6af80ee637",
  374. ],
  375. f.getvalue().splitlines(),
  376. )
  377. def test_object_diff_blob(self) -> None:
  378. f = BytesIO()
  379. b1 = Blob.from_string(b"old\nsame\n")
  380. b2 = Blob.from_string(b"new\nsame\n")
  381. store = MemoryObjectStore()
  382. store.add_objects([(b1, None), (b2, None)])
  383. write_object_diff(
  384. f, store, (b"foo.txt", 0o644, b1.id), (b"bar.txt", 0o644, b2.id)
  385. )
  386. self.assertEqual(
  387. [
  388. b"diff --git a/foo.txt b/bar.txt",
  389. b"index 3b0f961..a116b51 644",
  390. b"--- a/foo.txt",
  391. b"+++ b/bar.txt",
  392. b"@@ -1,2 +1,2 @@",
  393. b"-old",
  394. b"+new",
  395. b" same",
  396. ],
  397. f.getvalue().splitlines(),
  398. )
  399. def test_object_diff_add_blob(self) -> None:
  400. f = BytesIO()
  401. store = MemoryObjectStore()
  402. b2 = Blob.from_string(b"new\nsame\n")
  403. store.add_object(b2)
  404. write_object_diff(f, store, (None, None, None), (b"bar.txt", 0o644, b2.id))
  405. self.assertEqual(
  406. [
  407. b"diff --git a/bar.txt b/bar.txt",
  408. b"new file mode 644",
  409. b"index 0000000..a116b51",
  410. b"--- /dev/null",
  411. b"+++ b/bar.txt",
  412. b"@@ -0,0 +1,2 @@",
  413. b"+new",
  414. b"+same",
  415. ],
  416. f.getvalue().splitlines(),
  417. )
  418. def test_object_diff_remove_blob(self) -> None:
  419. f = BytesIO()
  420. b1 = Blob.from_string(b"new\nsame\n")
  421. store = MemoryObjectStore()
  422. store.add_object(b1)
  423. write_object_diff(f, store, (b"bar.txt", 0o644, b1.id), (None, None, None))
  424. self.assertEqual(
  425. [
  426. b"diff --git a/bar.txt b/bar.txt",
  427. b"deleted file mode 644",
  428. b"index a116b51..0000000",
  429. b"--- a/bar.txt",
  430. b"+++ /dev/null",
  431. b"@@ -1,2 +0,0 @@",
  432. b"-new",
  433. b"-same",
  434. ],
  435. f.getvalue().splitlines(),
  436. )
  437. def test_object_diff_bin_blob_force(self) -> None:
  438. f = BytesIO()
  439. # Prepare two slightly different PNG headers
  440. b1 = Blob.from_string(
  441. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  442. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  443. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  444. b"\x08\x04\x00\x00\x00\x05\x04\x8b"
  445. )
  446. b2 = Blob.from_string(
  447. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  448. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  449. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  450. b"\x08\x03\x00\x00\x00\x98\xd3\xb3"
  451. )
  452. store = MemoryObjectStore()
  453. store.add_objects([(b1, None), (b2, None)])
  454. write_object_diff(
  455. f,
  456. store,
  457. (b"foo.png", 0o644, b1.id),
  458. (b"bar.png", 0o644, b2.id),
  459. diff_binary=True,
  460. )
  461. self.assertEqual(
  462. [
  463. b"diff --git a/foo.png b/bar.png",
  464. b"index f73e47d..06364b7 644",
  465. b"--- a/foo.png",
  466. b"+++ b/bar.png",
  467. b"@@ -1,4 +1,4 @@",
  468. b" \x89PNG",
  469. b" \x1a",
  470. b" \x00\x00\x00",
  471. b"-IHDR\x00\x00\x01\xd5\x00\x00\x00"
  472. b"\x9f\x08\x04\x00\x00\x00\x05\x04\x8b",
  473. b"\\ No newline at end of file",
  474. b"+IHDR\x00\x00\x01\xd5\x00\x00\x00\x9f"
  475. b"\x08\x03\x00\x00\x00\x98\xd3\xb3",
  476. b"\\ No newline at end of file",
  477. ],
  478. f.getvalue().splitlines(),
  479. )
  480. def test_object_diff_bin_blob(self) -> None:
  481. f = BytesIO()
  482. # Prepare two slightly different PNG headers
  483. b1 = Blob.from_string(
  484. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  485. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  486. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  487. b"\x08\x04\x00\x00\x00\x05\x04\x8b"
  488. )
  489. b2 = Blob.from_string(
  490. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  491. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  492. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  493. b"\x08\x03\x00\x00\x00\x98\xd3\xb3"
  494. )
  495. store = MemoryObjectStore()
  496. store.add_objects([(b1, None), (b2, None)])
  497. write_object_diff(
  498. f, store, (b"foo.png", 0o644, b1.id), (b"bar.png", 0o644, b2.id)
  499. )
  500. self.assertEqual(
  501. [
  502. b"diff --git a/foo.png b/bar.png",
  503. b"index f73e47d..06364b7 644",
  504. b"Binary files a/foo.png and b/bar.png differ",
  505. ],
  506. f.getvalue().splitlines(),
  507. )
  508. def test_object_diff_add_bin_blob(self) -> None:
  509. f = BytesIO()
  510. b2 = Blob.from_string(
  511. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  512. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  513. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  514. b"\x08\x03\x00\x00\x00\x98\xd3\xb3"
  515. )
  516. store = MemoryObjectStore()
  517. store.add_object(b2)
  518. write_object_diff(f, store, (None, None, None), (b"bar.png", 0o644, b2.id))
  519. self.assertEqual(
  520. [
  521. b"diff --git a/bar.png b/bar.png",
  522. b"new file mode 644",
  523. b"index 0000000..06364b7",
  524. b"Binary files /dev/null and b/bar.png differ",
  525. ],
  526. f.getvalue().splitlines(),
  527. )
  528. def test_object_diff_remove_bin_blob(self) -> None:
  529. f = BytesIO()
  530. b1 = Blob.from_string(
  531. b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
  532. b"\x00\x00\x00\x0d\x49\x48\x44\x52"
  533. b"\x00\x00\x01\xd5\x00\x00\x00\x9f"
  534. b"\x08\x04\x00\x00\x00\x05\x04\x8b"
  535. )
  536. store = MemoryObjectStore()
  537. store.add_object(b1)
  538. write_object_diff(f, store, (b"foo.png", 0o644, b1.id), (None, None, None))
  539. self.assertEqual(
  540. [
  541. b"diff --git a/foo.png b/foo.png",
  542. b"deleted file mode 644",
  543. b"index f73e47d..0000000",
  544. b"Binary files a/foo.png and /dev/null differ",
  545. ],
  546. f.getvalue().splitlines(),
  547. )
  548. def test_object_diff_kind_change(self) -> None:
  549. f = BytesIO()
  550. b1 = Blob.from_string(b"new\nsame\n")
  551. store = MemoryObjectStore()
  552. store.add_object(b1)
  553. write_object_diff(
  554. f,
  555. store,
  556. (b"bar.txt", 0o644, b1.id),
  557. (
  558. b"bar.txt",
  559. 0o160000,
  560. b"06d0bdd9e2e20377b3180e4986b14c8549b393e4",
  561. ),
  562. )
  563. self.assertEqual(
  564. [
  565. b"diff --git a/bar.txt b/bar.txt",
  566. b"old file mode 644",
  567. b"new file mode 160000",
  568. b"index a116b51..06d0bdd 160000",
  569. b"--- a/bar.txt",
  570. b"+++ b/bar.txt",
  571. b"@@ -1,2 +1 @@",
  572. b"-new",
  573. b"-same",
  574. b"+Subproject commit 06d0bdd9e2e20377b3180e4986b14c8549b393e4",
  575. ],
  576. f.getvalue().splitlines(),
  577. )
  578. class GetSummaryTests(TestCase):
  579. def test_simple(self) -> None:
  580. c = Commit()
  581. c.committer = c.author = b"Jelmer <jelmer@samba.org>"
  582. c.commit_time = c.author_time = 1271350201
  583. c.commit_timezone = c.author_timezone = 0
  584. c.message = b"This is the first line\nAnd this is the second line.\n"
  585. c.tree = Tree().id
  586. self.assertEqual("This-is-the-first-line", get_summary(c))
  587. class DiffAlgorithmTests(TestCase):
  588. """Tests for diff algorithm selection."""
  589. def test_unified_diff_with_myers(self) -> None:
  590. """Test unified_diff_with_algorithm with default myers algorithm."""
  591. a = [b"line1\n", b"line2\n", b"line3\n"]
  592. b = [b"line1\n", b"line2 modified\n", b"line3\n"]
  593. result = list(
  594. unified_diff_with_algorithm(
  595. a, b, fromfile=b"a.txt", tofile=b"b.txt", algorithm="myers"
  596. )
  597. )
  598. # Should contain diff headers and the change
  599. self.assertTrue(any(b"---" in line for line in result))
  600. self.assertTrue(any(b"+++" in line for line in result))
  601. self.assertTrue(any(b"-line2" in line for line in result))
  602. self.assertTrue(any(b"+line2 modified" in line for line in result))
  603. def test_unified_diff_with_patience_not_available(self) -> None:
  604. """Test that DiffAlgorithmNotAvailable is raised when patience not available."""
  605. # Temporarily mock _get_sequence_matcher to simulate ImportError
  606. import dulwich.patch
  607. original = dulwich.patch._get_sequence_matcher
  608. def mock_get_sequence_matcher(algorithm, a, b):
  609. if algorithm == "patience":
  610. raise DiffAlgorithmNotAvailable(
  611. "patience", "Install with: pip install 'dulwich[patiencediff]'"
  612. )
  613. return original(algorithm, a, b)
  614. try:
  615. dulwich.patch._get_sequence_matcher = mock_get_sequence_matcher
  616. a = [b"line1\n", b"line2\n", b"line3\n"]
  617. b = [b"line1\n", b"line2 modified\n", b"line3\n"]
  618. with self.assertRaises(DiffAlgorithmNotAvailable) as cm:
  619. list(
  620. unified_diff_with_algorithm(
  621. a, b, fromfile=b"a.txt", tofile=b"b.txt", algorithm="patience"
  622. )
  623. )
  624. self.assertIn("patience", str(cm.exception))
  625. self.assertIn("pip install", str(cm.exception))
  626. finally:
  627. dulwich.patch._get_sequence_matcher = original
  628. class PatienceDiffTests(TestCase):
  629. """Tests for patience diff algorithm support."""
  630. def setUp(self) -> None:
  631. super().setUp()
  632. # Skip all patience diff tests if patiencediff is not available
  633. try:
  634. import patiencediff # noqa: F401
  635. except ImportError:
  636. raise SkipTest("patiencediff not available")
  637. def test_unified_diff_with_patience_available(self) -> None:
  638. """Test unified_diff_with_algorithm with patience if available."""
  639. a = [b"line1\n", b"line2\n", b"line3\n"]
  640. b = [b"line1\n", b"line2 modified\n", b"line3\n"]
  641. result = list(
  642. unified_diff_with_algorithm(
  643. a, b, fromfile=b"a.txt", tofile=b"b.txt", algorithm="patience"
  644. )
  645. )
  646. # Should contain diff headers and the change
  647. self.assertTrue(any(b"---" in line for line in result))
  648. self.assertTrue(any(b"+++" in line for line in result))
  649. self.assertTrue(any(b"-line2" in line for line in result))
  650. self.assertTrue(any(b"+line2 modified" in line for line in result))
  651. def test_unified_diff_with_patience_not_available(self) -> None:
  652. """Test that DiffAlgorithmNotAvailable is raised when patience not available."""
  653. # Temporarily mock _get_sequence_matcher to simulate ImportError
  654. import dulwich.patch
  655. original = dulwich.patch._get_sequence_matcher
  656. def mock_get_sequence_matcher(algorithm, a, b):
  657. if algorithm == "patience":
  658. raise DiffAlgorithmNotAvailable(
  659. "patience", "Install with: pip install 'dulwich[patiencediff]'"
  660. )
  661. return original(algorithm, a, b)
  662. try:
  663. dulwich.patch._get_sequence_matcher = mock_get_sequence_matcher
  664. a = [b"line1\n", b"line2\n", b"line3\n"]
  665. b = [b"line1\n", b"line2 modified\n", b"line3\n"]
  666. with self.assertRaises(DiffAlgorithmNotAvailable) as cm:
  667. list(
  668. unified_diff_with_algorithm(
  669. a, b, fromfile=b"a.txt", tofile=b"b.txt", algorithm="patience"
  670. )
  671. )
  672. self.assertIn("patience", str(cm.exception))
  673. self.assertIn("pip install", str(cm.exception))
  674. finally:
  675. dulwich.patch._get_sequence_matcher = original
  676. def test_write_blob_diff_with_patience(self) -> None:
  677. """Test write_blob_diff with patience algorithm if available."""
  678. f = BytesIO()
  679. old_blob = Blob()
  680. old_blob.data = b"line1\nline2\nline3\n"
  681. new_blob = Blob()
  682. new_blob.data = b"line1\nline2 modified\nline3\n"
  683. write_blob_diff(
  684. f,
  685. (b"file.txt", 0o100644, old_blob),
  686. (b"file.txt", 0o100644, new_blob),
  687. diff_algorithm="patience",
  688. )
  689. diff = f.getvalue()
  690. self.assertIn(b"diff --git", diff)
  691. self.assertIn(b"-line2", diff)
  692. self.assertIn(b"+line2 modified", diff)
  693. def test_write_object_diff_with_patience(self) -> None:
  694. """Test write_object_diff with patience algorithm if available."""
  695. f = BytesIO()
  696. store = MemoryObjectStore()
  697. old_blob = Blob()
  698. old_blob.data = b"line1\nline2\nline3\n"
  699. store.add_object(old_blob)
  700. new_blob = Blob()
  701. new_blob.data = b"line1\nline2 modified\nline3\n"
  702. store.add_object(new_blob)
  703. write_object_diff(
  704. f,
  705. store,
  706. (b"file.txt", 0o100644, old_blob.id),
  707. (b"file.txt", 0o100644, new_blob.id),
  708. diff_algorithm="patience",
  709. )
  710. diff = f.getvalue()
  711. self.assertIn(b"diff --git", diff)
  712. self.assertIn(b"-line2", diff)
  713. self.assertIn(b"+line2 modified", diff)