This repository has no description
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}