summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.pre-commit-config.yaml8
-rw-r--r--CHANGELOG.md20
-rw-r--r--azure-pipelines.yml8
-rw-r--r--pre_commit/clientlib.py3
-rw-r--r--pre_commit/commands/hook_impl.py5
-rw-r--r--pre_commit/commands/run.py5
-rw-r--r--pre_commit/constants.py2
-rw-r--r--pre_commit/git.py4
-rw-r--r--pre_commit/languages/all.py2
-rw-r--r--pre_commit/languages/r.py141
-rw-r--r--pre_commit/main.py11
-rw-r--r--pre_commit/resources/empty_template_go.mod1
-rw-r--r--pre_commit/resources/empty_template_renv.lock20
-rw-r--r--pre_commit/store.py2
-rw-r--r--setup.cfg2
-rwxr-xr-xtesting/gen-languages-all6
-rw-r--r--testing/get-r.ps16
-rwxr-xr-xtesting/get-r.sh9
-rw-r--r--testing/resources/golang_hooks_repo/go.mod1
-rw-r--r--testing/resources/r_hooks_repo/.pre-commit-hooks.yaml48
-rw-r--r--testing/resources/r_hooks_repo/DESCRIPTION19
-rwxr-xr-xtesting/resources/r_hooks_repo/additional-deps.R2
-rwxr-xr-xtesting/resources/r_hooks_repo/hello-world.R5
-rw-r--r--testing/resources/r_hooks_repo/renv.lock27
-rw-r--r--testing/util.py2
-rw-r--r--tests/clientlib_test.py3
-rw-r--r--tests/commands/hook_impl_test.py9
-rw-r--r--tests/commands/install_uninstall_test.py47
-rw-r--r--tests/commands/run_test.py9
-rw-r--r--tests/git_test.py11
-rw-r--r--tests/languages/r_test.py104
-rw-r--r--tests/main_test.py13
-rw-r--r--tests/repository_test.py50
33 files changed, 586 insertions, 19 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6cc66eb..2859e31 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -21,16 +21,16 @@ repos:
hooks:
- id: autopep8
- repo: https://github.com/pre-commit/pre-commit
- rev: v2.10.1
+ rev: v2.11.0
hooks:
- id: validate_manifest
- repo: https://github.com/asottile/pyupgrade
- rev: v2.9.0
+ rev: v2.10.0
hooks:
- id: pyupgrade
args: [--py36-plus]
- repo: https://github.com/asottile/reorder_python_imports
- rev: v2.3.6
+ rev: v2.4.0
hooks:
- id: reorder-python-imports
args: [--py3-plus]
@@ -44,7 +44,7 @@ repos:
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.800
+ rev: v0.812
hooks:
- id: mypy
exclude: ^testing/resources/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8af449..eea5863 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,23 @@
+2.11.0 - 2021-03-07
+===================
+
+### Features
+- Improve warning for mutable ref.
+ - #1809 PR by @JamMarHer.
+- Add support for `post-merge` hook.
+ - #1800 PR by @psacawa.
+ - #1762 issue by @psacawa.
+- Add `r` as a supported hook language.
+ - #1799 PR by @lorenzwalthert.
+
+### Fixes
+- Fix `pre-commit install` on `subst` / network drives on windows.
+ - #1814 PR by @asottile.
+ - #1802 issue by @goroderickgo.
+- Fix installation of `local` golang repositories for go 1.16.
+ - #1818 PR by @rafikdraoui.
+ - #1815 issue by @rafikdraoui.
+
2.10.1 - 2021-02-06
===================
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index e7256da..34ace23 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -26,6 +26,10 @@ jobs:
Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin"
Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin"
displayName: Add strawberry perl to PATH
+ - task: PowerShell@2
+ inputs:
+ filePath: "testing/get-r.ps1"
+ displayName: install R
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [py37]
@@ -42,6 +46,8 @@ jobs:
testing/get-swift.sh
echo '##vso[task.prependpath]/tmp/swift/usr/bin'
displayName: install swift
+ - bash: testing/get-r.sh
+ displayName: install R
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [pypy3, py36, py37, py38, py39]
@@ -56,3 +62,5 @@ jobs:
testing/get-swift.sh
echo '##vso[task.prependpath]/tmp/swift/usr/bin'
displayName: install swift
+ - bash: testing/get-r.sh
+ displayName: install R
diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py
index 8f35057..962c7fa 100644
--- a/pre_commit/clientlib.py
+++ b/pre_commit/clientlib.py
@@ -128,7 +128,8 @@ class WarnMutableRev(cfgv.ConditionalOptional):
f'(moving tag / branch). Mutable references are never '
f'updated after first install and are not supported. '
f'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501
- f'for more details.',
+ f'for more details. '
+ f'Hint: `pre-commit autoupdate` often fixes this.',
)
diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py
index 25c5fdf..a766ee9 100644
--- a/pre_commit/commands/hook_impl.py
+++ b/pre_commit/commands/hook_impl.py
@@ -76,6 +76,7 @@ def _ns(
remote_url: Optional[str] = None,
commit_msg_filename: Optional[str] = None,
checkout_type: Optional[str] = None,
+ is_squash_merge: Optional[str] = None,
) -> argparse.Namespace:
return argparse.Namespace(
color=color,
@@ -88,6 +89,7 @@ def _ns(
commit_msg_filename=commit_msg_filename,
all_files=all_files,
checkout_type=checkout_type,
+ is_squash_merge=is_squash_merge,
files=(),
hook=None,
verbose=False,
@@ -158,6 +160,7 @@ _EXPECTED_ARG_LENGTH_BY_HOOK = {
'post-commit': 0,
'pre-commit': 0,
'pre-merge-commit': 0,
+ 'post-merge': 1,
'pre-push': 2,
}
@@ -199,6 +202,8 @@ def _run_ns(
hook_type, color,
from_ref=args[0], to_ref=args[1], checkout_type=args[2],
)
+ elif hook_type == 'post-merge':
+ return _ns(hook_type, color, is_squash_merge=args[0])
else:
raise AssertionError(f'unexpected hook type: {hook_type}')
diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py
index 891488d..05c3268 100644
--- a/pre_commit/commands/run.py
+++ b/pre_commit/commands/run.py
@@ -245,7 +245,7 @@ def _compute_cols(hooks: Sequence[Hook]) -> int:
def _all_filenames(args: argparse.Namespace) -> Collection[str]:
# these hooks do not operate on files
- if args.hook_stage in {'post-checkout', 'post-commit'}:
+ if args.hook_stage in {'post-checkout', 'post-commit', 'post-merge'}:
return ()
elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}:
return (args.commit_msg_filename,)
@@ -379,6 +379,9 @@ def run(
if args.checkout_type:
environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type
+ if args.is_squash_merge:
+ environ['PRE_COMMIT_IS_SQUASH_MERGE'] = args.is_squash_merge
+
# Set pre_commit flag
environ['PRE_COMMIT'] = '1'
diff --git a/pre_commit/constants.py b/pre_commit/constants.py
index 5150fdc..3dcbbac 100644
--- a/pre_commit/constants.py
+++ b/pre_commit/constants.py
@@ -18,7 +18,7 @@ VERSION = importlib_metadata.version('pre_commit')
# `manual` is not invoked by any installed git hook. See #719
STAGES = (
'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg',
- 'post-commit', 'manual', 'post-checkout', 'push',
+ 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge',
)
DEFAULT = 'default'
diff --git a/pre_commit/git.py b/pre_commit/git.py
index bec816c..4bf2823 100644
--- a/pre_commit/git.py
+++ b/pre_commit/git.py
@@ -52,10 +52,10 @@ def get_root() -> str:
# "rev-parse --show-cdup" to get the appropriate path, but must perform
# an extra check to see if we are in the .git directory.
try:
- root = os.path.realpath(
+ root = os.path.abspath(
cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(),
)
- git_dir = os.path.realpath(get_git_dir())
+ git_dir = os.path.abspath(get_git_dir())
except CalledProcessError:
raise FatalError(
'git failed. Is it installed, and are you in a Git repository '
diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py
index 9c2e59d..fde6000 100644
--- a/pre_commit/languages/all.py
+++ b/pre_commit/languages/all.py
@@ -16,6 +16,7 @@ from pre_commit.languages import node
from pre_commit.languages import perl
from pre_commit.languages import pygrep
from pre_commit.languages import python
+from pre_commit.languages import r
from pre_commit.languages import ruby
from pre_commit.languages import rust
from pre_commit.languages import script
@@ -52,6 +53,7 @@ languages = {
'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501
'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501
'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501
+ 'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, healthy=r.healthy, install_environment=r.install_environment, run_hook=r.run_hook), # noqa: E501
'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501
'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501
'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501
diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py
new file mode 100644
index 0000000..1d42fea
--- /dev/null
+++ b/pre_commit/languages/r.py
@@ -0,0 +1,141 @@
+import contextlib
+import os
+import shlex
+import shutil
+from typing import Generator
+from typing import Sequence
+from typing import Tuple
+
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.hook import Hook
+from pre_commit.languages import helpers
+from pre_commit.prefix import Prefix
+from pre_commit.util import clean_path_on_failure
+from pre_commit.util import cmd_output_b
+
+ENVIRONMENT_DIR = 'renv'
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+
+
+def get_env_patch(venv: str) -> PatchesT:
+ return (
+ ('R_PROFILE_USER', os.path.join(venv, 'activate.R')),
+ )
+
+
+@contextlib.contextmanager
+def in_env(
+ prefix: Prefix,
+ language_version: str,
+) -> Generator[None, None, None]:
+ envdir = _get_env_dir(prefix, language_version)
+ with envcontext(get_env_patch(envdir)):
+ yield
+
+
+def _get_env_dir(prefix: Prefix, version: str) -> str:
+ return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))
+
+
+def _prefix_if_file_entry(
+ entry: Sequence[str],
+ prefix: Prefix,
+) -> Sequence[str]:
+ if entry[1] == '-e':
+ return entry[1:]
+ else:
+ return (prefix.path(entry[1]),)
+
+
+def _entry_validate(entry: Sequence[str]) -> None:
+ """
+ Allowed entries:
+ # Rscript -e expr
+ # Rscript path/to/file
+ """
+ if entry[0] != 'Rscript':
+ raise ValueError('entry must start with `Rscript`.')
+
+ if entry[1] == '-e':
+ if len(entry) > 3:
+ raise ValueError('You can supply at most one expression.')
+ elif len(entry) > 2:
+ raise ValueError(
+ 'The only valid syntax is `Rscript -e {expr}`',
+ 'or `Rscript path/to/hook/script`',
+ )
+
+
+def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]:
+ opts = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
+ entry = shlex.split(hook.entry)
+ _entry_validate(entry)
+
+ return (
+ *entry[:1], *opts,
+ *_prefix_if_file_entry(entry, hook.prefix),
+ *hook.args,
+ )
+
+
+def install_environment(
+ prefix: Prefix,
+ version: str,
+ additional_dependencies: Sequence[str],
+) -> None:
+ env_dir = _get_env_dir(prefix, version)
+ with clean_path_on_failure(env_dir):
+ os.makedirs(env_dir, exist_ok=True)
+ path_desc_source = prefix.path('DESCRIPTION')
+ if os.path.exists(path_desc_source):
+ shutil.copy(path_desc_source, env_dir)
+ shutil.copy(prefix.path('renv.lock'), env_dir)
+ cmd_output_b(
+ 'Rscript', '--vanilla', '-e',
+ """\
+ missing_pkgs <- setdiff(
+ "renv", unname(installed.packages()[, "Package"])
+ )
+ options(
+ repos = c(CRAN = "https://cran.rstudio.com"),
+ renv.consent = TRUE
+ )
+ install.packages(missing_pkgs)
+ renv::activate()
+ renv::restore()
+ activate_statement <- paste0(
+ 'renv::activate("', file.path(getwd()), '"); '
+ )
+ writeLines(activate_statement, 'activate.R')
+ is_package <- tryCatch(
+ suppressWarnings(
+ unname(read.dcf('DESCRIPTION')[,'Type'] == "Package")
+ ),
+ error = function(...) FALSE
+ )
+ if (is_package) {
+ renv::install(normalizePath('.'))
+ }
+ """,
+ cwd=env_dir,
+ )
+ if additional_dependencies:
+ cmd_output_b(
+ 'Rscript', '-e',
+ 'renv::install(commandArgs(trailingOnly = TRUE))',
+ *additional_dependencies,
+ cwd=env_dir,
+ )
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]:
+ with in_env(hook.prefix, hook.language_version):
+ return helpers.run_xargs(
+ hook, _cmd_from_hook(hook), file_args, color=color,
+ )
diff --git a/pre_commit/main.py b/pre_commit/main.py
index ce850c4..c66cfb9 100644
--- a/pre_commit/main.py
+++ b/pre_commit/main.py
@@ -67,8 +67,8 @@ class AppendReplaceDefault(argparse.Action):
def _add_hook_type_option(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'-t', '--hook-type', choices=(
- 'pre-commit', 'pre-merge-commit', 'pre-push',
- 'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout',
+ 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg',
+ 'commit-msg', 'post-commit', 'post-checkout', 'post-merge',
),
action=AppendReplaceDefault,
default=['pre-commit'],
@@ -136,6 +136,13 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
'file from the index, flag=0).'
),
)
+ parser.add_argument(
+ '--is-squash-merge',
+ help=(
+ 'During a post-merge hook, indicates whether the merge was a '
+ 'squash merge'
+ ),
+ )
def _adjust_args_and_chdir(args: argparse.Namespace) -> None:
diff --git a/pre_commit/resources/empty_template_go.mod b/pre_commit/resources/empty_template_go.mod
index e69de29..de3e241 100644
--- a/pre_commit/resources/empty_template_go.mod
+++ b/pre_commit/resources/empty_template_go.mod
@@ -0,0 +1 @@
+module pre-commit-dummy-empty-module
diff --git a/pre_commit/resources/empty_template_renv.lock b/pre_commit/resources/empty_template_renv.lock
new file mode 100644
index 0000000..d6e31f8
--- /dev/null
+++ b/pre_commit/resources/empty_template_renv.lock
@@ -0,0 +1,20 @@
+{
+ "R": {
+ "Version": "4.0.3",
+ "Repositories": [
+ {
+ "Name": "CRAN",
+ "URL": "https://cran.rstudio.com"
+ }
+ ]
+ },
+ "Packages": {
+ "renv": {
+ "Package": "renv",
+ "Version": "0.12.5",
+ "Source": "Repository",
+ "Repository": "CRAN",
+ "Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c"
+ }
+ }
+}
diff --git a/pre_commit/store.py b/pre_commit/store.py
index e5522ec..187c9d3 100644
--- a/pre_commit/store.py
+++ b/pre_commit/store.py
@@ -189,7 +189,7 @@ class Store:
LOCAL_RESOURCES = (
'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',
'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py',
- 'environment.yml', 'Makefile.PL',
+ 'environment.yml', 'Makefile.PL', 'renv.lock',
)
def make_local(self, deps: Sequence[str]) -> str:
diff --git a/setup.cfg b/setup.cfg
index 7e4a1c4..5a4ee6e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = pre_commit
-version = 2.10.1
+version = 2.11.0
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/gen-languages-all b/testing/gen-languages-all
index d9b01bd..eb7cd70 100755
--- a/testing/gen-languages-all
+++ b/testing/gen-languages-all
@@ -2,9 +2,9 @@
import sys
LANGUAGES = [
- 'conda', 'coursier', 'docker', 'dotnet', 'docker_image', 'fail', 'golang',
- 'node', 'perl', 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift',
- 'system',
+ 'conda', 'coursier', 'docker', 'docker_image', 'dotnet', 'fail', 'golang',
+ 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script',
+ 'swift', 'system',
]
FIELDS = [
'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment',
diff --git a/testing/get-r.ps1 b/testing/get-r.ps1
new file mode 100644
index 0000000..e7b7b61
--- /dev/null
+++ b/testing/get-r.ps1
@@ -0,0 +1,6 @@
+$dir = $Env:Temp
+$urlR = "https://cran.r-project.org/bin/windows/base/old/4.0.4/R-4.0.4-win.exe"
+$outputR = "$dir\R-win.exe"
+$wcR = New-Object System.Net.WebClient
+$wcR.DownloadFile($urlR, $outputR)
+Start-Process -FilePath $outputR -ArgumentList "/S /v/qn"
diff --git a/testing/get-r.sh b/testing/get-r.sh
new file mode 100755
index 0000000..5d09828
--- /dev/null
+++ b/testing/get-r.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+sudo apt install r-base
+# create empty folder for user library.
+# necessary for non-root users who have
+# never installed an R package before.
+# Alternatively, we require the renv
+# package to be installed already, then we can
+# omit that.
+Rscript -e 'dir.create(Sys.getenv("R_LIBS_USER"), recursive = TRUE)'
diff --git a/testing/resources/golang_hooks_repo/go.mod b/testing/resources/golang_hooks_repo/go.mod
new file mode 100644
index 0000000..523bfc9
--- /dev/null
+++ b/testing/resources/golang_hooks_repo/go.mod
@@ -0,0 +1 @@
+module golang-hello-world
diff --git a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..b3545d9
--- /dev/null
+++ b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,48 @@
+# parsing file
+- id: parse-file-no-opts-no-args
+ name: Say hi
+ entry: Rscript parse-file-no-opts-no-args.R
+ language: r
+ types: [r]
+- id: parse-file-no-opts-args
+ name: Say hi
+ entry: Rscript parse-file-no-opts-args.R
+ args: [--no-cache]
+ language: r
+ types: [r]
+## parsing expr
+- id: parse-expr-no-opts-no-args-1
+ name: Say hi
+ entry: Rscript -e '1+1'
+ language: r
+ types: [r]
+- id: parse-expr-args-in-entry-2
+ name: Say hi
+ entry: Rscript -e '1+1' -e '3' --no-cache3
+ language: r
+ types: [r]
+# real world
+- id: hello-world
+ name: Say hi
+ entry: Rscript hello-world.R
+ args: [blibla]
+ language: r
+ types: [r]
+- id: hello-world-inline
+ name: Say hi
+ entry: |
+ Rscript -e
+ 'stopifnot(
+ packageVersion("rprojroot") == "1.0",
+ packageVersion("gli.clu") == "0.0.0.9000"
+ )
+ cat(commandArgs(trailingOnly = TRUE), "from R!\n", sep = ", ")
+ '
+ args: ['Hi-there']
+ language: r
+ types: [r]
+- id: additional-deps
+ name: Check additional deps
+ entry: Rscript additional-deps.R
+ language: r
+ types: [r]
diff --git a/testing/resources/r_hooks_repo/DESCRIPTION b/testing/resources/r_hooks_repo/DESCRIPTION
new file mode 100644
index 0000000..0e597a8
--- /dev/null
+++ b/testing/resources/r_hooks_repo/DESCRIPTION
@@ -0,0 +1,19 @@
+Package: gli.clu
+Title: What the Package Does (One Line, Title Case)
+Type: Package
+Version: 0.0.0.9000
+Authors@R:
+ person(given = "First",
+ family = "Last",
+ role = c("aut", "cre"),
+ email = "first.last@example.com",
+ comment = c(ORCID = "YOUR-ORCID-ID"))
+Description: What the package does (one paragraph).
+License: `use_mit_license()`, `use_gpl3_license()` or friends to
+ pick a license
+Encoding: UTF-8
+LazyData: true
+Roxygen: list(markdown = TRUE)
+RoxygenNote: 7.1.1
+Imports:
+ rprojroot
diff --git a/testing/resources/r_hooks_repo/additional-deps.R b/testing/resources/r_hooks_repo/additional-deps.R
new file mode 100755
index 0000000..bc14595
--- /dev/null
+++ b/testing/resources/r_hooks_repo/additional-deps.R
@@ -0,0 +1,2 @@
+suppressPackageStartupMessages(library("cachem"))
+cat("OK\n")
diff --git a/testing/resources/r_hooks_repo/hello-world.R b/testing/resources/r_hooks_repo/hello-world.R
new file mode 100755
index 0000000..bf8d92f
--- /dev/null
+++ b/testing/resources/r_hooks_repo/hello-world.R
@@ -0,0 +1,5 @@
+stopifnot(
+ packageVersion('rprojroot') == '1.0',
+ packageVersion('gli.clu') == '0.0.0.9000'
+)
+cat("Hello, World, from R!\n")
diff --git a/testing/resources/r_hooks_repo/renv.lock b/testing/resources/r_hooks_repo/renv.lock
new file mode 100644
index 0000000..d7d5fdc
--- /dev/null
+++ b/testing/resources/r_hooks_repo/renv.lock
@@ -0,0 +1,27 @@
+{
+ "R": {
+ "Version": "4.0.3",
+ "Repositories": [
+ {
+ "Name": "CRAN",
+ "URL": "https://cloud.r-project.org"
+ }
+ ]
+ },
+ "Packages": {
+ "renv": {
+ "Package": "renv",
+ "Version": "0.12.5",
+ "Source": "Repository",
+ "Repository": "CRAN",
+ "Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c"
+ },
+ "rprojroot": {
+ "Package": "rprojroot",
+ "Version": "1.0",
+ "Source": "Repository",
+ "Repository": "CRAN",
+ "Hash": "86704667fe0860e4fec35afdfec137f3"
+ }
+ }
+}
diff --git a/testing/util.py b/testing/util.py
index 1f8cb35..1364453 100644
--- a/testing/util.py
+++ b/testing/util.py
@@ -70,6 +70,7 @@ def run_opts(
show_diff_on_failure=False,
commit_msg_filename='',
checkout_type='',
+ is_squash_merge='',
):
# These are mutually exclusive
assert not (all_files and files)
@@ -88,6 +89,7 @@ def run_opts(
show_diff_on_failure=show_diff_on_failure,
commit_msg_filename=commit_msg_filename,
checkout_type=checkout_type,
+ is_squash_merge=is_squash_merge,
)
diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py
index 6bdb0d6..ff3cce3 100644
--- a/tests/clientlib_test.py
+++ b/tests/clientlib_test.py
@@ -228,7 +228,8 @@ def test_warn_mutable_rev_invalid(caplog, rev):
'Mutable references are never updated after first install and are '
'not supported. '
'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501
- 'for more details.',
+ 'for more details. '
+ 'Hint: `pre-commit autoupdate` often fixes this.',
),
]
diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py
index 2fc0146..c38b9ca 100644
--- a/tests/commands/hook_impl_test.py
+++ b/tests/commands/hook_impl_test.py
@@ -97,6 +97,7 @@ def test_run_legacy_recursive(tmpdir):
('pre-push', ['branch_name', 'remote_name']),
('commit-msg', ['.git/COMMIT_EDITMSG']),
('post-commit', []),
+ ('post-merge', ['1']),
('post-checkout', ['old_head', 'new_head', '1']),
# multiple choices for commit-editmsg
('prepare-commit-msg', ['.git/COMMIT_EDITMSG']),
@@ -157,6 +158,14 @@ def test_run_ns_post_commit():
assert ns.color is True
+def test_run_ns_post_merge():
+ ns = hook_impl._run_ns('post-merge', True, ('1',), b'')
+ assert ns is not None
+ assert ns.hook_stage == 'post-merge'
+ assert ns.color is True
+ assert ns.is_squash_merge == '1'
+
+
def test_run_ns_post_checkout():
ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'')
assert ns is not None
diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py
index 7a4b906..bd28654 100644
--- a/tests/commands/install_uninstall_test.py
+++ b/tests/commands/install_uninstall_test.py
@@ -259,7 +259,10 @@ def _path_without_us():
exe = find_executable('pre-commit', _environ=env)
while exe:
parts = env['PATH'].split(os.pathsep)
- after = [x for x in parts if x.lower() != os.path.dirname(exe).lower()]
+ after = [
+ x for x in parts
+ if x.lower().rstrip(os.sep) != os.path.dirname(exe).lower()
+ ]
if parts == after:
raise AssertionError(exe, parts)
env['PATH'] = os.pathsep.join(after)
@@ -759,6 +762,48 @@ def test_post_commit_integration(tempdir_factory, store):
assert os.path.exists('post-commit.tmp')
+def test_post_merge_integration(tempdir_factory, store):
+ path = git_dir(tempdir_factory)
+ config = [
+ {
+ 'repo': 'local',
+ 'hooks': [{
+ 'id': 'post-merge',
+ 'name': 'Post merge',
+ 'entry': 'touch post-merge.tmp',
+ 'language': 'system',
+ 'always_run': True,
+ 'verbose': True,
+ 'stages': ['post-merge'],
+ }],
+ },
+ ]
+ write_config(path, config)
+ with cwd(path):
+ # create a simple diamond of commits for a non-trivial merge
+ open('init', 'a').close()
+ cmd_output('git', 'add', '.')
+ git_commit()
+
+ open('master', 'a').close()
+ cmd_output('git', 'add', '.')
+ git_commit()
+
+ cmd_output('git', 'checkout', '-b', 'branch', 'HEAD^')
+ open('branch', 'a').close()
+ cmd_output('git', 'add', '.')
+ git_commit()
+
+ cmd_output('git', 'checkout', 'master')
+ install(C.CONFIG_FILE, store, hook_types=['post-merge'])
+ retc, stdout, stderr = cmd_output_mocked_pre_commit_home(
+ 'git', 'merge', 'branch',
+ tempdir_factory=tempdir_factory,
+ )
+ assert retc == 0
+ assert os.path.exists('post-merge.tmp')
+
+
def test_post_checkout_integration(tempdir_factory, store):
path = git_dir(tempdir_factory)
config = [
diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py
index eaea813..4cd70fd 100644
--- a/tests/commands/run_test.py
+++ b/tests/commands/run_test.py
@@ -494,6 +494,15 @@ def test_all_push_options_ok(cap_out, store, repo_with_passing_hook):
assert b'Specify both --from-ref and --to-ref.' not in printed
+def test_is_squash_merge(cap_out, store, repo_with_passing_hook):
+ args = run_opts(is_squash_merge='1')
+ environ: MutableMapping[str, str] = {}
+ ret, printed = _do_run(
+ cap_out, store, repo_with_passing_hook, args, environ,
+ )
+ assert environ['PRE_COMMIT_IS_SQUASH_MERGE'] == '1'
+
+
def test_checkout_type(cap_out, store, repo_with_passing_hook):
args = run_opts(from_ref='', to_ref='', checkout_type='1')
environ: MutableMapping[str, str] = {}
diff --git a/tests/git_test.py b/tests/git_test.py
index 69fd206..51d5f8c 100644
--- a/tests/git_test.py
+++ b/tests/git_test.py
@@ -38,6 +38,17 @@ def test_get_root_bare_worktree(tmpdir):
assert git.get_root() == os.path.abspath('.')
+def test_get_root_worktree_in_git(tmpdir):
+ src = tmpdir.join('src').ensure_dir()
+ cmd_output('git', 'init', str(src))
+ git_commit(cwd=str(src))
+
+ cmd_output('git', 'worktree', 'add', '.git/trees/foo', 'HEAD', cwd=src)
+
+ with src.join('.git/trees/foo').as_cwd():
+ assert git.get_root() == os.path.abspath('.')
+
+
def test_get_staged_files_deleted(in_git_dir):
in_git_dir.join('test').ensure()
cmd_output('git', 'add', 'test')
diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py
new file mode 100644
index 0000000..5c046ef
--- /dev/null
+++ b/tests/languages/r_test.py
@@ -0,0 +1,104 @@
+import os.path
+
+import pytest
+
+from pre_commit.languages import r
+from testing.fixtures import make_config_from_repo
+from testing.fixtures import make_repo
+from tests.repository_test import _get_hook_no_install
+
+
+def _test_r_parsing(
+ tempdir_factory,
+ store,
+ hook_id,
+ expected_hook_expr={},
+ expected_args={},
+):
+ repo_path = 'r_hooks_repo'
+ path = make_repo(tempdir_factory, repo_path)
+ config = make_config_from_repo(path)
+ hook = _get_hook_no_install(config, store, hook_id)
+ ret = r._cmd_from_hook(hook)
+ expected_cmd = 'Rscript'
+ expected_opts = (
+ '--no-save', '--no-restore', '--no-site-file', '--no-environ',
+ )
+ expected_path = os.path.join(
+ hook.prefix.prefix_dir, '.'.join([hook_id, 'R']),
+ )
+ expected = (
+ expected_cmd,
+ *expected_opts,
+ *(expected_hook_expr or (expected_path,)),
+ *expected_args,
+ )
+ assert ret == expected
+
+
+def test_r_parsing_file_no_opts_no_args(tempdir_factory, store):
+ hook_id = 'parse-file-no-opts-no-args'
+ _test_r_parsing(tempdir_factory, store, hook_id)
+
+
+def test_r_parsing_file_opts_no_args(tempdir_factory, store):
+ with pytest.raises(ValueError) as excinfo:
+ r._entry_validate(['Rscript', '--no-init', '/path/to/file'])
+
+ msg = excinfo.value.args
+ assert msg == (
+ 'The only valid syntax is `Rscript -e {expr}`',
+ 'or `Rscript path/to/hook/script`',
+ )
+
+
+def test_r_parsing_file_no_opts_args(tempdir_factory, store):
+ hook_id = 'parse-file-no-opts-args'
+ expected_args = ['--no-cache']
+ _test_r_parsing(
+ tempdir_factory, store, hook_id, expected_args=expected_args,
+ )
+
+
+def test_r_parsing_expr_no_opts_no_args1(tempdir_factory, store):
+ hook_id = 'parse-expr-no-opts-no-args-1'
+ _test_r_parsing(
+ tempdir_factory, store, hook_id, expected_hook_expr=('-e', '1+1'),
+ )
+
+
+def test_r_parsing_expr_no_opts_no_args2(tempdir_factory, store):
+ with pytest.raises(ValueError) as execinfo:
+ r._entry_validate(['Rscript', '-e', '1+1', '-e', 'letters'])
+ msg = execinfo.value.args
+ assert msg == ('You can supply at most one expression.',)
+
+
+def test_r_parsing_expr_opts_no_args2(tempdir_factory, store):
+ with pytest.raises(ValueError) as execinfo:
+ r._entry_validate(
+ [
+ 'Rscript', '--vanilla', '-e', '1+1', '-e', 'letters',
+ ],
+ )
+ msg = execinfo.value.args
+ assert msg == (
+ 'The only valid syntax is `Rscript -e {expr}`',
+ 'or `Rscript path/to/hook/script`',
+ )
+
+
+def test_r_parsing_expr_args_in_entry2(tempdir_factory, store):
+ with pytest.raises(ValueError) as execinfo:
+ r._entry_validate(['Rscript', '-e', 'expr1', '--another-arg'])
+
+ msg = execinfo.value.args
+ assert msg == ('You can supply at most one expression.',)
+
+
+def test_r_parsing_expr_non_Rscirpt(tempdir_factory, store):
+ with pytest.raises(ValueError) as execinfo:
+ r._entry_validate(['AnotherScript', '-e', '{{}}'])
+
+ msg = execinfo.value.args
+ assert msg == ('entry must start with `Rscript`.',)
diff --git a/tests/main_test.py b/tests/main_test.py
index 2460bd8..1ad8d41 100644
--- a/tests/main_test.py
+++ b/tests/main_test.py
@@ -7,7 +7,9 @@ import pytest
import pre_commit.constants as C
from pre_commit import main
from pre_commit.errors import FatalError
+from pre_commit.util import cmd_output
from testing.auto_namedtuple import auto_namedtuple
+from testing.util import cwd
@pytest.mark.parametrize(
@@ -54,6 +56,17 @@ def test_adjust_args_and_chdir_relative_things(in_git_dir):
assert args.files == [os.path.join('foo', 'f1'), os.path.join('foo', 'f2')]
+@pytest.mark.skipif(os.name != 'nt', reason='windows feature')
+def test_install_on_subst(in_git_dir, store): # pragma: posix no cover
+ assert not os.path.exists('Z:')
+ cmd_output('subst', 'Z:', str(in_git_dir))
+ try:
+ with cwd('Z:'):
+ test_adjust_args_and_chdir_noop('Z:\\')
+ finally:
+ cmd_output('subst', '/d', 'Z:')
+
+
def test_adjust_args_and_chdir_non_relative_config(in_git_dir):
in_git_dir.join('foo').ensure_dir().chdir()
diff --git a/tests/repository_test.py b/tests/repository_test.py
index 1b58164..b6f7fb2 100644
--- a/tests/repository_test.py
+++ b/tests/repository_test.py
@@ -279,6 +279,54 @@ def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir):
test_run_a_node_hook(tempdir_factory, store)
+def test_r_hook(tempdir_factory, store):
+ _test_hook_repo(
+ tempdir_factory, store, 'r_hooks_repo',
+ 'hello-world', [os.devnull],
+ b'Hello, World, from R!\n',
+ )
+
+
+def test_r_inline_hook(tempdir_factory, store):
+ _test_hook_repo(
+ tempdir_factory, store, 'r_hooks_repo',
+ 'hello-world-inline', ['some-file'],
+ b'Hi-there, some-file, from R!\n',
+ )
+
+
+def test_r_with_additional_dependencies_hook(tempdir_factory, store):
+ _test_hook_repo(
+ tempdir_factory, store, 'r_hooks_repo',
+ 'additional-deps', [os.devnull],
+ b'OK\n',
+ config_kwargs={
+ 'hooks': [{
+ 'id': 'additional-deps',
+ 'additional_dependencies': ['cachem@1.0.4'],
+ }],
+ },
+ )
+
+
+def test_r_local_with_additional_dependencies_hook(store):
+ config = {
+ 'repo': 'local',
+ 'hooks': [{
+ 'id': 'local-r',
+ 'name': 'local-r',
+ 'entry': 'Rscript -e',
+ 'language': 'r',
+ 'args': ['if (packageVersion("R6") == "2.1.3") cat("OK\n")'],
+ 'additional_dependencies': ['R6@2.1.3'],
+ }],
+ }
+ hook = _get_hook(config, store, 'local-r')
+ ret, out = _hook_run(hook, (), color=False)
+ assert ret == 0
+ assert _norm_out(out) == b'OK\n'
+
+
def test_run_a_ruby_hook(tempdir_factory, store):
_test_hook_repo(
tempdir_factory, store, 'ruby_hooks_repo',
@@ -953,7 +1001,7 @@ def test_manifest_hooks(tempdir_factory, store):
require_serial=False,
stages=(
'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg',
- 'post-commit', 'manual', 'post-checkout', 'push',
+ 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge',
),
types=['file'],
types_or=[],