prek 0.4.12

A fast Git hook manager written in Rust, designed as a drop-in alternative to pre-commit, reimagined.
use assert_fs::fixture::{FileWriteStr, PathChild};
use prek_consts::PRE_COMMIT_CONFIG_YAML;

use crate::common::{TestContext, cmd_snapshot};

mod common;

#[test]
fn validate_config() -> anyhow::Result<()> {
    let context = TestContext::new();

    // No files to validate.
    cmd_snapshot!(context.filters(), context.validate_config(), @r"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    warning: No configs to check
    ");

    context.write_pre_commit_config(indoc::indoc! {r"
        repos:
          - repo: https://github.com/pre-commit/pre-commit-hooks
            rev: v5.0.0
            hooks:
              - id: trailing-whitespace
              - id: end-of-file-fixer
              - id: check-json
    "});
    // Validate one file.
    cmd_snapshot!(context.filters(), context.validate_config().arg(PRE_COMMIT_CONFIG_YAML), @r"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    success: All configs are valid
    ");

    context
        .work_dir()
        .child("config-1.yaml")
        .write_str(indoc::indoc! {r"
            repos:
              - repo: https://github.com/pre-commit/pre-commit-hooks
        "})?;

    // Validate multiple files.
    cmd_snapshot!(context.filters(), context.validate_config().arg(PRE_COMMIT_CONFIG_YAML).arg("config-1.yaml"), @"
    success: false
    exit_code: 1
    ----- stdout -----

    ----- stderr -----
    error: Failed to parse `config-1.yaml`
      caused by: error: line 2 column 5: missing field `rev`
     --> <input>:2:5
      |
    1 | repos:
    2 |   - repo: https://github.com/pre-commit/pre-commit-hooks
      |     ^ missing field `rev`
    ");

    Ok(())
}

#[test]
fn mutable_revision_warning_has_actionable_guidance() {
    let context = TestContext::new();
    context.write_pre_commit_config(indoc::indoc! {r"
        repos:
          - repo: https://example.com/hooks
            rev: main
            hooks: []
    "});

    cmd_snapshot!(context.filters(), context.validate_config().arg(PRE_COMMIT_CONFIG_YAML), @r"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    warning: The following repositories use mutable `rev` values (branches or moving tags):
    https://example.com/hooks: main
    `prek` does not automatically detect changes to these references after the first install.
    Use a tag or commit SHA for each `rev`, or run `prek update` to select the latest eligible tag.
    See https://prek.j178.dev/reference/configuration/#rev for details.
    success: All configs are valid
    ");
}

#[test]
fn invalid_config_error() {
    let context = TestContext::new();
    context.write_pre_commit_config(indoc::indoc! {r"
        repos:
          - repo: https://github.com/pre-commit/pre-commit-hooks
            hooks:
              - id: trailing-whitespace
              - id: end-of-file-fixer
              - id: check-json
            rev: 1.0
    "});

    cmd_snapshot!(context.filters(), context.validate_config().arg(PRE_COMMIT_CONFIG_YAML), @"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    success: All configs are valid
    ");

    context.write_pre_commit_config(indoc::indoc! {r"
        repos:
          - repo: https://github.com/pre-commit/pre-commit-hooks
            rev: v6.0.0
            hooks:
              - id: trailing-whitespace
              - id: end-of-file-fixer
          - repo: local
            hooks:
              - name: check-json
    "});

    cmd_snapshot!(context.filters(), context.validate_config().arg(PRE_COMMIT_CONFIG_YAML), @"
    success: false
    exit_code: 1
    ----- stdout -----

    ----- stderr -----
    error: Failed to parse `.pre-commit-config.yaml`
      caused by: error: line 9 column 9: missing field `id`
     --> <input>:9:9
      |
    7 |   - repo: local
    8 |     hooks:
    9 |       - name: check-json
      |         ^ missing field `id`
    ");
}

#[test]
fn unknown_priority_alias_is_invalid() {
    let context = TestContext::new();
    context.write_pre_commit_config(indoc::indoc! {r"
        priorities:
          checks: 10
        repos:
          - repo: local
            hooks:
              - id: format
                name: Format
                entry: ruff format
                language: system
                priority: formatting
    "});

    cmd_snapshot!(context.filters(), context.validate_config().arg(PRE_COMMIT_CONFIG_YAML), @r"
    success: false
    exit_code: 1
    ----- stdout -----

    ----- stderr -----
    error: Priority alias `formatting` referenced by hook `format` is not declared in `priorities`
    ");
}

#[test]
fn priority_aliases_cannot_contain_whitespace() {
    let context = TestContext::new();
    context.write_pre_commit_config(indoc::indoc! {r#"
        priorities:
          "static checks": 10
        repos: []
    "#});

    cmd_snapshot!(context.filters(), context.validate_config().arg(PRE_COMMIT_CONFIG_YAML), @r#"
    success: false
    exit_code: 1
    ----- stdout -----

    ----- stderr -----
    error: Failed to parse `.pre-commit-config.yaml`
      caused by: error: line 2 column 3: priority alias `static checks` cannot contain whitespace
     --> <input>:2:3
      |
    1 | priorities:
    2 |   "static checks": 10
      |   ^ priority alias `static checks` cannot contain whitespace
    3 | repos: []
      |
    "#);
}

#[test]
fn duplicate_and_unused_priority_aliases_are_valid() {
    let context = TestContext::new();
    context.write_pre_commit_config(indoc::indoc! {r"
        priorities:
          checks: 10
          verification: 10
          unused: 20
        repos:
          - repo: local
            hooks:
              - id: check
                name: Check
                entry: check
                language: system
                priority: checks
              - id: verify
                name: Verify
                entry: verify
                language: system
                priority: verification
    "});

    cmd_snapshot!(context.filters(), context.validate_config().arg(PRE_COMMIT_CONFIG_YAML), @r"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    success: All configs are valid
    ");
}

#[test]
fn validate_manifest() -> anyhow::Result<()> {
    let context = TestContext::new();

    // No files to validate.
    cmd_snapshot!(context.filters(), context.validate_manifest(), @r"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    warning: No manifests to check
    ");

    context
        .work_dir()
        .child(".pre-commit-hooks.yaml")
        .write_str(indoc::indoc! {r"
            -   id: check-added-large-files
                name: check for added large files
                description: prevents giant files from being committed.
                entry: check-added-large-files
                language: python
                stages: [pre-commit, pre-push, manual]
                minimum_pre_commit_version: 3.2.0
        "})?;
    // Validate one file.
    cmd_snapshot!(context.filters(), context.validate_manifest().arg(".pre-commit-hooks.yaml"), @r"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    success: All manifests are valid
    ");

    context
        .work_dir()
        .child("hooks-1.yaml")
        .write_str(indoc::indoc! {r"
            -   id: check-added-large-files
                name: check for added large files
                description: prevents giant files from being committed.
                language: python
                stages: [pre-commit, pre-push, manual]
                minimum_pre_commit_version: 3.2.0
        "})?;

    // Validate multiple files.
    cmd_snapshot!(context.filters(), context.validate_manifest().arg(".pre-commit-hooks.yaml").arg("hooks-1.yaml"), @"
    success: false
    exit_code: 1
    ----- stdout -----

    ----- stderr -----
    error: Failed to parse `hooks-1.yaml`
      caused by: error: line 6 column 5: missing field `entry`
     --> <input>:6:5
      |
    4 |     language: python
    5 |     stages: [pre-commit, pre-push, manual]
    6 |     minimum_pre_commit_version: 3.2.0
      |     ^ missing field `entry`
    ");

    Ok(())
}

#[test]
fn unexpected_keys_warning() {
    let context = TestContext::new();

    context.write_pre_commit_config(indoc::indoc! {r"
        repos:
          - repo: local
            unexpected_repo_key: some_value
            hooks:
              - id: test-hook
                name: Test Hook
                entry: echo test
                language: system
        unexpected_top_level_key: some_value
        another_unknown: test
        minimum_pre_commit_version: 1.0.0
    "});

    cmd_snapshot!(context.filters(), context.validate_config().arg(PRE_COMMIT_CONFIG_YAML), @r"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    warning: Ignored unexpected keys in `.pre-commit-config.yaml`: `another_unknown`, `unexpected_top_level_key`, `repos[0].unexpected_repo_key`
    success: All configs are valid
    ");

    context.write_pre_commit_config(indoc::indoc! {r"
        repos:
          - repo: local
            unexpected_repo_key: some_value
            hooks:
              - id: test-hook
                name: Test Hook
                entry: echo test
                language: system
                unexpected_hook_key_1: some_value
                unexpected_hook_key_2: some_value
                unexpected_hook_key_3: some_value
                unexpected_hook_key_4: some_value
        unexpected_top_level_key: some_value
        another_unknown: test
        minimum_pre_commit_version: 1.0.0
    "});

    cmd_snapshot!(context.filters(), context.validate_config().arg(PRE_COMMIT_CONFIG_YAML), @r"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    warning: Ignored unexpected keys in `.pre-commit-config.yaml`:
      - `another_unknown`
      - `unexpected_top_level_key`
      - `repos[0].unexpected_repo_key`
      - `repos[0].hooks[0].unexpected_hook_key_1`
      - `repos[0].hooks[0].unexpected_hook_key_2`
      - `repos[0].hooks[0].unexpected_hook_key_3`
      - `repos[0].hooks[0].unexpected_hook_key_4`
    success: All configs are valid
    ");
}