mirror of
https://github.com/j178/prek.git
synced 2026-04-03 17:34:03 +02:00
* feat: Implement check-executables-have-shebangs hook * Tweak * Minor * Tweak * Minor * feat: Implement check-executables-have-shebangs hook * Fix paths * Debug * Debug * Fix ls-files split * Update docs --------- Co-authored-by: Felix Blom <70511386+Felix-Blom@users.noreply.github.com> Co-authored-by: Jo <10510431+j178@users.noreply.github.com>
1856 lines
55 KiB
Rust
1856 lines
55 KiB
Rust
#[cfg(unix)]
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
use anyhow::Result;
|
|
use assert_fs::prelude::*;
|
|
use constants::CONFIG_FILE;
|
|
use insta::assert_snapshot;
|
|
|
|
use crate::common::{TestContext, cmd_snapshot};
|
|
|
|
mod common;
|
|
|
|
#[test]
|
|
fn end_of_file_fixer_hook() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: end-of-file-fixer
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files
|
|
cwd.child("correct_lf.txt").write_str("Hello World\n")?;
|
|
cwd.child("correct_crlf.txt").write_str("Hello World\r\n")?;
|
|
cwd.child("no_newline.txt")
|
|
.write_str("No trailing newline")?;
|
|
cwd.child("multiple_lf.txt")
|
|
.write_str("Multiple newlines\n\n\n")?;
|
|
cwd.child("multiple_crlf.txt")
|
|
.write_str("Multiple newlines\r\n\r\n")?;
|
|
cwd.child("empty.txt").touch()?;
|
|
cwd.child("only_newlines.txt").write_str("\n\n")?;
|
|
cwd.child("only_win_newlines.txt").write_str("\r\n\r\n")?;
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: hooks should fail and fix the files
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
fix end of files.........................................................Failed
|
|
- hook id: end-of-file-fixer
|
|
- exit code: 1
|
|
- files were modified by this hook
|
|
Fixing multiple_crlf.txt
|
|
Fixing only_newlines.txt
|
|
Fixing only_win_newlines.txt
|
|
Fixing no_newline.txt
|
|
Fixing multiple_lf.txt
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Assert that the files have been corrected
|
|
assert_snapshot!(context.read("correct_lf.txt"), @"Hello World\n");
|
|
assert_snapshot!(context.read("correct_crlf.txt"), @"Hello World\n");
|
|
assert_snapshot!(context.read("no_newline.txt"), @"No trailing newline\n");
|
|
assert_snapshot!(context.read("multiple_lf.txt"), @"Multiple newlines\n");
|
|
assert_snapshot!(context.read("multiple_crlf.txt"), @"Multiple newlines\n");
|
|
assert_snapshot!(context.read("empty.txt"), @"");
|
|
assert_snapshot!(context.read("only_newlines.txt"), @"\n");
|
|
assert_snapshot!(context.read("only_win_newlines.txt"), @"\n");
|
|
|
|
context.git_add(".");
|
|
|
|
// Second run: hooks should now pass. The output will be stable.
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
fix end of files.........................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn check_yaml_hook() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-yaml
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files
|
|
cwd.child("valid.yaml").write_str("a: 1")?;
|
|
cwd.child("invalid.yaml").write_str("a: b: c")?;
|
|
cwd.child("duplicate.yaml").write_str("a: 1\na: 2")?;
|
|
cwd.child("empty.yaml").touch()?;
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: hooks should fail
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check yaml...............................................................Failed
|
|
- hook id: check-yaml
|
|
- exit code: 1
|
|
duplicate.yaml: Failed to yaml decode (duplicate entry with key "a")
|
|
invalid.yaml: Failed to yaml decode (mapping values are not allowed in this context at line 1 column 5)
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Fix the files
|
|
cwd.child("invalid.yaml").write_str("a:\n b: c")?;
|
|
cwd.child("duplicate.yaml").write_str("a: 1\nb: 2")?;
|
|
|
|
context.git_add(".");
|
|
|
|
// Second run: hooks should now pass
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
check yaml...............................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// `--allow-multiple-documents` feature is not implemented in Rust,
|
|
/// it should work by delegating to the original Python implementation.
|
|
#[test]
|
|
fn check_yaml_multiple_document() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-yaml
|
|
name: Python version
|
|
args: [ --allow-multiple-documents ]
|
|
- id: check-yaml
|
|
name: Rust version
|
|
"});
|
|
|
|
context
|
|
.work_dir()
|
|
.child("multiple.yaml")
|
|
.write_str(indoc::indoc! {r"
|
|
---
|
|
a: 1
|
|
---
|
|
b: 2
|
|
"
|
|
})?;
|
|
|
|
context.git_add(".");
|
|
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
Python version...........................................................Passed
|
|
Rust version.............................................................Failed
|
|
- hook id: check-yaml
|
|
- exit code: 1
|
|
multiple.yaml: Failed to yaml decode (deserializing from YAML containing more than one document is not supported)
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn check_json_hook() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-json
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files
|
|
cwd.child("valid.json").write_str(r#"{"a": 1}"#)?;
|
|
cwd.child("invalid.json").write_str(r#"{"a": 1,}"#)?;
|
|
cwd.child("duplicate.json")
|
|
.write_str(r#"{"a": 1, "a": 2}"#)?;
|
|
cwd.child("empty.json").touch()?;
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: hooks should fail
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check json...............................................................Failed
|
|
- hook id: check-json
|
|
- exit code: 1
|
|
duplicate.json: Failed to json decode (duplicate key `a` at line 1 column 12)
|
|
invalid.json: Failed to json decode (trailing comma at line 1 column 9)
|
|
|
|
----- stderr -----
|
|
");
|
|
|
|
// Fix the files
|
|
cwd.child("invalid.json").write_str(r#"{"a": 1}"#)?;
|
|
cwd.child("duplicate.json")
|
|
.write_str(r#"{"a": 1, "b": 2}"#)?;
|
|
|
|
context.git_add(".");
|
|
|
|
// Second run: hooks should now pass
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
check json...............................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_line_ending_hook() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
context.disable_auto_crlf();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: mixed-line-ending
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files
|
|
cwd.child("mixed.txt")
|
|
.write_str("line1\nline2\r\nline3\r\n")?;
|
|
cwd.child("only_lf.txt").write_str("line1\nline2\n")?;
|
|
cwd.child("only_crlf.txt").write_str("line1\r\nline2\r\n")?;
|
|
cwd.child("no_endings.txt").write_str("hello world")?;
|
|
cwd.child("empty.txt").touch()?;
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: hooks should fail and fix the files
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
mixed line ending........................................................Failed
|
|
- hook id: mixed-line-ending
|
|
- exit code: 1
|
|
- files were modified by this hook
|
|
Fixing mixed.txt
|
|
|
|
----- stderr -----
|
|
");
|
|
|
|
// Assert that the files have been corrected
|
|
assert_snapshot!(context.read("mixed.txt"), @"line1\r\nline2\r\nline3\r\n");
|
|
assert_snapshot!(context.read("only_lf.txt"), @"line1\nline2\n");
|
|
assert_snapshot!(context.read("only_crlf.txt"), @"line1\r\nline2\r\n");
|
|
|
|
context.git_add(".");
|
|
|
|
// Second run: hooks should now pass.
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
mixed line ending........................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Test with --fix=no
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: mixed-line-ending
|
|
args: ['--fix=no']
|
|
"});
|
|
context
|
|
.work_dir()
|
|
.child("mixed.txt")
|
|
.write_str("line1\nline2\r\n")?;
|
|
context.git_add(".");
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
mixed line ending........................................................Failed
|
|
- hook id: mixed-line-ending
|
|
- exit code: 1
|
|
mixed.txt: mixed line endings
|
|
|
|
----- stderr -----
|
|
"#);
|
|
assert_snapshot!(context.read("mixed.txt"), @"line1\nline2\r\n");
|
|
|
|
// Test with --fix=crlf
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: mixed-line-ending
|
|
args: ['--fix', 'crlf']
|
|
"});
|
|
context
|
|
.work_dir()
|
|
.child("mixed.txt")
|
|
.write_str("line1\nline2\r\n")?;
|
|
context.git_add(".");
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
mixed line ending........................................................Failed
|
|
- hook id: mixed-line-ending
|
|
- exit code: 1
|
|
- files were modified by this hook
|
|
Fixing .pre-commit-config.yaml
|
|
Fixing mixed.txt
|
|
Fixing only_lf.txt
|
|
|
|
----- stderr -----
|
|
");
|
|
assert_snapshot!(context.read("mixed.txt"), @"line1\r\nline2\r\n");
|
|
|
|
// Test mixed args with missing value for `--fix`
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: mixed-line-ending
|
|
args: ['--fix']
|
|
"});
|
|
context
|
|
.work_dir()
|
|
.child("mixed.txt")
|
|
.write_str("line1\nline2\r\nline3\n")?;
|
|
context.git_add(".");
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: false
|
|
exit_code: 2
|
|
----- stdout -----
|
|
mixed line ending........................................................
|
|
----- stderr -----
|
|
error: Failed to run hook `mixed-line-ending`
|
|
caused by: error: a value is required for '--fix <FIX>' but none was supplied
|
|
[possible values: auto, no, lf, crlf, cr]
|
|
");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn check_added_large_files_hook() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
// Create an initial commit
|
|
let cwd = context.work_dir();
|
|
cwd.child("README.md").write_str("Initial commit")?;
|
|
context.git_add(".");
|
|
context.git_commit("Initial commit");
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-added-large-files
|
|
args: ['--maxkb', '1']
|
|
"});
|
|
|
|
// Create test files
|
|
cwd.child("small_file.txt").write_str("Hello World\n")?;
|
|
let large_file = cwd.child("large_file.txt");
|
|
large_file.write_binary(&[0; 2048])?; // 2KB file
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: hook should fail because of the large file
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check for added large files..............................................Failed
|
|
- hook id: check-added-large-files
|
|
- exit code: 1
|
|
large_file.txt (2 KB) exceeds 1 KB
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Commit the files
|
|
context.git_add(".");
|
|
context.git_commit("Add large file");
|
|
|
|
// Create a new unstaged large file
|
|
let unstaged_large_file = cwd.child("unstaged_large_file.txt");
|
|
unstaged_large_file.write_binary(&[0; 2048])?; // 2KB file
|
|
context.git_add("unstaged_large_file.txt");
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-added-large-files
|
|
args: ['--maxkb=1', '--enforce-all']
|
|
"});
|
|
|
|
// Second run: the hook should check all files even if not staged
|
|
cmd_snapshot!(context.filters(), context.run().arg("--all-files"), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check for added large files..............................................Failed
|
|
- hook id: check-added-large-files
|
|
- exit code: 1
|
|
unstaged_large_file.txt (2 KB) exceeds 1 KB
|
|
large_file.txt (2 KB) exceeds 1 KB
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
context.git_rm("unstaged_large_file.txt");
|
|
context.git_clean();
|
|
|
|
// Test git-lfs integration
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-added-large-files
|
|
args: ['--maxkb=1']
|
|
"});
|
|
cwd.child(".gitattributes")
|
|
.write_str("*.dat filter=lfs diff=lfs merge=lfs -text")?;
|
|
context.git_add(".gitattributes");
|
|
let lfs_file = cwd.child("lfs_file.dat");
|
|
lfs_file.write_binary(&[0; 2048])?; // 2KB file
|
|
context.git_add(".");
|
|
|
|
// Third run: hook should pass because the large file is tracked by git-lfs
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
check for added large files..............................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn builtin_hooks_workspace_mode() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
context.disable_auto_crlf();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: meta
|
|
hooks:
|
|
- id: identity
|
|
"});
|
|
|
|
// Subproject with built-in hooks.
|
|
let app = context.work_dir().child("app");
|
|
app.create_dir_all()?;
|
|
app.child(CONFIG_FILE).write_str(indoc::indoc! {r"
|
|
repos:
|
|
- repo: meta
|
|
hooks:
|
|
- id: identity
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: end-of-file-fixer
|
|
- id: check-yaml
|
|
- id: check-json
|
|
- id: mixed-line-ending
|
|
- id: trailing-whitespace
|
|
- id: check-added-large-files
|
|
args: ['--maxkb', '1']
|
|
"})?;
|
|
|
|
app.child("eof_no_newline.txt")
|
|
.write_str("No trailing newline")?;
|
|
app.child("eof_multiple_lf.txt").write_str("Multiple\n\n")?;
|
|
app.child("mixed.txt").write_str("line1\nline2\r\n")?;
|
|
app.child("trailing_ws.txt")
|
|
.write_str("line with trailing space \n")?;
|
|
app.child("correct.txt").write_str("All good here\n")?;
|
|
|
|
app.child("invalid.yaml").write_str("a: b: c")?;
|
|
app.child("duplicate.yaml").write_str("a: 1\na: 2")?;
|
|
app.child("empty.yaml").touch()?;
|
|
|
|
app.child("invalid.json").write_str(r#"{"a": 1,}"#)?;
|
|
app.child("duplicate.json")
|
|
.write_str(r#"{"a": 1, "a": 2}"#)?;
|
|
app.child("empty.json").touch()?;
|
|
|
|
// 2KB file to trigger check-added-large-files (1 KB threshold).
|
|
app.child("large.bin").write_binary(&[0u8; 2048])?;
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: expect failures and auto-fixes where applicable.
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
Running hooks for `app`:
|
|
identity.................................................................Passed
|
|
- hook id: identity
|
|
- duration: [TIME]
|
|
correct.txt
|
|
invalid.yaml
|
|
empty.json
|
|
duplicate.json
|
|
trailing_ws.txt
|
|
large.bin
|
|
eof_multiple_lf.txt
|
|
duplicate.yaml
|
|
empty.yaml
|
|
mixed.txt
|
|
invalid.json
|
|
.pre-commit-config.yaml
|
|
eof_no_newline.txt
|
|
fix end of files.........................................................Failed
|
|
- hook id: end-of-file-fixer
|
|
- exit code: 1
|
|
- files were modified by this hook
|
|
Fixing invalid.yaml
|
|
Fixing duplicate.json
|
|
Fixing eof_no_newline.txt
|
|
Fixing eof_multiple_lf.txt
|
|
Fixing duplicate.yaml
|
|
Fixing invalid.json
|
|
check yaml...............................................................Failed
|
|
- hook id: check-yaml
|
|
- exit code: 1
|
|
duplicate.yaml: Failed to yaml decode (duplicate entry with key "a")
|
|
invalid.yaml: Failed to yaml decode (mapping values are not allowed in this context at line 1 column 5)
|
|
check json...............................................................Failed
|
|
- hook id: check-json
|
|
- exit code: 1
|
|
duplicate.json: Failed to json decode (duplicate key `a` at line 1 column 12)
|
|
invalid.json: Failed to json decode (trailing comma at line 1 column 9)
|
|
mixed line ending........................................................Failed
|
|
- hook id: mixed-line-ending
|
|
- exit code: 1
|
|
- files were modified by this hook
|
|
Fixing mixed.txt
|
|
trim trailing whitespace.................................................Failed
|
|
- hook id: trailing-whitespace
|
|
- exit code: 1
|
|
- files were modified by this hook
|
|
Fixing trailing_ws.txt
|
|
check for added large files..............................................Passed
|
|
|
|
Running hooks for `.`:
|
|
identity.................................................................Passed
|
|
- hook id: identity
|
|
- duration: [TIME]
|
|
app/.pre-commit-config.yaml
|
|
app/invalid.json
|
|
app/duplicate.yaml
|
|
app/correct.txt
|
|
app/mixed.txt
|
|
app/invalid.yaml
|
|
app/empty.yaml
|
|
app/duplicate.json
|
|
app/empty.json
|
|
app/large.bin
|
|
app/eof_no_newline.txt
|
|
.pre-commit-config.yaml
|
|
app/eof_multiple_lf.txt
|
|
app/trailing_ws.txt
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Fix YAML and JSON issues, then stage.
|
|
app.child("invalid.yaml").write_str("a:\n b: c")?;
|
|
app.child("duplicate.yaml").write_str("a: 1\nb: 2")?;
|
|
app.child("invalid.json").write_str(r#"{"a": 1}"#)?;
|
|
app.child("duplicate.json")
|
|
.write_str(r#"{"a": 1, "b": 2}"#)?;
|
|
context.git_add(".");
|
|
|
|
// Second run: all hooks should pass.
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
Running hooks for `app`:
|
|
identity.................................................................Passed
|
|
- hook id: identity
|
|
- duration: [TIME]
|
|
correct.txt
|
|
invalid.yaml
|
|
empty.json
|
|
duplicate.json
|
|
trailing_ws.txt
|
|
large.bin
|
|
eof_multiple_lf.txt
|
|
duplicate.yaml
|
|
empty.yaml
|
|
mixed.txt
|
|
invalid.json
|
|
.pre-commit-config.yaml
|
|
eof_no_newline.txt
|
|
fix end of files.........................................................Failed
|
|
- hook id: end-of-file-fixer
|
|
- exit code: 1
|
|
- files were modified by this hook
|
|
Fixing invalid.yaml
|
|
Fixing duplicate.json
|
|
Fixing duplicate.yaml
|
|
Fixing invalid.json
|
|
check yaml...............................................................Passed
|
|
check json...............................................................Passed
|
|
mixed line ending........................................................Passed
|
|
trim trailing whitespace.................................................Passed
|
|
check for added large files..............................................Passed
|
|
|
|
Running hooks for `.`:
|
|
identity.................................................................Passed
|
|
- hook id: identity
|
|
- duration: [TIME]
|
|
app/.pre-commit-config.yaml
|
|
app/invalid.json
|
|
app/duplicate.yaml
|
|
app/correct.txt
|
|
app/mixed.txt
|
|
app/invalid.yaml
|
|
app/empty.yaml
|
|
app/duplicate.json
|
|
app/empty.json
|
|
app/large.bin
|
|
app/eof_no_newline.txt
|
|
.pre-commit-config.yaml
|
|
app/eof_multiple_lf.txt
|
|
app/trailing_ws.txt
|
|
|
|
----- stderr -----
|
|
");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn fix_byte_order_marker_hook() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: fix-byte-order-marker
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files
|
|
cwd.child("without_bom.txt").write_str("Hello, World!")?;
|
|
cwd.child("with_bom.txt").write_binary(&[
|
|
0xef, 0xbb, 0xbf, b'H', b'e', b'l', b'l', b'o', b',', b' ', b'W', b'o', b'r', b'l', b'd',
|
|
b'!',
|
|
])?;
|
|
cwd.child("bom_only.txt")
|
|
.write_binary(&[0xef, 0xbb, 0xbf])?;
|
|
cwd.child("empty.txt").touch()?;
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: hooks should fix files with BOM
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
fix utf-8 byte order marker..............................................Failed
|
|
- hook id: fix-byte-order-marker
|
|
- exit code: 1
|
|
- files were modified by this hook
|
|
bom_only.txt: removed byte-order marker
|
|
with_bom.txt: removed byte-order marker
|
|
|
|
----- stderr -----
|
|
");
|
|
|
|
// Verify the content is correct
|
|
assert_eq!(context.read("with_bom.txt"), "Hello, World!");
|
|
assert_eq!(context.read("bom_only.txt"), "");
|
|
assert_eq!(context.read("without_bom.txt"), "Hello, World!");
|
|
assert_eq!(context.read("empty.txt"), "");
|
|
|
|
context.git_add(".");
|
|
|
|
// Second run: all should pass now
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
fix utf-8 byte order marker..............................................Passed
|
|
|
|
----- stderr -----
|
|
");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn check_symlinks_hook_unix() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-symlinks
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files
|
|
cwd.child("regular.txt").write_str("regular file")?;
|
|
cwd.child("target.txt").write_str("target content")?;
|
|
|
|
// Create valid symlink
|
|
std::os::unix::fs::symlink(
|
|
cwd.child("target.txt").path(),
|
|
cwd.child("valid_link.txt").path(),
|
|
)?;
|
|
|
|
// Create broken symlink
|
|
std::os::unix::fs::symlink(
|
|
cwd.child("nonexistent.txt").path(),
|
|
cwd.child("broken_link.txt").path(),
|
|
)?;
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: should fail due to broken symlink
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check for broken symlinks................................................Failed
|
|
- hook id: check-symlinks
|
|
- exit code: 1
|
|
broken_link.txt: Broken symlink
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Remove broken symlink
|
|
std::fs::remove_file(cwd.child("broken_link.txt").path())?;
|
|
context.git_add(".");
|
|
|
|
// Second run: should pass
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
check for broken symlinks................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(windows)]
|
|
fn check_symlinks_hook_windows() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-symlinks
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files
|
|
cwd.child("regular.txt").write_str("regular file")?;
|
|
cwd.child("target.txt").write_str("target content")?;
|
|
|
|
// Try to create valid symlink (may fail without admin/developer mode)
|
|
let valid_link_result = std::os::windows::fs::symlink_file(
|
|
cwd.child("target.txt").path(),
|
|
cwd.child("valid_link.txt").path(),
|
|
);
|
|
|
|
// Try to create broken symlink (may fail without admin/developer mode)
|
|
let broken_link_result = std::os::windows::fs::symlink_file(
|
|
cwd.child("nonexistent.txt").path(),
|
|
cwd.child("broken_link.txt").path(),
|
|
);
|
|
|
|
// Skip test if we can't create symlinks (insufficient permissions)
|
|
if valid_link_result.is_err() || broken_link_result.is_err() {
|
|
// Skipping test: insufficient permissions for symlink creation on Windows
|
|
return Ok(());
|
|
}
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: should fail due to broken symlink
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check for broken symlinks................................................Failed
|
|
- hook id: check-symlinks
|
|
- exit code: 1
|
|
broken_link.txt: Broken symlink
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Remove broken symlink
|
|
std::fs::remove_file(cwd.child("broken_link.txt").path())?;
|
|
context.git_add(".");
|
|
|
|
// Second run: should pass
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
check for broken symlinks................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn detect_private_key_hook() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: detect-private-key
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files - various private key types
|
|
cwd.child("id_rsa")
|
|
.write_str("-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----\n")?;
|
|
cwd.child("id_dsa")
|
|
.write_str("-----BEGIN DSA PRIVATE KEY-----\nAAAAA...\n-----END DSA PRIVATE KEY-----\n")?;
|
|
cwd.child("id_ecdsa")
|
|
.write_str("-----BEGIN EC PRIVATE KEY-----\nMHc...\n-----END EC PRIVATE KEY-----\n")?;
|
|
cwd.child("id_ed25519").write_str(
|
|
"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNz...\n-----END OPENSSH PRIVATE KEY-----\n",
|
|
)?;
|
|
cwd.child("key.ppk")
|
|
.write_str("PuTTY-User-Key-File-2: ssh-rsa\nEncryption: none\n")?;
|
|
cwd.child("private.asc")
|
|
.write_str("-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: GnuPG...\n")?;
|
|
cwd.child("ta.key").write_str(
|
|
"#\n# 2048 bit OpenVPN static key\n#\n-----BEGIN OpenVPN Static key V1-----\n",
|
|
)?;
|
|
cwd.child("doc.txt").write_str(
|
|
"Some documentation\n\nHere is a key:\n-----BEGIN RSA PRIVATE KEY-----\ndata\n",
|
|
)?;
|
|
cwd.child("safe1.txt")
|
|
.write_str("This file talks about BEGIN_RSA_PRIVATE_KEY but doesn't contain one\n")?;
|
|
|
|
cwd.child("safe2.txt")
|
|
.write_str("This is just a regular file\nwith some content\n")?;
|
|
cwd.child("empty.txt").touch()?;
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: hooks should fail due to private keys
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
detect private key.......................................................Failed
|
|
- hook id: detect-private-key
|
|
- exit code: 1
|
|
Private key found: doc.txt
|
|
Private key found: id_ecdsa
|
|
Private key found: key.ppk
|
|
Private key found: id_rsa
|
|
Private key found: id_dsa
|
|
Private key found: id_ed25519
|
|
Private key found: ta.key
|
|
Private key found: private.asc
|
|
|
|
----- stderr -----
|
|
");
|
|
|
|
// Remove all private keys
|
|
context.git_rm("id_rsa");
|
|
context.git_rm("id_dsa");
|
|
context.git_rm("id_ecdsa");
|
|
context.git_rm("id_ed25519");
|
|
context.git_rm("key.ppk");
|
|
context.git_rm("private.asc");
|
|
context.git_rm("ta.key");
|
|
context.git_rm("doc.txt");
|
|
context.git_clean();
|
|
|
|
context.git_add(".");
|
|
|
|
// Second run: hooks should now pass
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
detect private key.......................................................Passed
|
|
|
|
----- stderr -----
|
|
");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn check_merge_conflict_hook() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-merge-conflict
|
|
args: ['--assume-in-merge']
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files with conflict markers
|
|
cwd.child("conflict.txt").write_str(indoc::indoc! {r"
|
|
Before conflict
|
|
<<<<<<< HEAD
|
|
Our changes
|
|
=======
|
|
Their changes
|
|
>>>>>>> branch
|
|
After conflict
|
|
"})?;
|
|
|
|
cwd.child("clean.txt").write_str("No conflicts here\n")?;
|
|
|
|
cwd.child("partial_conflict.txt")
|
|
.write_str(indoc::indoc! {r"
|
|
Some content
|
|
<<<<<<< HEAD
|
|
Conflicting line
|
|
"})?;
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: hooks should fail due to conflict markers
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check for merge conflicts................................................Failed
|
|
- hook id: check-merge-conflict
|
|
- exit code: 1
|
|
partial_conflict.txt:2: Merge conflict string "<<<<<<< " found
|
|
conflict.txt:2: Merge conflict string "<<<<<<< " found
|
|
conflict.txt:4: Merge conflict string "=======" found
|
|
conflict.txt:6: Merge conflict string ">>>>>>> " found
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Fix the files by removing conflict markers
|
|
cwd.child("conflict.txt").write_str(indoc::indoc! {r"
|
|
Before conflict
|
|
Our changes
|
|
After conflict
|
|
"})?;
|
|
|
|
cwd.child("partial_conflict.txt")
|
|
.write_str("Some content\nResolved line\n")?;
|
|
|
|
context.git_add(".");
|
|
|
|
// Second run: hooks should now pass
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
check for merge conflicts................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn check_merge_conflict_without_assume_flag() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
// Without --assume-in-merge, hook should pass even with conflict markers
|
|
// if we're not actually in a merge state
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-merge-conflict
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
cwd.child("conflict.txt").write_str(indoc::indoc! {r"
|
|
<<<<<<< HEAD
|
|
Our changes
|
|
=======
|
|
Their changes
|
|
>>>>>>> branch
|
|
"})?;
|
|
|
|
context.git_add(".");
|
|
|
|
// Should pass because we're not in a merge state and no --assume-in-merge flag
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
check for merge conflicts................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn check_xml_hook() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-xml
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files
|
|
cwd.child("valid.xml").write_str(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<root>
|
|
<element>value</element>
|
|
</root>"#,
|
|
)?;
|
|
cwd.child("invalid_unclosed.xml").write_str(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<root>
|
|
<element>value
|
|
</root>"#,
|
|
)?;
|
|
cwd.child("invalid_mismatched.xml").write_str(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<root>
|
|
<element>value</different>
|
|
</root>"#,
|
|
)?;
|
|
cwd.child("multiple_roots.xml").write_str(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<element>value</element>
|
|
<another>value</another>"#,
|
|
)?;
|
|
cwd.child("empty.xml").touch()?;
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: hooks should fail
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check xml................................................................Failed
|
|
- hook id: check-xml
|
|
- exit code: 1
|
|
invalid_mismatched.xml: Failed to xml parse (ill-formed document: expected `</element>`, but `</different>` was found)
|
|
empty.xml: Failed to xml parse (no element found)
|
|
invalid_unclosed.xml: Failed to xml parse (ill-formed document: expected `</element>`, but `</root>` was found)
|
|
multiple_roots.xml: Failed to xml parse (junk after document element)
|
|
|
|
----- stderr -----
|
|
");
|
|
|
|
// Fix the files
|
|
cwd.child("invalid_unclosed.xml").write_str(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<root>
|
|
<element>value</element>
|
|
</root>"#,
|
|
)?;
|
|
cwd.child("invalid_mismatched.xml").write_str(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<root>
|
|
<element>value</element>
|
|
</root>"#,
|
|
)?;
|
|
cwd.child("multiple_roots.xml").write_str(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<root>
|
|
<element>value</element>
|
|
<another>value</another>
|
|
</root>"#,
|
|
)?;
|
|
|
|
context.git_add(".");
|
|
|
|
// Second run: hooks should now pass
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check xml................................................................Failed
|
|
- hook id: check-xml
|
|
- exit code: 1
|
|
empty.xml: Failed to xml parse (no element found)
|
|
|
|
----- stderr -----
|
|
");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn check_xml_with_features() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-xml
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files with various XML features
|
|
cwd.child("with_attributes.xml").write_str(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<root xmlns="http://example.com">
|
|
<element id="1" type="test">value</element>
|
|
</root>"#,
|
|
)?;
|
|
cwd.child("with_cdata.xml").write_str(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<root>
|
|
<element><![CDATA[Some <special> characters & symbols]]></element>
|
|
</root>"#,
|
|
)?;
|
|
cwd.child("with_comments.xml").write_str(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<root>
|
|
<!-- This is a comment -->
|
|
<element>value</element>
|
|
</root>"#,
|
|
)?;
|
|
cwd.child("with_doctype.xml").write_str(
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE root SYSTEM "root.dtd">
|
|
<root>
|
|
<element>value</element>
|
|
</root>"#,
|
|
)?;
|
|
|
|
context.git_add(".");
|
|
|
|
// All should pass
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
check xml................................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn no_commit_to_branch_hook() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: no-commit-to-branch
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create a test file
|
|
cwd.child("test.txt").write_str("Hello World")?;
|
|
context.git_add(".");
|
|
context.git_commit("Initial commit");
|
|
|
|
// Test 1: Try to commit to master branch (should fail)
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
don't commit to branch...................................................Failed
|
|
- hook id: no-commit-to-branch
|
|
- exit code: 1
|
|
You are not allowed to commit to branch 'master'
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Test 2: Create and switch to a feature branch (should pass)
|
|
context.git_branch("feature/new-feature");
|
|
context.git_checkout("feature/new-feature");
|
|
|
|
cwd.child("feature.txt").write_str("Feature content")?;
|
|
context.git_add(".");
|
|
context.git_commit("Add feature");
|
|
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
don't commit to branch...................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Test 3: Try to commit to main branch (should fail)
|
|
context.git_branch("main");
|
|
context.git_checkout("main");
|
|
|
|
cwd.child("main.txt").write_str("Main content")?;
|
|
context.git_add(".");
|
|
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
don't commit to branch...................................................Failed
|
|
- hook id: no-commit-to-branch
|
|
- exit code: 1
|
|
You are not allowed to commit to branch 'main'
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn no_commit_to_branch_hook_with_custom_branches() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: no-commit-to-branch
|
|
args: ['--branch', 'develop', '--branch', 'production']
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create a test file
|
|
cwd.child("test.txt").write_str("Hello World")?;
|
|
context.git_add(".");
|
|
context.git_commit("Initial commit");
|
|
|
|
// Test 1: Try to commit to master branch (should pass - not in custom list)
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
don't commit to branch...................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Test 2: Create and switch to develop branch (should fail)
|
|
context.git_branch("develop");
|
|
context.git_checkout("develop");
|
|
|
|
cwd.child("develop.txt").write_str("Develop content")?;
|
|
context.git_add(".");
|
|
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
don't commit to branch...................................................Failed
|
|
- hook id: no-commit-to-branch
|
|
- exit code: 1
|
|
You are not allowed to commit to branch 'develop'
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Test 3: Create and switch to production branch (should fail)
|
|
context.git_branch("production");
|
|
context.git_checkout("production");
|
|
|
|
cwd.child("production.txt")
|
|
.write_str("Production content")?;
|
|
context.git_add(".");
|
|
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
don't commit to branch...................................................Failed
|
|
- hook id: no-commit-to-branch
|
|
- exit code: 1
|
|
You are not allowed to commit to branch 'production'
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn no_commit_to_branch_hook_with_patterns() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: no-commit-to-branch
|
|
args: ['--pattern', '^feature/.*', '--pattern', '.*-wip$']
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create a test file
|
|
cwd.child("test.txt").write_str("Hello World")?;
|
|
context.git_add(".");
|
|
context.git_commit("Initial commit");
|
|
|
|
// Test 1: Try to commit to master branch (should fail - If branch is not specified, branch defaults to master and main)
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
don't commit to branch...................................................Failed
|
|
- hook id: no-commit-to-branch
|
|
- exit code: 1
|
|
You are not allowed to commit to branch 'master'
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Test 2: Create and switch to feature branch (should fail - matches pattern)
|
|
context.git_branch("feature/new-feature");
|
|
context.git_checkout("feature/new-feature");
|
|
|
|
cwd.child("feature.txt").write_str("Feature content")?;
|
|
context.git_add(".");
|
|
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
don't commit to branch...................................................Failed
|
|
- hook id: no-commit-to-branch
|
|
- exit code: 1
|
|
You are not allowed to commit to branch 'feature/new-feature'
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Test 3: Create and switch to wip branch (should fail - matches pattern)
|
|
context.git_branch("my-branch-wip");
|
|
context.git_checkout("my-branch-wip");
|
|
|
|
cwd.child("wip.txt").write_str("WIP content")?;
|
|
context.git_add(".");
|
|
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
don't commit to branch...................................................Failed
|
|
- hook id: no-commit-to-branch
|
|
- exit code: 1
|
|
You are not allowed to commit to branch 'my-branch-wip'
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Test 4: Create and switch to normal branch (should pass - doesn't match patterns)
|
|
context.git_branch("normal-branch");
|
|
context.git_checkout("normal-branch");
|
|
|
|
cwd.child("normal.txt").write_str("Normal content")?;
|
|
context.git_add(".");
|
|
context.git_commit("Add normal content");
|
|
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
don't commit to branch...................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Test 5: Try to run with detached head pointer status (should pass - ignore this status)
|
|
context.git_checkout("HEAD~1");
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
don't commit to branch...................................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Test 6: Try to commit to branch with invalid pattern (should fail - invalid pattern)
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: no-commit-to-branch
|
|
args: ['--pattern', '*invalid-pattern*']
|
|
"});
|
|
|
|
context.git_branch("invalid-branch");
|
|
context.git_checkout("invalid-branch");
|
|
|
|
cwd.child("invalid.txt").write_str("Invalid content")?;
|
|
context.git_add(".");
|
|
|
|
cmd_snapshot!(context.filters(), context.run(), @r"
|
|
success: false
|
|
exit_code: 2
|
|
----- stdout -----
|
|
don't commit to branch...................................................
|
|
----- stderr -----
|
|
error: Failed to run hook `no-commit-to-branch`
|
|
caused by: Failed to compile regex patterns
|
|
caused by: Parsing error at position 0: Target of repeat operator is invalid
|
|
");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn check_executables_have_shebangs_hook() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-executables-have-shebangs
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files
|
|
cwd.child("script_with_shebang.sh")
|
|
.write_str("#!/bin/bash\necho ok\n")?;
|
|
cwd.child("script_without_shebang.sh")
|
|
.write_str("echo missing shebang\n")?;
|
|
cwd.child("not_executable.txt")
|
|
.write_str("not executable\n")?;
|
|
cwd.child("empty.sh").touch()?;
|
|
|
|
// Mark scripts as executable
|
|
std::fs::set_permissions(
|
|
cwd.child("script_with_shebang.sh").path(),
|
|
std::fs::Permissions::from_mode(0o755),
|
|
)?;
|
|
std::fs::set_permissions(
|
|
cwd.child("script_without_shebang.sh").path(),
|
|
std::fs::Permissions::from_mode(0o755),
|
|
)?;
|
|
std::fs::set_permissions(
|
|
cwd.child("empty.sh").path(),
|
|
std::fs::Permissions::from_mode(0o755),
|
|
)?;
|
|
|
|
context.git_add(".");
|
|
|
|
// First run: should fail for script_without_shebang.sh and empty.sh
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check that executables have shebangs.....................................Failed
|
|
- hook id: check-executables-have-shebangs
|
|
- exit code: 1
|
|
empty.sh marked executable but has no (or invalid) shebang!
|
|
If it isn't supposed to be executable, try: 'chmod -x empty.sh'
|
|
If on Windows, you may also need to: 'git add --chmod=-x empty.sh'
|
|
If it is supposed to be executable, double-check its shebang.
|
|
script_without_shebang.sh marked executable but has no (or invalid) shebang!
|
|
If it isn't supposed to be executable, try: 'chmod -x script_without_shebang.sh'
|
|
If on Windows, you may also need to: 'git add --chmod=-x script_without_shebang.sh'
|
|
If it is supposed to be executable, double-check its shebang.
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Fix the files: remove executable bit or add shebang
|
|
cwd.child("script_without_shebang.sh")
|
|
.write_str("#!/bin/sh\necho fixed\n")?;
|
|
std::fs::set_permissions(
|
|
cwd.child("empty.sh").path(),
|
|
std::fs::Permissions::from_mode(0o644),
|
|
)?;
|
|
|
|
context.git_add(".");
|
|
|
|
// Second run: should now pass
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
check that executables have shebangs.....................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn check_executables_have_shebangs_win() -> Result<()> {
|
|
use std::process::Command;
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
let repo_path = context.work_dir();
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-executables-have-shebangs
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
cwd.child("win_script_with_shebang.sh")
|
|
.write_str("#!/bin/bash\necho ok\n")?;
|
|
cwd.child("win_script_without_shebang.sh")
|
|
.write_str("missing shebang\n")?;
|
|
|
|
context.git_add(".");
|
|
|
|
Command::new("git")
|
|
.args(["update-index", "--chmod=+x", "win_script_with_shebang.sh"])
|
|
.current_dir(repo_path)
|
|
.status()?;
|
|
|
|
Command::new("git")
|
|
.args([
|
|
"update-index",
|
|
"--chmod=+x",
|
|
"win_script_without_shebang.sh",
|
|
])
|
|
.current_dir(repo_path)
|
|
.status()?;
|
|
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check that executables have shebangs.....................................Failed
|
|
- hook id: check-executables-have-shebangs
|
|
- exit code: 1
|
|
win_script_without_shebang.sh marked executable but has no (or invalid) shebang!
|
|
If it isn't supposed to be executable, try: 'chmod -x win_script_without_shebang.sh'
|
|
If on Windows, you may also need to: 'git add --chmod=-x win_script_without_shebang.sh'
|
|
If it is supposed to be executable, double-check its shebang.
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn check_executables_have_shebangs_various_cases() -> Result<()> {
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-executables-have-shebangs
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
// Create test files
|
|
cwd.child("partial_shebang.sh")
|
|
.write_str("#\necho partial\n")?;
|
|
cwd.child("shebang_with_space.sh")
|
|
.write_str("#! /bin/bash\necho ok\n")?;
|
|
cwd.child("non_executable.txt")
|
|
.write_str("not executable\n")?;
|
|
cwd.child("whitespace.sh").write_str(" \n")?;
|
|
cwd.child("invalid_shebang.sh")
|
|
.write_str("##!/bin/bash\necho bad\n")?;
|
|
|
|
// Mark scripts as executable
|
|
std::fs::set_permissions(
|
|
cwd.child("partial_shebang.sh").path(),
|
|
std::fs::Permissions::from_mode(0o755),
|
|
)?;
|
|
std::fs::set_permissions(
|
|
cwd.child("shebang_with_space.sh").path(),
|
|
std::fs::Permissions::from_mode(0o755),
|
|
)?;
|
|
std::fs::set_permissions(
|
|
cwd.child("whitespace.sh").path(),
|
|
std::fs::Permissions::from_mode(0o755),
|
|
)?;
|
|
std::fs::set_permissions(
|
|
cwd.child("invalid_shebang.sh").path(),
|
|
std::fs::Permissions::from_mode(0o755),
|
|
)?;
|
|
// non_executable.txt is not marked executable
|
|
|
|
context.git_add(".");
|
|
|
|
// Run: should fail for partial_shebang.sh, whitespace.sh, invalid_shebang.sh
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check that executables have shebangs.....................................Failed
|
|
- hook id: check-executables-have-shebangs
|
|
- exit code: 1
|
|
partial_shebang.sh marked executable but has no (or invalid) shebang!
|
|
If it isn't supposed to be executable, try: 'chmod -x partial_shebang.sh'
|
|
If on Windows, you may also need to: 'git add --chmod=-x partial_shebang.sh'
|
|
If it is supposed to be executable, double-check its shebang.
|
|
invalid_shebang.sh marked executable but has no (or invalid) shebang!
|
|
If it isn't supposed to be executable, try: 'chmod -x invalid_shebang.sh'
|
|
If on Windows, you may also need to: 'git add --chmod=-x invalid_shebang.sh'
|
|
If it is supposed to be executable, double-check its shebang.
|
|
whitespace.sh marked executable but has no (or invalid) shebang!
|
|
If it isn't supposed to be executable, try: 'chmod -x whitespace.sh'
|
|
If on Windows, you may also need to: 'git add --chmod=-x whitespace.sh'
|
|
If it is supposed to be executable, double-check its shebang.
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
// Fix the files: add valid shebangs or remove executable bit
|
|
cwd.child("partial_shebang.sh")
|
|
.write_str("#!/bin/sh\necho fixed\n")?;
|
|
cwd.child("whitespace.sh").write_str("#!/bin/sh\n")?;
|
|
cwd.child("invalid_shebang.sh")
|
|
.write_str("#!/bin/bash\necho fixed\n")?;
|
|
|
|
context.git_add(".");
|
|
|
|
// Second run: should now pass
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
check that executables have shebangs.....................................Passed
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn check_executables_have_shebangs_various_cases_win() -> Result<()> {
|
|
use std::process::Command;
|
|
let context = TestContext::new();
|
|
context.init_project();
|
|
context.configure_git_author();
|
|
context.write_pre_commit_config(indoc::indoc! {r"
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v5.0.0
|
|
hooks:
|
|
- id: check-executables-have-shebangs
|
|
"});
|
|
|
|
let cwd = context.work_dir();
|
|
|
|
cwd.child("partial_shebang.sh")
|
|
.write_str("#\necho partial\n")?;
|
|
cwd.child("shebang_with_space.sh")
|
|
.write_str("#! /bin/bash\necho ok\n")?;
|
|
cwd.child("non_executable.txt")
|
|
.write_str("not executable\n")?;
|
|
cwd.child("whitespace.sh").write_str(" \n")?;
|
|
cwd.child("invalid_shebang.sh")
|
|
.write_str("##!/bin/bash\necho bad\n")?;
|
|
|
|
context.git_add(".");
|
|
|
|
let executable_files = [
|
|
"partial_shebang.sh",
|
|
"shebang_with_space.sh",
|
|
"whitespace.sh",
|
|
"invalid_shebang.sh",
|
|
];
|
|
|
|
for file in &executable_files {
|
|
Command::new("git")
|
|
.args(["update-index", "--chmod=+x", file])
|
|
.current_dir(cwd.path())
|
|
.status()?;
|
|
}
|
|
|
|
// Run: should fail for partial_shebang.sh, whitespace.sh, invalid_shebang.sh
|
|
cmd_snapshot!(context.filters(), context.run(), @r#"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
check that executables have shebangs.....................................Failed
|
|
- hook id: check-executables-have-shebangs
|
|
- exit code: 1
|
|
invalid_shebang.sh marked executable but has no (or invalid) shebang!
|
|
If it isn't supposed to be executable, try: 'chmod -x invalid_shebang.sh'
|
|
If on Windows, you may also need to: 'git add --chmod=-x invalid_shebang.sh'
|
|
If it is supposed to be executable, double-check its shebang.
|
|
partial_shebang.sh marked executable but has no (or invalid) shebang!
|
|
If it isn't supposed to be executable, try: 'chmod -x partial_shebang.sh'
|
|
If on Windows, you may also need to: 'git add --chmod=-x partial_shebang.sh'
|
|
If it is supposed to be executable, double-check its shebang.
|
|
whitespace.sh marked executable but has no (or invalid) shebang!
|
|
If it isn't supposed to be executable, try: 'chmod -x whitespace.sh'
|
|
If on Windows, you may also need to: 'git add --chmod=-x whitespace.sh'
|
|
If it is supposed to be executable, double-check its shebang.
|
|
|
|
----- stderr -----
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|