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