diff --git a/pre_commit/commands.py b/pre_commit/commands.py index ac9d4487..33e36879 100644 --- a/pre_commit/commands.py +++ b/pre_commit/commands.py @@ -229,11 +229,21 @@ def _run_hook(runner, hook_id, args, write): return 1 +def _has_unmerged_paths(runner): + _, stdout, _ = runner.cmd_runner.run(['git', 'ls-files', '--unmerged']) + return bool(stdout.strip()) + + def run(runner, args, write=sys.stdout.write): # Set up our logging handler logger.addHandler(LoggingHandler(args.color, write=write)) logger.setLevel(logging.INFO) + # Check if we have unresolved merge conflict files and fail fast. + if _has_unmerged_paths(runner): + logger.error('Unmerged files. Resolve before committing.') + return 1 + if args.no_stash or args.all_files: ctx = noop_context() else: diff --git a/tests/commands_test.py b/tests/commands_test.py index ae4e3b4c..e3e6eb17 100644 --- a/tests/commands_test.py +++ b/tests/commands_test.py @@ -275,3 +275,47 @@ def test_no_stash(repo_with_passing_hook, no_stash, all_files, expect_stash): assert warning_msg in printed else: assert warning_msg not in printed + + +@pytest.mark.parametrize(('output', 'expected'), (('some', True), ('', False))) +def test_has_unmerged_paths(output, expected): + mock_runner = mock.Mock() + mock_runner.cmd_runner.run.return_value = (1, output, '') + assert commands._has_unmerged_paths(mock_runner) is expected + + +@pytest.yield_fixture +def in_merge_conflict(repo_with_passing_hook): + local['git']['add', C.CONFIG_FILE]() + local['git']['commit', '-m' 'add hooks file']() + local['git']['clone', '.', 'foo']() + with local.cwd('foo'): + local['git']['checkout', 'origin/master', '-b', 'foo']() + with open('conflict_file', 'w') as conflict_file: + conflict_file.write('herp\nderp\n') + local['git']['add', 'conflict_file']() + local['git']['commit', '-m', 'conflict_file']() + local['git']['checkout', 'origin/master', '-b', 'bar']() + with open('conflict_file', 'w') as conflict_file: + conflict_file.write('harp\nddrp\n') + local['git']['add', 'conflict_file']() + local['git']['commit', '-m', 'conflict_file']() + local['git']['merge', 'foo'](retcode=None) + yield os.path.join(repo_with_passing_hook, 'foo') + + +def test_merge_conflict(in_merge_conflict): + ret, printed = _do_run(in_merge_conflict, _get_opts()) + assert ret == 1 + assert 'Unmerged files. Resolve before committing.' in printed + + +def test_merge_conflict_modified(in_merge_conflict): + # Touch another file so we have unstaged non-conflicting things + assert os.path.exists('dummy') + with open('dummy', 'w') as dummy_file: + dummy_file.write('bar\nbaz\n') + + ret, printed = _do_run(in_merge_conflict, _get_opts()) + assert ret == 1 + assert 'Unmerged files. Resolve before committing.' in printed diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index adb0b015..e3533733 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -227,7 +227,7 @@ def fake_logging_handler(): self.logs = [] def emit(self, record): - self.logs.append(record) + self.logs.append(record) # pragma: no cover (only hit in failure) pre_commit_logger = logging.getLogger('pre_commit') original_level = pre_commit_logger.getEffectiveLevel()