#![warn(missing_docs)]
mod commands;
use std::path::PathBuf;
use clap::{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::args::{
AnalyzeArgs, ClassifyArgs, CollectArgs, DeploymentsSubcommand, DeploymentsSubcommandArgs,
IncidentsSubcommand, IncidentsSubcommandArgs, ReportArgs,
};
use crate::commands::author::AuthorArgs;
use crate::commands::backfill::BackfillArgs;
use crate::commands::dora::DoraArgs;
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/trusty-git-analytics/decisions/. See \
docs/trusty-git-analytics/decisions/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, global = true)]
database: Option<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 {
Author(AuthorArgs),
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),
}
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")
});
fn main() -> anyhow::Result<()> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let result = runtime.block_on(run());
runtime.shutdown_timeout(std::time::Duration::from_secs(0));
result
}
async fn run() -> 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();
if let Some(info) =
trusty_common::update::check_throttled(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
.await
{
eprintln!("{}", trusty_common::update::notice(&info));
}
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(());
}
let db_path = cli
.database
.or_else(|| config.resolved_database_path())
.unwrap_or_else(|| PathBuf::from("tga.db"));
tracing::info!(path = %db_path.display(), "opening database");
let mut db = Database::open(&db_path)?;
match cli.command {
Commands::Author(args) => commands::author::run(config, &db, args)?,
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).await?,
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(())
}