diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7ea4a444f..a7dc806ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.16.0 + rev: v3.17.0 hooks: - id: reorder-python-imports exclude: ^pre_commit/resources/ @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.20.1 + rev: v2.3.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] diff --git a/CHANGELOG.md b/CHANGELOG.md index 0620a8ae0..c31f6dbd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +4.6.1 - 2026-07-21 +================== + +### Fixes +- Install `language: node` hooks via `git`. + - Fixes npm 12.x compatibility + - #3719 PR by @asottile. + - #3517 issue by @ojob. +- Set `JULIA_DEPOT_PATH` for `language: julia`. + - #3711 PR by @damonbayer. + - pre-commit-ci/runner-image#335 issue by @damonbayer. +- Produce error on mistyped `--repo` for `pre-commit autoupdate`. + - #3701 PR by @mxr. + - #3695 issue by @mxr. +- Improve performance of commit existence check in `pre-push`. + - #3726 PR by @asottile. + - #3604 issue by @ptarjan. +- Avoid duplicating conflicted filenames during `pre-commit run --all-files`. + - #3727 PR by @asottile. + - #3706 issue by @RomanValov. + 4.6.0 - 2026-04-21 ================== diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index aa0c5e25e..f07e54b80 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -175,6 +175,13 @@ def autoupdate( repo for repo in load_config(config_file)['repos'] if repo['repo'] not in {LOCAL, META} ] + missing_repos = set(repos) - {r['repo'] for r in config_repos} + if missing_repos: + output.write_line( + f'repos ({", ".join(sorted(missing_repos))}) were ' + f'not found in {config_file}', + ) + return 1 rev_infos: list[RevInfo | None] = [None] * len(config_repos) jobs = jobs or xargs.cpu_count() # 0 => number of cpus diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 7b806f3b8..0ec303564 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -114,7 +114,7 @@ def _ns( def _rev_exists(rev: str) -> bool: - return not subprocess.call(('git', 'rev-list', '--quiet', rev)) + return not subprocess.call(('git', 'cat-file', '-e', f'{rev}^{{commit}}')) def _pre_push_ns( diff --git a/pre_commit/git.py b/pre_commit/git.py index ec1928f37..3d3f58016 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -152,7 +152,7 @@ def intent_to_add_files() -> list[str]: def get_all_files() -> list[str]: - return zsplit(cmd_output('git', 'ls-files', '-z')[1]) + return zsplit(cmd_output('git', 'ls-files', '-z', '--deduplicate')[1]) def get_changed_files(old: str, new: str) -> list[str]: diff --git a/pre_commit/languages/julia.py b/pre_commit/languages/julia.py index 7559b5ba6..5122c3fc2 100644 --- a/pre_commit/languages/julia.py +++ b/pre_commit/languages/julia.py @@ -51,6 +51,13 @@ def get_env_patch(target_dir: str, version: str) -> PatchesT: ('JULIA_LOAD_PATH', target_dir), # May be set, remove it to not interfer with LOAD_PATH ('JULIA_PROJECT', UNSET), + # Keep the package depot inside the hook environment so installed + # packages and precompile caches persist with the env instead of + # leaking into a shared depot. The trailing separator leaves an empty + # entry, which julia expands to its default depots. This keeps the + # bundled stdlib resources (and their precompile caches) available so + # hooks don't recompile from scratch on first run. + ('JULIA_DEPOT_PATH', os.path.join(target_dir, 'depot') + os.pathsep), ) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index af7dc6f87..304fdcdd8 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -17,7 +17,6 @@ from pre_commit.prefix import Prefix from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -from pre_commit.util import rmtree ENVIRONMENT_DIR = 'node_env' run_hook = lang_base.basic_run_hook @@ -77,7 +76,18 @@ def health_check(prefix: Prefix, version: str) -> str | None: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - assert prefix.exists('package.json') + if prefix.exists('.git') and prefix.exists('package.json'): + # this requires a new-enough npm (2019ish?) + pkgs = (f'git+file://{prefix.prefix_dir}', *additional_dependencies) + else: + pkgs = (*additional_dependencies,) + + if not pkgs: + raise AssertionError( + '`language: node` must have package.json or ' + 'additional_dependencies', + ) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath @@ -89,22 +99,5 @@ def install_environment( cmd_output_b(*cmd) with in_env(prefix, version): - # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 - # install as if we installed from git - - local_install_cmd = ( - 'npm', 'install', '--include=dev', '--include=prod', - '--ignore-prepublish', '--no-progress', '--no-save', - ) - lang_base.setup_cmd(prefix, local_install_cmd) - - _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) - pkg = prefix.path(pkg.strip()) - - install = ('npm', 'install', '-g', pkg, *additional_dependencies) + install = ('npm', 'install', '--allow-git=root', '-g', *pkgs) lang_base.setup_cmd(prefix, install) - - # clean these up after installation - if prefix.exists('node_modules'): # pragma: win32 no cover - rmtree(prefix.path('node_modules')) - os.remove(pkg) diff --git a/pre_commit/resources/empty_template_.npmignore b/pre_commit/resources/empty_template_.npmignore deleted file mode 100644 index 72e8ffc0d..000000000 --- a/pre_commit/resources/empty_template_.npmignore +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/pre_commit/resources/empty_template_package.json b/pre_commit/resources/empty_template_package.json deleted file mode 100644 index 042e9583c..000000000 --- a/pre_commit/resources/empty_template_package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "pre_commit_placeholder_package", - "version": "0.0.0" -} diff --git a/pre_commit/store.py b/pre_commit/store.py index dc90c0519..8bd1e1955 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -37,8 +37,8 @@ def _get_default_directory() -> str: _LOCAL_RESOURCES = ( - 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', - 'package.json', 'pre-commit-package-dev-1.rockspec', + 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', + 'pre-commit-package-dev-1.rockspec', 'pre_commit_placeholder_package.gemspec', 'setup.py', 'environment.yml', 'Makefile.PL', 'pubspec.yaml', 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', diff --git a/setup.cfg b/setup.cfg index 072fbbb0f..5d4ef5b2c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.6.0 +version = 4.6.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh index 958e73b24..0087feb82 100755 --- a/testing/get-coursier.sh +++ b/testing/get-coursier.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -if [ "$OSTYPE" = msys ]; then +if [ "$OSTYPE" = cygwin ]; then URL='https://github.com/coursier/coursier/releases/download/v2.1.0-RC4/cs-x86_64-pc-win32.zip' SHA256='0d07386ff0f337e3e6264f7dde29d137dda6eaa2385f29741435e0b93ccdb49d' TARGET='/tmp/coursier/cs.zip' diff --git a/testing/get-dart.sh b/testing/get-dart.sh index b4545e71e..ad7f8608a 100755 --- a/testing/get-dart.sh +++ b/testing/get-dart.sh @@ -3,7 +3,7 @@ set -euo pipefail VERSION=2.19.6 -if [ "$OSTYPE" = msys ]; then +if [ "$OSTYPE" = cygwin ]; then URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" cygpath -w /tmp/dart-sdk/bin >> "$GITHUB_PATH" else diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 71bd04446..cbf22f393 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -260,8 +260,8 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( assert 'local' in after -def test_autoupdate_out_of_date_repo_with_wrong_repo_name( - out_of_date, in_tmpdir, +def test_autoupdate_missing_repo_name( + out_of_date, in_tmpdir, cap_out, ): config = make_config_from_repo( out_of_date.path, rev=out_of_date.original_rev, check=False, @@ -270,15 +270,17 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( with open(C.CONFIG_FILE) as f: before = f.read() - # It will not update it, because the name doesn't match ret = autoupdate( C.CONFIG_FILE, freeze=False, tags_only=False, - repos=('dne',), + repos=('dne', 'foo'), ) with open(C.CONFIG_FILE) as f: after = f.read() - assert ret == 0 + assert ret == 1 assert before == after + assert cap_out.get() == ( + f"repos (dne, foo) were not found in {C.CONFIG_FILE}\n" + ) def test_does_not_reformat(tmpdir, out_of_date): diff --git a/tests/git_test.py b/tests/git_test.py index 02b6ce3ae..763aa0de5 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -225,6 +225,17 @@ def test_all_files_non_ascii(non_ascii_repo): assert ret == ['интервью'] +def test_all_files_merge_conflict(in_merge_conflict): + ret = git.get_all_files() + assert sorted(ret) == sorted([ + '.pre-commit-config.yaml', + 'bar_only_file', + 'conflict_file', + 'foo_only_file', + 'placeholder', + ]) + + def test_staged_files_non_ascii(non_ascii_repo): non_ascii_repo.join('интервью').write('hi') cmd_output('git', 'add', '.') diff --git a/tests/languages/julia_test.py b/tests/languages/julia_test.py index 175622d65..aa7098cfb 100644 --- a/tests/languages/julia_test.py +++ b/tests/languages/julia_test.py @@ -3,7 +3,9 @@ import os from unittest import mock +from pre_commit import constants as C from pre_commit.languages import julia +from pre_commit.prefix import Prefix from testing.language_helpers import run_language from testing.util import cwd @@ -42,6 +44,19 @@ def test_julia_hook_with_startup(tmp_path): test_julia_hook(tmp_path) +def test_julia_hook_installs_into_env_local_depot(tmp_path): + code = """ + using Example + println("Hello, world!") + """ + _make_hook(tmp_path, code) + + julia.install_environment(Prefix(str(tmp_path)), C.DEFAULT, ()) + + d = tmp_path.joinpath('juliaenv-default', 'depot', 'packages', 'Example') + assert d.is_dir() + + def test_julia_hook_manifest(tmp_path): code = """ using Example diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index 055cb1e92..0fb84a916 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -15,7 +15,9 @@ from pre_commit.prefix import Prefix from pre_commit.store import _make_local_repo from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from testing.language_helpers import run_language +from testing.util import git_commit from testing.util import xfailif_windows @@ -40,6 +42,12 @@ def find_exe_mck(): yield mck +def _make_repo(r): + cmd_output_b('git', 'init', r) + cmd_output_b('git', 'add', '.', cwd=r) + git_commit(cwd=r) + + @pytest.mark.usefixtures('is_linux') def test_sets_system_when_node_and_npm_are_available(find_exe_mck): find_exe_mck.return_value = '/path/to/exe' @@ -61,6 +69,7 @@ def test_sets_default_on_windows(find_exe_mck): @xfailif_windows # pragma: win32 no cover def test_healthy_system_node(tmpdir): tmpdir.join('package.json').write('{"name": "t", "version": "1.0.0"}') + _make_repo(str(tmpdir)) prefix = Prefix(str(tmpdir)) node.install_environment(prefix, 'system', ()) @@ -75,6 +84,7 @@ def test_unhealthy_if_system_node_goes_missing(tmpdir): prefix_dir = tmpdir.join('prefix').ensure_dir() prefix_dir.join('package.json').write('{"name": "t", "version": "1.0.0"}') + _make_repo(str(prefix_dir)) path = ('PATH', (str(bin_dir), os.pathsep, envcontext.Var('PATH'))) with envcontext.envcontext((path,)): @@ -101,6 +111,7 @@ def test_installs_without_links_outside_env(tmpdir): 'dependencies': {'lodash': '*'}, }), ) + _make_repo(str(tmpdir)) prefix = Prefix(str(tmpdir)) node.install_environment(prefix, 'system', ()) @@ -124,6 +135,7 @@ def _make_hello_world(tmp_path): '#!/usr/bin/env node\n' 'console.log("Hello World");\n', ) + _make_repo(str(tmp_path)) def test_node_hook_system(tmp_path):