From e3974cdd0627f4ac9dbf6e4dd92ec1be808cc09c Mon Sep 17 00:00:00 2001 From: Liangchen Zhang Date: Sun, 18 Oct 2026 12:35:55 -0300 Subject: [PATCH] Support shebang executable checks outside git repos --- .../check_shebang_scripts_are_executable.py | 22 +++++++++++++--- ...eck_shebang_scripts_are_executable_test.py | 25 +++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/pre_commit_hooks/check_shebang_scripts_are_executable.py b/pre_commit_hooks/check_shebang_scripts_are_executable.py index 937425b0..361e5f13 100644 --- a/pre_commit_hooks/check_shebang_scripts_are_executable.py +++ b/pre_commit_hooks/check_shebang_scripts_are_executable.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse +import os import shlex import sys from collections.abc import Sequence @@ -9,13 +10,26 @@ from pre_commit_hooks.check_executables_have_shebangs import EXECUTABLE_VALUES from pre_commit_hooks.check_executables_have_shebangs import git_ls_files from pre_commit_hooks.check_executables_have_shebangs import has_shebang +from pre_commit_hooks.util import cmd_output def check_shebangs(paths: list[str]) -> int: - # Cannot optimize on non-executability here if we intend this check to - # work on win32 -- and that's where problems caused by non-executability - # (elsewhere) are most likely to arise from. - return _check_git_filemode(paths) + fs_tracks_executable_bit = cmd_output( + 'git', 'config', 'core.fileMode', retcode=None, + ).strip() + + # Fall back to Git's file mode when the filesystem cannot reliably + # track executable bits, as is commonly the case on Windows. + if fs_tracks_executable_bit == 'false': # pragma: win32 cover + return _check_git_filemode(paths) + else: # pragma: win32 no cover + retv = 0 + for path in paths: + if has_shebang(path) and not os.access(path, os.X_OK): + _message(path) + retv = 1 + + return retv def _check_git_filemode(paths: Sequence[str]) -> int: diff --git a/tests/check_shebang_scripts_are_executable_test.py b/tests/check_shebang_scripts_are_executable_test.py index e4bd07ca..74ca24a9 100644 --- a/tests/check_shebang_scripts_are_executable_test.py +++ b/tests/check_shebang_scripts_are_executable_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import sys import pytest @@ -9,6 +10,30 @@ from pre_commit_hooks.check_shebang_scripts_are_executable import main from pre_commit_hooks.util import cmd_output +skip_win32 = pytest.mark.skipif( + sys.platform == 'win32', + reason="non-git checks aren't relevant on windows", +) + + +@skip_win32 # pragma: win32 no cover +@pytest.mark.parametrize( + ('content', 'mode', 'expected'), + ( + pytest.param('#!python', '+x', 0, id='shebang with executable'), + pytest.param('#!python', '-x', 1, id='shebang without executable'), + pytest.param('', '+x', 0, id='no shebang with executable'), + pytest.param('', '-x', 0, id='no shebang without executable'), + ), +) +def test_non_git_executable_shebang(tmpdir, content, mode, expected): + with tmpdir.as_cwd(): + path = tmpdir.join('path') + path.write(content) + cmd_output('chmod', mode, str(path)) + + assert main((str(path),)) == expected + def test_check_git_filemode_passing(tmpdir): with tmpdir.as_cwd():