You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

298 lines
12KB

  1. # coding: utf-8
  2. # test_diff.py
  3. # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
  4. #
  5. # This module is part of GitPython and is released under
  6. # the BSD License: http://www.opensource.org/licenses/bsd-license.php
  7. import ddt
  8. from git import (
  9. Repo,
  10. GitCommandError,
  11. Diff,
  12. DiffIndex,
  13. NULL_TREE,
  14. )
  15. from git.cmd import Git
  16. from git.test.lib import (
  17. TestBase,
  18. StringProcessAdapter,
  19. fixture,
  20. assert_equal,
  21. assert_true,
  22. )
  23. from git.test.lib import with_rw_directory
  24. import os.path as osp
  25. @ddt.ddt
  26. class TestDiff(TestBase):
  27. def tearDown(self):
  28. import gc
  29. gc.collect()
  30. def _assert_diff_format(self, diffs):
  31. # verify that the format of the diff is sane
  32. for diff in diffs:
  33. if diff.a_mode:
  34. assert isinstance(diff.a_mode, int)
  35. if diff.b_mode:
  36. assert isinstance(diff.b_mode, int)
  37. if diff.a_blob:
  38. assert not diff.a_blob.path.endswith('\n')
  39. if diff.b_blob:
  40. assert not diff.b_blob.path.endswith('\n')
  41. # END for each diff
  42. return diffs
  43. @with_rw_directory
  44. def test_diff_with_staged_file(self, rw_dir):
  45. # SETUP INDEX WITH MULTIPLE STAGES
  46. r = Repo.init(rw_dir)
  47. fp = osp.join(rw_dir, 'hello.txt')
  48. with open(fp, 'w') as fs:
  49. fs.write("hello world")
  50. r.git.add(Git.polish_url(fp))
  51. r.git.commit(message="init")
  52. with open(fp, 'w') as fs:
  53. fs.write("Hola Mundo")
  54. r.git.commit(all=True, message="change on master")
  55. r.git.checkout('HEAD~1', b='topic')
  56. with open(fp, 'w') as fs:
  57. fs.write("Hallo Welt")
  58. r.git.commit(all=True, message="change on topic branch")
  59. # there must be a merge-conflict
  60. self.failUnlessRaises(GitCommandError, r.git.cherry_pick, 'master')
  61. # Now do the actual testing - this should just work
  62. self.assertEqual(len(r.index.diff(None)), 2)
  63. self.assertEqual(len(r.index.diff(None, create_patch=True)), 0,
  64. "This should work, but doesn't right now ... it's OK")
  65. def test_list_from_string_new_mode(self):
  66. output = StringProcessAdapter(fixture('diff_new_mode'))
  67. diffs = Diff._index_from_patch_format(self.rorepo, output)
  68. self._assert_diff_format(diffs)
  69. assert_equal(1, len(diffs))
  70. assert_equal(8, len(diffs[0].diff.splitlines()))
  71. def test_diff_with_rename(self):
  72. output = StringProcessAdapter(fixture('diff_rename'))
  73. diffs = Diff._index_from_patch_format(self.rorepo, output)
  74. self._assert_diff_format(diffs)
  75. assert_equal(1, len(diffs))
  76. diff = diffs[0]
  77. assert_true(diff.renamed_file)
  78. assert_true(diff.renamed)
  79. assert_equal(diff.rename_from, u'Jérôme')
  80. assert_equal(diff.rename_to, u'müller')
  81. assert_equal(diff.raw_rename_from, b'J\xc3\xa9r\xc3\xb4me')
  82. assert_equal(diff.raw_rename_to, b'm\xc3\xbcller')
  83. assert isinstance(str(diff), str)
  84. output = StringProcessAdapter(fixture('diff_rename_raw'))
  85. diffs = Diff._index_from_raw_format(self.rorepo, output)
  86. self.assertEqual(len(diffs), 1)
  87. diff = diffs[0]
  88. self.assertIsNotNone(diff.renamed_file)
  89. self.assertIsNotNone(diff.renamed)
  90. self.assertEqual(diff.rename_from, 'this')
  91. self.assertEqual(diff.rename_to, 'that')
  92. self.assertEqual(diff.change_type, 'R')
  93. self.assertEqual(diff.score, 100)
  94. self.assertEqual(len(list(diffs.iter_change_type('R'))), 1)
  95. def test_diff_with_change_in_type(self):
  96. output = StringProcessAdapter(fixture('diff_change_in_type'))
  97. diffs = Diff._index_from_patch_format(self.rorepo, output)
  98. self._assert_diff_format(diffs)
  99. assert_equal(2, len(diffs))
  100. diff = diffs[0]
  101. self.assertIsNotNone(diff.deleted_file)
  102. assert_equal(diff.a_path, 'this')
  103. assert_equal(diff.b_path, 'this')
  104. assert isinstance(str(diff), str)
  105. diff = diffs[1]
  106. assert_equal(diff.a_path, None)
  107. assert_equal(diff.b_path, 'this')
  108. self.assertIsNotNone(diff.new_file)
  109. assert isinstance(str(diff), str)
  110. output = StringProcessAdapter(fixture('diff_change_in_type_raw'))
  111. diffs = Diff._index_from_raw_format(self.rorepo, output)
  112. self.assertEqual(len(diffs), 1)
  113. diff = diffs[0]
  114. self.assertEqual(diff.rename_from, None)
  115. self.assertEqual(diff.rename_to, None)
  116. self.assertEqual(diff.change_type, 'T')
  117. self.assertEqual(len(list(diffs.iter_change_type('T'))), 1)
  118. def test_diff_of_modified_files_not_added_to_the_index(self):
  119. output = StringProcessAdapter(fixture('diff_abbrev-40_full-index_M_raw_no-color'))
  120. diffs = Diff._index_from_raw_format(self.rorepo, output)
  121. self.assertEqual(len(diffs), 1, 'one modification')
  122. self.assertEqual(len(list(diffs.iter_change_type('M'))), 1, 'one modification')
  123. self.assertEqual(diffs[0].change_type, 'M')
  124. self.assertIsNone(diffs[0].b_blob,)
  125. @ddt.data(
  126. (Diff._index_from_patch_format, 'diff_patch_binary'),
  127. (Diff._index_from_raw_format, 'diff_raw_binary')
  128. )
  129. def test_binary_diff(self, case):
  130. method, file_name = case
  131. res = method(None, StringProcessAdapter(fixture(file_name)))
  132. self.assertEqual(len(res), 1)
  133. self.assertEqual(len(list(res.iter_change_type('M'))), 1)
  134. if res[0].diff:
  135. self.assertEqual(res[0].diff,
  136. b"Binary files a/rps and b/rps differ\n",
  137. "in patch mode, we get a diff text")
  138. self.assertIsNotNone(str(res[0]), "This call should just work")
  139. def test_diff_index(self):
  140. output = StringProcessAdapter(fixture('diff_index_patch'))
  141. res = Diff._index_from_patch_format(None, output)
  142. self.assertEqual(len(res), 6)
  143. for dr in res:
  144. self.assertTrue(dr.diff.startswith(b'@@'), dr)
  145. self.assertIsNotNone(str(dr), "Diff to string conversion should be possible")
  146. # end for each diff
  147. dr = res[3]
  148. assert dr.diff.endswith(b"+Binary files a/rps and b/rps differ\n")
  149. def test_diff_index_raw_format(self):
  150. output = StringProcessAdapter(fixture('diff_index_raw'))
  151. res = Diff._index_from_raw_format(None, output)
  152. self.assertIsNotNone(res[0].deleted_file)
  153. self.assertIsNone(res[0].b_path,)
  154. def test_diff_initial_commit(self):
  155. initial_commit = self.rorepo.commit('33ebe7acec14b25c5f84f35a664803fcab2f7781')
  156. # Without creating a patch...
  157. diff_index = initial_commit.diff(NULL_TREE)
  158. self.assertEqual(diff_index[0].b_path, 'CHANGES')
  159. self.assertIsNotNone(diff_index[0].new_file)
  160. self.assertEqual(diff_index[0].diff, '')
  161. # ...and with creating a patch
  162. diff_index = initial_commit.diff(NULL_TREE, create_patch=True)
  163. self.assertIsNone(diff_index[0].a_path, repr(diff_index[0].a_path))
  164. self.assertEqual(diff_index[0].b_path, 'CHANGES', repr(diff_index[0].b_path))
  165. self.assertIsNotNone(diff_index[0].new_file)
  166. self.assertEqual(diff_index[0].diff, fixture('diff_initial'))
  167. def test_diff_unsafe_paths(self):
  168. output = StringProcessAdapter(fixture('diff_patch_unsafe_paths'))
  169. res = Diff._index_from_patch_format(None, output)
  170. # The "Additions"
  171. self.assertEqual(res[0].b_path, u'path/ starting with a space')
  172. self.assertEqual(res[1].b_path, u'path/"with-quotes"')
  173. self.assertEqual(res[2].b_path, u"path/'with-single-quotes'")
  174. self.assertEqual(res[3].b_path, u'path/ending in a space ')
  175. self.assertEqual(res[4].b_path, u'path/with\ttab')
  176. self.assertEqual(res[5].b_path, u'path/with\nnewline')
  177. self.assertEqual(res[6].b_path, u'path/with spaces')
  178. self.assertEqual(res[7].b_path, u'path/with-question-mark?')
  179. self.assertEqual(res[8].b_path, u'path/¯\\_(ツ)_|¯')
  180. self.assertEqual(res[9].b_path, u'path/💩.txt')
  181. self.assertEqual(res[9].b_rawpath, b'path/\xf0\x9f\x92\xa9.txt')
  182. self.assertEqual(res[10].b_path, u'path/�-invalid-unicode-path.txt')
  183. self.assertEqual(res[10].b_rawpath, b'path/\x80-invalid-unicode-path.txt')
  184. # The "Moves"
  185. # NOTE: The path prefixes a/ and b/ here are legit! We're actually
  186. # verifying that it's not "a/a/" that shows up, see the fixture data.
  187. self.assertEqual(res[11].a_path, u'a/with spaces') # NOTE: path a/ here legit!
  188. self.assertEqual(res[11].b_path, u'b/with some spaces') # NOTE: path b/ here legit!
  189. self.assertEqual(res[12].a_path, u'a/ending in a space ')
  190. self.assertEqual(res[12].b_path, u'b/ending with space ')
  191. self.assertEqual(res[13].a_path, u'a/"with-quotes"')
  192. self.assertEqual(res[13].b_path, u'b/"with even more quotes"')
  193. def test_diff_patch_format(self):
  194. # test all of the 'old' format diffs for completness - it should at least
  195. # be able to deal with it
  196. fixtures = ("diff_2", "diff_2f", "diff_f", "diff_i", "diff_mode_only",
  197. "diff_new_mode", "diff_numstat", "diff_p", "diff_rename",
  198. "diff_tree_numstat_root", "diff_patch_unsafe_paths")
  199. for fixture_name in fixtures:
  200. diff_proc = StringProcessAdapter(fixture(fixture_name))
  201. Diff._index_from_patch_format(self.rorepo, diff_proc)
  202. # END for each fixture
  203. def test_diff_with_spaces(self):
  204. data = StringProcessAdapter(fixture('diff_file_with_spaces'))
  205. diff_index = Diff._index_from_patch_format(self.rorepo, data)
  206. self.assertIsNone(diff_index[0].a_path, repr(diff_index[0].a_path))
  207. self.assertEqual(diff_index[0].b_path, u'file with spaces', repr(diff_index[0].b_path))
  208. def test_diff_interface(self):
  209. # test a few variations of the main diff routine
  210. assertion_map = {}
  211. for i, commit in enumerate(self.rorepo.iter_commits('0.1.6', max_count=2)):
  212. diff_item = commit
  213. if i % 2 == 0:
  214. diff_item = commit.tree
  215. # END use tree every second item
  216. for other in (None, NULL_TREE, commit.Index, commit.parents[0]):
  217. for paths in (None, "CHANGES", ("CHANGES", "lib")):
  218. for create_patch in range(2):
  219. diff_index = diff_item.diff(other=other, paths=paths, create_patch=create_patch)
  220. assert isinstance(diff_index, DiffIndex)
  221. if diff_index:
  222. self._assert_diff_format(diff_index)
  223. for ct in DiffIndex.change_type:
  224. key = 'ct_%s' % ct
  225. assertion_map.setdefault(key, 0)
  226. assertion_map[key] = assertion_map[key] + len(list(diff_index.iter_change_type(ct)))
  227. # END for each changetype
  228. # check entries
  229. diff_set = set()
  230. diff_set.add(diff_index[0])
  231. diff_set.add(diff_index[0])
  232. self.assertEqual(len(diff_set), 1)
  233. self.assertEqual(diff_index[0], diff_index[0])
  234. self.assertFalse(diff_index[0] != diff_index[0])
  235. for dr in diff_index:
  236. self.assertIsNotNone(str(dr), "Diff to string conversion should be possible")
  237. # END diff index checking
  238. # END for each patch option
  239. # END for each path option
  240. # END for each other side
  241. # END for each commit
  242. # assert we could always find at least one instance of the members we
  243. # can iterate in the diff index - if not this indicates its not working correctly
  244. # or our test does not span the whole range of possibilities
  245. for key, value in assertion_map.items():
  246. self.assertIsNotNone(value, "Did not find diff for %s" % key)
  247. # END for each iteration type
  248. # test path not existing in the index - should be ignored
  249. c = self.rorepo.head.commit
  250. cp = c.parents[0]
  251. diff_index = c.diff(cp, ["does/not/exist"])
  252. self.assertEqual(len(diff_index), 0)