use anyhow::Result;
use std::fmt::Write;
use std::path::Path;
pub fn format_churn_markdown(analysis: &crate::models::churn::CodeChurnAnalysis) -> Result<String> {
let mut output = String::new();
writeln!(&mut output, "# Code Churn Analysis Report\n")?;
writeln!(
&mut output,
"Generated: {}",
analysis.generated_at.format("%Y-%m-%d %H:%M:%S UTC")
)?;
writeln!(
&mut output,
"Repository: {}",
analysis.repository_root.display()
)?;
writeln!(
&mut output,
"Analysis Period: {} days\n",
analysis.period_days
)?;
write_markdown_summary_table(&mut output, &analysis.summary)?;
write_markdown_file_details(&mut output, &analysis.files)?;
if !analysis.summary.author_contributions.is_empty() {
write_author_contributions(&mut output, &analysis.summary)?;
}
Ok(output)
}
pub fn write_markdown_summary_table(
output: &mut String,
summary: &crate::models::churn::ChurnSummary,
) -> Result<()> {
writeln!(output, "## Summary Statistics\n")?;
writeln!(output, "| Metric | Value |")?;
writeln!(output, "|--------|-------|")?;
writeln!(output, "| Total Commits | {} |", summary.total_commits)?;
writeln!(
output,
"| Files Changed | {} |",
summary.total_files_changed
)?;
writeln!(
output,
"| Hotspot Files | {} |",
summary.hotspot_files.len()
)?;
writeln!(output, "| Stable Files | {} |", summary.stable_files.len())?;
writeln!(
output,
"| Contributing Authors | {} |",
summary.author_contributions.len()
)?;
Ok(())
}
fn write_markdown_file_details(
output: &mut String,
files: &[crate::models::churn::FileChurnMetrics],
) -> Result<()> {
if files.is_empty() {
return Ok(());
}
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_by(|a, b| {
b.churn_score
.partial_cmp(&a.churn_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
for file in sorted_files.iter().take(20) {
write_file_row(output, file)?;
}
Ok(())
}
fn write_file_row(
output: &mut String,
file: &crate::models::churn::FileChurnMetrics,
) -> Result<()> {
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_author_contributions(
output: &mut String,
summary: &crate::models::churn::ChurnSummary,
) -> Result<()> {
writeln!(output, "\n## Author Contributions\n")?;
writeln!(output, "| Author | Files Modified |")?;
writeln!(output, "|--------|----------------|")?;
let mut sorted_authors: Vec<_> = summary.author_contributions.iter().collect();
sorted_authors.sort_by(|a, b| b.1.cmp(a.1));
for (author, count) in sorted_authors.iter().take(15) {
writeln!(output, "| {author} | {count} |")?;
}
Ok(())
}
#[must_use]
pub fn is_source_file(path: &Path) -> bool {
if !has_source_extension(path) {
return false;
}
if is_test_path(path) {
return false;
}
if is_test_filename(path) {
return false;
}
true
}
fn has_source_extension(path: &Path) -> bool {
matches!(
path.extension().and_then(|s| s.to_str()),
Some(
"rs" | "js"
| "ts"
| "py"
| "java"
| "cpp"
| "c"
| "go"
| "kt"
| "swift"
| "php"
| "rb"
| "scala"
)
)
}
fn is_test_path(path: &Path) -> bool {
let path_str = path.to_string_lossy();
let test_patterns = [
"/tests/",
"/test/",
"/examples/",
"/benches/",
"/fixtures/",
"/testdata/",
"/test_data/",
"/debug_test/",
"/test-",
"/__tests__/",
];
test_patterns
.iter()
.any(|pattern| path_str.contains(pattern))
}
fn is_test_filename(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|fname| {
fname.ends_with("_test.rs")
|| fname.ends_with("_tests.rs")
|| fname.starts_with("test_")
|| fname.contains("_test_")
|| fname.ends_with(".test.js")
|| fname.ends_with(".spec.js")
|| fname.ends_with("_test.py")
|| fname.ends_with("Test.java")
})
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}