Git hosting and a container registry in one Rust binary (axum + Astro)
Languages: skip generated files (Go DO NOT EDIT headers, protoc, Thrift, Cython, @generated) like Linguist, recompute cached stats
2 files changed, +59 -12
+3-0backend/migrations/0005_languages_generated.sql
| @@ -0,0 +1,3 @@ | ||
| 1 | +-- Generated files (Go "Code generated ... DO NOT EDIT", protoc, Thrift, | |
| 2 | +-- Cython, @generated) no longer count; recompute every cached breakdown. | |
| 3 | +delete from repo_languages; |
+56-12backend/src/languages.rs
| @@ -369,14 +369,29 @@ pub async fn compute(git: &Git, commit: &str) -> anyhow::Result<Vec<LanguageByte | ||
| 369 | 369 | continue; |
| 370 | 370 | } |
| 371 | 371 | if let Some(language) = language { |
| 372 | - let check = size > 1024 && matches!(language, "JavaScript" | "CSS") && !overrides.decides(path); | |
| 372 | + // Content is only checked where the repo has not said otherwise. | |
| 373 | + let check = size <= CONTENT_CHECK_MAX_FILE && !overrides.decides(path); | |
| 373 | 374 | counted.push((language, size, if check { sha.to_string() } else { String::new() })); |
| 374 | 375 | } |
| 375 | 376 | } |
| 376 | - let minified = minified_blobs(git, counted.iter().filter(|c| !c.2.is_empty()).map(|c| c.2.clone()).collect()).await.unwrap_or_default(); | |
| 377 | + // Read file contents within a budget; past it, keep to minified JS/CSS. | |
| 378 | + let mut budget = CONTENT_CHECK_BUDGET; | |
| 379 | + let candidates: Vec<(String, bool)> = counted | |
| 380 | + .iter() | |
| 381 | + .filter(|(_, _, sha)| !sha.is_empty()) | |
| 382 | + .filter_map(|(language, size, sha)| { | |
| 383 | + let minify_check = matches!(*language, "JavaScript" | "CSS") && *size > 1024; | |
| 384 | + let wanted = budget >= *size || minify_check; | |
| 385 | + if wanted { | |
| 386 | + budget = budget.saturating_sub(*size); | |
| 387 | + } | |
| 388 | + wanted.then(|| (sha.clone(), minify_check)) | |
| 389 | + }) | |
| 390 | + .collect(); | |
| 391 | + let generated = generated_blobs(git, candidates).await.unwrap_or_default(); | |
| 377 | 392 | let mut totals: std::collections::HashMap<&'static str, u64> = std::collections::HashMap::new(); |
| 378 | 393 | for (language, size, sha) in counted { |
| 379 | - if !sha.is_empty() && minified.contains(&sha) { | |
| 394 | + if !sha.is_empty() && generated.contains(&sha) { | |
| 380 | 395 | continue; |
| 381 | 396 | } |
| 382 | 397 | *totals.entry(language).or_default() += size; |
| @@ -386,18 +401,37 @@ pub async fn compute(git: &Git, commit: &str) -> anyhow::Result<Vec<LanguageByte | ||
| 386 | 401 | Ok(out) |
| 387 | 402 | } |
| 388 | 403 | |
| 389 | -/// Linguist's minified rule: a JS or CSS file whose lines average more than | |
| 390 | -/// 110 characters is generated (a bundle or minified library) and does not | |
| 391 | -/// count. Reads the candidates' contents in one `git cat-file --batch`. | |
| 392 | -async fn minified_blobs(git: &Git, shas: Vec<String>) -> anyhow::Result<std::collections::HashSet<String>> { | |
| 404 | +/// Files larger than this are never read for the content checks. | |
| 405 | +const CONTENT_CHECK_MAX_FILE: u64 = 8 * 1024 * 1024; | |
| 406 | +/// Total bytes read per computation before checks narrow to JS/CSS. | |
| 407 | +const CONTENT_CHECK_BUDGET: u64 = 64 * 1024 * 1024; | |
| 408 | + | |
| 409 | +/// A generated-file marker in the first lines, as Linguist recognises them: | |
| 410 | +/// Go's "Code generated ... DO NOT EDIT." convention and the headers protoc, | |
| 411 | +/// Thrift, Cython and Relay-style `@generated` tools write. | |
| 412 | +fn has_generated_header(content: &[u8]) -> bool { | |
| 413 | + let head = String::from_utf8_lossy(&content[..content.len().min(2048)]).to_ascii_lowercase(); | |
| 414 | + head.lines().take(10).any(|line| { | |
| 415 | + (line.contains("code generated") && line.contains("do not edit")) | |
| 416 | + || line.contains("@generated") | |
| 417 | + || line.contains("generated by the protocol buffer compiler") | |
| 418 | + || line.contains("autogenerated by thrift") | |
| 419 | + || line.contains("generated by cython") | |
| 420 | + }) | |
| 421 | +} | |
| 422 | + | |
| 423 | +/// Linguist's content rules: a file with a generated header, or a JS/CSS file | |
| 424 | +/// whose lines average more than 110 characters (a bundle or minified | |
| 425 | +/// library), does not count. Reads the candidates in one `cat-file --batch`. | |
| 426 | +async fn generated_blobs(git: &Git, blobs: Vec<(String, bool)>) -> anyhow::Result<std::collections::HashSet<String>> { | |
| 393 | 427 | use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; |
| 394 | 428 | let mut out = std::collections::HashSet::new(); |
| 395 | - if shas.is_empty() { | |
| 429 | + if blobs.is_empty() { | |
| 396 | 430 | return Ok(out); |
| 397 | 431 | } |
| 398 | 432 | let mut child = git.command().args(["cat-file", "--batch"]).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn()?; |
| 399 | 433 | let mut stdin = child.stdin.take().ok_or_else(|| anyhow::anyhow!("stdin"))?; |
| 400 | - let wanted = shas.clone(); | |
| 434 | + let wanted: Vec<String> = blobs.iter().map(|(sha, _)| sha.clone()).collect(); | |
| 401 | 435 | let writer = tokio::spawn(async move { |
| 402 | 436 | for sha in wanted { |
| 403 | 437 | if stdin.write_all(format!("{sha}\n").as_bytes()).await.is_err() { |
| @@ -407,17 +441,19 @@ async fn minified_blobs(git: &Git, shas: Vec<String>) -> anyhow::Result<std::col | ||
| 407 | 441 | }); |
| 408 | 442 | let mut reader = BufReader::new(child.stdout.take().ok_or_else(|| anyhow::anyhow!("stdout"))?); |
| 409 | 443 | let mut header = String::new(); |
| 410 | - for sha in &shas { | |
| 444 | + for (sha, minify_check) in &blobs { | |
| 411 | 445 | header.clear(); |
| 412 | 446 | if reader.read_line(&mut header).await? == 0 { |
| 413 | 447 | break; |
| 414 | 448 | } |
| 415 | - let size: usize = header.split_whitespace().nth(2).and_then(|s| s.parse().ok()).unwrap_or(0); | |
| 449 | + // "<sha> missing" carries no body to read past. | |
| 450 | + let Some(size) = header.split_whitespace().nth(2).and_then(|s| s.parse::<usize>().ok()) else { continue }; | |
| 416 | 451 | let mut content = vec![0u8; size + 1]; |
| 417 | 452 | reader.read_exact(&mut content).await?; |
| 418 | 453 | let content = &content[..size]; |
| 419 | 454 | let lines = content.split(|b| *b == b'\n').filter(|l| !l.is_empty()).count().max(1); |
| 420 | - if content.len() / lines > 110 { | |
| 455 | + let minified = *minify_check && content.len() / lines > 110; | |
| 456 | + if minified || has_generated_header(content) { | |
| 421 | 457 | out.insert(sha.clone()); |
| 422 | 458 | } |
| 423 | 459 | } |
| @@ -543,6 +579,14 @@ mod tests { | ||
| 543 | 579 | assert_eq!(overrides.apply("lib/a.inc", false, None), (false, Some("PHP"))); |
| 544 | 580 | } |
| 545 | 581 | |
| 582 | + #[test] | |
| 583 | + fn generated_headers() { | |
| 584 | + assert!(has_generated_header(b"// Code generated by gen_sort_variants.go; DO NOT EDIT.\n\npackage x")); | |
| 585 | + assert!(has_generated_header(b"// @generated by relay-compiler\n")); | |
| 586 | + assert!(has_generated_header(b"# Generated by the protocol buffer compiler. DO NOT EDIT!\n")); | |
| 587 | + assert!(!has_generated_header(b"// Package sort implements sorting.\npackage sort")); | |
| 588 | + } | |
| 589 | + | |
| 546 | 590 | #[test] |
| 547 | 591 | fn small_languages_fold_into_other() { |
| 548 | 592 | let stats = vec![ |