#![warn(missing_docs)]
mod commands;
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
use tracing_subscriber::EnvFilter;
use tga::core::config::{Config, ConfigValidator};
use tga::core::db::Database;
use crate::commands::aliases::AliasesArgs;
use crate::commands::backfill::BackfillArgs;
use crate::commands::install::InstallArgs;
use crate::commands::override_cmd::OverrideArgs;
use crate::commands::pr_metrics::PrMetricsArgs;
#[derive(Parser, Debug)]
#[command(
name = "tga",
about = "trusty-git-analytics — developer productivity analytics",
long_about = "trusty-git-analytics — developer productivity analytics.\n\n\
Three-stage pipeline: collect → classify → report. Run `tga analyze` \
for the full pipeline, or invoke each stage individually.\n\n\
Architecture decisions are documented in docs/adr/. See \
docs/adr/README.md for the format and process.",
version,
propagate_version = true
)]
struct Cli {
#[arg(short, long, default_value = "config.yaml", global = true)]
config: PathBuf,
#[arg(short, long, default_value = "tga.db", global = true)]
database: PathBuf,
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
#[arg(long, value_name = "LEVEL", global = true)]
log: Option<LogLevel>,
#[command(subcommand)]
command: Commands,
}
#[derive(Copy, Clone, Debug, ValueEnum)]
#[clap(rename_all = "lower")]
enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
impl From<LogLevel> for tracing::Level {
fn from(l: LogLevel) -> Self {
match l {
LogLevel::Error => tracing::Level::ERROR,
LogLevel::Warn => tracing::Level::WARN,
LogLevel::Info => tracing::Level::INFO,
LogLevel::Debug => tracing::Level::DEBUG,
LogLevel::Trace => tracing::Level::TRACE,
}
}
}
#[derive(Subcommand, Debug)]
enum Commands {
Analyze(AnalyzeArgs),
Collect(CollectArgs),
Classify(ClassifyArgs),
Report(ReportArgs),
PrMetrics(PrMetricsArgs),
Install(InstallArgs),
Aliases(AliasesArgs),
Backfill(BackfillArgs),
Override(OverrideArgs),
}
#[derive(Args, Debug)]
pub struct AnalyzeArgs {
#[arg(long)]
pub skip_collect: bool,
#[arg(long)]
pub skip_classify: bool,
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(long, short = 'f', default_value_t = false)]
pub force: bool,
#[arg(long, value_name = "N", conflicts_with_all = ["from", "to"])]
pub weeks: Option<u32>,
#[arg(long, value_name = "DATE", conflicts_with = "weeks")]
pub from: Option<String>,
#[arg(long, value_name = "DATE", conflicts_with = "weeks")]
pub to: Option<String>,
#[arg(long, default_value_t = false)]
pub no_fetch: bool,
#[arg(long, default_value_t = false)]
pub dry_run: bool,
#[arg(long, default_value_t = false)]
pub validate_only: bool,
#[arg(long, default_value_t = false)]
pub no_validate: bool,
}
#[derive(Args, Debug)]
pub struct CollectArgs {
#[arg(long, value_delimiter = ',')]
pub repos: Vec<String>,
#[arg(long)]
pub since: Option<String>,
#[arg(long)]
pub until: Option<String>,
#[arg(long, value_name = "DATE", conflicts_with = "weeks")]
pub from: Option<String>,
#[arg(long, value_name = "DATE", conflicts_with = "weeks")]
pub to: Option<String>,
#[arg(long, short = 'f', default_value_t = false)]
pub force: bool,
#[arg(long, value_name = "N", conflicts_with_all = ["from", "to"])]
pub weeks: Option<u32>,
#[arg(long, default_value_t = false)]
pub no_fetch: bool,
#[arg(long, default_value_t = false)]
pub dry_run: bool,
#[arg(long, default_value_t = false)]
pub force_refresh_prs: bool,
#[arg(long, default_value_t = false)]
pub validate_only: bool,
#[arg(long, default_value_t = false)]
pub no_validate: bool,
}
#[derive(Args, Debug)]
pub struct ClassifyArgs {
#[arg(long)]
pub rules: Option<PathBuf>,
#[arg(long)]
pub use_llm: bool,
}
#[derive(Args, Debug)]
pub struct ReportArgs {
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(long, value_delimiter = ',')]
pub formats: Vec<String>,
}
fn run_validation(config: &Config, no_validate: bool, validate_only: bool) -> anyhow::Result<bool> {
if no_validate {
if validate_only {
tracing::warn!("--no-validate overrides --validate-only; exiting without checks");
return Ok(true);
}
tracing::debug!("--no-validate: skipping configuration pre-flight checks");
return Ok(false);
}
let errors = ConfigValidator::new(config).validate();
if errors.is_empty() {
if validate_only {
println!("Configuration OK.");
return Ok(true);
}
return Ok(false);
}
eprintln!("Configuration validation found {} error(s):", errors.len());
for e in &errors {
eprintln!(" - {e}");
}
Err(anyhow::anyhow!(
"configuration validation failed ({} error(s)); use --no-validate to skip",
errors.len()
))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let level: tracing::Level = if let Some(l) = cli.log {
l.into()
} else {
match cli.verbose {
0 => tracing::Level::WARN,
1 => tracing::Level::INFO,
2 => tracing::Level::DEBUG,
_ => tracing::Level::TRACE,
}
};
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level.to_string()));
tracing_subscriber::fmt().with_env_filter(env_filter).init();
let config = if cli.config.exists() {
tracing::info!(path = %cli.config.display(), "loading config");
Config::load(&cli.config)?
} else {
tracing::warn!(
"config file {} not found, using defaults",
cli.config.display()
);
Config::default()
};
if let Commands::Install(args) = cli.command {
return commands::install::run(config, args);
}
let should_short_circuit = match &cli.command {
Commands::Analyze(args) => run_validation(&config, args.no_validate, args.validate_only)?,
Commands::Collect(args) => run_validation(&config, args.no_validate, args.validate_only)?,
_ => false,
};
if should_short_circuit {
return Ok(());
}
tracing::info!(path = %cli.database.display(), "opening database");
let mut db = Database::open(&cli.database)?;
match cli.command {
Commands::Analyze(args) => commands::analyze::run(config, &mut db, args).await?,
Commands::Collect(args) => commands::collect::run(config, &mut db, args).await?,
Commands::Classify(args) => commands::classify::run(config, &mut db, args).await?,
Commands::Report(args) => commands::report::run(config, &db, args)?,
Commands::PrMetrics(args) => commands::pr_metrics::run(config, &db, args)?,
Commands::Aliases(args) => commands::aliases::run(config, &mut db, args)?,
Commands::Backfill(args) => commands::backfill::run(config, &mut db, args)?,
Commands::Override(args) => commands::override_cmd::run(config, &mut db, args)?,
Commands::Install(_) => unreachable!("install dispatched above"),
}
Ok(())
}