#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
use std::process::ExitCode;
use clap::Parser as ClapParser;
use genawaiter::{rc::gen, yield_};
use if_changed::{Engine as _, GitEngine};
#[derive(ClapParser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
#[arg(long, env = "PRE_COMMIT_FROM_REF")]
pub from_ref: Option<String>,
#[arg(long, env = "PRE_COMMIT_TO_REF")]
pub to_ref: Option<String>,
#[arg()]
pub patterns: Vec<String>,
}
fn run(cli: Cli, repository: git2::Repository) -> impl Iterator<Item = String> {
gen!({
let engine = GitEngine::new(&repository, cli.from_ref.as_deref(), cli.to_ref.as_deref());
for result in engine.matches(cli.patterns) {
let Ok(path) = result else {
continue;
};
if engine.is_ignored(&path) {
continue;
}
if let Err(errors) = engine.check(path) {
for error in errors {
yield_!(error);
}
}
}
})
.into_iter()
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn main() -> ExitCode {
let mut has_error = false;
let repository = match git2::Repository::open_from_env() {
Ok(repository) => repository,
Err(error) => {
eprintln!("Could not open the repository: {error}");
return ExitCode::FAILURE;
}
};
for error in run(Cli::parse(), repository) {
has_error = true;
eprintln!("{error}");
}
if has_error {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
#[cfg(test)]
mod tests {
use if_changed::testing::git_test;
use indoc::indoc;
use super::*;
#[test]
fn test_run() {
let (tempdir, _repo) = git_test! {
"initial commit": [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(b.ts)
}
"},
"b.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(a.ts)
}
"}
]
};
let repository = git2::Repository::open(tempdir.path()).unwrap();
insta::assert_compact_json_snapshot!(run(Cli {
from_ref: None,
to_ref: Some("HEAD".into()),
patterns: vec![],
}, repository).collect::<Vec<_>>(), @"[]");
}
#[test]
fn test_run_fail() {
let (tempdir, _repo) = git_test! {
"initial commit": [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(b.ts)
}
"}
]
};
let repository = git2::Repository::open(tempdir.path()).unwrap();
insta::assert_compact_json_snapshot!(run(Cli {
from_ref: None,
to_ref: Some("HEAD".into()),
patterns: vec![],
}, repository).collect::<Vec<_>>(), @r###"["Expected \"b.ts\" to be modified because of \"then-change\" in \"a.ts\" at line 4."]"###);
}
#[test]
fn test_run_commit_footer() {
let (tempdir, _repo) = git_test! {
"initial commit\n\nignore-if-changed: a.ts": [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(b.ts)
}
"}
]
};
let repository = git2::Repository::open(tempdir.path()).unwrap();
insta::assert_compact_json_snapshot!(run(Cli {
from_ref: None,
to_ref: Some("HEAD".into()),
patterns: vec![],
}, repository).collect::<Vec<_>>(), @"[]");
}
#[test]
fn test_run_commit_footer_with_reason() {
let (tempdir, _repo) = git_test! {
"initial commit\n\nignore-if-changed: a.ts -- idky": [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(b.ts)
}
"}
]
};
let repository = git2::Repository::open(tempdir.path()).unwrap();
insta::assert_compact_json_snapshot!(run(Cli {
from_ref: None,
to_ref: Some("HEAD".into()),
patterns: vec![],
}, repository).collect::<Vec<_>>(), @"[]");
}
#[test]
fn test_run_no_matching() {
let (tempdir, _repo) = git_test! {
"initial commit": [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(b.ts)
}
"}
]
};
let repository = git2::Repository::open(tempdir.path()).unwrap();
insta::assert_compact_json_snapshot!(run(Cli {
from_ref: None,
to_ref: Some("HEAD".into()),
patterns: vec!["c.js".to_string()],
}, repository).collect::<Vec<_>>(), @"[]");
}
#[test]
fn test_run_working_dir() {
let (tempdir, _repo) = git_test! {
working: [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(b.ts)
}
"},
"b.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(a.ts)
}
"}
]
};
let repository = git2::Repository::open(tempdir.path()).unwrap();
insta::assert_compact_json_snapshot!(run(Cli {
from_ref: None,
to_ref: None,
patterns: vec![],
}, repository).collect::<Vec<_>>(), @"[]");
}
#[test]
fn test_run_working_dir_fail() {
let (tempdir, _repo) = git_test! {
working: [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(b.ts)
}
"}
]
};
let repository = git2::Repository::open(tempdir.path()).unwrap();
insta::assert_compact_json_snapshot!(run(Cli {
from_ref: None,
to_ref: None,
patterns: vec![],
}, repository).collect::<Vec<_>>(), @r###"["Expected \"b.ts\" to be modified because of \"then-change\" in \"a.ts\" at line 4."]"###);
}
#[test]
fn test_run_two_commits() {
let (tempdir, _repo) = git_test! {
"initial commit": [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(b.ts)
}
"},
"b.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(a.ts)
}
"}
]
"second commit": [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
B,
// then-change(b.ts)
}
"},
"b.ts" => indoc! {"
const enum G {
// if-changed
A,
B,
// then-change(a.ts)
}
"}
]
};
let repository = git2::Repository::open(tempdir.path()).unwrap();
insta::assert_compact_json_snapshot!(run(Cli {
from_ref: Some("HEAD^".into()),
to_ref: Some("HEAD".into()),
patterns: vec![],
}, repository).collect::<Vec<_>>(), @"[]");
}
#[test]
fn test_run_two_commits_fail() {
let (tempdir, _repo) = git_test! {
"initial commit": [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(b.ts)
}
"},
"b.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(a.ts)
}
"}
]
"second commit": [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
B,
// then-change(b.ts)
}
"}
]
};
let repository = git2::Repository::open(tempdir.path()).unwrap();
insta::assert_compact_json_snapshot!(run(Cli {
from_ref: Some("HEAD^".into()),
to_ref: Some("HEAD".into()),
patterns: vec![],
}, repository).collect::<Vec<_>>(), @r###"["Expected \"b.ts\" to be modified because of \"then-change\" in \"a.ts\" at line 5."]"###);
}
#[test]
fn test_run_two_commits_fail_no_change() {
let (tempdir, _repo) = git_test! {
"initial commit": [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(b.ts)
}
"},
"b.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(a.ts)
}
"}
]
"second commit": [
"a.ts" => indoc! {"
const enum G {
// if-changed
A,
B,
// then-change(b.ts)
}
"},
"b.ts" => indoc! {"
const enum G {
// if-changed
A,
// then-change(a.ts)
}
"}
]
};
let repository = git2::Repository::open(tempdir.path()).unwrap();
insta::assert_compact_json_snapshot!(run(Cli {
from_ref: Some("HEAD^".into()),
to_ref: Some("HEAD".into()),
patterns: vec![],
}, repository).collect::<Vec<_>>(), @r###"["Expected \"b.ts\" to be modified because of \"then-change\" in \"a.ts\" at line 5."]"###);
}
}