use crate::validators;
use anyhow::Context;
use clap::{Parser, builder::ValueParser, crate_version};
use globset::{Glob, GlobSet, GlobSetBuilder};
use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
#[derive(Parser, Debug)]
#[command(
author,
version = crate_version!(),
about = "Validate interdependent code/doc blocks in diffs to prevent drift.",
long_about = r"Blockwatch reads a unified git diff from stdin and validates that named blocks, sorted segments, and other constraints remain consistent across files. It is designed for use in pre-commit hooks and CI. Pipe `git diff --patch` to blockwatch.",
after_help = r"EXAMPLES:
# Filter files using glob patterns
blockwatch 'src/**/*.rs'
# Ignore files using glob patterns
blockwatch 'src/**/*.rs' --ignore '**/generated/**'
# Filter files with the diff input
git diff --patch | blockwatch 'src/**/*.rs'
# Validate current unstaged changes
git diff --patch | blockwatch
# Validate staged changes only
git diff --cached --patch | blockwatch
# With zero context for tighter diffs (recommended for hooks)
git diff --patch --unified=0 | blockwatch
# Provide extra extension mappings (map unknown extensions to supported grammars)
blockwatch -E cxx=cpp -E c++=cpp
# Disable specific validators
blockwatch -d keep-sorted -d line-count
# Enable specific validators only
blockwatch -e keep-sorted -e line-count
# List all found blocks
blockwatch list 'src/**/*.rs'",
)]
pub struct Args {
#[arg(
short = 'E',
long = "extension",
value_name = "KEY=VALUE",
action = clap::ArgAction::Append,
value_parser = ValueParser::new(parse_extensions),
global = true,
)]
extensions: Vec<(String, String)>,
#[arg(
short = 'd',
long = "disable",
value_name = "VALIDATOR",
action = clap::ArgAction::Append,
value_parser = ValueParser::new(parse_validator),
global = true,
)]
disabled_validators: Vec<String>,
#[arg(
short = 'e',
long = "enable",
value_name = "VALIDATOR",
action = clap::ArgAction::Append,
value_parser = ValueParser::new(parse_validator),
global = true,
)]
enabled_validators: Vec<String>,
#[arg(
long = "ignore",
value_name = "GLOBS",
action = clap::ArgAction::Append,
global = true,
)]
pub ignore: Vec<String>,
#[arg(value_name = "GLOBS")]
pub globs: Vec<String>,
#[command(subcommand)]
pub command: Option<SubCommand>,
}
#[derive(clap::Subcommand, Debug, Clone)]
pub enum SubCommand {
List {
#[arg(value_name = "GLOBS")]
globs: Vec<String>,
},
}
impl Args {
pub fn extensions(&self) -> HashMap<OsString, OsString> {
self.extensions
.iter()
.map(|(key, val)| (OsString::from(key), OsString::from(val)))
.collect()
}
pub fn disabled_validators(&self) -> HashSet<&str> {
self.disabled_validators.iter().map(AsRef::as_ref).collect()
}
pub fn enabled_validators(&self) -> HashSet<&str> {
self.enabled_validators.iter().map(AsRef::as_ref).collect()
}
pub fn globs(&self) -> anyhow::Result<GlobSet> {
let mut builder = GlobSetBuilder::new();
let mut globs = self.globs.clone();
if let Some(SubCommand::List { globs: list_globs }) = &self.command {
globs.extend(list_globs.clone());
}
for glob_str in &globs {
let glob = Glob::new(glob_str)
.with_context(|| format!("Invalid glob pattern: {}", glob_str))?;
builder.add(glob);
}
builder.build().context("Failed to build glob set")
}
pub fn ignored_globs(&self) -> anyhow::Result<GlobSet> {
let mut builder = GlobSetBuilder::new();
for glob_str in &self.ignore {
let glob = Glob::new(glob_str)
.with_context(|| format!("Invalid ignore glob pattern: {}", glob_str))?;
builder.add(glob);
}
builder.build().context("Failed to build ignore glob set")
}
pub fn validate(&self, supported_extensions: &HashSet<&OsString>) -> anyhow::Result<()> {
for (key, val) in &self.extensions {
if !supported_extensions.contains(&OsString::from(val)) {
anyhow::bail!("Unsupported extension mapping: {key}={val}");
}
}
if !self.disabled_validators.is_empty() && !self.enabled_validators.is_empty() {
anyhow::bail!("--enable and --disable flags must not be set at the same time");
}
Ok(())
}
}
fn parse_extensions(s: &str) -> anyhow::Result<(String, String)> {
s.split_once('=')
.map(|(key, value)| (key.trim().to_string(), value.trim().to_string()))
.with_context(|| format!("Invalid KEY=VALUE format: {s}"))
}
fn parse_validator(value: &str) -> anyhow::Result<String> {
let validators: Vec<&str> = validators::DETECTOR_FACTORIES
.iter()
.map(|(validator_name, _)| *validator_name)
.collect();
validators
.contains(&value)
.then(|| value.trim().to_string())
.with_context(|| {
format!(
"Unknown validator: {value}. Available validators: {}",
validators.join(", ")
)
})
}