#![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::deployments::DeploymentsCollectArgs;
use crate::commands::dora::DoraArgs;
use crate::commands::incidents::IncidentsCollectArgs;
use crate::commands::install::InstallArgs;
use crate::commands::override_cmd::OverrideArgs;
use crate::commands::pr_metrics::PrMetricsArgs;
use crate::commands::rules::RulesArgs;
#[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),
Rules(RulesArgs),
Deployments(DeploymentsSubcommandArgs),
Incidents(IncidentsSubcommandArgs),
Dora(DoraArgs),
}
#[derive(Args, Debug)]
pub struct DeploymentsSubcommandArgs {
#[command(subcommand)]
pub subcommand: DeploymentsSubcommand,
}
#[derive(Subcommand, Debug)]
pub enum DeploymentsSubcommand {
Collect(DeploymentsCollectArgs),
}
#[derive(Args, Debug)]
pub struct IncidentsSubcommandArgs {
#[command(subcommand)]
pub subcommand: IncidentsSubcommand,
}
#[derive(Subcommand, Debug)]
pub enum IncidentsSubcommand {
Collect(IncidentsCollectArgs),
}
#[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 skip_tag_reachability: 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,
#[arg(long)]
pub backfill_complexity: bool,
#[arg(long, short = 'f', default_value_t = false)]
pub force: bool,
#[arg(long, value_name = "DATE")]
pub since: Option<String>,
#[arg(long, default_value_t = false)]
pub no_external: 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()
))
}
static HELP: std::sync::LazyLock<trusty_common::help::HelpConfig> =
std::sync::LazyLock::new(|| {
trusty_common::help::load_help(include_str!("../help.yaml"))
.expect("tga help.yaml is bundled and valid")
});
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let argv: Vec<String> = std::env::args().collect();
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
e.print().ok();
if matches!(
e.kind(),
clap::error::ErrorKind::InvalidSubcommand | clap::error::ErrorKind::UnknownArgument
) {
trusty_common::help::print_suggestion_hint(&argv, &HELP);
}
std::process::exit(e.exit_code());
}
};
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::Rules(args) => commands::rules::run(config, &db, args)?,
Commands::Deployments(args) => match args.subcommand {
DeploymentsSubcommand::Collect(a) => {
commands::deployments::run(config, &mut db, a).await?
}
},
Commands::Incidents(args) => match args.subcommand {
IncidentsSubcommand::Collect(a) => commands::incidents::run(config, &mut db, a)?,
},
Commands::Dora(args) => commands::dora::run(config, &mut db, args)?,
Commands::Install(_) => unreachable!("install dispatched above"),
}
Ok(())
}