use anyhow::Context;
use blockwatch::blocks;
use blockwatch::blocks::BlockSeverity;
use blockwatch::diff_parser;
use blockwatch::flags;
use blockwatch::language_parsers;
use blockwatch::repo_path::RepoPath;
use blockwatch::report;
use blockwatch::validators;
use blockwatch::fs::FileSystem;
use blockwatch::validators::Violation;
use clap::Parser;
use globset::GlobSet;
use std::collections::HashMap;
use std::io::{IsTerminal, Read, Write};
use std::path::PathBuf;
use std::sync::Arc;
use std::{env, fs, process};
fn main() -> anyhow::Result<()> {
let args = flags::Args::parse();
match &args.command {
Some(flags::SubCommand::List { .. }) => run_list(&args),
None => run_validators(&args),
}
}
fn run_list(args: &flags::Args) -> anyhow::Result<()> {
let file_system = blockwatch::fs::FileSystemImpl::new(&repository_root()?)?;
let (scan_mode, line_changes) = run_inputs(args, &file_system)?;
let (context, _scan_stats) = build_context(args, scan_mode, line_changes, &file_system)?;
let report = context.to_serializable_report();
serde_json::to_writer_pretty(std::io::stdout(), &report).context("Failed to list blocks")
}
fn run_validators(args: &flags::Args) -> anyhow::Result<()> {
let file_system = Arc::new(blockwatch::fs::FileSystemImpl::new(&repository_root()?)?);
let (scan_mode, line_changes) = run_inputs(args, file_system.as_ref())?;
let (context, scan_stats) = build_context(args, scan_mode, line_changes, file_system.as_ref())?;
let (sync_validators, async_validators) = validators::detect_validators(
&context,
&validators::detector_factories::<blockwatch::fs::FileSystemImpl>(),
&args.disabled_validators(),
&args.enabled_validators(),
&file_system,
)?;
let context = Arc::new(context);
let log = validators::run(Arc::clone(&context), sync_validators, async_validators)?;
let has_error_severity = !log.violations.is_empty() && process_violations(&log.violations)?;
let blocks_needing_diff = (!args.diff).then(|| {
validators::diff_gated_block_count(
&context,
&args.disabled_validators(),
&args.enabled_validators(),
)
});
write_report(
args.verbosity,
report::RunMode::new(args.diff, args.only_changed),
blocks_needing_diff,
scan_stats,
&context,
&log,
)?;
if has_error_severity {
process::exit(1);
}
Ok(())
}
fn write_report(
verbosity: flags::Verbosity,
mode: report::RunMode,
blocks_needing_diff: Option<usize>,
scan_stats: blocks::ScanStats,
context: &validators::ValidationContext,
log: &validators::ValidationLog,
) -> anyhow::Result<()> {
if verbosity == flags::Verbosity::None {
return Ok(());
}
let report = report::RunReport::new(mode, blocks_needing_diff, scan_stats, context, log)?;
let mut stdout = std::io::stdout().lock();
match verbosity {
flags::Verbosity::None => {}
flags::Verbosity::Summary => writeln!(&mut stdout, "{}", report.summary_line())?,
flags::Verbosity::Full => {
serde_json::to_writer_pretty(&mut stdout, &report)?;
writeln!(&mut stdout)?;
}
}
stdout.flush()?;
Ok(())
}
fn run_inputs(
args: &flags::Args,
file_system: &impl FileSystem,
) -> anyhow::Result<(
blocks::ScanMode,
HashMap<RepoPath, Vec<diff_parser::LineChange>>,
)> {
let scan_mode = if args.only_changed {
blocks::ScanMode::OnlyChanged
} else {
blocks::ScanMode::All
};
if !args.diff {
return Ok((scan_mode, HashMap::new()));
}
if stdin_is_terminal() {
return Err(anyhow::anyhow!(
"--diff was given but stdin is a terminal, so there is no diff to read. \
Pipe a unified diff in, or drop --diff to check every block."
));
}
Ok((scan_mode, read_diff_from_stdin(file_system)?))
}
fn build_context(
args: &flags::Args,
scan_mode: blocks::ScanMode,
modified_lines_by_file: HashMap<RepoPath, Vec<diff_parser::LineChange>>,
file_system: &impl FileSystem,
) -> anyhow::Result<(validators::ValidationContext, blocks::ScanStats)> {
let language_parsers = language_parsers::language_parsers()?;
let supported_extensions = language_parsers.keys().collect();
args.validate(&supported_extensions)?;
let extra_file_extensions = args.extensions();
let mut glob_set = args.globs()?;
if glob_set.is_empty() {
glob_set = GlobSet::new([globset::Glob::new("**")?])?;
}
let path_checker = blockwatch::fs::PathCheckerImpl::new(glob_set, args.ignored_globs()?);
let parsed = blocks::parse_blocks(
&modified_lines_by_file,
scan_mode,
file_system,
&path_checker,
&language_parsers,
extra_file_extensions,
)?;
Ok((
validators::ValidationContext::new(parsed.blocks, language_parsers, modified_lines_by_file),
parsed.stats,
))
}
fn stdin_is_terminal() -> bool {
std::io::stdin().is_terminal()
}
fn read_diff_from_stdin(
file_system: &impl FileSystem,
) -> anyhow::Result<HashMap<RepoPath, Vec<diff_parser::LineChange>>> {
let mut diff = String::new();
std::io::stdin().read_to_string(&mut diff)?;
diff_parser::validate_diff_input(&diff)?;
diff_parser::line_changes_from_diff(&diff, file_system)
}
fn process_violations(violations: &HashMap<RepoPath, Vec<Violation>>) -> anyhow::Result<bool> {
let mut has_error_severity = false;
let mut diagnostics: HashMap<&RepoPath, Vec<serde_json::Value>> =
HashMap::with_capacity(violations.len());
for (file_path, file_violations) in violations {
let mut file_diagnostics = Vec::with_capacity(file_violations.len());
for violation in file_violations {
let diagnostic = violation.as_simple_diagnostic();
if diagnostic.severity() == BlockSeverity::Error {
has_error_severity = true;
}
file_diagnostics.push(serde_json::to_value(diagnostic)?);
}
diagnostics.insert(file_path, file_diagnostics);
}
let mut stderr = std::io::stderr().lock();
serde_json::to_writer_pretty(&mut stderr, &diagnostics)?;
writeln!(&mut stderr)?;
Ok(has_error_severity)
}
fn repository_root_path(current_path: PathBuf) -> anyhow::Result<PathBuf> {
current_path
.ancestors()
.find(|path| path.join(".git").is_dir() || path.join(".hg").is_dir())
.map(|path| path.to_path_buf())
.ok_or_else(|| anyhow::anyhow!("Could not find the repository root directory"))
}
fn repository_root() -> anyhow::Result<PathBuf> {
repository_root_path(fs::canonicalize(env::current_dir()?)?)
}