use anyhow::Result;
use std::io::IsTerminal;
use std::path::PathBuf;
use crate::cli::CouplingArgs;
use crate::coupling::builder::build_coupling_report;
use crate::coupling::collector::{collect_snapshots, CollectionResult};
use crate::coupling::dependency::{analyze_dependency_coupling, DependencyAnalysis};
use crate::coupling::discovery::discover_repos;
use crate::coupling::scorer::score_coupling_pairs;
use crate::coupling::team::analyze_team_coupling;
use crate::coupling::temporal::{analyze_temporal_coupling, TemporalCouplingPair};
use crate::coupling::types::CouplingPair;
use crate::coupling::RepoInfo;
use crate::renderer;
use crate::renderer::coupling_cli::render_coupling_table;
use crate::renderer::coupling_json::render_coupling_json;
use crate::runner;
use indicatif::{ProgressBar, ProgressStyle};
pub fn run_coupling(args: CouplingArgs) -> Result<()> {
let is_tty = std::io::stderr().is_terminal();
let make_spinner = |msg: &str| -> ProgressBar {
if !is_tty {
return ProgressBar::hidden();
}
let sp = ProgressBar::new_spinner();
sp.set_style(
ProgressStyle::default_spinner()
.template(" {spinner:.cyan} {msg}")
.unwrap(),
);
sp.set_message(msg.to_string());
sp.enable_steady_tick(std::time::Duration::from_millis(80));
sp
};
let sp = make_spinner("Discovering repositories...");
let discovery = discover_repos(&args.root_dir);
sp.finish_with_message(format!(
"Discovered {} repos (skipped {})",
discovery.discovered.len(),
discovery.skipped.len()
));
if discovery.discovered.len() < 2 {
eprintln!(
"Found {} repos under {}. Need at least 2 for coupling analysis.",
discovery.discovered.len(),
args.root_dir.display()
);
return Ok(());
}
let sp = make_spinner(&format!(
"Collecting snapshots from {} repos...",
discovery.discovered.len()
));
let config = crate::coupling::CouplingConfig {
root_dir: args.root_dir.clone(),
..Default::default()
};
let collection = collect_snapshots(&discovery.discovered, &config);
sp.finish_with_message(format!(
"Collected {} snapshots ({} failed)",
collection.snapshots.len(),
collection.failed.len()
));
let sp = make_spinner("Analyzing temporal coupling...");
let window = std::time::Duration::from_secs(24 * 60 * 60);
let temporal_pairs = analyze_temporal_coupling(&collection.snapshots, window);
sp.finish_with_message(format!("Temporal: {} coupled pairs", temporal_pairs.len()));
let sp = make_spinner("Analyzing team coupling...");
let team_pairs = analyze_team_coupling(&collection.snapshots);
sp.finish_with_message(format!("Team: {} pairs", team_pairs.len()));
let sp = make_spinner("Analyzing dependency coupling...");
let repo_paths: Vec<(String, std::path::PathBuf)> = collection
.snapshots
.iter()
.map(|(name, snap)| (name.clone(), snap.path.clone()))
.collect();
let dep_analysis = analyze_dependency_coupling(&repo_paths);
sp.finish_with_message(format!("Dependencies: {} pairs", dep_analysis.pairs.len()));
let sp = make_spinner("Computing combined scores...");
let combined_pairs = score_coupling_pairs(&temporal_pairs, &team_pairs, &dep_analysis);
sp.finish_with_message(format!("Scored {} pairs", combined_pairs.len()));
render_coupling_output(
&args,
&collection,
&combined_pairs,
&dep_analysis,
&temporal_pairs,
)?;
Ok(())
}
fn render_coupling_output(
args: &CouplingArgs,
collection: &CollectionResult,
combined_pairs: &[CouplingPair],
dep_analysis: &DependencyAnalysis,
temporal_pairs: &[TemporalCouplingPair],
) -> Result<()> {
let use_html = args.html || args.open;
if args.json || use_html {
let repos: Vec<RepoInfo> = collection
.snapshots
.iter()
.map(|(name, snap)| RepoInfo {
name: name.clone(),
path: snap.path.clone(),
commit_count: snap.commit_count,
author_count: snap.author_count,
})
.collect();
let report = build_coupling_report(
repos,
combined_pairs.to_vec(),
dep_analysis.blast_radius.clone(),
args.min_score,
);
if use_html {
let output = renderer::coupling_html::render_coupling_html(&report);
let path = if let Some(ref p) = args.output {
std::fs::write(p, &output)?;
p.clone()
} else {
let default_path = PathBuf::from("coupling-report.html");
std::fs::write(&default_path, &output)?;
default_path
};
eprintln!("Report written to {}", path.display());
if args.open {
runner::open_in_browser(&path)?;
}
} else {
let output = render_coupling_json(&report, args.pretty);
if let Some(path) = &args.output {
std::fs::write(path, &output)?;
eprintln!("Report written to {}", path.display());
} else {
print!("{}", output);
}
}
} else {
let output = render_coupling_table(temporal_pairs);
if let Some(path) = &args.output {
std::fs::write(path, &output)?;
eprintln!("Report written to {}", path.display());
} else {
print!("{}", output);
}
}
Ok(())
}