diff --git a/.config/toolbox.toml b/.config/toolbox.toml new file mode 100644 index 0000000..3b0f03e --- /dev/null +++ b/.config/toolbox.toml @@ -0,0 +1,7 @@ +# Configuration for trunk toolbox. Generate default by calling 'trunk-toolbox genconfig' + +[ifchange] +enabled = true + +[donotland] +enabled = true diff --git a/Cargo.toml b/Cargo.toml index 4dae09e..a4ebd9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ serde_json = "1.0.85" serde-sarif = "0.3.4" content_inspector = "0.2.4" rayon = "1.5.1" +confique = "0.2.5" [dev-dependencies] assert_cmd = "2.0" diff --git a/README.md b/README.md index 04e1e8e..ad571d5 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,14 @@ To enable the toolbox rules in your repository run: trunk check enable trunk-toolbox ``` +### Configuration + +Toolbox can be configured via the toolbox.toml file. There is an example config file [here](.config/toolbox.toml). A full example file can be generated by calling + +```bash +trunk-toolbox genconfig +``` + ### Rules #### do-not-land diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..0f4e49a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,31 @@ +// trunk-ignore-all(trunk-toolbox/do-not-land) +use confique::toml::{self, FormatOptions}; +use confique::Config; + +#[derive(Config)] +pub struct Conf { + #[config(nested)] + pub ifchange: IfChangeConf, + + #[config(nested)] + pub donotland: PlsNotLandConf, +} + +impl Conf { + pub fn print_default() { + let default_config = toml::template::(FormatOptions::default()); + println!("{}", default_config); + } +} + +#[derive(Config)] +pub struct IfChangeConf { + #[config(default = true)] + pub enabled: bool, +} + +#[derive(Config)] +pub struct PlsNotLandConf { + #[config(default = true)] + pub enabled: bool, +} diff --git a/src/lib.rs b/src/lib.rs index 1e0ab4f..80d7677 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +pub mod config; pub mod diagnostic; pub mod git; pub mod rules; +pub mod run; diff --git a/src/main.rs b/src/main.rs index d9096f7..23d693c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,37 +1,44 @@ use clap::Parser; +use confique::Config; +use horton::config::Conf; use horton::diagnostic; use horton::rules::if_change_then_change::ictc; use horton::rules::pls_no_land::pls_no_land; +use horton::run::{Cli, Run, Subcommands}; + use serde_sarif::sarif; -use std::collections::HashSet; use std::path::PathBuf; use std::time::Instant; -#[derive(Parser, Debug)] -#[clap(version = env!("CARGO_PKG_VERSION"), author = "Trunk Technologies Inc.")] -struct Opts { - // #[arg(short, long, num_args = 1..)] - files: Vec, - - #[clap(long)] - #[arg(default_value_t = String::from("HEAD"))] - upstream: String, - - #[clap(long)] - #[arg(default_value_t = String::from(""))] - results: String, -} fn run() -> anyhow::Result<()> { let start = Instant::now(); - let opts: Opts = Opts::parse(); + let cli: Cli = Cli::parse(); + + if let Some(Subcommands::Genconfig {}) = &cli.subcommand { + Conf::print_default(); + return Ok(()); + } let mut ret = diagnostic::Diagnostics::default(); - // Convert to PathBufs - let paths: HashSet = opts.files.into_iter().map(PathBuf::from).collect(); + let config = Conf::builder() + .env() + .file("toolbox.toml") + .file(".config/toolbox.toml") + .file(".trunk/config/toolbox.toml") + .load() + .unwrap_or_else(|err| { + eprintln!("Toolbox cannot run: {}", err); + std::process::exit(1); + }); + + let run = Run { + paths: cli.files.into_iter().map(PathBuf::from).collect(), + config, + }; let (pls_no_land_result, ictc_result): (Result<_, _>, Result<_, _>) = - rayon::join(|| pls_no_land(&paths), || ictc(&paths, &opts.upstream)); + rayon::join(|| pls_no_land(&run), || ictc(&run, &cli.upstream)); match pls_no_land_result { Ok(result) => ret.diagnostics.extend(result), @@ -90,7 +97,7 @@ fn run() -> anyhow::Result<()> { sarif::MessageBuilder::default() .text(format!( "{:?} files processed in {:?}", - paths.len(), + run.paths.len(), start.elapsed() )) .build() @@ -124,10 +131,10 @@ fn run() -> anyhow::Result<()> { let sarif = serde_json::to_string_pretty(&sarif_built)?; - if opts.results.is_empty() { - println!("{}", sarif); + if let Some(outfile) = &cli.results { + std::fs::write(outfile, sarif)?; } else { - std::fs::write(opts.results, sarif)?; + println!("{}", sarif); } Ok(()) diff --git a/src/rules/if_change_then_change.rs b/src/rules/if_change_then_change.rs index 5cab29c..2f5dca7 100644 --- a/src/rules/if_change_then_change.rs +++ b/src/rules/if_change_then_change.rs @@ -1,3 +1,4 @@ +use crate::run::Run; use anyhow::Context; use log::debug; use std::collections::{HashMap, HashSet}; @@ -112,13 +113,17 @@ pub fn find_ictc_blocks(path: &PathBuf) -> anyhow::Result> { Ok(blocks) } -pub fn ictc( - files: &HashSet, - upstream: &str, -) -> anyhow::Result> { +pub fn ictc(run: &Run, upstream: &str) -> anyhow::Result> { + let config = &run.config.ifchange; + + if !config.enabled { + return Ok(vec![]); + } + // Build up list of files that actually have a ifchange block - this way we can avoid // processing git modified chunks if none are present - let all_blocks: Vec<_> = files + let all_blocks: Vec<_> = run + .paths .par_iter() .filter_map(|file| find_ictc_blocks(file).ok()) .flatten() diff --git a/src/rules/pls_no_land.rs b/src/rules/pls_no_land.rs index 393e8fe..663daab 100644 --- a/src/rules/pls_no_land.rs +++ b/src/rules/pls_no_land.rs @@ -2,14 +2,14 @@ extern crate regex; use crate::diagnostic; +use crate::run::Run; use anyhow::Context; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use regex::Regex; -use std::collections::HashSet; use std::fs::File; use std::io::Read; use std::io::{BufRead, BufReader}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; lazy_static::lazy_static! { static ref RE: Regex = Regex::new(r"(?i)(DO[\s_-]*NOT[\s_-]*LAND)").unwrap(); @@ -22,12 +22,22 @@ pub fn is_binary_file(path: &PathBuf) -> std::io::Result { Ok(buffer[..n].contains(&0)) } +pub fn is_ignored_file(path: &Path) -> bool { + // Filter out well known files that should have the word donotland in them (like toolbox.toml) + path.file_name().map_or(false, |f| f == "toolbox.toml") +} + // Checks for $re and other forms thereof in source code // // Note that this is named "pls_no_land" to avoid causing DNL matches everywhere in trunk-toolbox. -pub fn pls_no_land(paths: &HashSet) -> anyhow::Result> { +pub fn pls_no_land(run: &Run) -> anyhow::Result> { + let config = &run.config.donotland; + if !config.enabled { + return Ok(vec![]); + } + // Scan files in parallel - let results: Result, _> = paths.par_iter().map(pls_no_land_impl).collect(); + let results: Result, _> = run.paths.par_iter().map(pls_no_land_impl).collect(); match results { Ok(v) => Ok(v.into_iter().flatten().collect()), @@ -41,6 +51,10 @@ fn pls_no_land_impl(path: &PathBuf) -> anyhow::Result, + + pub files: Vec, + + #[clap(long)] + #[arg(default_value_t = String::from("HEAD"))] + pub upstream: String, + + #[clap(long)] + /// optional path to write results to + pub results: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Subcommands { + // print default config for toolbox + /// Generate default configuration content for toolbox + Genconfig, +} + +pub struct Run { + pub paths: HashSet, + pub config: Conf, +} diff --git a/tests/do_not_land_test.rs b/tests/do_not_land_test.rs index 5253e5a..5245588 100644 --- a/tests/do_not_land_test.rs +++ b/tests/do_not_land_test.rs @@ -54,3 +54,29 @@ fn binary_files_ignored() -> anyhow::Result<()> { ); Ok(()) } + +#[test] +fn honor_disabled_in_config() -> anyhow::Result<()> { + let test_repo = TestRepo::make()?; + test_repo.write("alpha.foo", "do-not-land\n".as_bytes()); + test_repo.git_add_all()?; + + { + let horton = test_repo.run_horton()?; + assert_that(&horton.stdout).contains("Found 'do-not-land'"); + } + + let config = r#" + [donotland] + enabled = false + "#; + + // Now disable the rule + test_repo.write("toolbox.toml", config.as_bytes()); + { + let horton = test_repo.run_horton().unwrap(); + assert_that(&horton.stdout.contains("Found 'do-not-land'")).is_false(); + } + + Ok(()) +}