Git hosting and a container registry in one Rust binary (axum + Astro)
Imports: last-updated time is the newest commit, not the import time; fix existing imported repos at startup
3 files changed, +56 -1
+9-0backend/src/git.rs
| @@ -367,6 +367,15 @@ impl Git { | ||
| 367 | 367 | Ok(parse_commits(&self.run(&args).await?)) |
| 368 | 368 | } |
| 369 | 369 | |
| 370 | + /// When the newest commit on any branch was made: the honest "last | |
| 371 | + /// pushed" time for history that arrived all at once, as in an import. | |
| 372 | + /// Clamped to now, since commit clocks can run ahead. | |
| 373 | + pub async fn newest_commit_time(&self) -> anyhow::Result<Option<DateTime<Utc>>> { | |
| 374 | + let out = self.run(&["for-each-ref", "--sort=-committerdate", "--count=1", "--format=%(committerdate:unix)", "refs/heads"]).await?; | |
| 375 | + let seconds: Option<i64> = String::from_utf8_lossy(&out).trim().parse().ok(); | |
| 376 | + Ok(seconds.and_then(|s| DateTime::from_timestamp(s, 0)).map(|t| t.min(Utc::now()))) | |
| 377 | + } | |
| 378 | + | |
| 370 | 379 | pub async fn count_commits(&self, rev: &str) -> anyhow::Result<u64> { |
| 371 | 380 | let out = self.run_opt(&["rev-list", "--count", "--end-of-options", rev, "--"]).await?; |
| 372 | 381 | Ok(out.and_then(|o| String::from_utf8_lossy(&o).trim().parse().ok()).unwrap_or(0)) |
+46-1backend/src/importer/mod.rs
| @@ -544,9 +544,12 @@ async fn fill_repo(state: &AppState, gh: &GitHub, import: &ImportRow, repo: &Rep | ||
| 544 | 544 | } |
| 545 | 545 | |
| 546 | 546 | let size = git.disk_size().await.unwrap_or(0); |
| 547 | + // "Updated" should say when the work happened on GitHub, not when it | |
| 548 | + // was copied here. | |
| 549 | + let last_commit = git.newest_commit_time().await.unwrap_or(None); | |
| 547 | 550 | sqlx::query( |
| 548 | 551 | "update repos set default_branch = coalesce($2, default_branch), is_empty = $3, size_bytes = $4, archived = $5, |
| 549 | - pushed_at = case when $3 then pushed_at else now() end, updated_at = now() | |
| 552 | + pushed_at = case when $3 then pushed_at else coalesce($6, now()) end, updated_at = now() | |
| 550 | 553 | where id = $1", |
| 551 | 554 | ) |
| 552 | 555 | .bind(repo.id) |
| @@ -554,6 +557,7 @@ async fn fill_repo(state: &AppState, gh: &GitHub, import: &ImportRow, repo: &Rep | ||
| 554 | 557 | .bind(branches.is_empty()) |
| 555 | 558 | .bind(size as i64) |
| 556 | 559 | .bind(item.archived) |
| 560 | + .bind(last_commit) | |
| 557 | 561 | .execute(&state.db) |
| 558 | 562 | .await?; |
| 559 | 563 | |
| @@ -965,3 +969,44 @@ mod tests { | ||
| 965 | 969 | assert_eq!(stashed_token(&key, 7), None); |
| 966 | 970 | } |
| 967 | 971 | } |
| 972 | + | |
| 973 | +/// Imports used to stamp `pushed_at` with the time of the import, so every | |
| 974 | +/// imported repo read "Updated" at the same moment. Repos that have had no | |
| 975 | +/// real push since get the time of their newest commit instead. A repo only | |
| 976 | +/// qualifies while `pushed_at` still sits at its import, so this settles | |
| 977 | +/// after one run. | |
| 978 | +pub fn spawn_import_date_fix(state: AppState) { | |
| 979 | + tokio::spawn(async move { | |
| 980 | + let repos: Vec<Repo> = match sqlx::query_as(concat!( | |
| 981 | + crate::repo_select!(), | |
| 982 | + " where not r.is_empty | |
| 983 | + and exists (select 1 from push_events e where e.repo_id = r.id and e.via = 'import' | |
| 984 | + and abs(extract(epoch from e.created_at - r.pushed_at)) < 600) | |
| 985 | + and not exists (select 1 from push_events e where e.repo_id = r.id and e.via <> 'import')" | |
| 986 | + )) | |
| 987 | + .fetch_all(&state.db) | |
| 988 | + .await | |
| 989 | + { | |
| 990 | + Ok(repos) => repos, | |
| 991 | + Err(error) => { | |
| 992 | + tracing::warn!(%error, "import date fix query failed"); | |
| 993 | + return; | |
| 994 | + } | |
| 995 | + }; | |
| 996 | + let mut fixed = 0; | |
| 997 | + for repo in &repos { | |
| 998 | + let git = Git::new(repo.disk_path(&state.config)); | |
| 999 | + let Ok(Some(last)) = git.newest_commit_time().await else { continue }; | |
| 1000 | + if Some(last) == repo.pushed_at { | |
| 1001 | + continue; | |
| 1002 | + } | |
| 1003 | + match sqlx::query("update repos set pushed_at = $2 where id = $1").bind(repo.id).bind(last).execute(&state.db).await { | |
| 1004 | + Ok(_) => fixed += 1, | |
| 1005 | + Err(error) => tracing::warn!(repo = %repo.full_name(), %error, "import date fix failed"), | |
| 1006 | + } | |
| 1007 | + } | |
| 1008 | + if fixed > 0 { | |
| 1009 | + tracing::info!(fixed, "set imported repositories' last-updated time to their newest commit"); | |
| 1010 | + } | |
| 1011 | + }); | |
| 1012 | +} |
+1-0backend/src/jobs.rs
| @@ -10,6 +10,7 @@ pub fn spawn_all(state: AppState) { | ||
| 10 | 10 | tokio::spawn(async move { crate::importer::recover(&recover_state).await }); |
| 11 | 11 | crate::registry::spawn_gc(state.clone()); |
| 12 | 12 | crate::languages::spawn_backfill(state.clone()); |
| 13 | + crate::importer::spawn_import_date_fix(state.clone()); | |
| 13 | 14 | crate::backup::spawn(state.clone()); |
| 14 | 15 | tokio::spawn(prune_sessions(state)); |
| 15 | 16 | } |