anvilsign in

collin/anvil · 2bc1657f

Fix language detection for bare repositories

Collin Richards · 2026-08-18 01:02 UTC · 2bc1657f687b775447b6763e1eab6339dfd66ecd · parent 8cd56788 · browse files

modifiedcrates/anvil-core/src/language.rs+72 −0
⋯ 56 unchanged lines
5757 walk_dir(&mut counts, &mut total, entries);
5858 }
5959
60+ compute_language_stats(counts, total)
61+}
62+
63+/// Detect languages in a bare git repository by reading from a branch.
64+/// Returns primary language + JSON-encoded percentages.
65+pub fn detect_from_git(repo_path: &Path, branch: &str) -> Result<LanguageDetection> {
66+ let mut counts: BTreeMap<String, u64> = BTreeMap::new();
67+ let mut total = 0u64;
68+
69+ // Open the repository and traverse the tree at the given branch
70+ let repo = gix::open(repo_path)
71+ .map_err(|e| crate::error::Error::Storage(format!("open repository: {e}")))?;
72+ let tree = repo
73+ .rev_parse_single(branch)
74+ .map_err(|e| crate::error::Error::Storage(format!("resolve {branch}: {e}")))?
75+ .object()
76+ .map_err(|e| crate::error::Error::Storage(format!("get object: {e}")))?
77+ .peel_to_commit()
78+ .map_err(|e| crate::error::Error::Storage(format!("peel to commit: {e}")))?
79+ .tree()
80+ .map_err(|e| crate::error::Error::Storage(format!("get tree: {e}")))?;
81+ collect_languages(&tree, String::new(), &mut counts, &mut total)?;
82+
83+ compute_language_stats(counts, total)
84+}
85+
86+/// Recursively walk a git tree and count files by language
87+fn collect_languages(
88+ tree: &gix::Tree<'_>,
89+ path_prefix: String,
90+ counts: &mut BTreeMap<String, u64>,
91+ total: &mut u64,
92+) -> Result<()> {
93+ for entry in tree.iter() {
94+ let e = entry.map_err(|e| crate::error::Error::Storage(format!("read tree entry: {e}")))?;
95+ let file_name = e.filename();
96+ let name = std::str::from_utf8(file_name).unwrap_or("");
97+
98+ let new_path = if path_prefix.is_empty() {
99+ name.to_string()
100+ } else {
101+ format!("{}/{}", path_prefix, name)
102+ };
103+
104+ // Skip hidden files and directories
105+ if name.starts_with('.') {
106+ continue;
107+ }
108+
109+ let mode = e.mode();
110+ if mode.is_tree() {
111+ // Recursively walk subdirectories
112+ let subtree = e
113+ .object()
114+ .map_err(|e| crate::error::Error::Storage(format!("get subtree object: {e}")))?
115+ .peel_to_tree()
116+ .map_err(|e| crate::error::Error::Storage(format!("peel to tree: {e}")))?;
117+ collect_languages(&subtree, new_path, counts, total)?;
118+ } else if mode.is_blob() {
119+ // Count files by extension
120+ if let Some(ext) = Path::new(&new_path).extension().and_then(|s| s.to_str())
121+ && let Some(lang) = ext_to_language(ext)
122+ {
123+ *counts.entry(lang.to_string()).or_insert(0) += 1;
124+ *total += 1;
125+ }
126+ }
127+ }
128+ Ok(())
129+}
130+
131+fn compute_language_stats(counts: BTreeMap<String, u64>, total: u64) -> Result<LanguageDetection> {
60132 let primary = if total == 0 {
61133 String::new()
62134 } else {
⋯ 58 unchanged lines
modifiedcrates/anvil-core/src/periodic.rs+2 −5
⋯ 70 unchanged lines
7171 let repo_count = repos_list.len();
7272
7373 for repo in repos_list {
74- let repo_path = repos_dir
75- .join(repo.owner_id.to_string())
76- .join(&repo.name)
77- .join(".git");
74+ let repo_path = repos_dir.join(repo.owner_id.to_string()).join(&repo.name);
7875 if repo_path.exists()
79- && let Ok(detection) = language::detect(&repo_path).await
76+ && let Ok(detection) = language::detect_from_git(&repo_path, &repo.default_branch)
8077 {
8178 let _ =
8279 repos::update_languages(&app.db, repo.id, &detection.primary, &detection.json)
⋯ 94 unchanged lines