fix: make the workspace pass the CI lint/test gate
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:
@@ -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') {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user