mod report;
use std::cmp::Reverse;
use std::collections::HashMap;
use std::error::Error;
use std::path::{Path, PathBuf};
use crate::cli::OutputMode;
use crate::git::GitRepo;
use crate::util::parse_since;
use crate::walk::{self, WalkConfig};
use report::{print_json, print_report, print_short, print_terse};
pub struct FileHotspot {
pub path: PathBuf,
pub language: String,
pub commits: usize,
pub complexity: usize,
pub score: usize,
}
fn compute_complexity(
file_path: &Path,
spec: &crate::loc::language::LanguageSpec,
metric: &str,
exclude_tests: bool,
) -> Result<Option<usize>, Box<dyn Error>> {
Ok(match metric {
"cycom" => crate::cycom::analyze_file(file_path, spec, exclude_tests)?
.map(|file| file.total_complexity),
"cogcom" => crate::cogcom::analyze_file(file_path, spec, exclude_tests)?
.map(|file| file.total_complexity),
_ => crate::indent::analyze_file(file_path, spec, exclude_tests)?
.map(|file| file.total_indent),
})
}
pub fn run(
cfg: &WalkConfig<'_>,
output: OutputMode,
top: usize,
sort_by: &str,
since: Option<&str>,
complexity_metric: &str,
) -> Result<(), Box<dyn Error>> {
let git_repo = GitRepo::open(cfg.path)
.map_err(|e| format!("not a git repository (or any parent): {e}"))?;
let since_ts = since.map(parse_since).transpose()?;
let freqs = git_repo.file_frequencies(since_ts)?;
if freqs.is_empty() {
if since.is_some() {
eprintln!("No commits found in the specified time range.");
} else {
eprintln!("No commits found in the repository.");
}
return Ok(());
}
let freq_map: HashMap<PathBuf, usize> =
freqs.into_iter().map(|f| (f.path, f.commits)).collect();
let (walk_root, walk_prefix) = git_repo.walk_prefix(cfg.path)?;
let exclude_tests = cfg.exclude_tests();
let complexity_of =
|path: &Path, spec| compute_complexity(path, spec, complexity_metric, exclude_tests);
let mut results: Vec<FileHotspot> = Vec::new();
for (file_path, spec) in walk::source_files(&walk_root, cfg.exclude_tests(), cfg.filter) {
let rel_path = GitRepo::to_git_path(&walk_root, &walk_prefix, &file_path);
let commits = match freq_map.get(&rel_path) {
Some(&c) => c,
None => continue,
};
let complexity = match complexity_of(&file_path, spec) {
Ok(Some(c)) => c,
Ok(None) => continue,
Err(err) => {
eprintln!("warning: {}: {err}", file_path.display());
continue;
}
};
let score = commits * complexity;
results.push(FileHotspot {
path: rel_path,
language: spec.name.to_string(),
commits,
complexity,
score,
});
}
match sort_by {
"commits" => results.sort_by_key(|r| Reverse(r.commits)),
"complexity" => results.sort_by_key(|r| Reverse(r.complexity)),
_ => results.sort_by_key(|r| Reverse(r.score)),
}
results.truncate(top);
match output {
OutputMode::Json => print_json(&results, complexity_metric)?,
OutputMode::Short => print_short(&results),
OutputMode::Terse => print_terse(&results),
OutputMode::Github | OutputMode::Codeclimate => {
return Err(crate::cli::ERR_CI_FORMAT_ONLY.into());
}
OutputMode::Table => print_report(&results, complexity_metric),
}
Ok(())
}
#[cfg(test)]
#[path = "mod_test.rs"]
mod tests;