use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use insta::assert_snapshot;
use prek_consts::PRE_COMMIT_CONFIG_YAML;
use prek_consts::env_vars::EnvVars;
use crate::common::{TestEnv, cmd_snapshot};
#[test]
fn run_in_non_git_repo() {
let context = TestEnv::new();
cmd_snapshot!(context, context.run().env(EnvVars::LC_ALL, "fr_FR.UTF-8"), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Not in a Git repository. Change to a Git repository, or run `git init` to create one.
"#);
}
#[test]
fn run_preserves_git_discovery_errors() {
let context = TestEnv::new()
.with_config("repos: []")
.with_filter(
r"Command `[^`]*git(?:\.exe)? rev-parse --absolute-git-dir --git-common-dir --git-path hooks --show-toplevel`",
"Command `[GIT] rev-parse --absolute-git-dir --git-common-dir --git-path hooks --show-toplevel`",
)
.init_git()
.with_file("invalid.gitconfig", "[invalid\n");
cmd_snapshot!(context, context.run().env("GIT_CONFIG_GLOBAL", context.child("invalid.gitconfig").path()), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Command `[GIT] rev-parse --absolute-git-dir --git-common-dir --git-path hooks --show-toplevel` exited with an error:
[status]
exit status: 128
[stderr]
fatal: bad config line 1 in file [TEMP_DIR]/invalid.gitconfig
"#);
cmd_snapshot!(context, context.run().env(EnvVars::GIT_DIR, "missing"), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Command `[GIT] rev-parse --absolute-git-dir --git-common-dir --git-path hooks --show-toplevel` exited with an error:
[status]
exit status: 128
[stderr]
fatal: not a git repository: 'missing'
"#);
}
#[test]
fn staged_files_only() {
let context = TestEnv::new()
.with_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]
"#})
.with_file("file.txt", "Hello, world!")
.init_git();
context.write_file("file.txt", "Hello world again!");
cmd_snapshot!(context, 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!");
}
#[test]
fn intent_to_add_file_survives_conflicted_stash_restore() -> Result<()> {
let context = TestEnv::new()
.with_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$
"#})
.init_git();
context.git().add(PRE_COMMIT_CONFIG_YAML);
context.write_file("intent.txt", "preserve me\n");
context
.git()
.command()
.arg("add")
.arg("--intent-to-add")
.arg("intent.txt")
.assert()
.success();
context.write_file("test.py", "a=1\n");
context.git().add("test.py");
context.write_file("test.py", "a=1\nb = 2\n");
cmd_snapshot!(context, 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 = context
.git()
.command()
.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(())
}
#[test]
fn restore_intent_and_unstaged_changes_from_subdirectory() {
let context = TestEnv::new()
.with_file(
format!("project/{PRE_COMMIT_CONFIG_YAML}"),
indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: check-staged
name: check staged
language: system
entry: python3 -c 'assert open("tracked.txt").read() == "staged\n"'
files: ^tracked\.txt$
"#},
)
.with_file("project/tracked.txt", "staged\n")
.with_file("project/nested/.gitkeep", "")
.init_git();
context.git().run(["config", "diff.relative", "true"]);
context.write_file("project/tracked.txt", "unstaged\n");
context.write_file("project/intent.txt", "intent\n");
context.write_file("outside.txt", "outside\n");
context.git().run(["add", "--intent-to-add", "."]);
cmd_snapshot!(context, context.run().current_dir(context.child("project/nested")), @r#"
success: true
exit_code: 0
----- stdout -----
check staged.............................................................Passed
----- stderr -----
Unstaged changes detected. Temporarily saving them to `[HOME]/patches/[TIME]-[PID].patch`
Restored unstaged changes from `[HOME]/patches/[TIME]-[PID].patch`
"#);
assert_eq!(context.read("project/tracked.txt"), "unstaged\n");
assert_eq!(context.read("project/intent.txt"), "intent\n");
assert_eq!(context.read("outside.txt"), "outside\n");
cmd_snapshot!(context, context.git().command().args(["diff", "--name-only", "--diff-filter=A"]), @r#"
success: true
exit_code: 0
----- stdout -----
outside.txt
project/intent.txt
----- stderr -----
"#);
}
#[cfg(unix)]
#[test]
fn restore_on_interrupt() -> Result<()> {
let context = TestEnv::new()
.with_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]
"#})
.with_file("file.txt", "Hello, world!")
.init_git();
context.write_file("file.txt", "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() {
let context = TestEnv::new()
.with_file("file.txt", "Hello, world!")
.init_git();
context.git().commit("Initial commit");
context.git().branch("feature").checkout("feature");
context.write_file("file.txt", "Hello, world again!");
context.git().add(".").commit("Feature commit");
context.git().checkout("master");
context.write_file("file.txt", "Hello, world from master!");
context.git().add(".").commit("Master commit");
context
.git()
.command()
.arg("merge")
.arg("feature")
.assert()
.code(1);
context.write_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, 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, 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 -----
");
}
#[test]
fn run_last_commit() {
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
- id: end-of-file-fixer
"})
.with_file("file1.txt", "Hello, world!\n")
.with_file("file2.txt", "Initial content with trailing spaces \n")
.init_git();
context.git().commit("Initial commit");
context.write_file("file1.txt", "Hello, world! \n"); context.write_file("file3.txt", "New file"); context.git().add(".").commit("Second commit with issues");
cmd_snapshot!(context, context.run().arg("--last-commit"), @r"
success: false
exit_code: 1
----- stdout -----
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- description: trims 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
- description: ensures that a file is either empty, or ends with one newline
- exit code: 1
- files were modified by this hook
Fixing file3.txt
----- stderr -----
");
context.write_file("file1.txt", "Hello, world! \n"); context.write_file("file3.txt", "New file");
cmd_snapshot!(context, context.run().arg("--all-files"), @r"
success: false
exit_code: 1
----- stdout -----
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- description: trims 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
- description: ensures that a file is either empty, or ends with one newline
- exit code: 1
- files were modified by this hook
Fixing file3.txt
----- stderr -----
");
}
#[test]
fn git_commit_a() {
let context = TestEnv::new()
.with_filter("7c8398204bbc95c33a6d2543f86a27621647cf78", "[HASH]")
.with_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: echo
name: echo
language: system
entry: echo
verbose: true
"})
.with_file("file.txt", "Hello, world!\n")
.init_git();
cmd_snapshot!(context, context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
"#);
context.git().add(".").commit("Initial commit");
context.write_file("file.txt", "Hello, world again!\n");
let mut commit = context.git().command();
commit.arg("commit").arg("-a").arg("-m").arg("Update file");
cmd_snapshot!(context, 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
");
}
#[cfg(unix)]
#[test]
fn git_commit_a_currently_fails_when_hook_writes_to_temp_git_index() {
let context = TestEnv::new()
.with_filter(
r"invalid object 100644 [0-9a-f]{40}",
"invalid object 100644 [HASH]",
)
.with_file(
"hook.sh",
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
"#},
)
.with_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
"})
.with_file("file.txt", "Hello, world!\n")
.init_git();
cmd_snapshot!(context, context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
Installed Git hook at `.git/hooks/pre-commit`
----- stderr -----
"#);
context.git().add(".").commit("Initial commit");
context.write_file("file.txt", "Hello again!\n");
let mut commit = context.git().command();
commit.arg("commit").arg("-a").arg("-m").arg("Update file");
cmd_snapshot!(context, 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
"
);
}
#[test]
fn run_with_tree_object_as_ref() {
let context = TestEnv::new()
.with_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: echo-files
name: echo files
entry: echo
language: system
pass_filenames: true
"})
.with_file("file1.txt", "hello")
.init_git();
context.git().commit("Initial commit");
context.write_file("file2.txt", "world");
context.git().add("file2.txt");
let tree_output = context
.git()
.command()
.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, context.run()
.arg("--from-ref").arg("HEAD")
.arg("--to-ref").arg(&tree_sha), @r"
success: true
exit_code: 0
----- stdout -----
echo files...............................................................Passed
----- stderr -----
");
}