fix: make the workspace pass the CI lint/test gate
Some checks failed
deploy / Build api + worker + web (push) Failing after 5m59s
deploy / Deploy moments-api to nikola (push) Has been skipped
deploy / Deploy moments-worker to frootmig (push) Has been skipped
deploy / Deploy web to oolon (push) Has been skipped

The new Gitea Actions build gate runs `cargo fmt --check`, `clippy -D warnings`,
and `cargo test` — stricter than the old deploy.sh, which only `cargo build`d.
That surfaced pre-existing drift that never compiled under the test/clippy
profile:

- apply rustfmt across the workspace (formatting only, no logic changes)
- moments-data: add the missing `prune_events` to the test-only `NoopWriter`
  stub (the EventWriter trait gained it with the blog-prune feature; a plain
  `cargo build` never compiles the `#[cfg(test)]` stub, so it went stale)
- moments-api: `.max().min()` -> `.clamp()`, and build `usvg::Options` with
  struct-update syntax instead of post-Default field assignment

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7zF7Kf4JqDwa6M8Qgge9M
This commit is contained in:
2026-06-25 13:00:40 +03:00
parent 1b753f991f
commit 3761333ac4
16 changed files with 378 additions and 191 deletions

View File

@@ -21,11 +21,7 @@ use serde_json::{Value, json};
use tracing::{debug, warn};
const SOURCE_NAME: &str = "blog";
const USER_AGENT: &str = concat!(
"moments/",
env!("CARGO_PKG_VERSION"),
" (+https://rob.tn)"
);
const USER_AGENT: &str = concat!("moments/", env!("CARGO_PKG_VERSION"), " (+https://rob.tn)");
#[derive(Clone, Debug)]
pub struct BlogConfig {
@@ -130,8 +126,7 @@ impl BlogSource {
.filter(|e| e.get("type").and_then(Value::as_str) == Some("file"))
.filter_map(|e| e.get("path").and_then(Value::as_str))
.filter(|p| {
p.to_ascii_lowercase().ends_with(".md")
&& !p.eq_ignore_ascii_case("readme.md")
p.to_ascii_lowercase().ends_with(".md") && !p.eq_ignore_ascii_case("readme.md")
})
.map(String::from)
.collect())
@@ -202,7 +197,9 @@ struct Frontmatter {
/// open with a `---` fence on the first line.
fn split_frontmatter(content: &str) -> Option<(&str, &str)> {
let rest = content.strip_prefix("---")?;
let rest = rest.strip_prefix('\n').or_else(|| rest.strip_prefix("\r\n"))?;
let rest = rest
.strip_prefix('\n')
.or_else(|| rest.strip_prefix("\r\n"))?;
// The closing fence is a `---` alone on a line.
let mut offset = 0;
for line in rest.split_inclusive('\n') {

View File

@@ -21,11 +21,7 @@ use serde_json::Value;
use tracing::debug;
const SOURCE_NAME: &str = "bugzilla";
const USER_AGENT: &str = concat!(
"moments/",
env!("CARGO_PKG_VERSION"),
" (+https://rob.tn)"
);
const USER_AGENT: &str = concat!("moments/", env!("CARGO_PKG_VERSION"), " (+https://rob.tn)");
#[derive(Clone, Debug)]
pub struct BugzillaConfig {

View File

@@ -21,11 +21,7 @@ use serde_json::Value;
use tracing::debug;
const SOURCE_NAME: &str = "gitea";
const USER_AGENT: &str = concat!(
"moments/",
env!("CARGO_PKG_VERSION"),
" (+https://rob.tn)"
);
const USER_AGENT: &str = concat!("moments/", env!("CARGO_PKG_VERSION"), " (+https://rob.tn)");
const MAX_BACKFILL_PAGES: u32 = 20;
#[derive(Clone, Debug)]
@@ -289,7 +285,10 @@ fn parse_gitea_event(item: &Value, host: &str) -> Option<Event> {
let occurred_at = DateTime::parse_from_rfc3339(created_str)
.ok()?
.with_timezone(&Utc);
let private = item.get("is_private").and_then(Value::as_bool).unwrap_or(false);
let private = item
.get("is_private")
.and_then(Value::as_bool)
.unwrap_or(false);
let id = gitea_canonical_id(item, &op_type, created_str);
@@ -315,12 +314,20 @@ fn gitea_canonical_id(item: &Value, op_type: &str, created: &str) -> String {
let act_user_id = item
.get("act_user_id")
.and_then(Value::as_i64)
.or_else(|| item.get("act_user").and_then(|u| u.get("id")).and_then(Value::as_i64))
.or_else(|| {
item.get("act_user")
.and_then(|u| u.get("id"))
.and_then(Value::as_i64)
})
.unwrap_or(0);
let repo_id = item
.get("repo_id")
.and_then(Value::as_i64)
.or_else(|| item.get("repo").and_then(|r| r.get("id")).and_then(Value::as_i64))
.or_else(|| {
item.get("repo")
.and_then(|r| r.get("id"))
.and_then(Value::as_i64)
})
.unwrap_or(0);
let ref_name = item.get("ref_name").and_then(Value::as_str).unwrap_or("");
let comment_id = item.get("comment_id").and_then(Value::as_i64).unwrap_or(0);
@@ -346,7 +353,10 @@ mod tests {
"repo": { "id": 7, "full_name": "grenade/moments" }
});
let ev = parse_gitea_event(&raw, "git.lair.cafe").expect("parses");
assert_eq!(ev.id, "gitea:commit_repo:42:7:refs/heads/main:0:2026-05-03T16:37:45Z");
assert_eq!(
ev.id,
"gitea:commit_repo:42:7:refs/heads/main:0:2026-05-03T16:37:45Z"
);
assert_eq!(ev.source, Source::Gitea);
assert_eq!(ev.action, "commit_repo");
assert!(ev.public);

View File

@@ -8,11 +8,7 @@ use reqwest::{Client, StatusCode, header};
use tracing::debug;
const SOURCE_NAME: &str = "github";
const USER_AGENT: &str = concat!(
"moments/",
env!("CARGO_PKG_VERSION"),
" (+https://rob.tn)"
);
const USER_AGENT: &str = concat!("moments/", env!("CARGO_PKG_VERSION"), " (+https://rob.tn)");
/// Cap on initial backfill pagination. GitHub returns ~300 events max
/// across pages; this is a safety net, not an expected limit.
@@ -166,7 +162,9 @@ impl EventSource for GithubSource {
}
}
self.state.save(SOURCE_NAME, latest_etag.as_deref(), None).await?;
self.state
.save(SOURCE_NAME, latest_etag.as_deref(), None)
.await?;
Ok(total)
}
}
@@ -182,7 +180,10 @@ fn parse_github_event(raw: serde_json::Value) -> Option<Event> {
// `/events/public` are always true; `/events` may include false. Default
// to true if missing — that matches the safer-of-the-two-mistakes (under-
// expose) and the `/events/public` endpoint behaviour.
let public = raw.get("public").and_then(serde_json::Value::as_bool).unwrap_or(true);
let public = raw
.get("public")
.and_then(serde_json::Value::as_bool)
.unwrap_or(true);
Some(Event {
id: format!("github:{id}"),
source: Source::Github,
@@ -201,7 +202,10 @@ fn parse_link_next(header: Option<&header::HeaderValue>) -> Option<String> {
let part = part.trim();
// Each part: `<url>; rel="next"`
let (url_part, rel_part) = part.split_once(';')?;
let url = url_part.trim().trim_start_matches('<').trim_end_matches('>');
let url = url_part
.trim()
.trim_start_matches('<')
.trim_end_matches('>');
let rel = rel_part.trim();
if rel.eq_ignore_ascii_case("rel=\"next\"") {
return Some(url.to_string());

View File

@@ -51,11 +51,7 @@ const BRANCH_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b'%');
const SOURCE_NAME: &str = "github-repo";
const USER_AGENT: &str = concat!(
"moments/",
env!("CARGO_PKG_VERSION"),
" (+https://rob.tn)"
);
const USER_AGENT: &str = concat!("moments/", env!("CARGO_PKG_VERSION"), " (+https://rob.tn)");
const MAX_BACKFILL_PAGES: u32 = 100;
#[derive(Clone, Debug)]
@@ -199,10 +195,7 @@ impl GithubRepoSource {
.map_err(|e| SourceError::Http(e.to_string()))?;
if !resp.status().is_success() {
return Err(SourceError::Http(format!(
"{} POST graphql",
resp.status()
)));
return Err(SourceError::Http(format!("{} POST graphql", resp.status())));
}
let data: Value = resp
@@ -212,7 +205,11 @@ impl GithubRepoSource {
// Check for GraphQL-level errors
if let Some(errors) = data.get("errors").and_then(Value::as_array) {
if let Some(msg) = errors.first().and_then(|e| e.get("message")).and_then(Value::as_str) {
if let Some(msg) = errors
.first()
.and_then(|e| e.get("message"))
.and_then(Value::as_str)
{
return Err(SourceError::Http(format!("GraphQL error: {msg}")));
}
}
@@ -221,9 +218,7 @@ impl GithubRepoSource {
let nodes = contributed["nodes"].as_array();
if let Some(nodes) = nodes {
for node in nodes {
let full_name = node
.get("nameWithOwner")
.and_then(Value::as_str);
let full_name = node.get("nameWithOwner").and_then(Value::as_str);
let private = node
.get("isPrivate")
.and_then(Value::as_bool)
@@ -248,7 +243,10 @@ impl GithubRepoSource {
.map(String::from);
}
debug!(repos = repos.len(), "discovered contributed repos via GraphQL");
debug!(
repos = repos.len(),
"discovered contributed repos via GraphQL"
);
Ok(repos)
}
@@ -322,8 +320,14 @@ impl GithubRepoSource {
.await
.map_err(|e| SourceError::Parse(e.to_string()))?;
if let Some(errors) = data.get("errors").and_then(Value::as_array) {
if let Some(msg) = errors.first().and_then(|e| e.get("message")).and_then(Value::as_str) {
return Err(SourceError::Http(format!("GraphQL error listing branches: {msg}")));
if let Some(msg) = errors
.first()
.and_then(|e| e.get("message"))
.and_then(Value::as_str)
{
return Err(SourceError::Http(format!(
"GraphQL error listing branches: {msg}"
)));
}
}
let refs = &data["data"]["repository"]["refs"];
@@ -412,7 +416,10 @@ impl GithubRepoSource {
/// noise.
async fn scan_repo(&self, repo: &Repo) -> Result<usize, SourceError> {
let branches = if self.config.token.is_some() {
match self.list_branches_with_commits(repo, &self.config.user).await {
match self
.list_branches_with_commits(repo, &self.config.user)
.await
{
Ok(b) => b,
Err(e) => {
warn!(repo = %repo.full_name, error = %e, "graphql branch filter failed; falling back to REST");
@@ -434,7 +441,9 @@ impl GithubRepoSource {
for branch in &branches {
match self.scan_repo_branch(repo, branch, &mut seen_in_tick).await {
Ok(n) => total += n,
Err(SourceError::Http(ref msg)) if msg.starts_with("403") || msg.starts_with("429") => {
Err(SourceError::Http(ref msg))
if msg.starts_with("403") || msg.starts_with("429") =>
{
return Err(SourceError::Http(msg.clone()));
}
Err(e) => {
@@ -596,7 +605,11 @@ impl GithubRepoSource {
.map_err(|e| SourceError::Parse(e.to_string()))?;
if let Some(errors) = data.get("errors").and_then(Value::as_array) {
if let Some(msg) = errors.first().and_then(|e| e.get("message")).and_then(Value::as_str) {
if let Some(msg) = errors
.first()
.and_then(|e| e.get("message"))
.and_then(Value::as_str)
{
warn!(error = %msg, "GraphQL language fetch had errors");
}
}
@@ -664,7 +677,9 @@ impl EventSource for GithubRepoSource {
}
total += n;
}
Err(SourceError::Http(ref msg)) if msg.starts_with("403") || msg.starts_with("429") => {
Err(SourceError::Http(ref msg))
if msg.starts_with("403") || msg.starts_with("429") =>
{
warn!("rate limited during repo scan; ending poll early");
break;
}
@@ -679,7 +694,11 @@ impl EventSource for GithubRepoSource {
}
self.state.touch(SOURCE_NAME).await?;
debug!(ingested = total, repos = repos.len(), "github-repo poll complete");
debug!(
ingested = total,
repos = repos.len(),
"github-repo poll complete"
);
Ok(total)
}
}
@@ -692,7 +711,10 @@ struct Repo {
fn parse_repo(item: &Value) -> Option<Repo> {
let full_name = item.get("full_name").and_then(Value::as_str)?;
let private = item.get("private").and_then(Value::as_bool).unwrap_or(false);
let private = item
.get("private")
.and_then(Value::as_bool)
.unwrap_or(false);
Some(Repo {
full_name: full_name.to_string(),
private,

View File

@@ -26,11 +26,7 @@ use serde_json::Value;
use tracing::{debug, warn};
const SOURCE_NAME: &str = "github-search";
const USER_AGENT: &str = concat!(
"moments/",
env!("CARGO_PKG_VERSION"),
" (+https://rob.tn)"
);
const USER_AGENT: &str = concat!("moments/", env!("CARGO_PKG_VERSION"), " (+https://rob.tn)");
#[derive(Clone, Debug)]
pub struct GithubSearchConfig {
@@ -378,7 +374,10 @@ mod tests {
"repository": { "full_name": "faith1337z/Trade", "private": false }
});
let ev = parse_commit_event(&raw).expect("parses");
assert_eq!(ev.id, "github-commit:a6fcefbe909a97ad5a049b9fa48bc74309af10d9");
assert_eq!(
ev.id,
"github-commit:a6fcefbe909a97ad5a049b9fa48bc74309af10d9"
);
assert_eq!(ev.action, "Commit");
assert!(ev.public);
}

View File

@@ -21,11 +21,7 @@ use serde_json::Value;
use tracing::{debug, warn};
const SOURCE_NAME: &str = "hg";
const USER_AGENT: &str = concat!(
"moments/",
env!("CARGO_PKG_VERSION"),
" (+https://rob.tn)"
);
const USER_AGENT: &str = concat!("moments/", env!("CARGO_PKG_VERSION"), " (+https://rob.tn)");
/// Maximum changesets returned per json-log request.
const REV_COUNT: u32 = 500;
@@ -148,10 +144,7 @@ impl HgSource {
let mut payload = entry.clone();
if let Some(obj) = payload.as_object_mut() {
obj.insert("_repo".into(), Value::String(repo.into()));
obj.insert(
"_host".into(),
Value::String(self.config.host.clone()),
);
obj.insert("_host".into(), Value::String(self.config.host.clone()));
}
all_events.push(Event {
id: format!("hg:{repo}:{node}"),
@@ -254,6 +247,13 @@ mod tests {
) -> Result<usize, moments_core::StoreError> {
Ok(0)
}
async fn prune_events(
&self,
_source: moments_entities::Source,
_keep_ids: &[String],
) -> Result<usize, moments_core::StoreError> {
Ok(0)
}
}
struct NoopState;
#[async_trait]

View File

@@ -7,10 +7,13 @@ pub mod github_search;
pub mod hg;
use async_trait::async_trait;
use chrono::NaiveDate;
use chrono::{DateTime, Utc};
use moments_core::{EventReader, EventWriter, PollerState, PollerStateStore, StoreError};
use chrono::NaiveDate;
use moments_entities::{DailyCount, Event, EventQuery, HourlyAvg, LanguageDailyCount, ProjectSummary, RepoLanguage, Source, SourceSummary};
use moments_entities::{
DailyCount, Event, EventQuery, HourlyAvg, LanguageDailyCount, ProjectSummary, RepoLanguage,
Source, SourceSummary,
};
use sqlx::Row;
use sqlx::postgres::{PgPool, PgPoolOptions};
use std::str::FromStr;
@@ -99,7 +102,10 @@ impl EventReader for PgStore {
.collect()
}
async fn source_summaries(&self, include_private: bool) -> Result<Vec<SourceSummary>, StoreError> {
async fn source_summaries(
&self,
include_private: bool,
) -> Result<Vec<SourceSummary>, StoreError> {
let rows = sqlx::query(
r#"
SELECT source,
@@ -187,9 +193,18 @@ impl EventReader for PgStore {
source: Source::from_str(&source_str).map_err(map_err)?,
repo: r.try_get("repo").map_err(map_err)?,
host: r.try_get("host").map_err(map_err)?,
commit_count: r.try_get::<i64, _>("commit_count").map_err(map_err).unwrap_or(0),
issue_count: r.try_get::<i64, _>("issue_count").map_err(map_err).unwrap_or(0),
pr_count: r.try_get::<i64, _>("pr_count").map_err(map_err).unwrap_or(0),
commit_count: r
.try_get::<i64, _>("commit_count")
.map_err(map_err)
.unwrap_or(0),
issue_count: r
.try_get::<i64, _>("issue_count")
.map_err(map_err)
.unwrap_or(0),
pr_count: r
.try_get::<i64, _>("pr_count")
.map_err(map_err)
.unwrap_or(0),
first_activity: r.try_get("first_activity").map_err(map_err)?,
last_activity: r.try_get("last_activity").map_err(map_err)?,
})
@@ -197,7 +212,12 @@ impl EventReader for PgStore {
.collect()
}
async fn daily_counts(&self, from: NaiveDate, to: NaiveDate, include_private: bool) -> Result<Vec<DailyCount>, StoreError> {
async fn daily_counts(
&self,
from: NaiveDate,
to: NaiveDate,
include_private: bool,
) -> Result<Vec<DailyCount>, StoreError> {
let rows = sqlx::query(
r#"
SELECT d::date AS date,
@@ -228,7 +248,12 @@ impl EventReader for PgStore {
.collect()
}
async fn language_daily_counts(&self, from: NaiveDate, to: NaiveDate, include_private: bool) -> Result<Vec<LanguageDailyCount>, StoreError> {
async fn language_daily_counts(
&self,
from: NaiveDate,
to: NaiveDate,
include_private: bool,
) -> Result<Vec<LanguageDailyCount>, StoreError> {
let rows = sqlx::query(
r#"
SELECT date, language, color,