Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

85 linesCodeBlameRaw
1//! Live repository pages: every event a repository page shows is passed to
2//! the notify service as a small change (`g1t_contracts::live`), which
3//! tells the people watching that repository's pages. One call per batch
4//! (more for a very busy one); nothing written in the repository goes
5//! along, and who may see it is notify's to keep: a tab watches a
6//! repository only after the site checked its person may read it.
7//!
8//! A work item's events and comments go to every repository it is linked
9//! to (`repoIds`), not only its origin, which it may not have, and to its
10//! workspace's Work pages (`workspace_changed`), which hear of it with or
11//! without a repository.
12
13use g1t_contracts::events::Event;
14use g1t_contracts::live::{
15 MAX_CHANGES, REPO_CHANGED, RepoChange, RepoChangedArgs, WORKSPACE_CHANGED, WorkspaceChangedArgs, change_of, workspace_changes_of,
16};
17use worker::{Fetcher, Result};
18
19/// The changes an event makes: one for its repository, and one for each
20/// other repository it names in `repoIds`.
21pub(crate) fn changes_for(event: &Event) -> Vec<RepoChange> {
22 let mut changes: Vec<RepoChange> = change_of(event).into_iter().collect();
23 for repo_id in event.data["repoIds"].as_array().into_iter().flatten().filter_map(|id| id.as_str()) {
24 if repo_id.is_empty() || changes.iter().any(|change| change.repo_id == repo_id) {
25 continue;
26 }
27 let linked = Event { repo_id: Some(repo_id.to_owned()), ..event.clone() };
28 changes.extend(change_of(&linked));
29 }
30 changes
31}
32
33/// A batch's changes, in order, in calls of at most [`MAX_CHANGES`].
34pub(crate) fn changes_of(events: &[Event]) -> Vec<Vec<RepoChange>> {
35 let changes: Vec<RepoChange> = events.iter().flat_map(changes_for).collect();
36 changes.chunks(MAX_CHANGES).map(<[RepoChange]>::to_vec).collect()
37}
38
39/// Tells notify of the batch's changes. Logged and left when it cannot be
40/// reached: pages then catch up by themselves, and nothing else waits on it.
41pub(crate) async fn tell(notify: &Fetcher, events: &[Event]) {
42 for changes in changes_of(events) {
43 let told: Result<serde_json::Value> = g1t_kit::call(notify, REPO_CHANGED, &RepoChangedArgs { changes }).await;
44 if let Err(error) = told {
45 worker::console_error!("events: live pages not told: {error}");
46 }
47 }
48 for changes in workspace_changes_of(events) {
49 let told: Result<serde_json::Value> = g1t_kit::call(notify, WORKSPACE_CHANGED, &WorkspaceChangedArgs { changes }).await;
50 if let Err(error) = told {
51 worker::console_error!("events: live Work pages not told: {error}");
52 }
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use serde_json::json;
60
61 fn event(kind: &str, repo_id: Option<&str>, data: serde_json::Value) -> Event {
62 Event {
63 id: "evt_1".into(),
64 kind: kind.into(),
65 source: "work".into(),
66 time: "2026-10-11T10:00:00Z".into(),
67 repo_id: repo_id.map(Into::into),
68 actor: None,
69 data,
70 }
71 }
72
73 #[test]
74 fn a_work_item_changes_every_repository_it_is_linked_to() {
75 let data = json!({ "itemId": "wki_1", "workspace": "acme", "number": 40, "repoIds": ["rep_1", "rep_2"] });
76 let repos = |event: &Event| changes_for(event).into_iter().map(|change| (change.repo_id, change.number)).collect::<Vec<_>>();
77 let with_origin = event("work_item.updated", Some("rep_1"), data.clone());
78 assert_eq!(repos(&with_origin), vec![("rep_1".to_owned(), Some(40)), ("rep_2".to_owned(), Some(40))]);
79 // Filed in the workspace: only the repositories it is linked to.
80 let without = event("work_item.linked", None, data);
81 assert_eq!(repos(&without).len(), 2);
82 // What changes no page stays quiet.
83 assert!(changes_for(&event("audit.recorded", None, json!({ "repoIds": ["rep_1"] }))).is_empty());
84 }
85}