1# Copyright 2010-2021 The pygit2 contributors 2# 3# This file is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License, version 2, 5# as published by the Free Software Foundation. 6# 7# In addition to the permissions in the GNU General Public License, 8# the authors give you unlimited permission to link the compiled 9# version of this file into combinations with other programs, 10# and to distribute those combinations without any restriction 11# coming from the use of this file. (The General Public License 12# restrictions do apply in other respects; for example, they cover 13# modification of the file, and distribution when not linked into 14# a combined executable.) 15# 16# This file is distributed in the hope that it will be useful, but 17# WITHOUT ANY WARRANTY; without even the implied warranty of 18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19# General Public License for more details. 20# 21# You should have received a copy of the GNU General Public License 22# along with this program; see the file COPYING. If not, write to 23# the Free Software Foundation, 51 Franklin Street, Fifth Floor, 24# Boston, MA 02110-1301, USA. 25 26"""Tests for Diff objects.""" 27 28from itertools import chain 29import textwrap 30 31import pytest 32 33import pygit2 34from pygit2 import GIT_DIFF_INCLUDE_UNMODIFIED 35from pygit2 import GIT_DIFF_IGNORE_WHITESPACE, GIT_DIFF_IGNORE_WHITESPACE_EOL 36from pygit2 import GIT_DELTA_RENAMED 37 38 39COMMIT_SHA1_1 = '5fe808e8953c12735680c257f56600cb0de44b10' 40COMMIT_SHA1_2 = 'c2792cfa289ae6321ecf2cd5806c2194b0fd070c' 41COMMIT_SHA1_3 = '2cdae28389c059815e951d0bb9eed6533f61a46b' 42COMMIT_SHA1_4 = 'ccca47fbb26183e71a7a46d165299b84e2e6c0b3' 43COMMIT_SHA1_5 = '056e626e51b1fc1ee2182800e399ed8d84c8f082' 44COMMIT_SHA1_6 = 'f5e5aa4e36ab0fe62ee1ccc6eb8f79b866863b87' 45COMMIT_SHA1_7 = '784855caf26449a1914d2cf62d12b9374d76ae78' 46 47 48PATCH = """diff --git a/a b/a 49index 7f129fd..af431f2 100644 50--- a/a 51+++ b/a 52@@ -1 +1 @@ 53-a contents 2 54+a contents 55diff --git a/c/d b/c/d 56deleted file mode 100644 57index 297efb8..0000000 58--- a/c/d 59+++ /dev/null 60@@ -1 +0,0 @@ 61-c/d contents 62""" 63 64PATCHID = 'f31412498a17e6c3fbc635f2c5f9aa3ef4c1a9b7' 65 66DIFF_HEAD_TO_INDEX_EXPECTED = [ 67 'staged_changes', 68 'staged_changes_file_deleted', 69 'staged_changes_file_modified', 70 'staged_delete', 71 'staged_delete_file_modified', 72 'staged_new', 73 'staged_new_file_deleted', 74 'staged_new_file_modified' 75] 76 77DIFF_HEAD_TO_WORKDIR_EXPECTED = [ 78 'file_deleted', 79 'modified_file', 80 'staged_changes', 81 'staged_changes_file_deleted', 82 'staged_changes_file_modified', 83 'staged_delete', 84 'staged_delete_file_modified', 85 'subdir/deleted_file', 86 'subdir/modified_file' 87] 88 89DIFF_INDEX_TO_WORK_EXPECTED = [ 90 'file_deleted', 91 'modified_file', 92 'staged_changes_file_deleted', 93 'staged_changes_file_modified', 94 'staged_new_file_deleted', 95 'staged_new_file_modified', 96 'subdir/deleted_file', 97 'subdir/modified_file' 98] 99 100HUNK_EXPECTED = """- a contents 2 101+ a contents 102""" 103 104STATS_EXPECTED = """ a | 2 +- 105 c/d | 1 - 106 2 files changed, 1 insertion(+), 2 deletions(-) 107 delete mode 100644 c/d 108""" 109 110def test_diff_empty_index(dirtyrepo): 111 repo = dirtyrepo 112 head = repo[repo.lookup_reference('HEAD').resolve().target] 113 114 diff = head.tree.diff_to_index(repo.index) 115 files = [patch.delta.new_file.path for patch in diff] 116 assert DIFF_HEAD_TO_INDEX_EXPECTED == files 117 118 diff = repo.diff('HEAD', cached=True) 119 files = [patch.delta.new_file.path for patch in diff] 120 assert DIFF_HEAD_TO_INDEX_EXPECTED == files 121 122def test_workdir_to_tree(dirtyrepo): 123 repo = dirtyrepo 124 head = repo[repo.lookup_reference('HEAD').resolve().target] 125 126 diff = head.tree.diff_to_workdir() 127 files = [patch.delta.new_file.path for patch in diff] 128 assert DIFF_HEAD_TO_WORKDIR_EXPECTED == files 129 130 diff = repo.diff('HEAD') 131 files = [patch.delta.new_file.path for patch in diff] 132 assert DIFF_HEAD_TO_WORKDIR_EXPECTED == files 133 134def test_index_to_workdir(dirtyrepo): 135 diff = dirtyrepo.diff() 136 files = [patch.delta.new_file.path for patch in diff] 137 assert DIFF_INDEX_TO_WORK_EXPECTED == files 138 139 140def test_diff_invalid(barerepo): 141 commit_a = barerepo[COMMIT_SHA1_1] 142 commit_b = barerepo[COMMIT_SHA1_2] 143 with pytest.raises(TypeError): commit_a.tree.diff_to_tree(commit_b) 144 with pytest.raises(TypeError): commit_a.tree.diff_to_index(commit_b) 145 146def test_diff_empty_index_bare(barerepo): 147 repo = barerepo 148 head = repo[repo.lookup_reference('HEAD').resolve().target] 149 150 diff = barerepo.index.diff_to_tree(head.tree) 151 files = [patch.delta.new_file.path.split('/')[0] for patch in diff] 152 assert [x.name for x in head.tree] == files 153 154 diff = head.tree.diff_to_index(repo.index) 155 files = [patch.delta.new_file.path.split('/')[0] for patch in diff] 156 assert [x.name for x in head.tree] == files 157 158 diff = repo.diff('HEAD', cached=True) 159 files = [patch.delta.new_file.path.split('/')[0] for patch in diff] 160 assert [x.name for x in head.tree] == files 161 162def test_diff_tree(barerepo): 163 commit_a = barerepo[COMMIT_SHA1_1] 164 commit_b = barerepo[COMMIT_SHA1_2] 165 166 def _test(diff): 167 assert diff is not None 168 assert 2 == sum(map(lambda x: len(x.hunks), diff)) 169 170 patch = diff[0] 171 hunk = patch.hunks[0] 172 assert hunk.old_start == 1 173 assert hunk.old_lines == 1 174 assert hunk.new_start == 1 175 assert hunk.new_lines == 1 176 177 assert patch.delta.old_file.path == 'a' 178 assert patch.delta.new_file.path == 'a' 179 assert patch.delta.is_binary == False 180 181 _test(commit_a.tree.diff_to_tree(commit_b.tree)) 182 _test(barerepo.diff(COMMIT_SHA1_1, COMMIT_SHA1_2)) 183 184 185def test_diff_empty_tree(barerepo): 186 commit_a = barerepo[COMMIT_SHA1_1] 187 diff = commit_a.tree.diff_to_tree() 188 189 def get_context_for_lines(diff): 190 hunks = chain.from_iterable(map(lambda x: x.hunks, diff)) 191 lines = chain.from_iterable(map(lambda x: x.lines, hunks)) 192 return map(lambda x: x.origin, lines) 193 194 entries = [p.delta.new_file.path for p in diff] 195 assert all(commit_a.tree[x] for x in entries) 196 assert all('-' == x for x in get_context_for_lines(diff)) 197 198 diff_swaped = commit_a.tree.diff_to_tree(swap=True) 199 entries = [p.delta.new_file.path for p in diff_swaped] 200 assert all(commit_a.tree[x] for x in entries) 201 assert all('+' == x for x in get_context_for_lines(diff_swaped)) 202 203def test_diff_revparse(barerepo): 204 diff = barerepo.diff('HEAD', 'HEAD~6') 205 assert type(diff) == pygit2.Diff 206 207def test_diff_tree_opts(barerepo): 208 commit_c = barerepo[COMMIT_SHA1_3] 209 commit_d = barerepo[COMMIT_SHA1_4] 210 211 for flag in [GIT_DIFF_IGNORE_WHITESPACE, 212 GIT_DIFF_IGNORE_WHITESPACE_EOL]: 213 diff = commit_c.tree.diff_to_tree(commit_d.tree, flag) 214 assert diff is not None 215 assert 0 == len(diff[0].hunks) 216 217 diff = commit_c.tree.diff_to_tree(commit_d.tree) 218 assert diff is not None 219 assert 1 == len(diff[0].hunks) 220 221def test_diff_merge(barerepo): 222 commit_a = barerepo[COMMIT_SHA1_1] 223 commit_b = barerepo[COMMIT_SHA1_2] 224 commit_c = barerepo[COMMIT_SHA1_3] 225 226 diff_b = commit_a.tree.diff_to_tree(commit_b.tree) 227 assert diff_b is not None 228 229 diff_c = commit_b.tree.diff_to_tree(commit_c.tree) 230 assert diff_c is not None 231 assert 'b' not in [patch.delta.new_file.path for patch in diff_b] 232 assert 'b' in [patch.delta.new_file.path for patch in diff_c] 233 234 diff_b.merge(diff_c) 235 assert 'b' in [patch.delta.new_file.path for patch in diff_b] 236 237 patch = diff_b[0] 238 hunk = patch.hunks[0] 239 assert hunk.old_start == 1 240 assert hunk.old_lines == 1 241 assert hunk.new_start == 1 242 assert hunk.new_lines == 1 243 244 assert patch.delta.old_file.path == 'a' 245 assert patch.delta.new_file.path == 'a' 246 247def test_diff_patch(barerepo): 248 commit_a = barerepo[COMMIT_SHA1_1] 249 commit_b = barerepo[COMMIT_SHA1_2] 250 251 diff = commit_a.tree.diff_to_tree(commit_b.tree) 252 assert diff.patch == PATCH 253 assert len(diff) == len([patch for patch in diff]) 254 255def test_diff_ids(barerepo): 256 commit_a = barerepo[COMMIT_SHA1_1] 257 commit_b = barerepo[COMMIT_SHA1_2] 258 patch = commit_a.tree.diff_to_tree(commit_b.tree)[0] 259 delta = patch.delta 260 assert delta.old_file.id.hex == '7f129fd57e31e935c6d60a0c794efe4e6927664b' 261 assert delta.new_file.id.hex == 'af431f20fc541ed6d5afede3e2dc7160f6f01f16' 262 263def test_diff_patchid(barerepo): 264 commit_a = barerepo[COMMIT_SHA1_1] 265 commit_b = barerepo[COMMIT_SHA1_2] 266 diff = commit_a.tree.diff_to_tree(commit_b.tree) 267 assert diff.patch == PATCH 268 assert diff.patchid.hex == PATCHID 269 270def test_hunk_content(barerepo): 271 commit_a = barerepo[COMMIT_SHA1_1] 272 commit_b = barerepo[COMMIT_SHA1_2] 273 patch = commit_a.tree.diff_to_tree(commit_b.tree)[0] 274 hunk = patch.hunks[0] 275 lines = ('{0} {1}'.format(x.origin, x.content) for x in hunk.lines) 276 assert HUNK_EXPECTED == ''.join(lines) 277 for line in hunk.lines: 278 assert line.content == line.raw_content.decode() 279 280def test_find_similar(barerepo): 281 commit_a = barerepo[COMMIT_SHA1_6] 282 commit_b = barerepo[COMMIT_SHA1_7] 283 284 #~ Must pass GIT_DIFF_INCLUDE_UNMODIFIED if you expect to emulate 285 #~ --find-copies-harder during rename transformion... 286 diff = commit_a.tree.diff_to_tree(commit_b.tree, 287 GIT_DIFF_INCLUDE_UNMODIFIED) 288 assert all(x.delta.status != GIT_DELTA_RENAMED for x in diff) 289 assert all(x.delta.status_char() != 'R' for x in diff) 290 diff.find_similar() 291 assert any(x.delta.status == GIT_DELTA_RENAMED for x in diff) 292 assert any(x.delta.status_char() == 'R' for x in diff) 293 294def test_diff_stats(barerepo): 295 commit_a = barerepo[COMMIT_SHA1_1] 296 commit_b = barerepo[COMMIT_SHA1_2] 297 298 diff = commit_a.tree.diff_to_tree(commit_b.tree) 299 stats = diff.stats 300 assert 1 == stats.insertions 301 assert 2 == stats.deletions 302 assert 2 == stats.files_changed 303 formatted = stats.format(format=pygit2.GIT_DIFF_STATS_FULL | 304 pygit2.GIT_DIFF_STATS_INCLUDE_SUMMARY, 305 width=80) 306 assert STATS_EXPECTED == formatted 307 308def test_deltas(barerepo): 309 commit_a = barerepo[COMMIT_SHA1_1] 310 commit_b = barerepo[COMMIT_SHA1_2] 311 diff = commit_a.tree.diff_to_tree(commit_b.tree) 312 deltas = list(diff.deltas) 313 patches = list(diff) 314 assert len(deltas) == len(patches) 315 for i, delta in enumerate(deltas): 316 patch_delta = patches[i].delta 317 assert delta.status == patch_delta.status 318 assert delta.similarity == patch_delta.similarity 319 assert delta.nfiles == patch_delta.nfiles 320 assert delta.old_file.id == patch_delta.old_file.id 321 assert delta.new_file.id == patch_delta.new_file.id 322 323 # As explained in the libgit2 documentation, flags are not set 324 #assert delta.flags == patch_delta.flags 325 326def test_diff_parse(barerepo): 327 diff = pygit2.Diff.parse_diff(PATCH) 328 329 stats = diff.stats 330 assert 2 == stats.deletions 331 assert 1 == stats.insertions 332 assert 2 == stats.files_changed 333 334 deltas = list(diff.deltas) 335 assert 2 == len(deltas) 336 337def test_parse_diff_null(barerepo): 338 with pytest.raises(Exception): 339 barerepo.parse_diff(None) 340 341def test_parse_diff_bad(barerepo): 342 diff = textwrap.dedent( 343 """ 344 diff --git a/file1 b/file1 345 old mode 0644 346 new mode 0644 347 @@ -1,1 +1,1 @@ 348 -Hi! 349 """) 350 with pytest.raises(Exception): 351 barerepo.parse_diff(diff) 352