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 | |||
| 57 | 57 | walk_dir(&mut counts, &mut total, entries); | |
| 58 | 58 | } | |
| 59 | 59 | ||
| 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> { | |
| 60 | 132 | let primary = if total == 0 { | |
| 61 | 133 | String::new() | |
| 62 | 134 | } else { | |
| ⋯ 58 unchanged lines | |||
modifiedcrates/anvil-core/src/periodic.rs+2 −5
| ⋯ 70 unchanged lines | |||
| 71 | 71 | let repo_count = repos_list.len(); | |
| 72 | 72 | ||
| 73 | 73 | 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); | |
| 78 | 75 | 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) | |
| 80 | 77 | { | |
| 81 | 78 | let _ = | |
| 82 | 79 | repos::update_languages(&app.db, repo.id, &detection.primary, &detection.json) | |
| ⋯ 94 unchanged lines | |||