diff --git a/pre_commit/commands.py b/pre_commit/commands.py index ac9d4487..defd4199 100644 --- a/pre_commit/commands.py +++ b/pre_commit/commands.py @@ -30,6 +30,9 @@ COLS = int(subprocess.Popen(['tput', 'cols'], stdout=subprocess.PIPE).communicat PASS_FAIL_LENGTH = 6 +# Grabbed from `git help status` +CONFLICTING_GIT_STATUSES = set(('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU')) + def install(runner): """Install the pre-commit hooks.""" @@ -229,11 +232,24 @@ def _run_hook(runner, hook_id, args, write): return 1 +def _has_unmerged_paths(runner): + _, stdout, _ = runner.cmd_runner.run( + ['git', 'status', '--short'], retcode=None, + ) + codes = set(line[:2] for line in stdout.splitlines()) + return codes & CONFLICTING_GIT_STATUSES > set() + + 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 40530461..37fe3aea 100644 --- a/tests/commands_test.py +++ b/tests/commands_test.py @@ -277,6 +277,17 @@ def test_no_stash(repo_with_passing_hook, no_stash, all_files, expect_stash): assert warning_msg not in printed +@pytest.mark.parametrize( + ('mode', 'expected'), + [(status, True) for status in commands.CONFLICTING_GIT_STATUSES] + + [(' A', False), (' D', False), (' M', False)] +) +def test_has_unmerged_paths(mode, expected): + mock_runner = mock.Mock() + mock_runner.cmd_runner.run.return_value = (1, mode + ' foo', '') + 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]() @@ -298,6 +309,12 @@ def in_merge_conflict(repo_with_passing_hook): 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: @@ -305,4 +322,4 @@ def test_merge_conflict(in_merge_conflict): ret, printed = _do_run(in_merge_conflict, _get_opts()) assert ret == 1 - assert 'Resolve merge conflicts before committing' in printed + 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()