From 1512381d75c532acf7681548fcab7bc906411d86 Mon Sep 17 00:00:00 2001 From: Vyacheslav Kapitonov Date: Sat, 14 Oct 2023 05:16:27 +0300 Subject: [PATCH] Fix _has_unstaged_config false negative check report --- pre_commit/commands/run.py | 15 ++++++-- tests/commands/run_test.py | 78 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 41ba4ecf..692ebd7c 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -327,9 +327,18 @@ def _has_unmerged_paths() -> bool: def _has_unstaged_config(config_file: str) -> bool: - retcode, _, _ = cmd_output_b( - 'git', 'diff', '--quiet', '--no-ext-diff', config_file, check=False, - ) + retcode = 0 + if os.path.islink(config_file): + real_config_file = os.path.realpath(config_file) + _, is_real_file_in_git_dir, _, = cmd_output_b( + 'git', 'ls-files', config_file, + ) + if is_real_file_in_git_dir: + cmd = ('git', 'diff', '--quiet', '--no-ext-diff', real_config_file) + retcode, _, _ = cmd_output_b(*cmd, check=False) + else: + cmd = ('git', 'diff', '--quiet', '--no-ext-diff', config_file) + retcode, _, _ = cmd_output_b(*cmd, check=False) # be explicit, other git errors don't mean it has an unstaged config. return retcode == 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 6a0cd855..4b435d8a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -3,6 +3,7 @@ from __future__ import annotations import os.path import shlex import sys +import tempfile import time from typing import MutableMapping from unittest import mock @@ -16,6 +17,7 @@ from pre_commit.commands.run import _compute_cols from pre_commit.commands.run import _full_msg from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths +from pre_commit.commands.run import _has_unstaged_config from pre_commit.commands.run import _start_msg from pre_commit.commands.run import Classifier from pre_commit.commands.run import filter_by_include_exclude @@ -1217,3 +1219,79 @@ def test_pre_commit_env_variable_set(cap_out, store, repo_with_passing_hook): cap_out, store, repo_with_passing_hook, args, environ, ) assert environ['PRE_COMMIT'] == '1' + + +@pytest.mark.parametrize( + ('config_before', 'config_after', 'retval'), + ( + ({'config with change': 1}, {'config with change': 0}, True), + ({'config without change': 1}, {'config without change': 1}, False), + ), +) +def test_has_unstaged_config( + config_before, + config_after, + retval, + tempdir_factory, + monkeypatch, +): + git_path = git_dir(tempdir_factory) + monkeypatch.chdir(git_path) + add_config_to_repo(git_path, config_before) + write_config(git_path, config_before, C.CONFIG_FILE) + + if str(config_before) != str(config_after): + write_config(git_path, config_after, C.CONFIG_FILE) + + full_config_path = os.path.join(git_path, C.CONFIG_FILE) + assert retval == _has_unstaged_config(full_config_path) + + +@pytest.mark.parametrize( + ('config_before', 'config_after', 'retval'), + ( + ({'config with change': 1}, {'config with change': 0}, True), + ({'config without change': 1}, {'config without change': 1}, False), + ), +) +def test_has_unstaged_config_symlink( + config_before, + config_after, + retval, + tempdir_factory, + monkeypatch, +): + git_path = git_dir(tempdir_factory) + real_config_path = C.CONFIG_FILE.lstrip('.') + monkeypatch.chdir(git_path) + add_config_to_repo(git_path, config_before, real_config_path) + write_config(git_path, config_before, real_config_path) + os.symlink(real_config_path, C.CONFIG_FILE) + cmd_output('git', 'add', '-A') + git_commit() + + if str(config_before) != str(config_after): + write_config(git_path, config_after, real_config_path) + + full_config_path = os.path.join(git_path, C.CONFIG_FILE) + assert retval == _has_unstaged_config(full_config_path) + + +def test_has_unstaged_config_symlink_outside(tempdir_factory, monkeypatch): + config_before = {'config with change': 1} + config_after = {'config with change': 0} + with tempfile.NamedTemporaryFile() as tf: + git_path = git_dir(tempdir_factory) + real_config_path = tf.name + monkeypatch.chdir(git_path) + with open(real_config_path, 'w') as f: + f.write(str(config_before)) + os.symlink(real_config_path, C.CONFIG_FILE) + cmd_output('git', 'add', C.CONFIG_FILE) + git_commit() + + with open(real_config_path, 'w') as f: + f.write(str(config_after)) + + full_config_path = os.path.join(git_path, C.CONFIG_FILE) + assert not _has_unstaged_config(full_config_path)