test_patch.py 30 KB

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