use std::path::PathBuf;
#[derive(clap::Args, Debug)]
#[command(
about = "Analyze a git repository",
long_about = "Analyze a git repository for health, coupling, team dynamics, evolution patterns, \
and git hygiene.\n\n\
By default, all 5 categories are computed over the last 6 months. Use category \
flags to run a subset, and --since/--until/--all to control the time window.\n\n\
Output defaults to a colored CLI report. Use --json for machine consumption \
or --html for an interactive single-file report with charts and tables.",
after_long_help = "\
METRICS:\n\
Health (35%) Bus factor, churn hotspots, stale code, file complexity\n\
Coupling (20%) Afferent/efferent coupling, circular deps, change coupling smells\n\
Evolution (20%) Growth trend, refactoring ratio, code age, commit cadence\n\
Git Hygiene (15%) Commit message quality, history cleanliness, gitignore coverage\n\
Team (10%) Knowledge distribution (Gini), contributor activity, ownership, silos, merges\n\
Dependencies (0%) Dependency drift and vulnerabilities — enable with --deps\n\
\n\
TIME WINDOW FORMATS:\n\
Relative: 3months, 6months, 30days, 1year\n\
Absolute: 2024-01-01 (ISO 8601 date)\n\
\n\
EXAMPLES:\n \
barad-dur analyze . # all categories, last 6 months\n \
barad-dur analyze . -v # show per-metric scores\n \
barad-dur analyze . -vv # also show raw values\n \
barad-dur analyze . --json --pretty # pretty-printed JSON\n \
barad-dur analyze . --html -o report.html # interactive HTML report\n \
barad-dur analyze . --health --team # only Health + Team\n \
barad-dur analyze . --since 3months # custom time window\n \
barad-dur analyze . --since 2024-01-01 --until 2024-12-31\n \
barad-dur analyze . --all # full history\n \
barad-dur analyze https://github.com/user/repo # remote repository\n \
barad-dur analyze https://github.com/user/repo --token ghp_xxx # with GitHub API data"
)]
pub struct AnalyzeArgs {
#[arg(default_value = ".")]
pub target: String,
#[arg(long, help_heading = "Remote")]
pub token: Option<String>,
#[arg(long, help_heading = "Category Filters")]
pub health: bool,
#[arg(long, help_heading = "Category Filters")]
pub team: bool,
#[arg(long, help_heading = "Category Filters")]
pub evolution: bool,
#[arg(long, help_heading = "Category Filters")]
pub hygiene: bool,
#[arg(long, help_heading = "Category Filters")]
pub deps: bool,
#[arg(long, help_heading = "Time Window")]
pub since: Option<String>,
#[arg(long, help_heading = "Time Window")]
pub until: Option<String>,
#[arg(long, help_heading = "Time Window")]
pub all: bool,
#[arg(long, help_heading = "Output Format")]
pub json: bool,
#[arg(long, help_heading = "Output Format")]
pub html: bool,
#[arg(long, help_heading = "Output Format")]
pub open: bool,
#[arg(long, help_heading = "Output Format")]
pub trend: bool,
#[arg(long, help_heading = "Output Format")]
pub pretty: bool,
#[arg(short, long, help_heading = "Output Format")]
pub output: Option<PathBuf>,
#[arg(short, long, action = clap::ArgAction::Count, help_heading = "Output Format")]
pub verbose: u8,
#[arg(long, help_heading = "Filtering", action = clap::ArgAction::Append)]
pub exclude: Vec<String>,
#[arg(long, help_heading = "Filtering", action = clap::ArgAction::Append)]
pub exclude_ext: Vec<String>,
#[arg(long, help_heading = "Filtering", num_args = 0..=1, default_missing_value = "true")]
pub no_default_excludes: Option<bool>,
#[arg(long, help_heading = "Performance", num_args = 0..=1, default_missing_value = "true")]
pub skip_blame: Option<bool>,
#[arg(long, help_heading = "Cache")]
pub no_cache: bool,
#[arg(long, help_heading = "Cache")]
pub cache_only: bool,
}
impl AnalyzeArgs {
pub fn all_categories(&self) -> bool {
!self.health && !self.team && !self.evolution && !self.hygiene
}
pub fn should_run(&self, category: &str) -> bool {
if self.all_categories() {
return category != "deps"; }
match category {
"health" => self.health,
"team" => self.team,
"evolution" => self.evolution,
"hygiene" => self.hygiene,
"deps" => self.deps,
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::{Cli, Commands};
use clap::Parser;
fn parse(args: &[&str]) -> AnalyzeArgs {
let cli = Cli::parse_from(args);
match cli.command {
Commands::Analyze(a) => a,
_ => panic!("expected Analyze command"),
}
}
#[test]
fn default_args() {
let args = parse(&["barad-dur", "analyze", "."]);
assert_eq!(args.target, ".");
assert!(args.all_categories());
assert!(!args.json);
assert!(!args.no_cache);
assert_eq!(args.verbose, 0);
}
#[test]
fn category_flag_health_only() {
let args = parse(&["barad-dur", "analyze", ".", "--health"]);
assert!(args.health);
assert!(!args.team);
assert!(!args.all_categories());
assert!(args.should_run("health"));
assert!(!args.should_run("team"));
}
#[test]
fn since_flag() {
let args = parse(&["barad-dur", "analyze", ".", "--since", "3months"]);
assert_eq!(args.since, Some("3months".to_string()));
}
#[test]
fn date_range_flags() {
let args = parse(&[
"barad-dur",
"analyze",
".",
"--since",
"2024-01-01",
"--until",
"2024-06-30",
]);
assert_eq!(args.since, Some("2024-01-01".to_string()));
assert_eq!(args.until, Some("2024-06-30".to_string()));
}
#[test]
fn all_flag() {
let args = parse(&["barad-dur", "analyze", ".", "--all"]);
assert!(args.all);
}
#[test]
fn json_flag() {
let args = parse(&["barad-dur", "analyze", ".", "--json"]);
assert!(args.json);
}
#[test]
fn html_flag() {
let args = parse(&["barad-dur", "analyze", ".", "--html"]);
assert!(args.html);
assert!(!args.json);
}
#[test]
fn json_pretty_flags() {
let args = parse(&["barad-dur", "analyze", ".", "--json", "--pretty"]);
assert!(args.json);
assert!(args.pretty);
}
#[test]
fn output_file() {
let args = parse(&["barad-dur", "analyze", ".", "-o", "report.json"]);
assert_eq!(args.output, Some(PathBuf::from("report.json")));
}
#[test]
fn verbosity_single() {
let args = parse(&["barad-dur", "analyze", ".", "-v"]);
assert_eq!(args.verbose, 1);
}
#[test]
fn verbosity_double() {
let args = parse(&["barad-dur", "analyze", ".", "-vv"]);
assert_eq!(args.verbose, 2);
}
#[test]
fn no_cache_flag() {
let args = parse(&["barad-dur", "analyze", ".", "--no-cache"]);
assert!(args.no_cache);
}
#[test]
fn cache_only_flag() {
let args = parse(&["barad-dur", "analyze", ".", "--cache-only"]);
assert!(args.cache_only);
}
#[test]
fn open_flag() {
let args = parse(&["barad-dur", "analyze", ".", "--open"]);
assert!(args.open);
}
#[test]
fn open_with_output() {
let args = parse(&["barad-dur", "analyze", ".", "--open", "-o", "report.html"]);
assert!(args.open);
assert_eq!(args.output, Some(PathBuf::from("report.html")));
}
#[test]
fn skip_blame_flag() {
let args = parse(&["barad-dur", "analyze", ".", "--skip-blame"]);
assert_eq!(args.skip_blame, Some(true));
}
#[test]
fn skip_blame_absent() {
let args = parse(&["barad-dur", "analyze", "."]);
assert_eq!(args.skip_blame, None);
}
#[test]
fn exclude_flag_single() {
let args = parse(&["barad-dur", "analyze", ".", "--exclude", "*.resx"]);
assert_eq!(args.exclude, vec!["*.resx"]);
assert_eq!(args.no_default_excludes, None);
}
#[test]
fn exclude_flag_multiple() {
let args = parse(&[
"barad-dur",
"analyze",
".",
"--exclude",
"*.resx",
"--exclude",
"**/i18n/**",
]);
assert_eq!(args.exclude, vec!["*.resx", "**/i18n/**"]);
}
#[test]
fn no_default_excludes_flag() {
let args = parse(&["barad-dur", "analyze", ".", "--no-default-excludes"]);
assert_eq!(args.no_default_excludes, Some(true));
}
#[test]
fn exclude_ext_flag_single() {
let args = parse(&["barad-dur", "analyze", ".", "--exclude-ext", "jar"]);
assert_eq!(args.exclude_ext, vec!["jar"]);
}
#[test]
fn exclude_ext_flag_multiple() {
let args = parse(&[
"barad-dur",
"analyze",
".",
"--exclude-ext",
"jar",
"--exclude-ext",
"min.js",
]);
assert_eq!(args.exclude_ext, vec!["jar", "min.js"]);
}
#[test]
fn exclude_ext_absent_by_default() {
let args = parse(&["barad-dur", "analyze", "."]);
assert!(args.exclude_ext.is_empty());
}
#[test]
fn all_categories_when_none_selected() {
let args = parse(&["barad-dur", "analyze", "."]);
assert!(args.should_run("health"));
assert!(args.should_run("team"));
assert!(args.should_run("evolution"));
assert!(args.should_run("hygiene"));
}
#[test]
fn deps_flag() {
let args = parse(&["barad-dur", "analyze", ".", "--deps"]);
assert!(args.deps);
}
#[test]
fn deps_not_in_all_categories() {
let args = parse(&["barad-dur", "analyze", "."]);
assert!(!args.deps);
}
}