from __future__ import annotations import os import shutil import pytest from pre_commit_hooks.check_merge_conflict import main from pre_commit_hooks.util import cmd_output from testing.util import get_resource_path from testing.util import git_commit @pytest.fixture def f1_is_a_conflict_file(tmpdir): # Make a merge conflict repo1 = tmpdir.join('repo1') repo1_f1 = repo1.join('f1') repo2 = tmpdir.join('repo2') repo2_f1 = repo2.join('f1') cmd_output('git', 'init', '--', str(repo1)) with repo1.as_cwd(): repo1_f1.ensure() cmd_output('git', 'add', '.') git_commit('-m', 'commit1') cmd_output('git', 'clone', str(repo1), str(repo2)) # Commit in master with repo1.as_cwd(): repo1_f1.write('parent\n') git_commit('-am', 'master commit2') # Commit in clone and pull with repo2.as_cwd(): repo2_f1.write('child\n') git_commit('-am', 'clone commit2') cmd_output('git', 'pull', '--no-rebase', retcode=None) # We should end up in a merge conflict! f1 = repo2_f1.read() assert f1.startswith( '<<<<<<< HEAD\n' 'child\n' '=======\n' 'parent\n' '>>>>>>>', ) or f1.startswith( '<<<<<<< HEAD\n' 'child\n' # diff3 conflict style git merges add this line: '||||||| merged common ancestors\n' '=======\n' 'parent\n' '>>>>>>>', ) or f1.startswith( # .gitconfig with [pull] rebase = preserve causes a rebase which # flips parent / child '<<<<<<< HEAD\n' 'parent\n' '=======\n' 'child\n' '>>>>>>>', ) assert os.path.exists(os.path.join('.git', 'MERGE_MSG')) yield repo2 @pytest.fixture def repository_pending_merge(tmpdir): # Make a (non-conflicting) merge repo1 = tmpdir.join('repo1') repo1_f1 = repo1.join('f1') repo2 = tmpdir.join('repo2') repo2_f1 = repo2.join('f1') repo2_f2 = repo2.join('f2') cmd_output('git', 'init', str(repo1)) with repo1.as_cwd(): repo1_f1.ensure() cmd_output('git', 'add', '.') git_commit('-m', 'commit1') cmd_output('git', 'clone', str(repo1), str(repo2)) # Commit in master with repo1.as_cwd(): repo1_f1.write('parent\n') git_commit('-am', 'master commit2') # Commit in clone and pull without committing with repo2.as_cwd(): repo2_f2.write('child\n') cmd_output('git', 'add', '.') git_commit('-m', 'clone commit2') cmd_output('git', 'pull', '--no-commit', '--no-rebase') # We should end up in a pending merge assert repo2_f1.read() == 'parent\n' assert repo2_f2.read() == 'child\n' assert os.path.exists(os.path.join('.git', 'MERGE_HEAD')) yield repo2 @pytest.mark.usefixtures('f1_is_a_conflict_file') def test_merge_conflicts_git(capsys): assert main(['f1']) == 1 out, _ = capsys.readouterr() assert out == ( "f1:1: Merge conflict string '<<<<<<<' found\n" "f1:3: Merge conflict string '=======' found\n" "f1:5: Merge conflict string '>>>>>>>' found\n" ) @pytest.mark.parametrize( 'contents', (b'<<<<<<< HEAD\n', b'=======\n', b'>>>>>>> master\n'), ) def test_merge_conflicts_failing(contents, repository_pending_merge): repository_pending_merge.join('f2').write_binary(contents) assert main(['f2']) == 1 @pytest.mark.parametrize( 'contents', (b'# <<<<<<< HEAD\n', b'# =======\n', b'import mod', b''), ) def test_merge_conflicts_ok(contents, f1_is_a_conflict_file): f1_is_a_conflict_file.join('f1').write_binary(contents) assert main(['f1']) == 0 @pytest.mark.usefixtures('f1_is_a_conflict_file') def test_ignores_binary_files(): shutil.copy(get_resource_path('img1.jpg'), 'f1') assert main(['f1']) == 0 def test_does_not_care_when_not_in_a_merge(tmpdir): f = tmpdir.join('README.md') f.write_binary(b'problem\n=======\n') assert main([str(f.realpath())]) == 0 def test_care_when_assumed_merge(tmpdir): f = tmpdir.join('README.md') f.write_binary(b'problem\n=======\n') assert main([str(f.realpath()), '--assume-in-merge']) == 1 def test_worktree_merge_conflicts(f1_is_a_conflict_file, tmpdir, capsys): worktree = tmpdir.join('worktree') cmd_output('git', 'worktree', 'add', str(worktree)) with worktree.as_cwd(): cmd_output( 'git', 'pull', '--no-rebase', 'origin', 'master', retcode=None, ) msg = f1_is_a_conflict_file.join('.git/worktrees/worktree/MERGE_MSG') assert msg.exists() test_merge_conflicts_git(capsys)