This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-langs / src / langs.rs
3.9 kB 127 lines
1use std::collections::HashMap; 2use std::ops::ControlFlow; 3use std::time::Instant; 4 5use gengo_language::{Category, Language}; 6use knot_git::{EntryKind, GitError, MAX_TREE_DEPTH, Repo, SizedEntry}; 7use knot_types::{LanguageBytes, Oid}; 8 9use crate::linguist; 10 11const READ_LIMIT: usize = 16 * 1024; 12const SIZE_LIMIT: u64 = 1024 * 1024; 13 14pub use knot_types::LanguageName; 15 16fn looks_binary(content: &[u8]) -> bool { 17 content.contains(&0) 18} 19 20fn category_of(name: &'static str, fallback: Category) -> Category { 21 name.parse::<Language>() 22 .map(|language| language.category()) 23 .unwrap_or(fallback) 24} 25 26pub fn analyze( 27 repo: &Repo, 28 commit: Oid, 29 deadline: Option<Instant>, 30) -> Result<HashMap<LanguageName, LanguageBytes>, GitError> { 31 let mut sizes: HashMap<LanguageName, LanguageBytes> = HashMap::new(); 32 let root = repo.peel_to_tree(commit)?; 33 let _budget = walk(repo, root, "", 0, deadline, &mut sizes)?; 34 Ok(sizes) 35} 36 37fn walk( 38 repo: &Repo, 39 tree: Oid, 40 dir: &str, 41 depth: usize, 42 deadline: Option<Instant>, 43 sizes: &mut HashMap<LanguageName, LanguageBytes>, 44) -> Result<ControlFlow<()>, GitError> { 45 if depth > MAX_TREE_DEPTH || deadline.is_some_and(|deadline| Instant::now() >= deadline) { 46 return Ok(ControlFlow::Break(())); 47 } 48 let entries = repo.tree_entries(tree)?; 49 entries 50 .iter() 51 .try_fold(ControlFlow::Continue(()), |flow, entry| { 52 if flow.is_break() || deadline.is_some_and(|deadline| Instant::now() >= deadline) { 53 return Ok(ControlFlow::Break(())); 54 } 55 let path = match dir.is_empty() { 56 true => entry.name.clone(), 57 false => format!("{dir}/{}", entry.name), 58 }; 59 match entry.kind { 60 EntryKind::Tree => match linguist::is_vendor_dir(&path) { 61 true => Ok(ControlFlow::Continue(())), 62 false => walk(repo, entry.oid, &path, depth + 1, deadline, sizes), 63 }, 64 EntryKind::Blob | EntryKind::BlobExecutable => { 65 if !linguist::is_skipped_path(&path) { 66 tally(repo, entry, &path, sizes)?; 67 } 68 Ok(ControlFlow::Continue(())) 69 } 70 EntryKind::Link | EntryKind::Commit => Ok(ControlFlow::Continue(())), 71 } 72 }) 73} 74 75fn tally( 76 repo: &Repo, 77 entry: &SizedEntry, 78 path: &str, 79 sizes: &mut HashMap<LanguageName, LanguageBytes>, 80) -> Result<(), GitError> { 81 let content = match entry.size <= SIZE_LIMIT { 82 true => { 83 let blob = repo.read_blob(entry.oid)?; 84 blob[..blob.len().min(READ_LIMIT)].to_vec() 85 } 86 false => Vec::new(), 87 }; 88 if looks_binary(&content) { 89 return Ok(()); 90 } 91 let Some(language) = Language::pick(path, &content, READ_LIMIT) else { 92 return Ok(()); 93 }; 94 let name = LanguageName::new(linguist::group(language.name())); 95 if !matches!( 96 category_of(name.as_str(), language.category()), 97 Category::Programming | Category::Markup 98 ) { 99 return Ok(()); 100 } 101 let slot = sizes.entry(name).or_default(); 102 *slot = slot.saturating_add_bytes(entry.size); 103 Ok(()) 104} 105 106#[cfg(test)] 107mod tests { 108 use super::*; 109 110 #[test] 111 fn language_name_round_trips_through_as_str() { 112 let rust = LanguageName::new("Rust"); 113 assert_eq!(rust.as_str(), "Rust"); 114 } 115 116 #[test] 117 fn language_names_compare_and_hash_by_value() { 118 use std::collections::HashSet; 119 120 assert_eq!(LanguageName::new("Go"), LanguageName::new("Go")); 121 assert_ne!(LanguageName::new("Go"), LanguageName::new("Zig")); 122 let set: HashSet<LanguageName> = [LanguageName::new("Go"), LanguageName::new("Go")] 123 .into_iter() 124 .collect(); 125 assert_eq!(set.len(), 1); 126 } 127}