use crate::common::{TestEnv, cmd_snapshot, snapshot};
use assert_cmd::assert::OutputAssertExt;
use assert_fs::assert::PathAssert;
use assert_fs::fixture::{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() {
let context = TestEnv::new().init_git();
cmd_snapshot!(context, context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
"#);
snapshot!(context, 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.write_file(".git/hooks/pre-commit", "#!/bin/sh\necho 'pre-commit'\n");
cmd_snapshot!(context, 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 `--force` to remove legacy hooks.
Installed Git hook at `.git/hooks/pre-commit`
Installed Git hook at `.git/hooks/post-commit`
----- stderr -----
"#);
snapshot!(context, 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'
"##);
snapshot!(context, 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, 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`
Installed Git hook at `.git/hooks/pre-commit`
Overwriting existing hook at `.git/hooks/post-commit`
Installed Git hook at `.git/hooks/post-commit`
----- stderr -----
"#);
snapshot!(context, 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 -- "$@"
"#);
snapshot!(context, 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 -- "$@"
"#);
}
#[test]
fn install_with_git_dir() {
let context = TestEnv::new().init_git();
cmd_snapshot!(context, context.install().arg("--git-dir").arg("custom-git-dir"), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `custom-git-dir/hooks/pre-commit`
----- stderr -----
"#);
context
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context
.child("custom-git-dir/hooks/pre-commit")
.assert(predicates::path::exists());
snapshot!(context, 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 = TestEnv::new().init_git();
context
.git()
.run(["config", "core.hooksPath", "custom-hooks"]);
cmd_snapshot!(context, context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `custom-hooks/pre-commit`
----- stderr -----
"#);
context
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context
.child("custom-hooks/pre-commit")
.assert(predicates::path::exists());
snapshot!(context, 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 = TestEnv::new().init_git();
let global_gitconfig = context.work_dir().join("global.gitconfig");
context
.git()
.command()
.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, 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: To install into this repository's default hooks directory anyway, rerun with:
prek install --force
Otherwise, remove the global/system setting or move `core.hooksPath` into repo scope:
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, install, @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
"#);
}
#[test]
fn install_force_uses_repository_hooks_with_external_hooks_path_set() {
let context = TestEnv::new()
.with_file("custom-hooks/pre-commit", "#!/bin/sh\necho global hook\n")
.init_git();
let global_gitconfig = context.work_dir().join("global.gitconfig");
context
.git()
.command()
.env("GIT_CONFIG_GLOBAL", &global_gitconfig)
.args(["config", "--global", "core.hooksPath", "custom-hooks"])
.assert()
.success();
let mut install = context.install();
install
.arg("-f")
.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
cmd_snapshot!(context, install, @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
warning: `core.hooksPath` is configured outside this repository. Installing Git shims to `.git/hooks` because `--force` was used.
"#);
context
.child(".git/hooks/pre-commit")
.assert(predicates::path::exists());
assert_eq!(
context.read("custom-hooks/pre-commit"),
"#!/bin/sh\necho global hook\n"
);
}
#[test]
fn install_refuses_empty_external_hooks_path_set() {
let context = TestEnv::new().init_git();
let global_gitconfig = context.work_dir().join("global.gitconfig");
context
.git()
.command()
.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, 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: To install into this repository's default hooks directory anyway, rerun with:
prek install --force
Otherwise, remove the global/system setting or move `core.hooksPath` into repo scope:
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 = TestEnv::new().init_git();
context.git().run(["config", "core.hooksPath", ""]);
cmd_snapshot!(context, 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 = TestEnv::new().init_git();
context.git().run(["config", "core.hooksPath", "."]);
context.install().assert().success();
context
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context
.child("pre-commit")
.assert(predicates::path::exists());
}
#[test]
fn install_with_included_local_hooks_path_installs_to_configured_directory() {
let context = TestEnv::new()
.with_file(
"included-hooks.cfg",
indoc! {r"
[core]
hooksPath = custom-hooks
"},
)
.init_git();
context
.git()
.run(["config", "--local", "include.path", "../included-hooks.cfg"]);
context.install().assert().success();
context
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context
.child("custom-hooks/pre-commit")
.assert(predicates::path::exists());
}
#[test]
fn install_with_worktree_hooks_path_installs_to_configured_directory() -> anyhow::Result<()> {
let context = TestEnv::new().with_file("README.md", "hello\n").init_git();
context.git().commit("Initial commit");
context
.git()
.run(["config", "extensions.worktreeConfig", "true"])
.run(["worktree", "add", "worktree", "HEAD"]);
let worktree = context.child("worktree");
let output = context
.git_at(&worktree)
.command()
.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();
context
.git_at(&worktree)
.command()
.args([
"config",
"--worktree",
"core.hooksPath",
&worktree_hooks_str,
])
.assert()
.success();
let mut install = context.install();
install.current_dir(&worktree);
install.assert().success();
context
.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 = TestEnv::new().init_git();
context.install().assert().success();
let hook_path = context.work_dir().join(".git/hooks/pre-commit");
let metadata = fs_err::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 = TestEnv::new().init_git();
context
.git()
.run(["config", "core.sharedRepository", "group"]);
context.install().assert().success();
let hook_path = context.work_dir().join(".git/hooks/pre-commit");
let metadata = fs_err::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 = TestEnv::new().init_git();
context
.git()
.run(["config", "core.sharedRepository", "0640"]);
context.install().assert().success();
let hook_path = context.work_dir().join(".git/hooks/pre-commit");
let metadata = fs_err::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 = TestEnv::new()
.with_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
"})
.init_git();
context
.home_dir()
.child("repos")
.assert(predicates::path::missing());
context
.home_dir()
.child("hooks")
.assert(predicates::path::missing());
cmd_snapshot!(context, context.install().arg("--prepare-hooks"), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook 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);
snapshot!(context, 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 = TestEnv::new()
.with_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c 'print("test")'
"#})
.init_git();
context
.home_dir()
.child("hooks")
.assert(predicates::path::missing());
cmd_snapshot!(context, context.install().arg("--install-hooks"), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook 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() {
let context = TestEnv::new().init_git();
context.install().assert().success();
context.write_file(".git/hooks/pre-commit.legacy", "#!/bin/sh\necho 'legacy'\n");
cmd_snapshot!(context, context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
Migration mode: prek will also run legacy hook `.git/hooks/pre-commit.legacy`. Use `--force` to remove legacy hooks.
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
"#);
context
.child(".git/hooks/pre-commit.legacy")
.assert(predicates::path::exists());
cmd_snapshot!(context, context.install().arg("--overwrite"), @r#"
success: true
exit_code: 0
----- stdout -----
Overwriting existing hook at `.git/hooks/pre-commit`
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
"#);
context
.child(".git/hooks/pre-commit.legacy")
.assert(predicates::path::missing());
}
#[test]
fn install_hooks_only() -> anyhow::Result<()> {
let context = TestEnv::new()
.with_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
"})
.init_git();
context
.home_dir()
.child("repos")
.assert(predicates::path::missing());
context
.home_dir()
.child("hooks")
.assert(predicates::path::missing());
cmd_snapshot!(context, 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
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
Ok(())
}
#[test]
fn install_with_legacy_install_hooks_subcommand_alias() -> anyhow::Result<()> {
let context = TestEnv::new()
.with_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c 'print("test")'
"#})
.init_git();
context
.home_dir()
.child("hooks")
.assert(predicates::path::missing());
cmd_snapshot!(context,
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() {
let context = TestEnv::new().init_git();
cmd_snapshot!(context, 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, context.uninstall(), @r#"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
----- stderr -----
"#);
context
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context.write_file(".git/hooks/pre-commit", "#!/bin/sh\necho 'pre-commit'\n");
cmd_snapshot!(context, 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, 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, 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 -----
");
}
#[test]
fn uninstall_with_local_hooks_path_removes_configured_hook() {
let context = TestEnv::new().init_git();
context
.git()
.run(["config", "core.hooksPath", "custom-hooks"]);
context.install().assert().success();
cmd_snapshot!(context, context.uninstall(), @r#"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
----- stderr -----
"#);
context
.child("custom-hooks/pre-commit")
.assert(predicates::path::missing());
}
#[test]
fn uninstall_with_worktree_hooks_path_removes_configured_hook() -> anyhow::Result<()> {
let context = TestEnv::new().with_file("README.md", "hello\n").init_git();
context.git().commit("Initial commit");
context
.git()
.run(["config", "extensions.worktreeConfig", "true"])
.run(["worktree", "add", "worktree", "HEAD"]);
let worktree = context.child("worktree");
let output = context
.git_at(&worktree)
.command()
.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();
context
.git_at(&worktree)
.command()
.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, 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
.child(".git/hooks/pre-commit")
.assert(predicates::path::exists());
Ok(())
}
#[test]
fn uninstall_refuses_external_hooks_path_set() {
let context = TestEnv::new().init_git();
let global_gitconfig = context.work_dir().join("global.gitconfig");
context
.git()
.command()
.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, 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 = TestEnv::new().init_git();
let global_gitconfig = context.work_dir().join("global.gitconfig");
context
.git()
.command()
.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, uninstall, @r#"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
----- stderr -----
"#);
context
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
}
#[test]
fn uninstall_refuses_empty_external_hooks_path_set() {
let context = TestEnv::new().init_git();
let global_gitconfig = context.work_dir().join("global.gitconfig");
context
.git()
.command()
.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, 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 = TestEnv::new().init_git();
context.git().run(["config", "core.hooksPath", ""]);
cmd_snapshot!(context, 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 = TestEnv::new().init_git();
context.git().run(["config", "core.hooksPath", "."]);
context.install().assert().success();
context.uninstall().assert().success();
context
.child("pre-commit")
.assert(predicates::path::missing());
}
#[test]
fn uninstall_with_included_local_hooks_path_removes_configured_hook() {
let context = TestEnv::new()
.with_file(
"included-hooks.cfg",
indoc! {r"
[core]
hooksPath = custom-hooks
"},
)
.init_git();
context
.git()
.run(["config", "--local", "include.path", "../included-hooks.cfg"]);
context.install().assert().success();
context.uninstall().assert().success();
context
.child("custom-hooks/pre-commit")
.assert(predicates::path::missing());
}
#[test]
fn uninstall_all_managed_hooks() {
let context = TestEnv::new().init_git();
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.write_file(".git/hooks/pre-commit", custom_hook);
cmd_snapshot!(context, 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());
}
#[test]
fn uninstall_remove_legacy_hook() -> anyhow::Result<()> {
let context = TestEnv::new().init_git();
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, context.uninstall(), @"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
----- stderr -----
Found legacy hook at `.git/hooks/pre-commit.legacy`, removing it.
");
context
.child(".git/hooks/pre-commit")
.assert(predicates::path::missing());
context
.child(".git/hooks/pre-commit.legacy")
.assert(predicates::path::missing());
context.install().assert().success();
context.write_file(".git/hooks/pre-commit.legacy", "#!/bin/sh\necho 'legacy'\n");
cmd_snapshot!(context, context.uninstall(), @"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
Restored `.git/hooks/pre-commit.legacy` to `.git/hooks/pre-commit`
----- stderr -----
");
context
.child(".git/hooks/pre-commit")
.assert(predicates::path::exists());
context
.child(".git/hooks/pre-commit.legacy")
.assert(predicates::path::missing());
Ok(())
}
#[test]
fn init_templatedir() -> anyhow::Result<()> {
let context = TestEnv::new().init_git();
cmd_snapshot!(context, context.command().arg("init-templatedir").arg(".git"), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
warning: `init.templateDir` does not point to the target directory. Run `git config --global init.templateDir '.git'` to set it
"#);
snapshot!(context, 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.child("subdir");
child.create_dir_all()?;
cmd_snapshot!(context, context.command().arg("init-templatedir").arg("temp-dir").current_dir(child), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `temp-dir/hooks/pre-commit`
----- stderr -----
warning: `init.templateDir` does not point to the target directory. Run `git config --global init.templateDir 'temp-dir'` to set it
"#);
snapshot!(context, 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, context.command().arg("init-templatedir").arg("-c").arg("non-exist-config").arg("subdir2"), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `subdir2/hooks/pre-commit` with specified config `non-exist-config`
----- stderr -----
warning: `init.templateDir` does not point to the target directory. Run `git config --global init.templateDir 'subdir2'` to set it
"#);
snapshot!(context, 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 = TestEnv::new().init_git();
cmd_snapshot!(context, context.command().arg("util").arg("init-template-dir").arg(".git"), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
warning: `init.templateDir` does not point to the target directory. Run `git config --global init.templateDir '.git'` to set it
"#);
snapshot!(context, 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 = TestEnv::new();
cmd_snapshot!(context, context.command().arg("util").arg("init-template-dir").arg(".git"), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
warning: `init.templateDir` does not point to the target directory. Run `git config --global init.templateDir '.git'` to set it
"#);
context.write_config(
"
default_install_hook_types:
- pre-commit
- commit-msg
- pre-push
repos:
",
);
cmd_snapshot!(context, context.command().arg("util").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`
Installed Git hook at `.git/hooks/pre-commit` with specified config `[TEMP_DIR]/.pre-commit-config.yaml`
Installed Git hook at `.git/hooks/commit-msg` with specified config `[TEMP_DIR]/.pre-commit-config.yaml`
Installed Git hook at `.git/hooks/pre-push` with specified config `[TEMP_DIR]/.pre-commit-config.yaml`
----- stderr -----
warning: `init.templateDir` does not point to the target directory. Run `git config --global init.templateDir '.git'` to set it
"#);
}
#[test]
fn workspace_install() {
let context = TestEnv::new().init_git();
let config = indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c 'print("test")'
"#};
context.write_workspace(
[
"project2",
"project3",
"nested/project4",
"project3/project5",
],
config,
);
context.git().add(".");
cmd_snapshot!(context, context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
"#);
snapshot!(context, 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, context.install().current_dir(context.work_dir().join("project3")), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook 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 -----
"#);
snapshot!(context, 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, context.install().arg("project3/").arg("--skip").arg("project2/"), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
"#);
snapshot!(context, 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, context.install().arg(":"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid selector: `:`
caused by: hook ID part is empty
");
cmd_snapshot!(context, context.install().arg("project3/").env(EnvVars::SKIP, "project5/"), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
warning: Skip selectors from environment variables `SKIP` are ignored during installing hooks.
"#);
snapshot!(context, 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 -- "$@"
"#);
}
#[test]
fn workspace_install_hooks() -> anyhow::Result<()> {
let context = TestEnv::new().init_git();
let config = indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c 'print("test")'
"#};
context.write_workspace(
[
"project2",
"project3",
"nested/project4",
"project3/project5",
],
config,
);
context.git().add(".");
cmd_snapshot!(context, context.prepare_hooks().arg("project3").arg("--skip").arg("project3/project5/"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
cmd_snapshot!(context, 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() {
let context = TestEnv::new().init_git();
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")'
"#};
let context = context
.with_file(PRE_COMMIT_CONFIG_YAML, root_config)
.with_file("project2/.pre-commit-config.yaml", nested_config);
context.git().add(".");
cmd_snapshot!(context, context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
Installed Git hook 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());
}
#[test]
fn workspace_uninstall() {
let context = TestEnv::new().init_git();
let config = indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c 'print("test")'
"#};
context.write_workspace(
[
"project2",
"project3",
"nested/project4",
"project3/project5",
],
config,
);
context.git().add(".");
context.install().assert().success();
cmd_snapshot!(context, context.uninstall(), @r"
success: true
exit_code: 0
----- stdout -----
Uninstalled `pre-commit`
----- stderr -----
");
assert!(!context.work_dir().join(".git/hooks/pre-commit").exists());
}
#[test]
fn workspace_init_template_dir() -> anyhow::Result<()> {
let context = TestEnv::new().init_git();
let config = indoc! {r#"
repos:
- repo: local
hooks:
- id: test-hook
name: Test Hook
language: python
entry: python -c "print('test')"
"#};
context.write_workspace(
[
"project2",
"project3",
"nested/project4",
"project3/project5",
],
config,
);
context.git().add(".");
let template_dir = context.child("template");
template_dir.create_dir_all()?;
cmd_snapshot!(context, context.command().arg("util").arg("init-template-dir").arg(&*template_dir), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `template/hooks/pre-commit`
----- stderr -----
warning: `init.templateDir` does not point to the target directory. Run `git config --global init.templateDir '[TEMP_DIR]/template'` to set it
"#);
assert!(template_dir.join("hooks/pre-commit").exists());
snapshot!(context, 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 = TestEnv::new().init_git();
context.write_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
hooks:
- id: trailing-whitespace
"});
cmd_snapshot!(context, context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook 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
|
"#);
}