use clap::Parser;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
#[derive(clap::Parser)]
#[expect(
clippy::struct_excessive_bools,
reason = "the bools are clap CLI flags, not an accumulating design"
)]
#[command(
name = "echoes",
bin_name = "cargo echoes",
version,
about = "Find duplicated Rust doctests, functions and context blocks across a workspace or GitHub organization"
)]
struct Args {
#[arg(short, long)]
package: Option<String>,
#[arg(
long,
value_name = "ORG",
num_args = 0..=1,
default_missing_value = "",
action = clap::ArgAction::Append
)]
organization: Option<Vec<String>>,
#[arg(long, value_name = "OWNER/REPO")]
repository: Vec<String>,
#[arg(long)]
all_targets: bool,
#[arg(long)]
json: bool,
#[arg(short = 't', long, value_name = "N")]
threshold: Option<usize>,
#[arg(long, value_name = "N")]
min_tokens: Option<usize>,
#[arg(long)]
functions: bool,
#[arg(long, value_name = "N")]
fn_min_tokens: Option<usize>,
#[arg(long)]
context_blocks: bool,
#[arg(long, value_name = "N")]
context_min_tokens: Option<usize>,
#[arg(long, value_name = "SCOPE")]
context_select: Option<echoes::ContextScope>,
#[arg(long, value_name = "SCOPE")]
context_search: Option<echoes::ContextScope>,
#[arg(long, value_name = "N")]
context_dependency_depth: Option<usize>,
#[arg(long)]
context_dev_dependencies: bool,
#[arg(long)]
context_build_dependencies: bool,
#[arg(long)]
context_all_optional_dependencies: bool,
#[arg(long, value_delimiter = ',', value_name = "FEATURES")]
context_features: Option<Vec<String>>,
#[arg(long)]
context_all_features: bool,
#[arg(long)]
context_no_default_features: bool,
#[arg(long, value_name = "TRIPLE")]
context_target: Option<String>,
#[arg(long, value_name = "PATH")]
context_cache_dir: Option<String>,
#[arg(long, value_name = "BOOL")]
context_check_latest: Option<bool>,
#[arg(long)]
context_fail_on_reuse: bool,
#[arg(long, value_name = "GLOB")]
exclude: Vec<String>,
#[arg(long, value_name = "PATH")]
config: Option<PathBuf>,
#[arg(long)]
no_fail: bool,
#[arg(short, long)]
verbose: bool,
#[arg(long)]
github: bool,
}
fn main() -> ExitCode {
let mut args: Vec<String> = std::env::args().collect();
if args.get(1).is_some_and(|a| a == "echoes") {
args.remove(1);
}
match run(Args::parse_from(args)) {
Ok(code) => code,
Err(err) => {
eprintln!("echoes: {err}");
ExitCode::from(2)
}
}
}
fn run(args: Args) -> echoes::Result<ExitCode> {
let mut scan = echoes::Echoes::default();
if let Some(package) = args.package {
scan = scan.package(package);
}
if args.all_targets {
scan = scan.all_targets();
}
if let Some(threshold) = args.threshold {
scan = scan.threshold(threshold);
}
if let Some(min_tokens) = args.min_tokens {
scan = scan.min_tokens(min_tokens);
}
if args.functions {
scan = scan.functions();
}
if let Some(n) = args.fn_min_tokens {
scan = scan.fn_min_tokens(n);
}
if args.context_blocks {
scan = scan.context_blocks();
}
if let Some(n) = args.context_min_tokens {
scan = scan.context_min_tokens(n);
}
if let Some(scope) = args.context_select {
scan = scan.context_select(scope);
}
if let Some(scope) = args.context_search {
scan = scan.context_search(scope);
}
for pattern in &args.exclude {
scan = scan.exclude(pattern)?;
}
scan = scan.context_dependencies(echoes::DependencySearch {
depth: args.context_dependency_depth,
include_dev: args.context_dev_dependencies.then_some(true),
include_build: args.context_build_dependencies.then_some(true),
all_optional: args.context_all_optional_dependencies.then_some(true),
features: args.context_features,
all_features: args.context_all_features.then_some(true),
no_default_features: args.context_no_default_features.then_some(true),
target: args.context_target,
cache_dir: args.context_cache_dir,
check_latest: args.context_check_latest,
fail_on_findings: args.context_fail_on_reuse.then_some(true),
});
if let Some(config) = args.config {
scan = scan.config(config)?;
}
let report = if args.organization.is_some() || !args.repository.is_empty() {
let owners = args
.organization
.unwrap_or_default()
.into_iter()
.map(|name| {
if name.is_empty() {
echoes::repository_owner(".")
} else {
Ok(name)
}
})
.collect::<Result<Vec<_>, _>>()?;
scan.run_selected(&owners, &args.repository)?
} else {
scan.run(Path::new("."))?
};
let out = if args.json {
echoes::json(&report)
} else {
let mut out = echoes::human(&report, args.verbose);
if args.github {
out.push('\n');
out.push_str(&echoes::annotations(&report, args.no_fail));
}
out
};
println!("{out}");
Ok(echoes::exit_code(&report, args.no_fail))
}