use std::path::Path;
use std::time::SystemTime;
use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::prelude::*;
use insta::assert_snapshot;
use predicates::prelude::predicate;
use prek_consts::env_vars::{EnvVars, EnvVarsRead};
use prek_consts::{
PRE_COMMIT_CONFIG_YAML, PRE_COMMIT_CONFIG_YML, PRE_COMMIT_HOOKS_YAML, PREK_TOML,
};
use crate::common::{TestContext, cmd_snapshot, git_cmd};
mod common;
#[test]
fn run_basic() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: requirements-txt-fixer
files: ^file\.txt$
- id: check-json
"});
cwd.child("file.txt").write_str("z-project\na-project\n")?;
cwd.child("valid.json").write_str("{}")?;
cwd.child("main.py").write_str(r#"print "abc" "#)?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("-q"), @"
success: false
exit_code: 1
----- stdout -----
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
Fixing main.py
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing valid.json
Fixing main.py
fix requirements.txt.....................................................Failed
- hook id: requirements-txt-fixer
- exit code: 1
- files were modified by this hook
Sorting file.txt
----- stderr -----
");
assert_eq!(context.read("file.txt"), "a-project\nz-project\n");
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("trailing-whitespace"), @r#"
success: true
exit_code: 0
----- stdout -----
trim trailing whitespace.................................................Passed
----- stderr -----
"#);
Ok(())
}
#[test]
fn fast_path_checks_filenames_from_entry_and_args() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-json
entry: check-json from-entry.json
args: [from-args.json]
files: ^selected\.json$
"});
let cwd = context.work_dir();
cwd.child("from-entry.json").write_str("invalid")?;
cwd.child("from-args.json").write_str("invalid")?;
cwd.child("selected.json").write_str("{}")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
check json...............................................................Failed
- hook id: check-json
- exit code: 1
from-entry.json: Failed to json decode (expected value at line 1 column 1)
from-args.json: Failed to json decode (expected value at line 1 column 1)
----- stderr -----
");
Ok(())
}
#[test]
fn run_preserves_stdout_stderr_order() -> Result<()> {
let context = TestContext::new();
context.init_project();
context
.work_dir()
.child("output.py")
.write_str(indoc::indoc! {r#"
import os
os.write(2, b"__PREK_STDERR_1__\n")
os.write(1, b"__PREK_STDOUT_1__\n")
os.write(2, b"__PREK_STDERR_2__\n")
os.write(1, b"__PREK_STDOUT_2__\n")
"#})?;
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: output-order
name: output-order
language: system
entry: python3 output.py
always_run: true
pass_filenames: false
verbose: true
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().args(["--all-files", "--color=never"]), @r"
success: true
exit_code: 0
----- stdout -----
output-order.............................................................Passed
- hook id: output-order
- duration: [TIME]
__PREK_STDERR_1__
__PREK_STDOUT_1__
__PREK_STDERR_2__
__PREK_STDOUT_2__
----- stderr -----
");
Ok(())
}
#[test]
fn run_does_not_rewrite_unchanged_config_tracking_file() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: noop
name: Noop
language: system
entry: "true"
always_run: true
"#});
context.git_add(".");
context.run().arg("--all-files").assert().success();
let tracking_file = context.home_dir().child("config-tracking.json");
tracking_file.assert(predicate::path::is_file());
let original_content = fs_err::read_to_string(tracking_file.path())?;
fs_err::OpenOptions::new()
.write(true)
.open(tracking_file.path())?
.set_modified(SystemTime::UNIX_EPOCH)?;
let original_modified = fs_err::metadata(tracking_file.path())?.modified()?;
context.run().arg("--all-files").assert().success();
assert_eq!(
fs_err::read_to_string(tracking_file.path())?,
original_content
);
assert_eq!(
fs_err::metadata(tracking_file.path())?.modified()?,
original_modified
);
Ok(())
}
#[test]
fn run_glob_patterns_with_multiple_hooks() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: echo-py
name: echo-py
entry: python3 -c "import sys; print('PY:' + ' '.join(sys.argv[2:]))" _
language: system
files:
glob: src/**/*.py
verbose: true
- id: echo-md
name: echo-md
entry: python3 -c "import sys; print('MD:' + ' '.join(sys.argv[2:]))" _
language: system
files:
glob: "**/*.md"
verbose: true
"#});
let src_dir = cwd.child("src");
src_dir.create_dir_all()?;
src_dir.child("main.py").write_str("print('hi')")?;
cwd.child("docs").create_dir_all()?;
cwd.child("docs/readme.md").write_str("# Docs")?;
cwd.child("notes.txt").write_str("note")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--all-files"), @r"
success: true
exit_code: 0
----- stdout -----
echo-py..................................................................Passed
- hook id: echo-py
- duration: [TIME]
PY:src/main.py
echo-md..................................................................Passed
- hook id: echo-md
- duration: [TIME]
MD:docs/readme.md
----- stderr -----
");
Ok(())
}
#[test]
fn run_in_non_git_repo() {
let context = TestContext::new();
let mut filters = context.filters();
filters.push((r"exit code: ", "exit status: "));
filters.push((
r"Command `[^`]*git(?:\.exe)? rev-parse --show-toplevel`",
"Command `[GIT] rev-parse --show-toplevel`",
));
cmd_snapshot!(filters, context.run(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Command `[GIT] rev-parse --show-toplevel` exited with an error:
[status]
exit status: 128
[stderr]
fatal: not a git repository (or any of the parent directories): .git
");
}
#[test]
fn invalid_config() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config("invalid: config");
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `.pre-commit-config.yaml`
caused by: error: line 1 column 1: missing field `repos`
--> <input>:1:1
|
1 | invalid: config
| ^ missing field `repos`
");
context.write_pre_commit_config(indoc::indoc! {r"
files: 12
repos: []
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `.pre-commit-config.yaml`
caused by: error: line 1 column 1: invalid type: integer `12`, expected a regex string or a mapping with `glob` set to a string or list of strings
--> <input>:1:1
|
1 | files: 12
| ^ invalid type: integer `12`, expected a regex string or a mapping with `glob` set to a string or list of strings
2 | repos: []
|
");
context.write_pre_commit_config(indoc::indoc! {r#"
files:
glog: "*.rs"
repos: []
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `.pre-commit-config.yaml`
caused by: error: line 2 column 3: unknown field `glog`, expected one of glob
--> <input>:2:3
|
1 | files:
2 | glog: \"*.rs\"
| ^ unknown field `glog`, expected one of glob
3 | repos: []
|
");
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: swift
additional_dependencies: ["swift-format@5.0.0"]
entry: echo Hello, world!
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to init hooks
caused by: Invalid hook `trailing-whitespace`
caused by: Hook specified `additional_dependencies: swift-format@5.0.0` but the language `swift` does not support installing dependencies for now
");
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: fail
language_version: '6'
entry: echo Hello, world!
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to init hooks
caused by: Invalid hook `trailing-whitespace`
caused by: Hook specified `language_version: 6` but the language `fail` does not support toolchain installation for now
");
}
#[test]
fn same_repo() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
"});
cwd.child("file.txt").write_str("Hello, world!\n")?;
cwd.child("valid.json").write_str("{}")?;
cwd.child("invalid.json").write_str("{}")?;
cwd.child("main.py").write_str(r#"print "abc" "#)?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
Fixing main.py
trim trailing whitespace.................................................Passed
trim trailing whitespace.................................................Passed
----- stderr -----
");
Ok(())
}
#[test]
fn local() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: local
name: local
language: system
entry: echo Hello, world!
always_run: true
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
local....................................................................Passed
----- stderr -----
"#);
}
#[test]
fn multiple_hook_ids() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: hook1
name: First Hook
language: system
entry: echo hook1
- id: hook2
name: Second Hook
language: system
entry: echo hook2
- id: shared-name
name: Shared Hook A
language: system
entry: echo shared-a
- id: shared-name-2
name: Shared Hook B
language: system
entry: echo shared-b
alias: shared-name
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("hook1").arg("hook1").arg("hook1"), @r#"
success: true
exit_code: 0
----- stdout -----
First Hook...............................................................Passed
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("shared-name"), @r#"
success: true
exit_code: 0
----- stdout -----
Shared Hook A............................................................Passed
Shared Hook B............................................................Passed
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("nonexistent-hook"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: selector `nonexistent-hook` did not match any hooks
error: No hooks found after filtering with the given selectors
");
cmd_snapshot!(context.filters(), context.run().arg("nonexistent-hook").arg("nonexistent-hook").arg("nonexistent-hook-2"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: the following selectors did not match any hooks or projects:
- `nonexistent-hook`
- `nonexistent-hook-2`
error: No hooks found after filtering with the given selectors
");
cmd_snapshot!(context.filters(), context.run().arg("hook2"), @r#"
success: true
exit_code: 0
----- stdout -----
Second Hook..............................................................Passed
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("hook1").arg("nonexistent").arg("hook2"), @r"
success: true
exit_code: 0
----- stdout -----
First Hook...............................................................Passed
Second Hook..............................................................Passed
----- stderr -----
warning: selector `nonexistent` did not match any hooks
");
cmd_snapshot!(context.filters(), context.run().arg("hook1").arg("hook2").arg("nonexistent-hook"), @r"
success: true
exit_code: 0
----- stdout -----
First Hook...............................................................Passed
Second Hook..............................................................Passed
----- stderr -----
warning: selector `nonexistent-hook` did not match any hooks
");
cmd_snapshot!(context.filters(), context.run().arg("hook1").arg("shared-name").arg("hook1"), @r#"
success: true
exit_code: 0
----- stdout -----
First Hook...............................................................Passed
Shared Hook A............................................................Passed
Shared Hook B............................................................Passed
----- stderr -----
"#);
}
#[test]
fn priorities_respected() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: late
name: Late Hook
language: system
entry: python3 -c "print('late')"
always_run: true
priority: 10
- id: early
name: Early Hook
language: system
entry: python3 -c "print('early')"
always_run: true
priority: 0
- id: middle
name: Middle Hook
language: system
entry: python3 -c "print('middle')"
always_run: true
priority: 5
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
Early Hook...............................................................Passed
Middle Hook..............................................................Passed
Late Hook................................................................Passed
----- stderr -----
"#);
}
#[test]
fn priority_aliases_are_resolved_before_scheduling() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
priorities:
early: 0
late: 10
repos:
- repo: local
hooks:
- id: late
name: Late Hook
language: system
entry: python3 -c "print('late')"
always_run: true
priority: late
- id: early
name: Early Hook
language: system
entry: python3 -c "print('early')"
always_run: true
priority: early
- id: numeric
name: Numeric Hook
language: system
entry: python3 -c "print('numeric')"
always_run: true
priority: 5
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
Early Hook...............................................................Passed
Numeric Hook.............................................................Passed
Late Hook................................................................Passed
----- stderr -----
"#);
}
#[test]
fn run_group_without_stage_selects_hooks_across_stages() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: ci-files
name: CI Files
language: system
entry: python3 -c "print('ci-files')"
always_run: true
stages: [pre-commit]
groups: [ci]
- id: ci-check
name: CI Check
language: system
entry: python3 -c "print('ci')"
always_run: true
stages: [pre-push]
groups: [ci]
- id: commit-msg-check
name: Commit Msg Check
language: system
entry: python3 -c "raise SystemExit('commit-msg hook should not run')"
always_run: true
stages: [commit-msg]
groups: [ci]
- id: prepare-commit-msg-check
name: Prepare Commit Msg Check
language: system
entry: python3 -c "raise SystemExit('prepare-commit-msg hook should not run')"
always_run: true
stages: [prepare-commit-msg]
groups: [ci]
- id: local-check
name: Local Check
language: system
entry: python3 -c "print('local')"
always_run: true
stages: [pre-commit]
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--group").arg("ci"), @r#"
success: true
exit_code: 0
----- stdout -----
CI Files.................................................................Passed
CI Check.................................................................Passed
----- stderr -----
"#);
}
#[test]
fn run_required_group_without_stage_warns_when_only_message_file_hooks_match() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: commit-msg-check
name: Commit Msg Check
language: system
entry: python3 -c "print('commit-msg')"
always_run: true
stages: [commit-msg]
groups: [ci]
- id: prepare-commit-msg-check
name: Prepare Commit Msg Check
language: system
entry: python3 -c "print('prepare-commit-msg')"
always_run: true
stages: [prepare-commit-msg]
groups: [ci]
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--require-group").arg("ci"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: all hooks selected by group filters require `commit-msg` or `prepare-commit-msg` stage and were not run; pass `--stage commit-msg` or `--stage prepare-commit-msg` to run them
"#);
}
#[test]
fn run_no_group_excludes_matching_hooks_and_keeps_ungrouped_hooks() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: format
name: Format
language: system
entry: python3 -c "print('format')"
always_run: true
groups: [format]
- id: lint
name: Lint
language: system
entry: python3 -c "print('lint')"
always_run: true
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--no-group").arg("format"), @r#"
success: true
exit_code: 0
----- stdout -----
Lint.....................................................................Passed
----- stderr -----
"#);
}
#[test]
fn run_group_exclusion_wins_over_inclusion() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: fast-lint
name: Fast Lint
language: system
entry: python3 -c "print('fast')"
always_run: true
groups: [ci]
- id: slow-lint
name: Slow Lint
language: system
entry: python3 -c "print('slow')"
always_run: true
groups: [ci, slow]
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--group").arg("ci").arg("--no-group").arg("slow"), @r#"
success: true
exit_code: 0
----- stdout -----
Fast Lint................................................................Passed
----- stderr -----
"#);
}
#[test]
fn run_required_groups_intersect_and_compose_with_other_group_filters() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: ty
name: Ty
language: system
entry: python3 -c "print('ty')"
always_run: true
groups: [lint, fast, local]
- id: ruff-format
name: Ruff Format
language: system
entry: python3 -c "print('ruff-format')"
always_run: true
groups: [format, fast, local]
- id: mypy
name: Mypy
language: system
entry: python3 -c "print('mypy')"
always_run: true
groups: [lint, slow, ci]
- id: black
name: Black
language: system
entry: python3 -c "print('black')"
always_run: true
groups: [format, slow, ci]
- id: black-fast
name: Black Fast
language: system
entry: python3 -c "print('black-fast')"
always_run: true
groups: [format, slow, ci, fast]
"#});
context.git_add(".");
cmd_snapshot!(
context.filters(),
context
.run()
.arg("--all-files")
.arg("--no-group")
.arg("fast")
.arg("--require-group")
.arg("format")
.arg("--group")
.arg("lint")
.arg("--no-group")
.arg("local")
.arg("--require-group")
.arg("slow")
.arg("--group")
.arg("ci"),
@r"
success: true
exit_code: 0
----- stdout -----
Black....................................................................Passed
----- stderr -----
"
);
cmd_snapshot!(
context.filters(),
context
.run()
.arg("--all-files")
.arg("--require-group")
.arg("lint")
.arg("--require-group")
.arg("format"),
@r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
error: No hooks found after filtering with the given selectors
"
);
}
#[test]
fn run_unknown_group_selectors_warn_and_empty_selection_fails() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: lint
name: Lint
language: system
entry: python3 -c "print('lint')"
always_run: true
groups: [ci]
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--group").arg("missing"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: group selector `--group=missing` did not match any hooks
error: No hooks found after filtering with the given selectors
"#);
cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--require-group").arg("missing"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: group selector `--require-group=missing` did not match any hooks
error: No hooks found after filtering with the given selectors
"#);
}
#[test]
fn run_group_selectors_reject_whitespace() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: lint
name: Lint
language: system
entry: python3 -c "print('lint')"
always_run: true
groups: [ci]
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--group").arg("ci slow"), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid group selector: `--group=ci slow`
caused by: group name cannot contain whitespace
"#);
cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--no-group").arg("ci slow"), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid group selector: `--no-group=ci slow`
caused by: group name cannot contain whitespace
"#);
}
#[test]
fn run_required_group_and_stage_filters_intersect() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: ci-push
name: CI Push
language: system
entry: python3 -c "print('ci-push')"
always_run: true
stages: [pre-push]
groups: [ci]
- id: ci-manual
name: CI Manual
language: system
entry: python3 -c "print('ci-manual')"
always_run: true
stages: [manual]
groups: [ci]
- id: other-push
name: Other Push
language: system
entry: python3 -c "print('other-push')"
always_run: true
stages: [pre-push]
groups: [other]
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--require-group").arg("ci").arg("--stage").arg("pre-push"), @r#"
success: true
exit_code: 0
----- stdout -----
CI Push..................................................................Passed
----- stderr -----
"#);
}
#[test]
fn priority_fail_fast_stops_later_groups() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: fail-fast
name: Failing Hook
language: system
entry: python3 -c "import sys; sys.exit(1)"
always_run: true
priority: 5
fail_fast: true
- id: sibling
name: Same Priority Sibling
language: system
entry: python3 -c "import time; time.sleep(0.2)"
always_run: true
priority: 5
- id: later
name: Later Hook
language: system
entry: python3 -c "print('later ran')"
always_run: true
priority: 10
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
Failing Hook.............................................................Failed
- hook id: fail-fast
- exit code: 1
Same Priority Sibling....................................................Passed
----- stderr -----
"#);
}
#[test]
fn priority_group_modified_files_is_group_failure_and_output_is_indented() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
cwd.child("file.txt").write_str("hello\n")?;
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: modify
name: Modifies File
language: system
entry: python3 -c "from pathlib import Path; p = Path('file.txt'); p.write_text(p.read_text() + 'x')"
always_run: true
verbose: true
priority: 0
- id: loud
name: Prints Output
language: system
entry: python3 -c "print('hello from loud')"
always_run: true
verbose: true
priority: 0
- id: quiet
name: No Output
language: system
entry: python3 -c "import time; time.sleep(0.1)"
always_run: true
priority: 0
- id: later
name: Later Hook
language: system
entry: python3 -c "print('later ran')"
always_run: true
verbose: true
priority: 10
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
Files were modified by following hooks...................................Failed
┌ Modifies File........................................................Passed
│ - hook id: modify
│ - duration: [TIME]
│ Prints Output........................................................Passed
│ - hook id: loud
│ - duration: [TIME]
│
│ hello from loud
â”” No Output............................................................Passed
Later Hook...............................................................Passed
- hook id: later
- duration: [TIME]
later ran
----- stderr -----
");
Ok(())
}
#[test]
fn config_not_staged() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.work_dir().child(PRE_COMMIT_CONFIG_YAML).touch()?;
context.git_add(".");
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -V
"});
cmd_snapshot!(context.filters(), context.run().arg("invalid-hook-id"), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Configuration file `.pre-commit-config.yaml` is not staged. Stage it with `git add` and try again
"#);
Ok(())
}
#[test]
fn config_outside_repo() -> Result<()> {
let context = TestContext::new();
let root = context.work_dir().child("work");
root.create_dir_all()?;
git_cmd(&root).arg("init").assert().success();
context
.work_dir()
.child("c.yaml")
.write_str(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'print("Hello world")'
"#})?;
cmd_snapshot!(context.filters(), context.run().current_dir(&root).arg("-c").arg("../c.yaml"), @r#"
success: true
exit_code: 0
----- stdout -----
trailing-whitespace..................................(no files to check)Skipped
----- stderr -----
"#);
Ok(())
}
#[test]
fn cjk_hook_name() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: åŽ»é™¤è¡Œå°¾ç©ºæ ¼
language: system
entry: python3 -V
- id: end-of-file-fixer
name: fix end of files
language: system
entry: python3 -V
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
åŽ»é™¤è¡Œå°¾ç©ºæ ¼.............................................................Passed
fix end of files.........................................................Passed
----- stderr -----
"#);
}
#[test]
fn skips() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c "exit(1)"
- id: end-of-file-fixer
name: fix end of files
language: system
entry: python3 -c "exit(1)"
- id: check-json
name: check json
language: system
entry: python3 -c "exit(1)"
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().env("SKIP", "end-of-file-fixer"), @r"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
check json...............................................................Failed
- hook id: check-json
- exit code: 1
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("SKIP", "trailing-whitespace,end-of-file-fixer"), @r"
success: false
exit_code: 1
----- stdout -----
check json...............................................................Failed
- hook id: check-json
- exit code: 1
----- stderr -----
");
}
#[test]
fn stage() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: manual-stage
name: manual-stage
language: system
entry: echo manual-stage
stages: [ manual ]
# Defaults to all stages.
- id: default-stage
name: default-stage
language: system
entry: echo default-stage
- id: post-commit-stage
name: post-commit-stage
language: system
entry: echo post-commit-stage
stages: [ post-commit ]
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
default-stage............................................................Passed
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--hook-stage").arg("manual"), @r#"
success: true
exit_code: 0
----- stdout -----
manual-stage.............................................................Passed
default-stage............................................................Passed
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--hook-stage").arg("post-commit"), @r#"
success: true
exit_code: 0
----- stdout -----
default-stage........................................(no files to check)Skipped
post-commit-stage....................................(no files to check)Skipped
----- stderr -----
"#);
}
#[test]
fn fallback_to_manual_stage() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: manual-only
name: manual-only
language: system
entry: echo manual-only
stages: [ manual ]
- id: another-manual
name: another-manual
language: system
entry: echo another-manual
stages: [ manual ]
- id: default-stage
name: default-stage
language: system
entry: echo default-stage
- id: pre-push
name: pre-push
language: system
entry: echo pre-push
stages: [ pre-push ]
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
default-stage............................................................Passed
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--hook-stage").arg("pre-commit").arg("default-stage").arg("manual-only"), @r"
success: true
exit_code: 0
----- stdout -----
default-stage............................................................Passed
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("manual-only").arg("default-stage"), @r"
success: true
exit_code: 0
----- stdout -----
default-stage............................................................Passed
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("manual-only").arg("another-manual"), @r"
success: true
exit_code: 0
----- stdout -----
manual-only..............................................................Passed
another-manual...........................................................Passed
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("pre-push").arg("manual-only"), @r"
success: true
exit_code: 0
----- stdout -----
manual-only..............................................................Passed
----- stderr -----
");
}
#[test]
fn files_and_exclude() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
cwd.child("file.txt").write_str("Hello, world! \n")?;
cwd.child("valid.json").write_str("{}\n ")?;
cwd.child("invalid.json").write_str("{}")?;
cwd.child("main.py").write_str(r#"print "abc" "#)?;
context.write_pre_commit_config(indoc::indoc! {r"
files: file.txt
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types: [text]
- id: end-of-file-fixer
name: fix end of files
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types: [text]
- id: check-json
name: check json
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types: [json]
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
trailing whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
['file.txt']
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
['file.txt']
check json...........................................(no files to check)Skipped
----- stderr -----
");
context.write_pre_commit_config(indoc::indoc! {r"
files: file.txt
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
files: valid.json
- id: end-of-file-fixer
name: fix end of files
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
exclude: (valid.json|main.py)
- id: check-json
name: check json
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
trailing whitespace..................................(no files to check)Skipped
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
['file.txt']
check json...............................................................Failed
- hook id: check-json
- exit code: 1
['file.txt']
----- stderr -----
");
Ok(())
}
#[test]
fn file_types() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
cwd.child("file.txt").write_str("Hello, world! ")?;
cwd.child("json.json").write_str("{}\n ")?;
cwd.child("main.py").write_str(r#"print "abc" "#)?;
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types: ["json"]
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types_or: ["json", "python"]
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
exclude_types: ["json"]
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types: ["json" ]
exclude_types: ["json"]
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
['json.json']
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
['json.json', 'main.py']
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
['.pre-commit-config.yaml', 'file.txt', 'main.py']
trailing-whitespace..................................(no files to check)Skipped
----- stderr -----
"#);
Ok(())
}
#[test]
fn fail_fast() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'print("Fixing files"); exit(1)'
always_run: true
fail_fast: false
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'print("Fixing files"); exit(1)'
always_run: true
fail_fast: true
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -V
always_run: true
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -V
always_run: true
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
Fixing files
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
Fixing files
----- stderr -----
");
}
#[test]
fn fail_fast_cli_flag() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: failing-hook
name: failing-hook
language: system
entry: python3 -c 'print("Failed"); exit(1)'
always_run: true
- id: passing-hook
name: passing-hook
language: system
entry: python3 -c 'print("Passed")'
always_run: true
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
failing-hook.............................................................Failed
- hook id: failing-hook
- exit code: 1
Failed
passing-hook.............................................................Passed
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--fail-fast"), @r"
success: false
exit_code: 1
----- stdout -----
failing-hook.............................................................Failed
- hook id: failing-hook
- exit code: 1
Failed
----- stderr -----
");
}
#[test]
fn no_fail_fast_cli_flag() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
fail_fast: true
repos:
- repo: local
hooks:
- id: failing-hook
name: failing-hook
language: system
entry: python3 -c 'print("Failed"); exit(1)'
always_run: true
- id: passing-hook
name: passing-hook
language: system
entry: python3 -c 'print("Passed")'
always_run: true
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
failing-hook.............................................................Failed
- hook id: failing-hook
- exit code: 1
Failed
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--no-fail-fast"), @r"
success: false
exit_code: 1
----- stdout -----
failing-hook.............................................................Failed
- hook id: failing-hook
- exit code: 1
Failed
passing-hook.............................................................Passed
----- stderr -----
");
}
#[test]
fn subdirectory() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
let child = cwd.child("foo/bar/baz");
child.create_dir_all()?;
child.child("file.txt").write_str("Hello, world!\n")?;
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1]); exit(1)'
always_run: true
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().current_dir(&child).arg("--files").arg("file.txt"), @r"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
foo/bar/baz/file.txt
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--cd").arg(&*child).arg("--files").arg("file.txt"), @r"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
foo/bar/baz/file.txt
----- stderr -----
");
Ok(())
}
#[test]
fn global_path_options_expand_tilde() -> Result<()> {
let context = TestContext::new();
let cd = context.home_dir().child("project");
cd.create_dir_all()?;
cmd_snapshot!(context.filters(), context
.command()
.arg("--show-settings")
.arg("--config=~/prek.toml")
.arg("--cd=~/project")
.env("HOME", context.home_dir().path())
.env("USERPROFILE", context.home_dir().path()), @r#"
success: true
exit_code: 0
----- stdout -----
GlobalArgs {
config: Some(
"[HOME]/prek.toml",
),
cd: Some(
"[HOME]/project",
),
color: Auto,
refresh: false,
help: (),
no_progress: false,
quiet: 0,
verbose: 0,
log_file: None,
no_log_file: false,
version: (),
show_settings: true,
}
RunArgs {
options: RunOptions {
includes: [],
skips: [],
file_selection: FileSelectionArgs {
all_files: false,
files: [],
glob: [],
directory: [],
from_ref: None,
to_ref: None,
last_commit: false,
},
show_diff_on_failure: false,
fail_fast: false,
no_fail_fast: false,
dry_run: false,
extra: RunExtraArgs {
remote_branch: None,
local_branch: None,
pre_rebase_upstream: None,
pre_rebase_branch: None,
commit_msg_filename: None,
prepare_commit_message_source: None,
commit_object_name: None,
remote_name: None,
remote_url: None,
checkout_type: None,
is_squash_merge: false,
rewrite_command: None,
},
},
stage: None,
groups: [],
required_groups: [],
no_groups: [],
}
----- stderr -----
"#);
Ok(())
}
#[test]
fn log_file() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'print("Fixing files"); exit(1)'
always_run: true
log_file: log.txt
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
----- stderr -----
"#);
let log = context.read("log.txt");
assert_eq!(log, "Fixing files");
}
#[test]
fn pass_env_vars() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: env-vars
name: Pass environment
language: system
entry: python3 -c "import os, sys; print(os.getenv('PRE_COMMIT')); sys.exit(1)"
always_run: true
"#});
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
Pass environment.........................................................Failed
- hook id: env-vars
- exit code: 1
1
----- stderr -----
");
}
#[test]
fn staged_files_only() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'print(open("file.txt", "rt").read())'
verbose: true
types: [text]
"#});
context
.work_dir()
.child("file.txt")
.write_str("Hello, world!")?;
context.git_add(".");
context
.work_dir()
.child("file.txt")
.write_str("Hello world again!")?;
let filters: Vec<_> = context
.filters()
.into_iter()
.chain([(r"/\d+-\d+.patch", "/[TIME]-[PID].patch")])
.collect();
cmd_snapshot!(filters, context.run(), @r"
success: true
exit_code: 0
----- stdout -----
trailing-whitespace......................................................Passed
- hook id: trailing-whitespace
- duration: [TIME]
Hello, world!
----- stderr -----
Unstaged changes detected. Temporarily saving them to `[HOME]/patches/[TIME]-[PID].patch`
Restored unstaged changes from `[HOME]/patches/[TIME]-[PID].patch`
");
let content = context.read("file.txt");
assert_snapshot!(content, @"Hello world again!");
Ok(())
}
#[test]
fn intent_to_add_file_survives_conflicted_stash_restore() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: rewrite-python
name: rewrite-python
language: system
entry: python3 -c 'open("test.py", "w").write("a = 1\n")'
files: ^test\.py$
"#});
let cwd = context.work_dir();
context.git_add(PRE_COMMIT_CONFIG_YAML);
cwd.child("intent.txt").write_str("preserve me\n")?;
git_cmd(cwd)
.arg("add")
.arg("--intent-to-add")
.arg("intent.txt")
.assert()
.success();
cwd.child("test.py").write_str("a=1\n")?;
context.git_add("test.py");
cwd.child("test.py").write_str("a=1\nb = 2\n")?;
let filters: Vec<_> = context
.filters()
.into_iter()
.chain([(r"/\d+-\d+.patch", "/[TIME]-[PID].patch")])
.collect();
cmd_snapshot!(filters, context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
rewrite-python...........................................................Failed
- hook id: rewrite-python
- files were modified by this hook
----- stderr -----
Unstaged changes detected. Temporarily saving them to `[HOME]/patches/[TIME]-[PID].patch`
Hook changes conflicted with the saved unstaged changes. Reverting the hook changes
Restored unstaged changes from `[HOME]/patches/[TIME]-[PID].patch`
"#);
assert_eq!(context.read("intent.txt"), "preserve me\n");
assert_eq!(context.read("test.py"), "a=1\nb = 2\n");
let output = git_cmd(cwd)
.arg("diff")
.arg("--diff-filter=A")
.arg("--name-only")
.arg("--")
.arg("intent.txt")
.output()?;
assert!(output.status.success(), "{output:?}");
assert_eq!(String::from_utf8(output.stdout)?, "intent.txt\n");
Ok(())
}
#[cfg(unix)]
#[test]
fn restore_on_interrupt() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import time; open("out.txt", "wt").write(open("file.txt", "rt").read()); time.sleep(10)'
verbose: true
types: [text]
"#});
context
.work_dir()
.child("file.txt")
.write_str("Hello, world!")?;
context.git_add(".");
context
.work_dir()
.child("file.txt")
.write_str("Hello world again!")?;
let mut child = context.run().spawn()?;
let child_id = child.id();
let handle = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(1));
#[allow(clippy::cast_possible_wrap)]
unsafe {
libc::kill(child_id as i32, libc::SIGINT)
};
});
handle.join().unwrap();
child.wait()?;
let content = context.read("out.txt");
assert_snapshot!(content, @"Hello, world!");
let content = context.read("file.txt");
assert_snapshot!(content, @"Hello world again!");
Ok(())
}
#[test]
fn merge_conflicts() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
cwd.child("file.txt").write_str("Hello, world!")?;
context.git_add(".");
context.git_commit("Initial commit");
git_cmd(cwd)
.arg("checkout")
.arg("-b")
.arg("feature")
.assert()
.success();
cwd.child("file.txt").write_str("Hello, world again!")?;
context.git_add(".");
context.git_commit("Feature commit");
git_cmd(cwd)
.arg("checkout")
.arg("master")
.assert()
.success();
cwd.child("file.txt")
.write_str("Hello, world from master!")?;
context.git_add(".");
context.git_commit("Master commit");
git_cmd(cwd).arg("merge").arg("feature").assert().code(1);
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sorted(sys.argv[1:]))'
verbose: true
"});
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Found unresolved merge conflicts. Resolve the conflicts, stage the files with `git add`, and try again
"#);
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
trailing-whitespace......................................................Passed
- hook id: trailing-whitespace
- duration: [TIME]
['.pre-commit-config.yaml', 'file.txt']
----- stderr -----
");
Ok(())
}
#[test]
fn local_python_hook() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: local-python-hook
name: local-python-hook
language: python
entry: python3 -c 'import sys; print("Hello, world!"); sys.exit(1)'
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
local-python-hook........................................................Failed
- hook id: local-python-hook
- exit code: 1
Hello, world!
----- stderr -----
");
}
#[test]
fn invalid_entry() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: entry
name: entry
language: python
entry: '"'
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to run hook `entry`
caused by: Invalid hook `entry`
caused by: Failed to parse entry `"` as commands
"#);
}
#[test]
fn init_nonexistent_repo() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://notexistentatallnevergonnahappen.com/nonexistent/repo
rev: v1.0.0
hooks:
- id: nonexistent
name: nonexistent
"});
context.git_add(".");
let filters = context
.filters()
.into_iter()
.chain([
(r"exit code: ", "exit status: "),
(
r"Command `[^`]*git(?:\.exe)? fetch origin --tags`",
"Command `[GIT] fetch origin --tags`",
),
(
r"fatal: unable to access 'https://notexistentatallnevergonnahappen\.com/nonexistent/repo/':.*",
r"fatal: unable to access 'https://notexistentatallnevergonnahappen.com/nonexistent/repo/': [error]",
),
])
.collect::<Vec<_>>();
cmd_snapshot!(filters, context.run(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to init hooks
caused by: Failed to clone repo `https://notexistentatallnevergonnahappen.com/nonexistent/repo`
caused by: Command `[GIT] fetch origin --tags` exited with an error:
[status]
exit status: 128
[stderr]
fatal: unable to access 'https://notexistentatallnevergonnahappen.com/nonexistent/repo/': [error]
"#);
}
#[test]
fn skipped_remote_repo_is_not_cloned() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: builtin
hooks:
- id: end-of-file-fixer
- repo: https://notexistentatallnevergonnahappen.com/nonexistent/repo
rev: v1.0.0
hooks:
- id: ruff-check
"});
context.git_add(".");
cmd_snapshot!(
context.filters(),
context.run().arg("--all-files").arg("--skip").arg("ruff-check"),
@r"
success: true
exit_code: 0
----- stdout -----
fix end of files.........................................................Passed
----- stderr -----
"
);
}
#[test]
fn skipped_same_key_remote_repo_entry_is_not_initialized() -> Result<()> {
let context = TestContext::new();
context.init_project();
let hook_repo = context.home_dir().child("duplicate-key-hook-repo");
hook_repo.create_dir_all()?;
git_cmd(&hook_repo)
.arg("-c")
.arg("init.defaultBranch=master")
.arg("init")
.assert()
.success();
hook_repo
.child(PRE_COMMIT_HOOKS_YAML)
.write_str(indoc::indoc! {r"
- id: test-hook
name: Test Hook
entry: echo ok
language: system
always_run: true
"})?;
git_cmd(&hook_repo).arg("add").arg(".").assert().success();
git_cmd(&hook_repo)
.arg("commit")
.arg("-m")
.arg("Initial commit")
.assert()
.success();
git_cmd(&hook_repo)
.arg("tag")
.arg("v1.0.0")
.assert()
.success();
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: {repo}
rev: v1.0.0
hooks:
- id: missing-hook
- repo: {repo}
rev: v1.0.0
hooks:
- id: test-hook
", repo = hook_repo.display()});
context.git_add(".");
context
.run()
.arg("--all-files")
.arg("--skip")
.arg("missing-hook")
.assert()
.success();
Ok(())
}
#[test]
fn required_group_excluded_remote_repo_is_not_cloned() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: builtin
hooks:
- id: end-of-file-fixer
groups: [ci, fast]
- repo: https://notexistentatallnevergonnahappen.com/nonexistent/repo
rev: v1.0.0
hooks:
- id: ruff-check
groups: [ci]
"});
context.git_add(".");
cmd_snapshot!(
context.filters(),
context
.run()
.arg("--all-files")
.arg("--require-group")
.arg("ci")
.arg("--require-group")
.arg("fast"),
@r"
success: true
exit_code: 0
----- stdout -----
fix end of files.........................................................Passed
----- stderr -----
"
);
}
#[test]
fn unmatched_skip_does_not_suppress_remote_clone() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: builtin
hooks:
- id: end-of-file-fixer
- repo: https://notexistentatallnevergonnahappen.com/nonexistent/repo
rev: v1.0.0
hooks:
- id: ruff-check
"});
context.git_add(".");
let filters = context
.filters()
.into_iter()
.chain([
(r"exit code: ", "exit status: "),
(
r"Command `[^`]*git(?:\.exe)? fetch origin --tags`",
"Command `[GIT] fetch origin --tags`",
),
(
r"fatal: unable to access 'https://notexistentatallnevergonnahappen\.com/nonexistent/repo/':.*",
r"fatal: unable to access 'https://notexistentatallnevergonnahappen.com/nonexistent/repo/': [error]"
),
])
.collect::<Vec<_>>();
cmd_snapshot!(
filters,
context.run().arg("--all-files").arg("--skip").arg("other-hook"),
@r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to init hooks
caused by: Failed to clone repo `https://notexistentatallnevergonnahappen.com/nonexistent/repo`
caused by: Command `[GIT] fetch origin --tags` exited with an error:
[status]
exit status: 128
[stderr]
fatal: unable to access 'https://notexistentatallnevergonnahappen.com/nonexistent/repo/': [error]
"#
);
}
#[test]
fn types_directory() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: directory
name: directory
language: system
entry: echo
types: [directory]
"});
context.work_dir().child("dir").create_dir_all()?;
context
.work_dir()
.child("dir/file.txt")
.write_str("Hello, world!")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
directory............................................(no files to check)Skipped
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--files").arg("dir"), @r#"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--all-files"), @r#"
success: true
exit_code: 0
----- stdout -----
directory............................................(no files to check)Skipped
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--files").arg("non-exist-files"), @r"
success: true
exit_code: 0
----- stdout -----
directory............................................(no files to check)Skipped
----- stderr -----
warning: This file does not exist and will be ignored: `non-exist-files`
");
Ok(())
}
#[test]
fn run_last_commit() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
"});
cwd.child("file1.txt").write_str("Hello, world!\n")?;
cwd.child("file2.txt")
.write_str("Initial content with trailing spaces \n")?; context.git_add(".");
context.git_commit("Initial commit");
cwd.child("file1.txt").write_str("Hello, world! \n")?; cwd.child("file3.txt").write_str("New file")?; context.git_add(".");
context.git_commit("Second commit with issues");
cmd_snapshot!(context.filters(), context.run().arg("--last-commit"), @r"
success: false
exit_code: 1
----- stdout -----
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
Fixing file1.txt
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing file3.txt
----- stderr -----
");
cwd.child("file1.txt").write_str("Hello, world! \n")?; cwd.child("file3.txt").write_str("New file")?;
cmd_snapshot!(context.filters(), context.run().arg("--all-files"), @r"
success: false
exit_code: 1
----- stdout -----
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
Fixing file1.txt
Fixing file2.txt
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing file3.txt
----- stderr -----
");
Ok(())
}
#[test]
fn run_multiple_files() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: multiple-files
name: multiple-files
language: system
entry: echo
verbose: true
types: [text]
"});
let cwd = context.work_dir();
cwd.child("file1.txt").write_str("Hello, world!")?;
cwd.child("file2.txt").write_str("Hello, world!")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--files").arg("file1.txt").arg("file2.txt"), @r#"
success: true
exit_code: 0
----- stdout -----
multiple-files...........................................................Passed
- hook id: multiple-files
- duration: [TIME]
file1.txt file2.txt
----- stderr -----
"#);
Ok(())
}
#[test]
fn run_glob() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: glob
name: glob
language: system
entry: echo
verbose: true
types: [text]
"});
let cwd = context.work_dir();
cwd.child("root.rs").write_str("fn main() {}")?;
cwd.child("src/lib.rs").write_str("pub fn lib() {}")?;
cwd.child("src/lib.py").write_str("print('hello')")?;
cwd.child("src/nested/mod.rs")
.write_str("pub mod nested;")?;
cwd.child("docs/readme.md").write_str("# Readme")?;
context.git_add(".");
cwd.child("src/untracked.rs")
.write_str("pub fn untracked() {}")?;
cmd_snapshot!(context.filters(), context.run().arg("--glob").arg("src/**/*.rs"), @r#"
success: true
exit_code: 0
----- stdout -----
glob.....................................................................Passed
- hook id: glob
- duration: [TIME]
src/lib.rs src/nested/mod.rs
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--cd").arg("src").arg("--files").arg("../root.rs").arg("--directory").arg("../docs").arg("--glob").arg("**/*.rs"), @r#"
success: true
exit_code: 0
----- stdout -----
glob.....................................................................Passed
- hook id: glob
- duration: [TIME]
docs/readme.md root.rs src/nested/mod.rs src/lib.rs
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--cd").arg("src").arg("--glob").arg("../*.rs"), @r"
success: true
exit_code: 0
----- stdout -----
glob.................................................(no files to check)Skipped
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--glob").arg("missing/**/*.rs"), @r"
success: true
exit_code: 0
----- stdout -----
glob.................................................(no files to check)Skipped
----- stderr -----
");
Ok(())
}
#[test]
fn run_no_files() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: no-files
name: no-files
language: system
entry: echo
verbose: true
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--files"), @r"
success: true
exit_code: 0
----- stdout -----
no-files.................................................................Passed
- hook id: no-files
- duration: [TIME]
.pre-commit-config.yaml
----- stderr -----
");
}
#[test]
fn run_directory() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: directory
name: directory
language: system
entry: echo
verbose: true
"});
let cwd = context.work_dir();
cwd.child("dir1").create_dir_all()?;
cwd.child("dir1/file.txt").write_str("Hello, world!")?;
cwd.child("dir2").create_dir_all()?;
cwd.child("dir2/file.txt").write_str("Hello, world!")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("dir1"), @r"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("dir1").arg("--directory").arg("dir1"), @r"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("dir1").arg("--directory").arg("dir2"), @r#"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt dir2/file.txt
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("non-existing-dir"), @r"
success: true
exit_code: 0
----- stdout -----
directory............................................(no files to check)Skipped
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("dir1").arg("--files").arg("dir1/file.txt"), @r"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("dir1").arg("--files").arg("dir2/file.txt"), @r#"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt dir2/file.txt
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().current_dir(cwd.join("dir1")).arg("--directory").arg("."), @r"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--cd").arg("dir1").arg("--directory").arg("."), @r"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt
----- stderr -----
");
Ok(())
}
#[test]
fn minimum_prek_version() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
minimum_prek_version: 10.0.0
repos:
- repo: local
hooks:
- id: directory
name: directory
language: system
entry: echo
verbose: true
"});
context.git_add(".");
let filters = context
.filters()
.into_iter()
.chain([(
r"but version `\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?` is installed",
"but version `[CURRENT_VERSION]` is installed",
)])
.collect::<Vec<_>>();
cmd_snapshot!(filters, context.run(), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `.pre-commit-config.yaml`
caused by: error: line 1 column 23: `prek` version `10.0.0` or newer is required, but version `[CURRENT_VERSION]` is installed. Upgrade `prek` and try again.
--> <input>:1:23
|
1 | minimum_prek_version: 10.0.0
| ^ `prek` version `10.0.0` or newer is required, but version `[CURRENT_VERSION]` is installed. Upgrade `prek` and try again.
2 | repos:
3 | - repo: local
|
");
}
#[test]
#[cfg(not(windows))]
fn color() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: color
name: color
language: python
entry: python ./color.py
verbose: true
pass_filenames: false
"});
let script = indoc::indoc! {r"
import sys
if sys.stdout.isatty():
print('\033[1;32mHello, world!\033[0m')
else:
print('Hello, world!')
sys.stdout.flush()
"};
context.work_dir().child("color.py").write_str(script)?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
color....................................................................Passed
- hook id: color
- duration: [TIME]
Hello, world!
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--color=always"), @r#"
success: true
exit_code: 0
----- stdout -----
color[32m....................................................................[39m[42mPassed[49m
[2m- hook id: color[0m
[2m- duration: [TIME][0m
[1;32mHello, world![0m
----- stderr -----
"#);
Ok(())
}
#[test]
fn shebang_script() -> Result<()> {
let context = TestContext::new();
context.init_project();
let script = indoc::indoc! {r"
#!/usr/bin/env python
import sys
print('Hello, world!')
sys.exit(0)
"};
context.work_dir().child("script.py").write_str(script)?;
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: shebang-script
name: shebang-script
language: python
entry: script.py
verbose: true
pass_filenames: false
always_run: true
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
shebang-script...........................................................Passed
- hook id: shebang-script
- duration: [TIME]
Hello, world!
----- stderr -----
");
Ok(())
}
#[test]
fn git_commit_a() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: echo
name: echo
language: system
entry: echo
verbose: true
"});
let cwd = context.work_dir();
let file = cwd.child("file.txt");
file.write_str("Hello, world!\n")?;
cmd_snapshot!(context.filters(), context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
"#);
context.git_add(".");
context.git_commit("Initial commit");
file.write_str("Hello, world again!\n")?;
let mut commit = git_cmd(cwd);
commit
.arg("commit")
.arg("-a")
.arg("-m")
.arg("Update file")
.env(EnvVars::PREK_HOME, &**context.home_dir());
let filters = context
.filters()
.into_iter()
.chain([
(r"\[master \w{7}\]", r"[master COMMIT]"),
("7c8398204bbc95c33a6d2543f86a27621647cf78", "[HASH]"),
])
.collect::<Vec<_>>();
cmd_snapshot!(filters, commit, @r"
success: true
exit_code: 0
----- stdout -----
[master COMMIT] Update file
1 file changed, 1 insertion(+), 1 deletion(-)
----- stderr -----
echo.....................................................................Passed
- hook id: echo
- duration: [TIME]
file.txt
");
Ok(())
}
#[cfg(unix)]
#[test]
fn git_commit_a_currently_fails_when_hook_writes_to_temp_git_index() -> Result<()> {
let context = TestContext::new();
context.init_project();
context
.work_dir()
.child("hook.sh")
.write_str(indoc::indoc! {r#"
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cd "$tmpdir"
git init >/dev/null 2>&1
printf 'hook version\n' > file.txt
git add file.txt
"#})?;
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: write-temp-index
name: write-temp-index
language: system
entry: sh hook.sh
pass_filenames: false
always_run: true
verbose: true
"});
let cwd = context.work_dir();
let file = cwd.child("file.txt");
file.write_str("Hello, world!\n")?;
cmd_snapshot!(context.filters(), context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
"#);
context.git_add(".");
context.git_commit("Initial commit");
file.write_str("Hello again!\n")?;
let mut commit = git_cmd(cwd);
commit
.arg("commit")
.arg("-a")
.arg("-m")
.arg("Update file")
.env(EnvVars::PREK_HOME, &**context.home_dir());
let filters = context
.filters()
.into_iter()
.chain([
(r"\[master \w{7}\]", r"[master COMMIT]"),
(
r"invalid object 100644 [0-9a-f]{40}",
"invalid object 100644 [HASH]",
),
])
.collect::<Vec<_>>();
cmd_snapshot!(filters, commit, @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
write-temp-index.........................................................Passed
- hook id: write-temp-index
- duration: [TIME]
error: invalid object 100644 [HASH] for 'file.txt'
error: Error building trees
"
);
Ok(())
}
fn write_pre_commit_config(path: &Path, hooks: &[(&str, &str)]) -> Result<()> {
let mut yaml = String::from(indoc::indoc! {"
repos:
- repo: local
hooks:
"});
for (id, name) in hooks {
let hook = textwrap::indent(
&indoc::formatdoc! {"
- id: {}
name: {}
entry: echo
language: system
", id, name
},
" ",
);
yaml.push_str(&hook);
}
fs_err::create_dir_all(path)?;
fs_err::write(path.join(PRE_COMMIT_CONFIG_YAML), yaml)?;
Ok(())
}
#[cfg(unix)]
#[test]
fn selectors_completion() -> Result<()> {
let context = TestContext::new();
let cwd = context.work_dir();
context.init_project();
write_pre_commit_config(
cwd,
&[("root-hook", "Root Hook"), ("lint:ruff", "Ruff Lint")],
)?;
let app = cwd.join("app");
write_pre_commit_config(&app, &[("app-hook", "App Hook")])?;
let app_lib = app.join("lib");
write_pre_commit_config(&app_lib, &[("lib-hook", "Lib Hook")])?;
cwd.child("scratch").create_dir_all()?;
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg(""), @"
success: true
exit_code: 0
----- stdout -----
install Install prek Git hook shims
prepare-hooks Prepare environments for configured hooks
run Run configured hooks
list List configured hooks
uninstall Uninstall prek Git hook shims
validate-config Validate prek configuration files
validate-manifest Validate pre-commit hook manifests (`.pre-commit-hooks.yaml`)
sample-config Generate a sample prek configuration file
update Update configured repositories
cache Manage the prek cache
try-repo Try hooks from a repository
util Run utility commands
self Manage the prek installation
app/
app:
app-hook App Hook
lib-hook Lib Hook
:lint:ruff Ruff Lint
root-hook Root Hook
--skip Skip the specified hooks or projects
--all-files Run hooks on all tracked files in the repository
--files Run hooks on the specified file paths
--glob Run hooks on tracked files matching the specified glob pattern
--directory Run hooks on tracked files under the specified directory
--from-ref The original ref in a `<from_ref>...<to_ref>` diff expression. Files changed in this diff will be run through the hooks
--to-ref The destination ref in a `from_ref...to_ref` diff expression. Defaults to `HEAD` if `from_ref` is specified
--last-commit Run hooks against the last commit. Equivalent to `--from-ref HEAD~1 --to-ref HEAD`
--show-diff-on-failure When hooks fail, run `git diff` directly afterward
--fail-fast Stop running hooks after the first failure
--dry-run Do not run the hooks, but print the hooks that would have been run
--stage The stage during which the hook is fired
--group Run hooks belonging to the specified group
--require-group Run hooks belonging to every specified group
--no-group Do not run hooks belonging to the specified group
--config Path to alternate config file
--cd Change to directory before running
--color Whether to use color in output
--refresh Refresh all cached data
--help Display the concise help for this command
--no-progress Hide all progress outputs
--quiet Use quiet output
--verbose Use verbose output
--log-file Write trace logs to the specified file. If not specified, trace logs will be written to `$PREK_HOME/prek.log`
--version Display the prek version
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("."), @r"
success: true
exit_code: 0
----- stdout -----
./
.:
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("ap"), @r"
success: true
exit_code: 0
----- stdout -----
app/
app:
app-hook App Hook
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app:"), @r"
success: true
exit_code: 0
----- stdout -----
app:app-hook App Hook
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app:app"), @r"
success: true
exit_code: 0
----- stdout -----
app:app-hook App Hook
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app/"), @r"
success: true
exit_code: 0
----- stdout -----
app/lib/
app/lib:
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app/li"), @r"
success: true
exit_code: 0
----- stdout -----
app/lib/
app/lib:
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app/lib:"), @r"
success: true
exit_code: 0
----- stdout -----
app/lib:lib-hook Lib Hook
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app/lib/"), @r"
success: true
exit_code: 0
----- stdout -----
app/lib/
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg(".:root"), @r"
success: true
exit_code: 0
----- stdout -----
.:root-hook Root Hook
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg(":lint:"), @r"
success: true
exit_code: 0
----- stdout -----
:lint:ruff Ruff Lint
----- stderr -----
");
Ok(())
}
#[test]
fn reuse_env() -> Result<()> {
let context = TestContext::new();
context.init_project();
let pkg_dir = context.work_dir().child("local_pkg");
pkg_dir.create_dir_all()?;
pkg_dir.child("setup.py").write_str(indoc::indoc! {r#"
from setuptools import setup
setup(
name="local-pkg",
version="0.1.0",
py_modules=["local_pkg"],
)
"#})?;
pkg_dir
.child("local_pkg.py")
.write_str("def hello():\n print('hello')\n")?;
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: reuse-env
name: reuse-env
language: python
entry: python -c "import local_pkg; local_pkg.hello()"
pass_filenames: false
additional_dependencies: ["./local_pkg"]
verbose: true
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
reuse-env................................................................Passed
- hook id: reuse-env
- duration: [TIME]
hello
----- stderr -----
");
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: reuse-env
name: reuse-env
language: python
entry: python -c "print('ok')"
pass_filenames: false
verbose: true
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
reuse-env................................................................Passed
- hook id: reuse-env
- duration: [TIME]
ok
----- stderr -----
");
assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 2);
Ok(())
}
#[test]
fn dry_run() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: fail
name: fail
entry: fail
language: fail
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--dry-run").arg("-v"), @r"
success: true
exit_code: 0
----- stdout -----
fail....................................................................Dry Run
- hook id: fail
- duration: [TIME]
`fail` would be run on 1 files:
- .pre-commit-config.yaml
----- stderr -----
");
}
#[test]
fn alternate_config_file() -> Result<()> {
let context = TestContext::new();
context.init_project();
context
.work_dir()
.child(PRE_COMMIT_CONFIG_YML)
.write_str(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: local-python-hook
name: local-python-hook
language: python
entry: python3 -c 'import sys; print("Hello, world!")'
"#})?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("-v"), @r"
success: true
exit_code: 0
----- stdout -----
local-python-hook........................................................Passed
- hook id: local-python-hook
- duration: [TIME]
Hello, world!
----- stderr -----
");
context
.work_dir()
.child(PRE_COMMIT_CONFIG_YAML)
.write_str(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: local-python-hook
name: local-python-hook
language: python
entry: python3 -c 'import sys; print("Hello, world!")'
"#})?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--refresh").arg("-v"), @r"
success: true
exit_code: 0
----- stdout -----
local-python-hook........................................................Passed
- hook id: local-python-hook
- duration: [TIME]
Hello, world!
----- stderr -----
warning: Multiple configuration files found (`.pre-commit-config.yaml`, `.pre-commit-config.yml`); using `[TEMP_DIR]/.pre-commit-config.yaml`
");
context
.work_dir()
.child(PREK_TOML)
.write_str(indoc::indoc! {r#"
[[repos]]
repo = "local"
hooks = [
{
id = "local-python-hook",
name = "local-python-hook",
language = "python",
entry = "python3 -c 'import sys; print(\"Hello, world!\")'"
}
]
"#})?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--refresh").arg("-v"), @r"
success: true
exit_code: 0
----- stdout -----
local-python-hook........................................................Passed
- hook id: local-python-hook
- duration: [TIME]
Hello, world!
----- stderr -----
warning: Multiple configuration files found (`prek.toml`, `.pre-commit-config.yaml`, `.pre-commit-config.yml`); using `[TEMP_DIR]/prek.toml`
");
Ok(())
}
#[test]
fn prek_toml() -> Result<()> {
let context = TestContext::new();
context.init_project();
context
.work_dir()
.child(PREK_TOML)
.write_str(indoc::indoc! {r#"
[[repos]]
repo = "local"
hooks = [
{
id = "local-python-hook",
name = "local-python-hook",
language = "python",
entry = "python3 -c 'import sys; print(\"Hello, world!\")'"
}
]
"#})?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("-v"), @r"
success: true
exit_code: 0
----- stdout -----
local-python-hook........................................................Passed
- hook id: local-python-hook
- duration: [TIME]
Hello, world!
----- stderr -----
");
Ok(())
}
#[test]
fn prek_toml_resolves_priority_aliases() -> Result<()> {
let context = TestContext::new();
context.init_project();
context
.work_dir()
.child(PREK_TOML)
.write_str(indoc::indoc! {r#"
[priorities]
early = 0
late = 10
[[repos]]
repo = "local"
hooks = [
{
id = "late",
name = "Late Hook",
language = "system",
entry = "python3 -c \"print('late')\"",
always_run = true,
priority = "late",
},
{
id = "early",
name = "Early Hook",
language = "system",
entry = "python3 -c \"print('early')\"",
always_run = true,
priority = "early",
},
]
"#})?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
Early Hook...............................................................Passed
Late Hook................................................................Passed
----- stderr -----
");
Ok(())
}
#[test]
fn show_diff_on_failure() -> Result<()> {
let context = TestContext::new();
context.init_project();
let config = indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: modify
name: modify
language: python
entry: python -c "import sys; open('file.txt', 'a').write('Added line\n')"
pass_filenames: false
"#};
context.write_pre_commit_config(config);
context
.work_dir()
.child("file.txt")
.write_str("Original line\n")?;
context.git_add(".");
let mut filters = context.filters();
filters.push((r"index \w{7}\.\.\w{7} \d{6}", "index [OLD]..[NEW] 100644"));
cmd_snapshot!(filters.clone(), context.run().env(EnvVars::CI, "1").arg("--show-diff-on-failure").arg("-v"), @"
success: false
exit_code: 1
----- stdout -----
modify...................................................................Failed
- hook id: modify
- duration: [TIME]
- files were modified by this hook
hint: Some hooks made changes to the files.
If you are seeing this message in CI, reproduce locally with: `prek run --all-files`
To run prek as part of Git workflow, use `prek install` to set up Git shims.
All changes made by hooks:
diff --git a/file.txt b/file.txt
index [OLD]..[NEW] 100644
--- a/file.txt
+++ b/file.txt
@@ -1 +1,2 @@
Original line
+Added line
----- stderr -----
");
context
.work_dir()
.child("file.txt")
.write_str("Original line\n")?;
context.git_add(".");
cmd_snapshot!(filters.clone(), context.run().env_remove(EnvVars::CI).arg("--show-diff-on-failure").arg("-v"), @r"
success: false
exit_code: 1
----- stdout -----
modify...................................................................Failed
- hook id: modify
- duration: [TIME]
- files were modified by this hook
All changes made by hooks:
diff --git a/file.txt b/file.txt
index [OLD]..[NEW] 100644
--- a/file.txt
+++ b/file.txt
@@ -1 +1,2 @@
Original line
+Added line
----- stderr -----
");
let app = context.work_dir().child("app");
app.create_dir_all()?;
app.child("file.txt").write_str("Original line\n")?;
app.child(PRE_COMMIT_CONFIG_YAML).write_str(config)?;
git_cmd(&app).arg("add").arg(".").assert().success();
cmd_snapshot!(filters.clone(), context.run().env_remove(EnvVars::CI).current_dir(&app).arg("--show-diff-on-failure"), @r"
success: false
exit_code: 1
----- stdout -----
modify...................................................................Failed
- hook id: modify
- files were modified by this hook
All changes made by hooks:
diff --git a/app/file.txt b/app/file.txt
index [OLD]..[NEW] 100644
--- a/app/file.txt
+++ b/app/file.txt
@@ -1 +1,2 @@
Original line
+Added line
----- stderr -----
");
context.git_add(".");
cmd_snapshot!(filters.clone(), context.run().env_remove(EnvVars::CI).arg("--show-diff-on-failure").arg("--refresh"), @r#"
success: false
exit_code: 1
----- stdout -----
× app
modify.................................................................Failed
- hook id: modify
- files were modified by this hook
× <workspace>
modify.................................................................Failed
- hook id: modify
- files were modified by this hook
All changes made by hooks:
diff --git a/app/file.txt b/app/file.txt
index [OLD]..[NEW] 100644
--- a/app/file.txt
+++ b/app/file.txt
@@ -1,2 +1,3 @@
Original line
Added line
+Added line
diff --git a/file.txt b/file.txt
index [OLD]..[NEW] 100644
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,3 @@
Original line
Added line
+Added line
----- stderr -----
"#);
Ok(())
}
#[test]
fn run_quiet() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: success
name: success
entry: echo
language: system
- id: fail
name: fail
entry: fail
language: fail
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--quiet"), @r"
success: false
exit_code: 1
----- stdout -----
fail.....................................................................Failed
- hook id: fail
- exit code: 1
fail
.pre-commit-config.yaml
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("-qq"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
");
}
#[test]
fn run_quiet_env() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: success
name: success
entry: echo
language: system
- id: fail
name: fail
entry: fail
language: fail
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().env(EnvVars::PREK_QUIET, "1"), @r"
success: false
exit_code: 1
----- stdout -----
fail.....................................................................Failed
- hook id: fail
- exit code: 1
fail
.pre-commit-config.yaml
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env(EnvVars::PREK_QUIET, "2"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
");
}
#[test]
fn run_log_file() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: fail
name: fail
entry: fail
language: fail
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--no-log-file"), @r"
success: false
exit_code: 1
----- stdout -----
fail.....................................................................Failed
- hook id: fail
- exit code: 1
fail
.pre-commit-config.yaml
----- stderr -----
");
context
.home_dir()
.child("prek.log")
.assert(predicate::path::missing());
cmd_snapshot!(context.filters(), context.run().arg("--log-file").arg("log"), @r"
success: false
exit_code: 1
----- stdout -----
fail.....................................................................Failed
- hook id: fail
- exit code: 1
fail
.pre-commit-config.yaml
----- stderr -----
");
context
.work_dir()
.child("log")
.assert(predicate::path::exists());
}
#[test]
fn system_language_version() {
if !EnvVars.is_set(EnvVars::CI) {
return;
}
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: system-node
name: system-node
language: node
language_version: system
entry: node -v
pass_filenames: false
- id: system-go
name: system-go
language: golang
language_version: system
entry: go version
pass_filenames: false
- id: system-bun
name: system-bun
language: bun
language_version: system
entry: bun -e 'console.log(`Bun ${Bun.version}`)'
pass_filenames: false
- id: system-dotnet
name: system-dotnet
language: dotnet
language_version: system
entry: dotnet --version
pass_filenames: false
"});
context.git_add(".");
cmd_snapshot!(
context.filters(),
context.run()
.arg("system-node")
.env(EnvVars::PREK_INTERNAL__NODE_BINARY_NAME, "node-never-exist"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to install hook `system-node`
caused by: Failed to install node
caused by: No suitable system Node version found and downloads are disabled
");
cmd_snapshot!(
context.filters(),
context.run()
.arg("system-go")
.env(EnvVars::PREK_INTERNAL__GO_BINARY_NAME, "go-never-exist"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to install hook `system-go`
caused by: Failed to install go
caused by: No suitable system Go version found and downloads are disabled
");
cmd_snapshot!(
context.filters(),
context.run()
.arg("system-bun")
.env(EnvVars::PREK_INTERNAL__BUN_BINARY_NAME, "bun-never-exist"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to install hook `system-bun`
caused by: Failed to install bun
caused by: No suitable system Bun version found and downloads are disabled
");
cmd_snapshot!(
context.filters(),
context.run()
.arg("system-dotnet")
.env(EnvVars::PREK_INTERNAL__DOTNET_BINARY_NAME, "dotnet-never-exist"), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to install hook `system-dotnet`
caused by: Failed to install dotnet SDK
caused by: No suitable dotnet version found and downloads are disabled
");
}
#[test]
fn empty_entry() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: local
name: local
language: python
entry: ''
pass_filenames: false
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to run hook `local`
caused by: Invalid hook `local`
caused by: Failed to parse entry: entry is empty
");
}
#[test]
fn run_with_stdin_closed() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: check-stdin
name: check-stdin
language: python
entry: python -c 'import sys; sys.stdin.read(); print("STDIN closed"); sys.stdout.flush()'
pass_filenames: false
verbose: true
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
check-stdin..............................................................Passed
- hook id: check-stdin
- duration: [TIME]
STDIN closed
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().arg("--color").arg("always"), @r#"
success: true
exit_code: 0
----- stdout -----
check-stdin[32m..............................................................[39m[42mPassed[49m
[2m- hook id: check-stdin[0m
[2m- duration: [TIME][0m
STDIN closed
----- stderr -----
"#);
}
#[test]
fn version_info() {
if option_env!("PREK_COMMIT_HASH").is_none() {
return;
}
let context = TestContext::new();
let filters = context
.filters()
.into_iter()
.chain([(
r"prek \d+\.\d+\.\d+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?(\+\d+)? \(\w{9} [\d\-T:\.]+\)",
"prek [CURRENT_VERSION] ([COMMIT] [DATE])",
)])
.collect::<Vec<_>>();
cmd_snapshot!(filters, context.command().arg("--version"), @r"
success: true
exit_code: 0
----- stdout -----
prek [CURRENT_VERSION] ([COMMIT] [DATE])
----- stderr -----
");
}
#[test]
fn expands_tilde_in_prek_home() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: ok
name: ok
entry: echo ok
language: system
"});
context.git_add(".");
let fake_home = context.work_dir().child("fake-home");
fake_home.create_dir_all()?;
cmd_snapshot!(context.filters(), context
.run()
.env("HOME", fake_home.path())
.env("USERPROFILE", fake_home.path()) .env(EnvVars::PREK_HOME, "~/prek-store"), @r"
success: true
exit_code: 0
----- stdout -----
ok.......................................................................Passed
----- stderr -----
");
let store = fake_home.child("prek-store");
store.child("README").assert(predicate::path::exists());
store.child("repos").assert(predicate::path::is_dir());
store.child("hooks").assert(predicate::path::is_dir());
store.child("scratch").assert(predicate::path::is_dir());
context
.work_dir()
.child("~")
.assert(predicate::path::missing());
Ok(())
}
#[test]
fn run_with_tree_object_as_ref() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: echo-files
name: echo files
entry: echo
language: system
pass_filenames: true
"});
cwd.child("file1.txt").write_str("hello")?;
context.git_add(".");
context.git_commit("Initial commit");
cwd.child("file2.txt").write_str("world")?;
context.git_add("file2.txt");
let tree_output = git_cmd(cwd)
.arg("write-tree")
.output()
.expect("Failed to run git write-tree");
let tree_sha = String::from_utf8_lossy(&tree_output.stdout)
.trim()
.to_string();
cmd_snapshot!(context.filters(), context.run()
.arg("--from-ref").arg("HEAD")
.arg("--to-ref").arg(&tree_sha), @r"
success: true
exit_code: 0
----- stdout -----
echo files...............................................................Passed
----- stderr -----
");
Ok(())
}
#[test]
fn pass_filenames_1_limits_batch_size() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: one-at-a-time
name: one at a time
entry: python -c "import sys; args = sys.argv[1:]; sys.exit(0 if len(args) <= 1 else 1)"
language: system
pass_filenames: 1
require_serial: true
verbose: true
"#});
cwd.child("a.txt").write_str("a")?;
cwd.child("b.txt").write_str("b")?;
cwd.child("c.txt").write_str("c")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--all-files"), @r"
success: true
exit_code: 0
----- stdout -----
one at a time............................................................Passed
- hook id: one-at-a-time
- duration: [TIME]
----- stderr -----
");
Ok(())
}
#[test]
fn pass_filenames_2_limits_batch_size() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: two-at-a-time
name: two at a time
entry: python -c "import sys; args = sys.argv[1:]; sys.exit(0 if len(args) <= 2 else 1)"
language: system
pass_filenames: 2
require_serial: true
verbose: true
"#});
cwd.child("a.txt").write_str("a")?;
cwd.child("b.txt").write_str("b")?;
cwd.child("c.txt").write_str("c")?;
cwd.child("d.txt").write_str("d")?;
cwd.child("e.txt").write_str("e")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--all-files"), @r"
success: true
exit_code: 0
----- stdout -----
two at a time............................................................Passed
- hook id: two-at-a-time
- duration: [TIME]
----- stderr -----
");
Ok(())
}