use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use tldr_core::quality::hotspots::{analyze_hotspots, HotspotsOptions, HotspotsReport};
use crate::output::{OutputFormat, OutputWriter};
#[derive(Debug, Args)]
pub struct HotspotsArgs {
#[arg(default_value = ".")]
pub path: PathBuf,
#[arg(long, default_value = "365")]
pub days: u32,
#[arg(long, default_value = "20")]
pub top: usize,
#[arg(long)]
pub by_function: bool,
#[arg(long)]
pub show_trend: bool,
#[arg(long, default_value = "3")]
pub min_commits: u32,
#[arg(long, short = 'e')]
pub exclude: Vec<String>,
#[arg(long)]
pub threshold: Option<f64>,
#[arg(long)]
pub since: Option<String>,
#[arg(long, default_value = "90")]
pub recency_halflife: f64,
#[arg(long)]
pub include_bots: bool,
}
impl HotspotsArgs {
pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
let writer = OutputWriter::new(format, quiet);
writer.progress(&format!(
"Analyzing hotspots in {} (last {} days)...",
self.path.display(),
self.days
));
let mut options = HotspotsOptions::new()
.with_days(self.days)
.with_top(self.top)
.with_min_commits(self.min_commits)
.with_by_function(self.by_function)
.with_show_trend(self.show_trend)
.with_exclude(self.exclude.clone());
if let Some(threshold) = self.threshold {
options = options.with_threshold(threshold);
}
if let Some(ref since) = self.since {
options = options.with_since(since.clone());
}
options = options
.with_recency_halflife(self.recency_halflife)
.with_include_bots(self.include_bots);
let report = analyze_hotspots(&self.path, &options)?;
if writer.is_text() {
let text = format_hotspots_text(&report);
writer.write_text(&text)?;
} else {
writer.write(&report)?;
}
Ok(())
}
}
fn format_hotspots_text(report: &HotspotsReport) -> String {
use colored::Colorize;
let mut output = String::new();
output.push_str(&format!(
"Hotspots Analysis ({} files, {} days)\n",
report.summary.total_files_analyzed.to_string().yellow(),
report.metadata.days
));
output.push_str(&format!(
"Total commits: {}, Concentration: {:.1}%\n",
report.summary.total_commits.to_string().cyan(),
report.summary.hotspot_concentration
));
if let Some(bot_filtered) = report.summary.total_bot_commits_filtered {
if bot_filtered > 0 {
output.push_str(&format!(
"Bot commits filtered: {}\n",
bot_filtered.to_string().dimmed()
));
}
}
output.push_str(&format!(
"Mode: {}, Algorithm: v{}\n\n",
if report.metadata.by_function {
"function-level"
} else {
"file-level"
},
report.metadata.algorithm_version
));
for warning in &report.warnings {
output.push_str(&format!("{} {}\n", "Warning:".yellow(), warning));
}
if !report.warnings.is_empty() {
output.push('\n');
}
if !report.hotspots.is_empty() {
output.push_str(
&"Hotspots (high churn + high complexity):\n"
.bold()
.to_string(),
);
if report.metadata.by_function {
output.push_str(&format!(
" {:>3} {:>5} {:>5} {:>5} {:>7} {:>4} {:>8} {:<20} {}\n",
"#", "Score", "Churn", "Cmplx", "Commits", "Cog", "Priority", "Function", "File"
));
for (i, h) in report.hotspots.iter().enumerate() {
let priority = short_priority(&h.recommendation);
output.push_str(&format!(
" {:>3} {:>5.2} {:>5.2} {:>5.2} {:>7} {:>4} {:>8} {:<20} {}\n",
i + 1,
h.hotspot_score,
h.churn_score,
h.complexity_score,
h.commit_count,
h.complexity,
priority,
h.function.as_deref().unwrap_or("-"),
h.file
));
}
} else {
output.push_str(&format!(
" {:>3} {:>5} {:>5} {:>5} {:>7} {:>4} {:>8} {}\n",
"#", "Score", "Churn", "Cmplx", "Commits", "Cog", "Priority", "File"
));
for (i, h) in report.hotspots.iter().enumerate() {
let priority = short_priority(&h.recommendation);
output.push_str(&format!(
" {:>3} {:>5.2} {:>5.2} {:>5.2} {:>7} {:>4} {:>8} {}\n",
i + 1,
h.hotspot_score,
h.churn_score,
h.complexity_score,
h.commit_count,
h.complexity,
priority,
h.file
));
}
}
output.push('\n');
} else {
output.push_str("No hotspots found.\n\n");
}
output.push_str(&format!(
"{}: {}\n",
"Summary".bold(),
report.summary.recommendation
));
output
}
fn short_priority(recommendation: &str) -> &'static str {
if recommendation.starts_with("Critical") {
"Critical"
} else if recommendation.starts_with("High") {
"High"
} else if recommendation.starts_with("Medium") {
"Medium"
} else {
"Monitor"
}
}