atypical-commit 0.4.0

(Non)-standard enforcing DX
Documentation
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};

const BIN: &str = env!("CARGO_BIN_EXE_commit-lint");

fn lint(args: &[&str], stdin: Option<&str>) -> Output {
    lint_in(Path::new("."), args, stdin)
}

fn lint_in(dir: &Path, args: &[&str], stdin: Option<&str>) -> Output {
    let mut child = Command::new(BIN)
        .current_dir(dir)
        .args(args)
        .stdin(if stdin.is_some() {
            Stdio::piped()
        } else {
            Stdio::null()
        })
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();

    if let Some(input) = stdin {
        child
            .stdin
            .take()
            .unwrap()
            .write_all(input.as_bytes())
            .unwrap();
    }

    child.wait_with_output().unwrap()
}

fn fixture(name: &str, contents: &str) -> PathBuf {
    let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(name);

    std::fs::write(&path, contents).unwrap();

    path
}

fn stderr(output: &Output) -> String {
    String::from_utf8_lossy(&output.stderr).into_owned()
}

#[test]
fn valid_message_from_stdin() {
    let output = lint(&["-"], Some("add(lib)[int]: something\n"));

    assert_eq!(output.status.code(), Some(0));
    assert!(output.stdout.is_empty());
}

#[test]
fn valid_message_file_with_body_and_comments() {
    let path = fixture(
        "valid-body",
        "fix(ci): report separately\n\nSome body.\n\n# comment\n",
    );

    let output = lint(&[path.to_str().unwrap()], None);

    assert_eq!(output.status.code(), Some(0));
}

#[test]
fn valid_header_after_blank_lines_and_crlf() {
    let path = fixture("valid-blanks", "\n\nadd(exe)[ux]: from line three\r\n");

    let output = lint(&["--", path.to_str().unwrap()], None);

    assert_eq!(output.status.code(), Some(0));
}

#[test]
fn generated_messages_are_ignored() {
    for header in [
        "Merge pull request #1 from repo/branch\n",
        "Merge branch 'main' into fix/default-ignore\n",
        "Revert \"add(lib)[int]: something\"\n",
        "fixup! add(lib)[int]: something\n",
        "chore(release): v1.2.3 [skip ci]\n",
    ] {
        let output = lint(&["-"], Some(header));

        assert_eq!(output.status.code(), Some(0), "not ignored: {header}");
    }
}

#[test]
fn default_ignores_can_be_disabled() {
    let config =
        fixture("no-ignores.toml", "[commit]\ndefault-ignores = false\n");
    let config = config.to_str().unwrap();
    let header = Some("Merge branch 'main'\n");

    let output = lint(&["--config", config, "-"], header);

    assert_eq!(output.status.code(), Some(1));
    assert!(stderr(&output).contains("unknown keyword `Merge`"));
}

#[test]
fn invalid_keyword_reports_and_fails() {
    let output = lint(&["-"], Some("feat: wrong style\n"));

    assert_eq!(output.status.code(), Some(1));
    assert!(stderr(&output).contains("unknown keyword `feat`"));
}

#[test]
fn missing_description_fails() {
    let output = lint(&["-"], Some("add:\n"));

    assert_eq!(output.status.code(), Some(1));
    assert!(stderr(&output).contains("expected a description"));
}

#[test]
fn comments_only_is_a_usage_error() {
    let path = fixture("comments-only", "# only\n# comments\n");

    let output = lint(&[path.to_str().unwrap()], None);

    assert_eq!(output.status.code(), Some(2));
    assert!(stderr(&output).contains("No commit message to lint."));
}

#[test]
fn no_input_is_a_usage_error() {
    let output = lint(&[], None);

    assert_eq!(output.status.code(), Some(2));
    assert!(stderr(&output).contains("No input provided."));
}

#[test]
fn unreadable_file_fails() {
    let output = lint(&["/nonexistent/commit-msg"], None);

    assert_eq!(output.status.code(), Some(1));
}

#[test]
fn config_flag_overrides_the_keywords() {
    let config = fixture(
        "keywords.toml",
        "[commit]\nkeywords = [\"feat\", \"fix\"]\n",
    );
    let config = config.to_str().unwrap();

    let output = lint(&["--config", config, "-"], Some("feat: now valid\n"));

    assert_eq!(output.status.code(), Some(0));

    let output = lint(&["--config", config, "-"], Some("add: now unknown\n"));

    assert_eq!(output.status.code(), Some(1));
    assert!(stderr(&output).contains("expected one of: feat, fix"));
}

#[test]
fn config_is_discovered_from_the_working_directory() {
    let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("discovery");

    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(
        dir.join("atypical.toml"),
        "[commit]\nkeywords = [\"feat\"]\n",
    )
    .unwrap();

    let output = lint_in(&dir, &["-"], Some("feat: discovered\n"));

    assert_eq!(output.status.code(), Some(0));
}

#[test]
fn invalid_config_fails() {
    let config = fixture("invalid.toml", "[commit]\nkeyword = [\"typo\"]\n");

    let output = lint(
        &["--config", config.to_str().unwrap(), "-"],
        Some("add: message\n"),
    );

    assert_eq!(output.status.code(), Some(1));

    let output = lint(
        &["--config", "/nonexistent/atypical.toml", "-"],
        Some("add: message\n"),
    );

    assert_eq!(output.status.code(), Some(1));
}