feat(worker): add hg-edge and bugzilla pollers
Wires two historical sources for completeness with the 2019 timeline: - hg-edge.mozilla.org: scans json-pushes for a configured set of build/* repos and matches changeset author client-side, since the pushlog `user=` filter targets the pusher (sheriffs/reviewers in this case) rather than the author. Daily poll cadence — mozilla retired hg, no new events expected. - bugzilla.mozilla.org: queries /rest/bug?creator=<email>. Without an api key the unauthenticated endpoint only returns public bugs, which is what the public timeline wants anyway. Reshape renders "<author> committed <short_node> in <repo>" for hg and "filed bug #<id> in <product>" for bugzilla, both linking back to the canonical upstream URL via a stamped `_host` payload field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,31 +3,18 @@
|
||||
//! Storage holds the upstream payload verbatim; transformation lives here so
|
||||
//! the rendering can evolve without re-fetching upstream data.
|
||||
|
||||
use moments_entities::{Event, Source, TimelineIcon, TimelineItem, TitleSegment};
|
||||
use moments_entities::{Event, Source, TimelineItem};
|
||||
|
||||
mod bugzilla;
|
||||
mod gitea;
|
||||
mod github;
|
||||
mod hg;
|
||||
|
||||
pub fn reshape(event: &Event) -> TimelineItem {
|
||||
match event.source {
|
||||
Source::Github => github::reshape(event),
|
||||
Source::Gitea => gitea::reshape(event),
|
||||
Source::Hg | Source::Bugzilla => generic_fallback(event),
|
||||
}
|
||||
}
|
||||
|
||||
fn generic_fallback(event: &Event) -> TimelineItem {
|
||||
TimelineItem {
|
||||
id: event.id.clone(),
|
||||
source: event.source,
|
||||
action: event.action.clone(),
|
||||
occurred_at: event.occurred_at,
|
||||
icon: TimelineIcon::Generic,
|
||||
title: vec![
|
||||
TitleSegment::text(format!("{} from ", event.action)),
|
||||
TitleSegment::text(event.source.as_str().to_string()),
|
||||
],
|
||||
subtitle: None,
|
||||
body: None,
|
||||
Source::Hg => hg::reshape(event),
|
||||
Source::Bugzilla => bugzilla::reshape(event),
|
||||
}
|
||||
}
|
||||
|
||||
79
crates/moments-core/src/presentation/bugzilla.rs
Normal file
79
crates/moments-core/src/presentation/bugzilla.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use moments_entities::{Event, Source, TimelineIcon, TimelineItem, TitleSegment};
|
||||
use serde_json::Value;
|
||||
|
||||
const FALLBACK_HOST: &str = "bugzilla.mozilla.org";
|
||||
|
||||
pub(crate) fn reshape(event: &Event) -> TimelineItem {
|
||||
let p = &event.payload;
|
||||
let host = p
|
||||
.get("_host")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(FALLBACK_HOST);
|
||||
let id = p.get("id").and_then(Value::as_i64).unwrap_or(0);
|
||||
let summary = p.get("summary").and_then(Value::as_str).unwrap_or("");
|
||||
let product = p.get("product").and_then(Value::as_str);
|
||||
|
||||
let mut title = vec![
|
||||
TitleSegment::text("filed bug "),
|
||||
TitleSegment::link(
|
||||
format!("#{id}"),
|
||||
format!("https://{host}/show_bug.cgi?id={id}"),
|
||||
),
|
||||
];
|
||||
if let Some(prod) = product {
|
||||
title.push(TitleSegment::text(format!(" in {prod}")));
|
||||
}
|
||||
|
||||
let subtitle = (!summary.is_empty()).then(|| vec![TitleSegment::text(summary.to_string())]);
|
||||
|
||||
TimelineItem {
|
||||
id: event.id.clone(),
|
||||
source: Source::Bugzilla,
|
||||
action: event.action.clone(),
|
||||
occurred_at: event.occurred_at,
|
||||
icon: TimelineIcon::Bug,
|
||||
title,
|
||||
subtitle,
|
||||
body: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn reshape_bug_create() {
|
||||
let raw = json!({
|
||||
"_host": "bugzilla.mozilla.org",
|
||||
"id": 1158879,
|
||||
"summary": "Commit Access (Level 1) for Rob Thijssen",
|
||||
"product": "mozilla.org"
|
||||
});
|
||||
let event = Event {
|
||||
id: "bugzilla:1158879".into(),
|
||||
source: Source::Bugzilla,
|
||||
action: "BugCreate".into(),
|
||||
occurred_at: Utc.with_ymd_and_hms(2015, 4, 27, 16, 29, 59).unwrap(),
|
||||
public: true,
|
||||
payload: raw,
|
||||
};
|
||||
let item = reshape(&event);
|
||||
assert_eq!(item.icon, TimelineIcon::Bug);
|
||||
let r: String = item
|
||||
.title
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
TitleSegment::Text { text } => text.clone(),
|
||||
TitleSegment::Link { text, .. } => text.clone(),
|
||||
})
|
||||
.collect();
|
||||
assert!(r.contains("filed bug #1158879 in mozilla.org"), "got: {r}");
|
||||
assert_eq!(
|
||||
item.subtitle.unwrap(),
|
||||
vec![TitleSegment::text("Commit Access (Level 1) for Rob Thijssen")]
|
||||
);
|
||||
}
|
||||
}
|
||||
126
crates/moments-core/src/presentation/hg.rs
Normal file
126
crates/moments-core/src/presentation/hg.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use moments_entities::{Event, Source, TimelineIcon, TimelineItem, TitleSegment};
|
||||
use serde_json::Value;
|
||||
|
||||
const FALLBACK_HOST: &str = "hg-edge.mozilla.org";
|
||||
|
||||
pub(crate) fn reshape(event: &Event) -> TimelineItem {
|
||||
let p = &event.payload;
|
||||
let host = p
|
||||
.get("_host")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(FALLBACK_HOST);
|
||||
let repo = p
|
||||
.get("_repo")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("(unknown repo)");
|
||||
let node = p.get("node").and_then(Value::as_str).unwrap_or("");
|
||||
let short_node: String = node.chars().take(12).collect();
|
||||
let desc = p
|
||||
.get("desc")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let author = p
|
||||
.get("author")
|
||||
.and_then(Value::as_str)
|
||||
.map(author_name);
|
||||
|
||||
let mut title = Vec::new();
|
||||
if let Some(name) = author {
|
||||
title.push(TitleSegment::text(format!("{name} ")));
|
||||
}
|
||||
title.push(TitleSegment::text("committed "));
|
||||
title.push(TitleSegment::link(
|
||||
short_node,
|
||||
format!("https://{host}/{repo}/rev/{node}"),
|
||||
));
|
||||
title.push(TitleSegment::text(" in "));
|
||||
title.push(TitleSegment::link(
|
||||
repo.to_string(),
|
||||
format!("https://{host}/{repo}"),
|
||||
));
|
||||
|
||||
let subtitle = (!desc.is_empty()).then(|| vec![TitleSegment::text(desc)]);
|
||||
|
||||
TimelineItem {
|
||||
id: event.id.clone(),
|
||||
source: Source::Hg,
|
||||
action: event.action.clone(),
|
||||
occurred_at: event.occurred_at,
|
||||
icon: TimelineIcon::GitCommit,
|
||||
title,
|
||||
subtitle,
|
||||
body: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop the `<email>` portion of an hg author string ("Name <email>") and
|
||||
/// trim — leaves just the display name. If there's no email, return the
|
||||
/// trimmed input.
|
||||
fn author_name(s: &str) -> String {
|
||||
if let Some(idx) = s.find('<') {
|
||||
s[..idx].trim().to_string()
|
||||
} else {
|
||||
s.trim().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use serde_json::json;
|
||||
|
||||
fn ev(payload: Value) -> Event {
|
||||
Event {
|
||||
id: "hg:build/puppet:abc".into(),
|
||||
source: Source::Hg,
|
||||
action: "Commit".into(),
|
||||
occurred_at: Utc.with_ymd_and_hms(2018, 5, 1, 12, 0, 0).unwrap(),
|
||||
public: true,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(item: &TimelineItem) -> String {
|
||||
item.title
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
TitleSegment::Text { text } => text.clone(),
|
||||
TitleSegment::Link { text, .. } => text.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reshape_hg_commit() {
|
||||
let raw = json!({
|
||||
"_host": "hg-edge.mozilla.org",
|
||||
"_repo": "build/puppet",
|
||||
"node": "abcdef1234567890abcdef",
|
||||
"desc": "Bug 1234 - fix something\n\nlonger body",
|
||||
"author": "Rob Thijssen <rthijssen@mozilla.com>"
|
||||
});
|
||||
let item = reshape(&ev(raw));
|
||||
assert_eq!(item.icon, TimelineIcon::GitCommit);
|
||||
let r = render(&item);
|
||||
assert!(
|
||||
r.contains("Rob Thijssen committed abcdef123456 in build/puppet"),
|
||||
"got: {r}"
|
||||
);
|
||||
assert_eq!(
|
||||
item.subtitle.unwrap(),
|
||||
vec![TitleSegment::text("Bug 1234 - fix something")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_email_from_author() {
|
||||
assert_eq!(author_name("Rob Thijssen <rob@example>"), "Rob Thijssen");
|
||||
assert_eq!(author_name("nobody"), "nobody");
|
||||
assert_eq!(author_name(" spaced "), "spaced");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user