use crate::common::{TestContext, cmd_snapshot, git_cmd};
use assert_cmd::assert::OutputAssertExt;
use assert_fs::assert::PathAssert;
use assert_fs::fixture::{FileWriteStr, PathChild, PathCreateDir};
use indoc::indoc;
use insta::assert_snapshot;
use prek_consts::PRE_COMMIT_CONFIG_YAML;
use prek_consts::env_vars::EnvVars;
mod common;
#[test]
fn install() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
cmd_snapshot!(context.filters(), context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
"#);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
"#);
}
);
context
.work_dir()
.child(".git/hooks/pre-commit")
.write_str("#!/bin/sh\necho 'pre-commit'\n")?;
cmd_snapshot!(context.filters(), context.install().arg("--hook-type").arg("pre-commit").arg("--hook-type").arg("post-commit"), @r"
success: true
exit_code: 0
----- stdout -----
Hook already exists at `.git/hooks/pre-commit`, moved it to `.git/hooks/pre-commit.legacy`
Migration mode: prek will also run legacy hook `.git/hooks/pre-commit.legacy`. Use `--overwrite` to remove legacy hooks.
prek installed at `.git/hooks/pre-commit`
prek installed at `.git/hooks/post-commit`
----- stderr -----
");
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
"#);
}
);
assert_snapshot!(context.read(".git/hooks/pre-commit.legacy"), @r##"
#!/bin/sh
echo 'pre-commit'
"##);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/post-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=post-commit -- "$@"
"#);
}
);
cmd_snapshot!(context.filters(), context.install().arg("-t").arg("pre-commit").arg("--hook-type").arg("post-commit").arg("--overwrite"), @r#"
success: true
exit_code: 0
----- stdout -----
Overwriting existing hook at `.git/hooks/pre-commit`
prek installed at `.git/hooks/pre-commit`
Overwriting existing hook at `.git/hooks/post-commit`
prek installed at `.git/hooks/post-commit`
----- stderr -----
"#);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
"#);
}
);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/post-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=post-commit -- "$@"
"#);
}
);
Ok(())
}
#[test]
fn install_with_quiet_flag() {
let context = TestContext::new();
context.init_project();
cmd_snapshot!(context.filters(), context.install().arg("-q"), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"#);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" -q hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
"#);
}
);
}
#[test]
fn install_with_silent_flag() {
let context = TestContext::new();
context.init_project();
cmd_snapshot!(context.filters(), context.install().arg("-qq"), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"#);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" -qq hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
"#);
}
);
}
#[test]
fn install_with_verbose_flag() {
let context = TestContext::new();
context.init_project();
cmd_snapshot!(context.filters(), context.install().arg("-v"), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
"#);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" -v hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
"#);
}
);
}
#[test]
fn install_with_no_progress_flag() {
let context = TestContext::new();
context.init_project();
cmd_snapshot!(context.filters(), context.install().arg("--no-progress"), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
"#);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" --no-progress hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
"#);
}
);
}
#[test]
fn install_with_git_dir() {
let context = TestContext::new();
context.init_project();
cmd_snapshot!(context.filters(), context.install().arg("--git-dir").arg("custom-git-dir"), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `custom-git-dir/hooks/pre-commit`
----- stderr -----
"#);
context
.work_dir()
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context
.work_dir()
.child("custom-git-dir/hooks/pre-commit")
.assert(predicates::path::exists());
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read("custom-git-dir/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
"#);
}
);
}
#[test]
fn install_with_local_hooks_path_installs_to_configured_directory() {
let context = TestContext::new();
context.init_project();
git_cmd(context.work_dir())
.args(["config", "core.hooksPath", "custom-hooks"])
.assert()
.success();
cmd_snapshot!(context.filters(), context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `custom-hooks/pre-commit`
----- stderr -----
"#);
context
.work_dir()
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context
.work_dir()
.child("custom-hooks/pre-commit")
.assert(predicates::path::exists());
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read("custom-hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
"#);
}
);
}
#[test]
fn install_with_git_dir_allows_external_hooks_path_set() {
let context = TestContext::new();
context.init_project();
let global_gitconfig = context.work_dir().join("global.gitconfig");
git_cmd(context.work_dir())
.env("GIT_CONFIG_GLOBAL", &global_gitconfig)
.args(["config", "--global", "core.hooksPath", "custom-hooks"])
.assert()
.success();
let mut install = context.install();
install.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
cmd_snapshot!(context.filters(), install, @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Refusing to install hooks because `core.hooksPath` is configured outside this repository.
note: Git will execute hooks from the configured global/system hooks directory, not from this repository's hooks directory.
hint: Remove the global/system setting, or move `core.hooksPath` into repo scope for this repository instead.
git config --unset-all --global core.hooksPath
git config --unset-all --system core.hooksPath
git config --local core.hooksPath <path>
");
let mut install = context.install();
install
.arg("--git-dir")
.arg(".git")
.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
cmd_snapshot!(context.filters(), install, @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
"#);
}
#[test]
fn install_refuses_empty_external_hooks_path_set() {
let context = TestContext::new();
context.init_project();
let global_gitconfig = context.work_dir().join("global.gitconfig");
git_cmd(context.work_dir())
.env("GIT_CONFIG_GLOBAL", &global_gitconfig)
.args(["config", "--global", "core.hooksPath", ""])
.assert()
.success();
let mut install = context.install();
install.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
cmd_snapshot!(context.filters(), install, @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Refusing to install hooks because `core.hooksPath` is configured outside this repository.
note: Git will execute hooks from the configured global/system hooks directory, not from this repository's hooks directory.
hint: Remove the global/system setting, or move `core.hooksPath` into repo scope for this repository instead.
git config --unset-all --global core.hooksPath
git config --unset-all --system core.hooksPath
git config --local core.hooksPath <path>
");
}
#[test]
fn install_refuses_empty_local_hooks_path_set() {
let context = TestContext::new();
context.init_project();
git_cmd(context.work_dir())
.args(["config", "core.hooksPath", ""])
.assert()
.success();
cmd_snapshot!(context.filters(), context.install(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Git resolved hooks directory to the current directory (`.`). Unset `core.hooksPath` or set it to a real directory path.
"#);
}
#[test]
fn install_with_dot_hooks_path_installs_to_repo_root() {
let context = TestContext::new();
context.init_project();
git_cmd(context.work_dir())
.args(["config", "core.hooksPath", "."])
.assert()
.success();
context.install().assert().success();
context
.work_dir()
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context
.work_dir()
.child("pre-commit")
.assert(predicates::path::exists());
}
#[test]
fn install_with_included_local_hooks_path_installs_to_configured_directory() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
context
.work_dir()
.child("included-hooks.cfg")
.write_str(indoc! {r"
[core]
hooksPath = custom-hooks
"})?;
git_cmd(context.work_dir())
.args(["config", "--local", "include.path", "../included-hooks.cfg"])
.assert()
.success();
context.install().assert().success();
context
.work_dir()
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context
.work_dir()
.child("custom-hooks/pre-commit")
.assert(predicates::path::exists());
Ok(())
}
#[test]
fn install_with_worktree_hooks_path_installs_to_configured_directory() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
context.work_dir().child("README.md").write_str("hello\n")?;
context.git_add(".");
context.git_commit("Initial commit");
git_cmd(context.work_dir())
.args(["config", "extensions.worktreeConfig", "true"])
.assert()
.success();
git_cmd(context.work_dir())
.args(["worktree", "add", "worktree", "HEAD"])
.assert()
.success();
let worktree = context.work_dir().child("worktree");
let output = git_cmd(&worktree)
.args(["rev-parse", "--path-format=absolute", "--git-dir"])
.output()?;
let worktree_git_dir = String::from_utf8(output.stdout)?.trim().to_string();
let worktree_hooks = std::path::PathBuf::from(&worktree_git_dir).join("hooks");
let worktree_hooks_str = worktree_hooks.display().to_string();
git_cmd(&worktree)
.args([
"config",
"--worktree",
"core.hooksPath",
&worktree_hooks_str,
])
.assert()
.success();
let mut install = context.install();
install.current_dir(&worktree);
install.assert().success();
context
.work_dir()
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
assert!(
worktree_hooks.join("pre-commit").exists(),
"expected hook to be installed into {}",
worktree_hooks.display()
);
Ok(())
}
#[test]
#[cfg(unix)]
fn install_uses_standard_permissions_by_default() {
use std::os::unix::fs::PermissionsExt;
let context = TestContext::new();
context.init_project();
context.install().assert().success();
let hook_path = context.work_dir().join(".git/hooks/pre-commit");
let metadata = std::fs::metadata(&hook_path).unwrap();
let mode = metadata.permissions().mode() & 0o777;
assert_eq!(
mode, 0o755,
"Hook should have standard permissions (0o755), got {mode:o}"
);
}
#[test]
#[cfg(unix)]
fn install_uses_group_permissions_for_shared_repository() {
use std::os::unix::fs::PermissionsExt;
let context = TestContext::new();
context.init_project();
git_cmd(context.work_dir())
.args(["config", "core.sharedRepository", "group"])
.assert()
.success();
context.install().assert().success();
let hook_path = context.work_dir().join(".git/hooks/pre-commit");
let metadata = std::fs::metadata(&hook_path).unwrap();
let mode = metadata.permissions().mode() & 0o777;
assert_eq!(
mode, 0o775,
"Hook should have group-writable permissions (0o775), got {mode:o}"
);
}
#[test]
#[cfg(unix)]
fn install_uses_explicit_shared_repository_mode() {
use std::os::unix::fs::PermissionsExt;
let context = TestContext::new();
context.init_project();
git_cmd(context.work_dir())
.args(["config", "core.sharedRepository", "0640"])
.assert()
.success();
context.install().assert().success();
let hook_path = context.work_dir().join(".git/hooks/pre-commit");
let metadata = std::fs::metadata(&hook_path).unwrap();
let mode = metadata.permissions().mode() & 0o777;
assert_eq!(
mode, 0o750,
"Hook should respect explicit shared mode (0o750), got {mode:o}"
);
}
#[test]
fn install_with_hooks() -> anyhow::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: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
"});
context
.home_dir()
.child("repos")
.assert(predicates::path::missing());
context
.home_dir()
.child("hooks")
.assert(predicates::path::missing());
cmd_snapshot!(context.filters(), context.install().arg("--prepare-hooks"), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
"#);
assert_eq!(context.home_dir().child("repos").read_dir()?.count(), 1);
assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 1);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
"#);
}
);
Ok(())
}
#[test]
fn install_with_legacy_install_hooks_flag_alias() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c 'print("test")'
"#});
context
.home_dir()
.child("hooks")
.assert(predicates::path::missing());
cmd_snapshot!(context.filters(), context.install().arg("--install-hooks"), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
"#);
assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 1);
Ok(())
}
#[test]
fn install_with_existing_legacy_hook() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
context.install().assert().success();
context
.work_dir()
.child(".git/hooks/pre-commit.legacy")
.write_str("#!/bin/sh\necho 'legacy'\n")?;
cmd_snapshot!(context.filters(), context.install(), @r"
success: true
exit_code: 0
----- stdout -----
Migration mode: prek will also run legacy hook `.git/hooks/pre-commit.legacy`. Use `--overwrite` to remove legacy hooks.
prek installed at `.git/hooks/pre-commit`
----- stderr -----
");
context
.work_dir()
.child(".git/hooks/pre-commit.legacy")
.assert(predicates::path::exists());
cmd_snapshot!(context.filters(), context.install().arg("--overwrite"), @r#"
success: true
exit_code: 0
----- stdout -----
Overwriting existing hook at `.git/hooks/pre-commit`
prek installed at `.git/hooks/pre-commit`
----- stderr -----
"#);
context
.work_dir()
.child(".git/hooks/pre-commit.legacy")
.assert(predicates::path::missing());
Ok(())
}
#[test]
fn install_hooks_only() -> anyhow::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: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
"});
context
.home_dir()
.child("repos")
.assert(predicates::path::missing());
context
.home_dir()
.child("hooks")
.assert(predicates::path::missing());
cmd_snapshot!(context.filters(), context.prepare_hooks(), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"#);
assert_eq!(context.home_dir().child("repos").read_dir()?.count(), 1);
assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 1);
context
.work_dir()
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
Ok(())
}
#[test]
fn install_with_legacy_install_hooks_subcommand_alias() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c 'print("test")'
"#});
context
.home_dir()
.child("hooks")
.assert(predicates::path::missing());
cmd_snapshot!(
context.filters(),
context.command().arg("install-hooks"),
@r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"#);
assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 1);
Ok(())
}
#[test]
fn uninstall() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
cmd_snapshot!(context.filters(), context.uninstall(), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
`.git/hooks/pre-commit` does not exist, skipping.
"#);
context.install().assert().success();
cmd_snapshot!(context.filters(), context.uninstall(), @r#"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
----- stderr -----
"#);
context
.work_dir()
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context
.work_dir()
.child(".git/hooks/pre-commit")
.write_str("#!/bin/sh\necho 'pre-commit'\n")?;
cmd_snapshot!(context.filters(), context.uninstall(), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
`.git/hooks/pre-commit` is not managed by prek, skipping.
"#);
context.install().assert().success();
cmd_snapshot!(context.filters(), context.uninstall(), @"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
Restored `.git/hooks/pre-commit.legacy` to `.git/hooks/pre-commit`
----- stderr -----
");
context
.install()
.arg("-t")
.arg("pre-commit")
.arg("-t")
.arg("post-commit")
.assert()
.success();
cmd_snapshot!(context.filters(), context.uninstall().arg("-t").arg("pre-commit").arg("-t").arg("post-commit"), @"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
Restored `.git/hooks/pre-commit.legacy` to `.git/hooks/pre-commit`
Uninstalled `post-commit`
----- stderr -----
");
Ok(())
}
#[test]
fn uninstall_with_local_hooks_path_removes_configured_hook() {
let context = TestContext::new();
context.init_project();
git_cmd(context.work_dir())
.args(["config", "core.hooksPath", "custom-hooks"])
.assert()
.success();
context.install().assert().success();
cmd_snapshot!(context.filters(), context.uninstall(), @r#"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
----- stderr -----
"#);
context
.work_dir()
.child("custom-hooks/pre-commit")
.assert(predicates::path::missing());
}
#[test]
fn uninstall_with_worktree_hooks_path_removes_configured_hook() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
context.work_dir().child("README.md").write_str("hello\n")?;
context.git_add(".");
context.git_commit("Initial commit");
git_cmd(context.work_dir())
.args(["config", "extensions.worktreeConfig", "true"])
.assert()
.success();
git_cmd(context.work_dir())
.args(["worktree", "add", "worktree", "HEAD"])
.assert()
.success();
let worktree = context.work_dir().child("worktree");
let output = git_cmd(&worktree)
.args(["rev-parse", "--path-format=absolute", "--git-dir"])
.output()?;
let worktree_git_dir = String::from_utf8(output.stdout)?.trim().to_string();
let worktree_hooks = std::path::PathBuf::from(&worktree_git_dir).join("hooks");
let worktree_hooks_str = worktree_hooks.display().to_string();
git_cmd(&worktree)
.args([
"config",
"--worktree",
"core.hooksPath",
&worktree_hooks_str,
])
.assert()
.success();
context.install().assert().success();
assert!(
context.work_dir().join(".git/hooks/pre-commit").exists(),
"expected hook to remain installed in the main worktree",
);
let mut install = context.install();
install.current_dir(&worktree);
install.assert().success();
assert!(
worktree_hooks.join("pre-commit").exists(),
"expected hook to be installed into {}",
worktree_hooks.display()
);
let mut uninstall = context.uninstall();
uninstall.current_dir(&worktree);
cmd_snapshot!(context.filters(), uninstall, @r#"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
----- stderr -----
"#);
assert!(
!worktree_hooks.join("pre-commit").exists(),
"expected hook to be removed from {}",
worktree_hooks.display()
);
context
.work_dir()
.child(".git/hooks/pre-commit")
.assert(predicates::path::exists());
Ok(())
}
#[test]
fn uninstall_refuses_external_hooks_path_set() {
let context = TestContext::new();
context.init_project();
let global_gitconfig = context.work_dir().join("global.gitconfig");
git_cmd(context.work_dir())
.env("GIT_CONFIG_GLOBAL", &global_gitconfig)
.args(["config", "--global", "core.hooksPath", "custom-hooks"])
.assert()
.success();
let mut uninstall = context.uninstall();
uninstall.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
cmd_snapshot!(context.filters(), uninstall, @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Refusing to uninstall hooks because `core.hooksPath` is configured outside this repository.
note: Git will execute hooks from the configured global/system hooks directory, not from this repository's hooks directory.
hint: Remove the global/system setting, or move `core.hooksPath` into repo scope for this repository instead.
git config --unset-all --global core.hooksPath
git config --unset-all --system core.hooksPath
git config --local core.hooksPath <path>
");
}
#[test]
fn uninstall_with_git_dir_allows_external_hooks_path_set() {
let context = TestContext::new();
context.init_project();
let global_gitconfig = context.work_dir().join("global.gitconfig");
git_cmd(context.work_dir())
.env("GIT_CONFIG_GLOBAL", &global_gitconfig)
.args(["config", "--global", "core.hooksPath", "custom-hooks"])
.assert()
.success();
let mut install = context.install();
install
.arg("--git-dir")
.arg(".git")
.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
install.assert().success();
let mut uninstall = context.uninstall();
uninstall
.arg("--git-dir")
.arg(".git")
.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
cmd_snapshot!(context.filters(), uninstall, @r#"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
----- stderr -----
"#);
context
.work_dir()
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
}
#[test]
fn uninstall_refuses_empty_external_hooks_path_set() {
let context = TestContext::new();
context.init_project();
let global_gitconfig = context.work_dir().join("global.gitconfig");
git_cmd(context.work_dir())
.env("GIT_CONFIG_GLOBAL", &global_gitconfig)
.args(["config", "--global", "core.hooksPath", ""])
.assert()
.success();
let mut uninstall = context.uninstall();
uninstall.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
cmd_snapshot!(context.filters(), uninstall, @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Refusing to uninstall hooks because `core.hooksPath` is configured outside this repository.
note: Git will execute hooks from the configured global/system hooks directory, not from this repository's hooks directory.
hint: Remove the global/system setting, or move `core.hooksPath` into repo scope for this repository instead.
git config --unset-all --global core.hooksPath
git config --unset-all --system core.hooksPath
git config --local core.hooksPath <path>
");
}
#[test]
fn uninstall_refuses_empty_local_hooks_path_set() {
let context = TestContext::new();
context.init_project();
git_cmd(context.work_dir())
.args(["config", "core.hooksPath", ""])
.assert()
.success();
cmd_snapshot!(context.filters(), context.uninstall(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Git resolved hooks directory to the current directory (`.`). Unset `core.hooksPath` or set it to a real directory path.
"#);
}
#[test]
fn uninstall_with_dot_hooks_path_removes_root_hook() {
let context = TestContext::new();
context.init_project();
git_cmd(context.work_dir())
.args(["config", "core.hooksPath", "."])
.assert()
.success();
context.install().assert().success();
context.uninstall().assert().success();
context
.work_dir()
.child("pre-commit")
.assert(predicates::path::missing());
}
#[test]
fn uninstall_with_included_local_hooks_path_removes_configured_hook() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
context
.work_dir()
.child("included-hooks.cfg")
.write_str(indoc! {r"
[core]
hooksPath = custom-hooks
"})?;
git_cmd(context.work_dir())
.args(["config", "--local", "include.path", "../included-hooks.cfg"])
.assert()
.success();
context.install().assert().success();
context.uninstall().assert().success();
context
.work_dir()
.child("custom-hooks/pre-commit")
.assert(predicates::path::missing());
Ok(())
}
#[test]
fn uninstall_all_managed_hooks() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
context
.install()
.arg("-t")
.arg("pre-commit")
.arg("-t")
.arg("pre-push")
.assert()
.success();
assert!(context.work_dir().join(".git/hooks/pre-commit").exists());
assert!(context.work_dir().join(".git/hooks/pre-push").exists());
let custom_hook = "#!/bin/sh\necho 'custom pre-commit'\n";
context
.work_dir()
.child(".git/hooks/pre-commit")
.write_str(custom_hook)?;
cmd_snapshot!(context.filters(), context.uninstall().arg("--all"), @r"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-push`
----- stderr -----
");
assert_eq!(context.read(".git/hooks/pre-commit"), custom_hook);
assert!(!context.work_dir().join(".git/hooks/pre-push").exists());
Ok(())
}
#[test]
fn uninstall_remove_legacy_hook() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
context.install().assert().success();
fs_err::copy(
context.work_dir().join(".git/hooks/pre-commit"),
context.work_dir().join(".git/hooks/pre-commit.legacy"),
)?;
cmd_snapshot!(context.filters(), context.uninstall(), @"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
----- stderr -----
Found legacy hook at `.git/hooks/pre-commit.legacy`, removing it.
");
context
.work_dir()
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context
.work_dir()
.child(".git/hooks/pre-commit.legacy")
.assert(predicates::path::missing());
context.install().assert().success();
context
.work_dir()
.child(".git/hooks/pre-commit.legacy")
.write_str("#!/bin/sh\necho 'legacy'\n")?;
cmd_snapshot!(context.filters(), context.uninstall(), @"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
Restored `.git/hooks/pre-commit.legacy` to `.git/hooks/pre-commit`
----- stderr -----
");
context
.work_dir()
.child(".git/hooks/pre-commit")
.assert(predicates::path::exists());
context
.work_dir()
.child(".git/hooks/pre-commit.legacy")
.assert(predicates::path::missing());
Ok(())
}
#[test]
fn init_template_dir() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
cmd_snapshot!(context.filters(), context.command().arg("init-templatedir").arg(".git"), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir '.git'`
"#);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --skip-on-missing-config -- "$@"
"#);
}
);
let child = context.work_dir().child("subdir");
child.create_dir_all()?;
cmd_snapshot!(context.filters(), context.command().arg("init-templatedir").arg("temp-dir").current_dir(child), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `temp-dir/hooks/pre-commit`
----- stderr -----
warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir 'temp-dir'`
"#);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read("subdir/temp-dir/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --skip-on-missing-config -- "$@"
"#);
}
);
cmd_snapshot!(context.filters(), context.command().arg("init-templatedir").arg("-c").arg("non-exist-config").arg("subdir2"), @r"
success: true
exit_code: 0
----- stdout -----
prek installed at `subdir2/hooks/pre-commit` with specified config `non-exist-config`
----- stderr -----
warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir 'subdir2'`
");
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read("subdir2/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --config="non-exist-config" --skip-on-missing-config -- "$@"
"#);
}
);
Ok(())
}
#[test]
fn util_init_template_dir() {
let context = TestContext::new();
context.init_project();
cmd_snapshot!(context.filters(), context.command().arg("util").arg("init-templatedir").arg(".git"), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir '.git'`
"#);
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --skip-on-missing-config -- "$@"
"#);
}
);
}
#[test]
fn init_template_dir_non_git_repo() {
let context = TestContext::new();
cmd_snapshot!(context.filters(), context.command().arg("init-template-dir").arg(".git"), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir '.git'`
"#);
context.write_pre_commit_config(indoc::indoc! {"
default_install_hook_types:
- pre-commit
- commit-msg
- pre-push
repos:
"});
cmd_snapshot!(context.filters(), context.command().arg("init-template-dir").arg("-c").arg(context.work_dir().join(PRE_COMMIT_CONFIG_YAML)).arg(".git"), @r"
success: true
exit_code: 0
----- stdout -----
Overwriting existing hook at `.git/hooks/pre-commit`
prek installed at `.git/hooks/pre-commit` with specified config `[TEMP_DIR]/.pre-commit-config.yaml`
prek installed at `.git/hooks/commit-msg` with specified config `[TEMP_DIR]/.pre-commit-config.yaml`
prek installed at `.git/hooks/pre-push` with specified config `[TEMP_DIR]/.pre-commit-config.yaml`
----- stderr -----
warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir '.git'`
");
}
#[test]
fn workspace_install() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
let config = indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c 'print("test")'
"#};
context.setup_workspace(
&[
"project2",
"project3",
"nested/project4",
"project3/project5",
],
config,
)?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.install(), @r"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
");
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
"#);
}
);
cmd_snapshot!(context.filters(), context.install().current_dir(context.work_dir().join("project3")), @r"
success: true
exit_code: 0
----- stdout -----
prek installed at `../.git/hooks/pre-commit` for workspace `[TEMP_DIR]/project3`
hint: this hook installed for `[TEMP_DIR]/project3` only; run `prek install` from `[TEMP_DIR]/` to install for the entire repo.
----- stderr -----
");
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --cd="project3" -- "$@"
"#);
}
);
cmd_snapshot!(context.filters(), context.install().arg("project3/").arg("--skip").arg("project2/"), @r"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
");
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 project3/ --skip=project2/ --hook-type=pre-commit -- "$@"
"#);
}
);
cmd_snapshot!(context.filters(), context.install().arg(":"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid selector: `:`
caused by: hook ID part is empty
");
cmd_snapshot!(context.filters(), context.install().arg("project3/").env(EnvVars::SKIP, "project5/"), @r"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
warning: Skip selectors from environment variables `SKIP` are ignored during installing hooks.
");
insta::with_settings!(
{ filters => context.filters() },
{
assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 project3/ --hook-type=pre-commit -- "$@"
"#);
}
);
Ok(())
}
#[test]
fn workspace_install_hooks() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
let config = indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c 'print("test")'
"#};
context.setup_workspace(
&[
"project2",
"project3",
"nested/project4",
"project3/project5",
],
config,
)?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.prepare_hooks().arg("project3").arg("--skip").arg("project3/project5/"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
cmd_snapshot!(context.filters(), context.prepare_hooks(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 1);
Ok(())
}
#[test]
fn workspace_install_only_root_hook_types() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
let root_config = indoc! {r#"
default_install_hook_types: [pre-commit, post-commit]
repos:
- repo: local
hooks:
- id: root-hook
name: Root Hook
language: python
entry: python -c 'print("root")'
"#};
let nested_config = indoc! {r#"
default_install_hook_types: [pre-push, post-merge]
repos:
- repo: local
hooks:
- id: nested-hook
name: Nested Hook
language: python
entry: python -c 'print("nested")'
"#};
context
.work_dir()
.child(PRE_COMMIT_CONFIG_YAML)
.write_str(root_config)?;
context.work_dir().child("project2").create_dir_all()?;
context
.work_dir()
.child("project2")
.child(PRE_COMMIT_CONFIG_YAML)
.write_str(nested_config)?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.install(), @r"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
prek installed at `.git/hooks/post-commit`
----- stderr -----
");
assert!(context.work_dir().join(".git/hooks/pre-commit").exists());
assert!(context.work_dir().join(".git/hooks/post-commit").exists());
assert!(!context.work_dir().join(".git/hooks/pre-push").exists());
assert!(!context.work_dir().join(".git/hooks/post-merge").exists());
Ok(())
}
#[test]
fn workspace_uninstall() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
let config = indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c 'print("test")'
"#};
context.setup_workspace(
&[
"project2",
"project3",
"nested/project4",
"project3/project5",
],
config,
)?;
context.git_add(".");
context.install().assert().success();
cmd_snapshot!(context.filters(), context.uninstall(), @r"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
----- stderr -----
");
assert!(!context.work_dir().join(".git/hooks/pre-commit").exists());
Ok(())
}
#[test]
fn workspace_init_template_dir() -> anyhow::Result<()> {
let context = TestContext::new();
context.init_project();
let config = indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c "print('test')"
"#};
context.setup_workspace(
&[
"project2",
"project3",
"nested/project4",
"project3/project5",
],
config,
)?;
context.git_add(".");
let template_dir = context.work_dir().child("template");
template_dir.create_dir_all()?;
cmd_snapshot!(context.filters(), context.command().arg("init-template-dir").arg(&*template_dir), @r"
success: true
exit_code: 0
----- stdout -----
prek installed at `template/hooks/pre-commit`
----- stderr -----
warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir '[TEMP_DIR]/template'`
");
assert!(template_dir.join("hooks/pre-commit").exists());
let filters = context.filters();
insta::with_settings!(
{ filters => filters.clone() },
{
insta::assert_snapshot!(context.read("template/hooks/pre-commit"), @r#"
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688
HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="[CURRENT_EXE]"
# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
PREK="prek"
fi
exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --skip-on-missing-config -- "$@"
"#);
}
);
Ok(())
}
#[test]
fn install_invalid_config_warning() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
hooks:
- id: trailing-whitespace
"});
cmd_snapshot!(context.filters(), context.install(), @"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
warning: Failed to parse `.pre-commit-config.yaml`: error: line 3 column 5: missing field `rev`
--> <input>:3:5
|
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | hooks:
| ^ missing field `rev`
4 | - id: trailing-whitespace
|
");
}