pub async fn handle_analyze_churn(
project_path: PathBuf,
days: u32,
format: crate::models::churn::ChurnOutputFormat,
output: Option<PathBuf>,
top_files: usize,
) -> Result<()> {
use crate::services::git_analysis::GitAnalysisService;
eprintln!("📊 Analyzing code churn for the last {days} days...");
let mut analysis = GitAnalysisService::analyze_code_churn(&project_path, days)
.map_err(|e| anyhow::anyhow!("Churn analysis failed: {e}"))?;
eprintln!("✅ Analyzed {} files with changes", analysis.files.len());
apply_churn_file_filtering(&mut analysis, top_files);
let content = format_churn_content(&analysis, format)?;
write_churn_output(content, output).await?;
Ok(())
}
fn format_churn_as_json(analysis: &crate::models::churn::CodeChurnAnalysis) -> Result<String> {
Ok(serde_json::to_string_pretty(analysis)?)
}
pub fn format_churn_as_summary(
analysis: &crate::models::churn::CodeChurnAnalysis,
) -> Result<String> {
let mut output = String::new();
write_summary_header(&mut output, analysis)?;
write_summary_top_files(&mut output, analysis)?;
write_summary_hotspot_files(&mut output, &analysis.summary)?;
write_summary_stable_files(&mut output, &analysis.summary)?;
write_summary_top_contributors(&mut output, &analysis.summary)?;
Ok(output)
}
fn write_summary_header(
output: &mut String,
analysis: &crate::models::churn::CodeChurnAnalysis,
) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(output, "{}{}Code Churn Analysis Summary{}\n", c::BOLD, c::UNDERLINE, c::RESET)?;
writeln!(output, " {}Period:{} {}{}{}", c::BOLD, c::RESET, c::BOLD_WHITE, analysis.period_days, c::RESET)?;
writeln!(
output,
" {}Total commits:{} {}{}{}",
c::BOLD, c::RESET, c::BOLD_WHITE, analysis.summary.total_commits, c::RESET
)?;
writeln!(
output,
" {}Files changed:{} {}{}{}",
c::BOLD, c::RESET, c::BOLD_WHITE, analysis.summary.total_files_changed, c::RESET
)?;
Ok(())
}
fn write_summary_top_files(
output: &mut String,
analysis: &crate::models::churn::CodeChurnAnalysis,
) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
if !analysis.files.is_empty() {
writeln!(output, "\n{}Top Files by Churn{}\n", c::BOLD, c::RESET)?;
let mut sorted_files: Vec<_> = analysis.files.iter().collect();
sorted_files.sort_unstable_by(|a, b| {
match b.commit_count.cmp(&a.commit_count) {
std::cmp::Ordering::Equal => b
.churn_score
.partial_cmp(&a.churn_score)
.unwrap_or(std::cmp::Ordering::Equal),
other => other,
}
});
for (i, file) in sorted_files.iter().take(10).enumerate() {
let filename = file
.path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&file.relative_path);
let score_color = if file.churn_score > 0.5 {
c::RED
} else if file.churn_score > 0.3 {
c::YELLOW
} else {
c::GREEN
};
writeln!(
output,
" {}. {}{}{} - {}{}{} commits, {} authors, score: {}{:.2}{}",
i + 1,
c::CYAN, filename, c::RESET,
c::BOLD_WHITE, file.commit_count, c::RESET,
file.unique_authors.len(),
score_color, file.churn_score, c::RESET
)?;
}
}
Ok(())
}
fn write_summary_hotspot_files(
output: &mut String,
summary: &crate::models::churn::ChurnSummary,
) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
if !summary.hotspot_files.is_empty() {
writeln!(output, "\n{}Hotspot Files (High Churn){}\n", c::BOLD, c::RESET)?;
for (i, file) in summary.hotspot_files.iter().take(10).enumerate() {
writeln!(output, " {}. {}{}{}", i + 1, c::CYAN, file.display(), c::RESET)?;
}
}
Ok(())
}
fn write_summary_stable_files(
output: &mut String,
summary: &crate::models::churn::ChurnSummary,
) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
if !summary.stable_files.is_empty() {
writeln!(output, "\n{}Stable Files (Low Churn){}\n", c::BOLD, c::RESET)?;
for (i, file) in summary.stable_files.iter().take(10).enumerate() {
writeln!(output, " {}. {}{}{}", i + 1, c::CYAN, file.display(), c::RESET)?;
}
}
Ok(())
}
fn write_summary_top_contributors(
output: &mut String,
summary: &crate::models::churn::ChurnSummary,
) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
if !summary.author_contributions.is_empty() {
writeln!(output, "\n{}Top Contributors{}\n", c::BOLD, c::RESET)?;
let mut authors: Vec<_> = summary.author_contributions.iter().collect();
authors.sort_unstable_by(|a, b| b.1.cmp(a.1));
for (author, files) in authors.iter().take(10) {
writeln!(output, " {}{}{}: {}{}{} files", c::CYAN, author, c::RESET, c::BOLD_WHITE, files, c::RESET)?;
}
}
Ok(())
}
pub fn format_churn_as_markdown(
analysis: &crate::models::churn::CodeChurnAnalysis,
) -> Result<String> {
let mut output = String::new();
write_markdown_header(&mut output, analysis)?;
write_markdown_summary_table(&mut output, &analysis.summary)?;
write_markdown_file_details(&mut output, &analysis.files)?;
write_markdown_author_contributions(&mut output, &analysis.summary)?;
write_markdown_recommendations(&mut output)?;
Ok(output)
}
fn write_markdown_header(
output: &mut String,
analysis: &crate::models::churn::CodeChurnAnalysis,
) -> Result<()> {
use std::fmt::Write;
writeln!(output, "# Code Churn Analysis Report\n")?;
writeln!(
output,
"Generated: {}",
analysis.generated_at.format("%Y-%m-%d %H:%M:%S UTC")
)?;
writeln!(output, "Repository: {}", analysis.repository_root.display())?;
writeln!(output, "Analysis Period: {} days\n", analysis.period_days)?;
Ok(())
}
fn write_markdown_summary_table(
output: &mut String,
summary: &crate::models::churn::ChurnSummary,
) -> Result<()> {
write_markdown_table_header(output)?;
write_summary_data_rows(output, summary)?;
Ok(())
}
fn write_markdown_table_header(output: &mut String) -> Result<()> {
use std::fmt::Write;
writeln!(output, "## Summary Statistics\n")?;
writeln!(output, "| Metric | Value |")?;
writeln!(output, "|--------|-------|")?;
Ok(())
}
fn write_summary_data_rows(
output: &mut String,
summary: &crate::models::churn::ChurnSummary,
) -> Result<()> {
write_commits_row(output, summary.total_commits)?;
write_files_changed_row(output, summary.total_files_changed)?;
write_hotspot_files_row(output, summary.hotspot_files.len())?;
write_stable_files_row(output, summary.stable_files.len())?;
write_authors_row(output, summary.author_contributions.len())?;
Ok(())
}
fn write_commits_row(output: &mut String, total_commits: usize) -> Result<()> {
use std::fmt::Write;
writeln!(output, "| Total Commits | {total_commits} |")?;
Ok(())
}
fn write_files_changed_row(output: &mut String, files_changed: usize) -> Result<()> {
use std::fmt::Write;
writeln!(output, "| Files Changed | {files_changed} |")?;
Ok(())
}
fn write_hotspot_files_row(output: &mut String, hotspot_count: usize) -> Result<()> {
use std::fmt::Write;
writeln!(output, "| Hotspot Files | {hotspot_count} |")?;
Ok(())
}
fn write_stable_files_row(output: &mut String, stable_count: usize) -> Result<()> {
use std::fmt::Write;
writeln!(output, "| Stable Files | {stable_count} |")?;
Ok(())
}
fn write_authors_row(output: &mut String, author_count: usize) -> Result<()> {
use std::fmt::Write;
writeln!(output, "| Contributing Authors | {author_count} |")?;
Ok(())
}
fn write_markdown_file_details(
output: &mut String,
files: &[crate::models::churn::FileChurnMetrics],
) -> Result<()> {
use std::fmt::Write;
if !files.is_empty() {
writeln!(output, "\n## File Churn Details\n")?;
writeln!(
output,
"| File | Commits | Authors | Additions | Deletions | Churn Score | Last Modified |"
)?;
writeln!(
output,
"|------|---------|---------|-----------|-----------|-------------|----------------|"
)?;
let mut sorted_files = files.to_vec();
sorted_files.sort_unstable_by(|a, b| {
b.churn_score
.partial_cmp(&a.churn_score)
.expect("NaN values should not occur in churn scores")
});
for file in sorted_files.iter().take(20) {
writeln!(
output,
"| {} | {} | {} | {} | {} | {:.2} | {} |",
file.relative_path,
file.commit_count,
file.unique_authors.len(),
file.additions,
file.deletions,
file.churn_score,
file.last_modified.format("%Y-%m-%d")
)?;
}
}
Ok(())
}
fn write_markdown_author_contributions(
output: &mut String,
summary: &crate::models::churn::ChurnSummary,
) -> Result<()> {
use std::fmt::Write;
if !summary.author_contributions.is_empty() {
writeln!(output, "\n## Author Contributions\n")?;
writeln!(output, "| Author | Files Modified |")?;
writeln!(output, "|--------|----------------|")?;
let mut authors: Vec<_> = summary.author_contributions.iter().collect();
authors.sort_unstable_by(|a, b| b.1.cmp(a.1));
for (author, count) in authors.iter().take(15) {
writeln!(output, "| {author} | {count} |")?;
}
}
Ok(())
}
fn write_markdown_recommendations(output: &mut String) -> Result<()> {
use std::fmt::Write;
writeln!(output, "\n## Recommendations\n")?;
writeln!(
output,
"1. **Review Hotspot Files**: Files with high churn scores may benefit from refactoring"
)?;
writeln!(
output,
"2. **Add Tests**: High-churn files should have comprehensive test coverage"
)?;
writeln!(
output,
"3. **Code Review**: Frequently modified files may indicate design issues"
)?;
writeln!(
output,
"4. **Documentation**: Document the reasons for frequent changes in hotspot files"
)?;
Ok(())
}
pub fn format_churn_as_csv(analysis: &crate::models::churn::CodeChurnAnalysis) -> Result<String> {
use std::fmt::Write;
let mut output = String::new();
writeln!(&mut output, "file_path,relative_path,commit_count,unique_authors,additions,deletions,churn_score,last_modified,first_seen")?;
for file in &analysis.files {
writeln!(
&mut output,
"{},{},{},{},{},{},{:.3},{},{}",
file.path.display(),
file.relative_path,
file.commit_count,
file.unique_authors.len(),
file.additions,
file.deletions,
file.churn_score,
file.last_modified.to_rfc3339(),
file.first_seen.to_rfc3339()
)?;
}
Ok(output)
}
pub async fn write_churn_output(content: String, output: Option<PathBuf>) -> Result<()> {
if let Some(output_path) = output {
tokio::fs::write(&output_path, &content).await?;
eprintln!("✅ Churn analysis written to: {}", output_path.display());
} else {
println!("{content}");
}
Ok(())
}
fn apply_churn_file_filtering(
analysis: &mut crate::models::churn::CodeChurnAnalysis,
top_files: usize,
) {
if top_files > 0 && analysis.files.len() > top_files {
analysis
.files
.sort_unstable_by(|a, b| b.commit_count.cmp(&a.commit_count));
analysis.files.truncate(top_files);
}
}
fn format_churn_content(
analysis: &crate::models::churn::CodeChurnAnalysis,
format: crate::models::churn::ChurnOutputFormat,
) -> Result<String> {
use crate::models::churn::ChurnOutputFormat;
match format {
ChurnOutputFormat::Json => format_churn_as_json(analysis),
ChurnOutputFormat::Summary => format_churn_as_summary(analysis),
ChurnOutputFormat::Markdown => format_churn_as_markdown(analysis),
ChurnOutputFormat::Csv => format_churn_as_csv(analysis),
}
}