# coding: utf-8 # test_diff.py # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php import ddt import shutil import tempfile from git import ( Repo, GitCommandError, Diff, DiffIndex, NULL_TREE, Submodule, ) from git.cmd import Git from git.test.lib import ( TestBase, StringProcessAdapter, fixture, assert_equal, assert_true, ) from git.test.lib import with_rw_directory import os.path as osp @ddt.ddt class TestDiff(TestBase): def setUp(self): self.repo_dir = tempfile.mkdtemp() self.submodule_dir = tempfile.mkdtemp() def tearDown(self): import gc gc.collect() shutil.rmtree(self.repo_dir) shutil.rmtree(self.submodule_dir) def _assert_diff_format(self, diffs): # verify that the format of the diff is sane for diff in diffs: if diff.a_mode: assert isinstance(diff.a_mode, int) if diff.b_mode: assert isinstance(diff.b_mode, int) if diff.a_blob: assert not diff.a_blob.path.endswith('\n') if diff.b_blob: assert not diff.b_blob.path.endswith('\n') # END for each diff return diffs @with_rw_directory def test_diff_with_staged_file(self, rw_dir): # SETUP INDEX WITH MULTIPLE STAGES r = Repo.init(rw_dir) fp = osp.join(rw_dir, 'hello.txt') with open(fp, 'w') as fs: fs.write("hello world") r.git.add(Git.polish_url(fp)) r.git.commit(message="init") with open(fp, 'w') as fs: fs.write("Hola Mundo") r.git.add(Git.polish_url(fp)) self.assertEqual(len(r.index.diff("HEAD", create_patch=True)), 1, "create_patch should generate patch of diff to HEAD") r.git.commit(message="change on master") self.assertEqual(len(r.index.diff("HEAD", create_patch=True)), 0, "create_patch should generate no patch, already on HEAD") r.git.checkout('HEAD~1', b='topic') with open(fp, 'w') as fs: fs.write("Hallo Welt") r.git.commit(all=True, message="change on topic branch") # there must be a merge-conflict with self.assertRaises(GitCommandError): r.git.cherry_pick('master') # Now do the actual testing - this should just work self.assertEqual(len(r.index.diff(None)), 2) self.assertEqual(len(r.index.diff(None, create_patch=True)), 0, "This should work, but doesn't right now ... it's OK") def test_list_from_string_new_mode(self): output = StringProcessAdapter(fixture('diff_new_mode')) diffs = Diff._index_from_patch_format(self.rorepo, output) self._assert_diff_format(diffs) assert_equal(1, len(diffs)) assert_equal(8, len(diffs[0].diff.splitlines())) def test_diff_with_rename(self): output = StringProcessAdapter(fixture('diff_rename')) diffs = Diff._index_from_patch_format(self.rorepo, output) self._assert_diff_format(diffs) assert_equal(1, len(diffs)) diff = diffs[0] assert_true(diff.renamed_file) assert_true(diff.renamed) assert_equal(diff.rename_from, u'Jérôme') assert_equal(diff.rename_to, u'müller') assert_equal(diff.raw_rename_from, b'J\xc3\xa9r\xc3\xb4me') assert_equal(diff.raw_rename_to, b'm\xc3\xbcller') assert isinstance(str(diff), str) output = StringProcessAdapter(fixture('diff_rename_raw')) diffs = Diff._index_from_raw_format(self.rorepo, output) self.assertEqual(len(diffs), 1) diff = diffs[0] self.assertIsNotNone(diff.renamed_file) self.assertIsNotNone(diff.renamed) self.assertEqual(diff.rename_from, 'this') self.assertEqual(diff.rename_to, 'that') self.assertEqual(diff.change_type, 'R') self.assertEqual(diff.score, 100) self.assertEqual(len(list(diffs.iter_change_type('R'))), 1) def test_diff_with_copied_file(self): output = StringProcessAdapter(fixture('diff_copied_mode')) diffs = Diff._index_from_patch_format(self.rorepo, output) self._assert_diff_format(diffs) assert_equal(1, len(diffs)) diff = diffs[0] assert_true(diff.copied_file) assert_true(diff.a_path, u'test1.txt') assert_true(diff.b_path, u'test2.txt') assert isinstance(str(diff), str) output = StringProcessAdapter(fixture('diff_copied_mode_raw')) diffs = Diff._index_from_raw_format(self.rorepo, output) self.assertEqual(len(diffs), 1) diff = diffs[0] self.assertEqual(diff.change_type, 'C') self.assertEqual(diff.score, 100) self.assertEqual(diff.a_path, u'test1.txt') self.assertEqual(diff.b_path, u'test2.txt') self.assertEqual(len(list(diffs.iter_change_type('C'))), 1) def test_diff_with_change_in_type(self): output = StringProcessAdapter(fixture('diff_change_in_type')) diffs = Diff._index_from_patch_format(self.rorepo, output) self._assert_diff_format(diffs) assert_equal(2, len(diffs)) diff = diffs[0] self.assertIsNotNone(diff.deleted_file) assert_equal(diff.a_path, 'this') assert_equal(diff.b_path, 'this') assert isinstance(str(diff), str) diff = diffs[1] assert_equal(diff.a_path, None) assert_equal(diff.b_path, 'this') self.assertIsNotNone(diff.new_file) assert isinstance(str(diff), str) output = StringProcessAdapter(fixture('diff_change_in_type_raw')) diffs = Diff._index_from_raw_format(self.rorepo, output) self.assertEqual(len(diffs), 1) diff = diffs[0] self.assertEqual(diff.rename_from, None) self.assertEqual(diff.rename_to, None) self.assertEqual(diff.change_type, 'T') self.assertEqual(len(list(diffs.iter_change_type('T'))), 1) def test_diff_of_modified_files_not_added_to_the_index(self): output = StringProcessAdapter(fixture('diff_abbrev-40_full-index_M_raw_no-color')) diffs = Diff._index_from_raw_format(self.rorepo, output) self.assertEqual(len(diffs), 1, 'one modification') self.assertEqual(len(list(diffs.iter_change_type('M'))), 1, 'one modification') self.assertEqual(diffs[0].change_type, 'M') self.assertIsNone(diffs[0].b_blob,) @ddt.data( (Diff._index_from_patch_format, 'diff_patch_binary'), (Diff._index_from_raw_format, 'diff_raw_binary') ) def test_binary_diff(self, case): method, file_name = case res = method(None, StringProcessAdapter(fixture(file_name))) self.assertEqual(len(res), 1) self.assertEqual(len(list(res.iter_change_type('M'))), 1) if res[0].diff: self.assertEqual(res[0].diff, b"Binary files a/rps and b/rps differ\n", "in patch mode, we get a diff text") self.assertIsNotNone(str(res[0]), "This call should just work") def test_diff_index(self): output = StringProcessAdapter(fixture('diff_index_patch')) res = Diff._index_from_patch_format(None, output) self.assertEqual(len(res), 6) for dr in res: self.assertTrue(dr.diff.startswith(b'@@'), dr) self.assertIsNotNone(str(dr), "Diff to string conversion should be possible") # end for each diff dr = res[3] assert dr.diff.endswith(b"+Binary files a/rps and b/rps differ\n") def test_diff_index_raw_format(self): output = StringProcessAdapter(fixture('diff_index_raw')) res = Diff._index_from_raw_format(None, output) self.assertIsNotNone(res[0].deleted_file) self.assertIsNone(res[0].b_path,) def test_diff_initial_commit(self): initial_commit = self.rorepo.commit('33ebe7acec14b25c5f84f35a664803fcab2f7781') # Without creating a patch... diff_index = initial_commit.diff(NULL_TREE) self.assertEqual(diff_index[0].b_path, 'CHANGES') self.assertIsNotNone(diff_index[0].new_file) self.assertEqual(diff_index[0].diff, '') # ...and with creating a patch diff_index = initial_commit.diff(NULL_TREE, create_patch=True) self.assertIsNone(diff_index[0].a_path, repr(diff_index[0].a_path)) self.assertEqual(diff_index[0].b_path, 'CHANGES', repr(diff_index[0].b_path)) self.assertIsNotNone(diff_index[0].new_file) self.assertEqual(diff_index[0].diff, fixture('diff_initial')) def test_diff_unsafe_paths(self): output = StringProcessAdapter(fixture('diff_patch_unsafe_paths')) res = Diff._index_from_patch_format(None, output) # The "Additions" self.assertEqual(res[0].b_path, u'path/ starting with a space') self.assertEqual(res[1].b_path, u'path/"with-quotes"') self.assertEqual(res[2].b_path, u"path/'with-single-quotes'") self.assertEqual(res[3].b_path, u'path/ending in a space ') self.assertEqual(res[4].b_path, u'path/with\ttab') self.assertEqual(res[5].b_path, u'path/with\nnewline') self.assertEqual(res[6].b_path, u'path/with spaces') self.assertEqual(res[7].b_path, u'path/with-question-mark?') self.assertEqual(res[8].b_path, u'path/¯\\_(ツ)_|¯') self.assertEqual(res[9].b_path, u'path/💩.txt') self.assertEqual(res[9].b_rawpath, b'path/\xf0\x9f\x92\xa9.txt') self.assertEqual(res[10].b_path, u'path/�-invalid-unicode-path.txt') self.assertEqual(res[10].b_rawpath, b'path/\x80-invalid-unicode-path.txt') # The "Moves" # NOTE: The path prefixes a/ and b/ here are legit! We're actually # verifying that it's not "a/a/" that shows up, see the fixture data. self.assertEqual(res[11].a_path, u'a/with spaces') # NOTE: path a/ here legit! self.assertEqual(res[11].b_path, u'b/with some spaces') # NOTE: path b/ here legit! self.assertEqual(res[12].a_path, u'a/ending in a space ') self.assertEqual(res[12].b_path, u'b/ending with space ') self.assertEqual(res[13].a_path, u'a/"with-quotes"') self.assertEqual(res[13].b_path, u'b/"with even more quotes"') def test_diff_patch_format(self): # test all of the 'old' format diffs for completness - it should at least # be able to deal with it fixtures = ("diff_2", "diff_2f", "diff_f", "diff_i", "diff_mode_only", "diff_new_mode", "diff_numstat", "diff_p", "diff_rename", "diff_tree_numstat_root", "diff_patch_unsafe_paths") for fixture_name in fixtures: diff_proc = StringProcessAdapter(fixture(fixture_name)) Diff._index_from_patch_format(self.rorepo, diff_proc) # END for each fixture def test_diff_with_spaces(self): data = StringProcessAdapter(fixture('diff_file_with_spaces')) diff_index = Diff._index_from_patch_format(self.rorepo, data) self.assertIsNone(diff_index[0].a_path, repr(diff_index[0].a_path)) self.assertEqual(diff_index[0].b_path, u'file with spaces', repr(diff_index[0].b_path)) def test_diff_submodule(self): """Test that diff is able to correctly diff commits that cover submodule changes""" # Init a temp git repo that will be referenced as a submodule sub = Repo.init(self.submodule_dir) with open(self.submodule_dir + "/subfile", "w") as sub_subfile: sub_subfile.write("") sub.index.add(["subfile"]) sub.index.commit("first commit") # Init a temp git repo that will incorporate the submodule repo = Repo.init(self.repo_dir) with open(self.repo_dir + "/test", "w") as foo_test: foo_test.write("") repo.index.add(['test']) Submodule.add(repo, "subtest", "sub", url="file://" + self.submodule_dir) repo.index.commit("first commit") repo.create_tag('1') # Add a commit to the submodule submodule = repo.submodule('subtest') with open(self.repo_dir + "/sub/subfile", "w") as foo_sub_subfile: foo_sub_subfile.write("blub") submodule.module().index.add(["subfile"]) submodule.module().index.commit("changed subfile") submodule.binsha = submodule.module().head.commit.binsha # Commit submodule updates in parent repo repo.index.add([submodule]) repo.index.commit("submodule changed") repo.create_tag('2') diff = repo.commit('1').diff(repo.commit('2'))[0] # If diff is unable to find the commit hashes (looks in wrong repo) the *_blob.size # property will be a string containing exception text, an int indicates success self.assertIsInstance(diff.a_blob.size, int) self.assertIsInstance(diff.b_blob.size, int) def test_diff_interface(self): # test a few variations of the main diff routine assertion_map = {} for i, commit in enumerate(self.rorepo.iter_commits('0.1.6', max_count=2)): diff_item = commit if i % 2 == 0: diff_item = commit.tree # END use tree every second item for other in (None, NULL_TREE, commit.Index, commit.parents[0]): for paths in (None, "CHANGES", ("CHANGES", "lib")): for create_patch in range(2): diff_index = diff_item.diff(other=other, paths=paths, create_patch=create_patch) assert isinstance(diff_index, DiffIndex) if diff_index: self._assert_diff_format(diff_index) for ct in DiffIndex.change_type: key = 'ct_%s' % ct assertion_map.setdefault(key, 0) assertion_map[key] = assertion_map[key] + len(list(diff_index.iter_change_type(ct))) # END for each changetype # check entries diff_set = set() diff_set.add(diff_index[0]) diff_set.add(diff_index[0]) self.assertEqual(len(diff_set), 1) self.assertEqual(diff_index[0], diff_index[0]) self.assertFalse(diff_index[0] != diff_index[0]) for dr in diff_index: self.assertIsNotNone(str(dr), "Diff to string conversion should be possible") # END diff index checking # END for each patch option # END for each path option # END for each other side # END for each commit # assert we could always find at least one instance of the members we # can iterate in the diff index - if not this indicates its not working correctly # or our test does not span the whole range of possibilities for key, value in assertion_map.items(): self.assertIsNotNone(value, "Did not find diff for %s" % key) # END for each iteration type # test path not existing in the index - should be ignored c = self.rorepo.head.commit cp = c.parents[0] diff_index = c.diff(cp, ["does/not/exist"]) self.assertEqual(len(diff_index), 0)