use crate::data::check::{CommitIssue, IssueSeverity};
use crate::data::context::{FileContext, ProjectSignificance};
pub(crate) fn truncate_hash(hash: &str) -> &str {
let len = crate::git::SHORT_HASH_LEN;
if hash.len() > len {
&hash[..len]
} else {
hash
}
}
pub(crate) fn format_severity_label(severity: IssueSeverity) -> &'static str {
match severity {
IssueSeverity::Error => "\x1b[31mERROR\x1b[0m ",
IssueSeverity::Warning => "\x1b[33mWARNING\x1b[0m",
IssueSeverity::Info => "\x1b[36mINFO\x1b[0m ",
}
}
pub(crate) fn determine_commit_icon(passes: bool, issues: &[CommitIssue]) -> &'static str {
if passes {
"\u{2705}"
} else if issues.iter().any(|i| i.severity == IssueSeverity::Error) {
"\u{274c}"
} else {
"\u{26a0}\u{fe0f} "
}
}
pub(crate) fn resolve_short_hash<'a>(short: &str, candidates: &'a [String]) -> Option<&'a str> {
candidates.iter().find_map(|c| {
if c.starts_with(short) || short.starts_with(c.as_str()) {
Some(c.as_str())
} else {
None
}
})
}
pub(crate) fn format_file_analysis(files: &[FileContext]) -> Option<String> {
if files.is_empty() {
return None;
}
let critical_count = files
.iter()
.filter(|f| matches!(f.project_significance, ProjectSignificance::Critical))
.count();
if critical_count > 0 {
Some(format!(
"\u{1f4c2} Files: {} analyzed ({critical_count} critical)",
files.len()
))
} else {
Some(format!("\u{1f4c2} Files: {} analyzed", files.len()))
}
}
pub(crate) fn parse_editor_command(editor: &str) -> (&str, Vec<&str>) {
let mut parts = editor.split_whitespace();
let cmd = parts.next().unwrap_or(editor);
let args: Vec<&str> = parts.collect();
(cmd, args)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_hash_long() {
let hash = "abc1234567890abcdef1234567890abcdef123456";
let result = truncate_hash(hash);
assert_eq!(result.len(), crate::git::SHORT_HASH_LEN);
assert_eq!(result, &hash[..crate::git::SHORT_HASH_LEN]);
}
#[test]
fn truncate_hash_short() {
let hash = "abc12";
assert_eq!(truncate_hash(hash), "abc12");
}
#[test]
fn truncate_hash_exact() {
let hash = "abcd1234"; assert_eq!(truncate_hash(hash), "abcd1234");
}
#[test]
fn truncate_hash_empty() {
assert_eq!(truncate_hash(""), "");
}
#[test]
fn severity_label_error() {
let label = format_severity_label(IssueSeverity::Error);
assert!(label.contains("ERROR"));
assert!(label.contains("\x1b[31m")); }
#[test]
fn severity_label_warning() {
let label = format_severity_label(IssueSeverity::Warning);
assert!(label.contains("WARNING"));
assert!(label.contains("\x1b[33m")); }
#[test]
fn severity_label_info() {
let label = format_severity_label(IssueSeverity::Info);
assert!(label.contains("INFO"));
assert!(label.contains("\x1b[36m")); }
#[test]
fn icon_passing() {
let icon = determine_commit_icon(true, &[]);
assert_eq!(icon, "\u{2705}");
}
#[test]
fn icon_errors() {
let issues = vec![CommitIssue {
severity: IssueSeverity::Error,
section: "subject".to_string(),
rule: "length".to_string(),
explanation: "too long".to_string(),
}];
let icon = determine_commit_icon(false, &issues);
assert_eq!(icon, "\u{274c}");
}
#[test]
fn icon_warnings_only() {
let issues = vec![CommitIssue {
severity: IssueSeverity::Warning,
section: "body".to_string(),
rule: "style".to_string(),
explanation: "minor style issue".to_string(),
}];
let icon = determine_commit_icon(false, &issues);
assert_eq!(icon, "\u{26a0}\u{fe0f} ");
}
#[test]
fn resolve_hash_prefix_match() {
let candidates = vec![
"abc1234567890abcdef1234567890abcdef123456".to_string(),
"def1234567890abcdef1234567890abcdef123456".to_string(),
];
let result = resolve_short_hash("abc1234", &candidates);
assert_eq!(
result,
Some("abc1234567890abcdef1234567890abcdef123456" as &str)
);
}
#[test]
fn resolve_hash_reverse_match() {
let candidates = vec!["abc1234".to_string()];
let result = resolve_short_hash("abc1234567890abcdef1234567890abcdef123456", &candidates);
assert_eq!(result, Some("abc1234" as &str));
}
#[test]
fn resolve_hash_no_match() {
let candidates = vec!["abc1234567890abcdef1234567890abcdef123456".to_string()];
let result = resolve_short_hash("zzz9999", &candidates);
assert_eq!(result, None);
}
#[test]
fn parse_editor_simple() {
let (cmd, args) = parse_editor_command("vim");
assert_eq!(cmd, "vim");
assert!(args.is_empty());
}
#[test]
fn parse_editor_with_args() {
let (cmd, args) = parse_editor_command("code --wait --new-window");
assert_eq!(cmd, "code");
assert_eq!(args, vec!["--wait", "--new-window"]);
}
use crate::data::context::{ArchitecturalLayer, ChangeImpact, FilePurpose};
use std::path::PathBuf;
fn make_file_context(path: &str, significance: ProjectSignificance) -> FileContext {
FileContext {
path: PathBuf::from(path),
file_purpose: FilePurpose::CoreLogic,
architectural_layer: ArchitecturalLayer::Business,
change_impact: ChangeImpact::Modification,
project_significance: significance,
}
}
#[test]
fn file_analysis_empty() {
assert!(format_file_analysis(&[]).is_none());
}
#[test]
fn file_analysis_no_critical() {
let files = vec![
make_file_context("src/foo.rs", ProjectSignificance::Routine),
make_file_context("src/bar.rs", ProjectSignificance::Important),
];
let label = format_file_analysis(&files).unwrap();
assert!(label.contains("2 analyzed"));
assert!(!label.contains("critical"));
}
#[test]
fn file_analysis_with_critical() {
let files = vec![
make_file_context("src/main.rs", ProjectSignificance::Critical),
make_file_context("src/foo.rs", ProjectSignificance::Routine),
make_file_context("src/lib.rs", ProjectSignificance::Critical),
];
let label = format_file_analysis(&files).unwrap();
assert!(label.contains("3 analyzed"));
assert!(label.contains("2 critical"));
}
#[test]
fn file_analysis_single_file() {
let files = vec![make_file_context(
"src/foo.rs",
ProjectSignificance::Routine,
)];
let label = format_file_analysis(&files).unwrap();
assert!(label.contains("1 analyzed"));
}
}